Compare commits

...
2176 Commits
Author SHA1 Message Date
Ozzie Isaacs e732b3b614 Output message only in production mode 2026-05-24 10:19:49 +02:00
Ozzie Isaacs 69f8767be5 Fixes from testrun 2026-05-17 10:21:19 +02:00
Ozzie Isaacs 6cbba6d170 Merge remote-tracking branch 'fix/commentxss' into Develop 2026-05-09 10:30:59 +02:00
Ozzie Isaacs 888ab4dc8f Update requirements 2026-05-09 10:28:12 +02:00
Ozzie Isaacs 753c19e700 Merge remote-tracking branch 'py314/py3.14-janeczku' into Develop 2026-05-09 10:24:58 +02:00
Ozzie Isaacs 4f9390332a Merge remote-tracking branch 'fix/attr' into Develop 2026-05-09 10:20:45 +02:00
Ozzie Isaacs ffddd85800 Merge remote-tracking branch 'fix/fixlter' into Develop 2026-05-09 10:19:57 +02:00
Ozzie Isaacs 652b0f20e0 Merge remote-tracking branch 'fix/token' into Develop 2026-05-09 10:17:57 +02:00
Ozzie Isaacs 598f08b916 Merge branch 'master' into Develop 2026-05-09 10:17:48 +02:00
Jacob Chapman 55cedba693 bump lxml to v6 for python 3.14 2026-04-29 22:05:40 -05:00
Jacob Chapman 10caf68d8b fix manifest reference 2026-04-29 22:05:40 -05:00
Ozzie Isaacs f4ed816f6e Fixes from testrun 2026-04-26 07:27:06 +02:00
Ozzie Isaacs ba77f41325 Merge remote-tracking branch 'fixes/notrace' 2026-04-25 16:04:51 +02:00
Ozzie Isaacs ea567fcbf0 Updated translation 2026-04-25 13:26:40 +02:00
Ozzie Isaacs edb05abcbe Merge remote-tracking branch 'tmp-orphan-on-download/fix-calibredb-cover-sibling' 2026-04-25 13:25:34 +02:00
Ozzie Isaacs 46811219ca Updated Requirements 2026-04-25 13:20:25 +02:00
Ozzie Isaacs 0aab0cac73 Merge remote-tracking branch 'tmp-orphan-on-download/fix-tmp-orphan-on-download' 2026-04-25 13:13:36 +02:00
Ozzie Isaacs 3257a36892 Merge remote-tracking branch 'ko/ko-translate-update' 2026-04-25 13:11:14 +02:00
limeade23 f10b680b0e Update Korean language 2026-04-23 17:41:46 +09:00
Ozzie Isaacs c2b1950cd5 Merge remote-tracking branch 'opds_date_update/fix-opds-atom-updated-reflects-modifications' 2026-04-20 15:55:24 +02:00
Ozzie Isaacs 00c8ff8ac4 Merge remote-tracking branch 'metadata-provider-none-crash/fix/metadata-provider-none-crash' 2026-04-20 15:49:26 +02:00
walrusec 0ca6e86177 Fix metadata search crash when ComicVine or Douban return None on error
Both comicvine.py and douban.py return None instead of [] when an HTTP
error occurs during a metadata search. The consumer in search_metadata.py
iterates the result directly, which raises TypeError: 'NoneType' object
is not iterable and crashes the server on single-threaded deployments.

Fixes #3606
2026-04-19 08:54:47 -04:00
haraldpdl ab17992ebb fix(opds): atom:updated reflects last modification, not date added
Books.atom_timestamp returned Books.timestamp (date added), which is
set at import and never changes. OPDS clients use atom:updated to
decide whether a book has changed on the server, so cover swaps,
metadata edits, and any other post-import change were invisible to
sync clients; they would keep serving the stale cover and title
until a manual refresh.

Atom RFC 4287 defines updated as "the most recent instant in time
when an entry or feed was modified", and Calibre already tracks that
field as last_modified, bumping it on every metadata and cover edit.
Switching the property to return last_modified (with a fallback to
timestamp when last_modified is NULL) aligns Calibre-Web's behaviour
with the Atom contract.

This change only affects the OPDS feed's atom:updated element. Kobo
sync uses its own last_modified comparison path, so it is unaffected.
2026-04-19 10:51:45 +02:00
haraldpdl 570371cc82 Don't write a cover sibling per download (--dont-save-cover)
calibredb export writes <uuid>.jpg next to <uuid>.<format> by
default, leaving an unused cover image in /tmp/calibre_web for
every download that goes through do_calibre_export. The cover
file is never read by calibre-web (cover serving uses a separate
path), so it is pure waste of disk space and IO.

Pass --dont-save-cover, paired with the existing --dont-write-opf,
to skip the unwanted side-effect at the source. Verified that
none of the three do_calibre_export call sites (download via
do_download_file, email via tasks/mail.py, kepubify via
tasks/convert.py) read the cover sibling.
2026-04-18 07:27:33 +02:00
haraldpdl 674b47bdbd Remove staged download from /tmp/calibre_web after response is sent
When config_embed_metadata is enabled with config_binariesdir or
config_kepubifypath set, do_download_file stages a copy of every
downloaded book under get_temp_dir() but never removes it. A bulk
OPDS or Kobo sync can fill the host filesystem in minutes; the
effect is amplified for comic formats (CBZ/CBR), where each
staged file can run to hundreds of MB or several GB. Once the
disk fills, downloads silently 404 and a container restart does
not recover the space.

Add an after_this_request hook that removes the staged copy after
the response is sent, gated on filename == get_temp_dir() so the
non-embed and gdrive-direct paths are untouched.
2026-04-18 07:12:38 +02:00
jvoisin 8cff413c4d Clean atributes as well in clean_string
`<img src=x onerror=alert(1)>` womp womp
2026-04-15 23:30:49 +02:00
jvoisin fd744af75d Don't give non-admin users a full stacktrace on 500
As `traceback.format_exc()` might contain internal file paths, library versions,
function names, and variable values.
2026-04-15 23:28:27 +02:00
jvoisin 6208d5e264 Correctly filter results kobo API
Multiple Kobo API endpoints use calibre_db.get_book_by_uuid() at db.py:748-749,
which performs a raw unfiltered query, meaning that common_filters() isn't
applied, so tag-based ACLs, language restrictions, and archived-book filtering
are all bypassed.
2026-04-15 23:14:09 +02:00
jvoisin 42dc36cc10 Escape comment columns names
Custom columns of type comments are rendered with `|safe` (disabling Jinja2
auto-escaping) and no `clean_string` sanitization. Compare with regular book
comments which correctly use `{{ entry.comments[0].text|clean_string|safe }}`.

Any user with edit permissions can set a custom comment column to
`<script>alert(document.cookie)</script>` and it will execute for every user who
views the book detail page or the OPDS feed. This is stored XSS with no
authentication barrier beyond edit permission.
2026-04-15 23:03:30 +02:00
jvoisin c23d35db4a Use 128 bits of entropy instead of only 32 in csp/ub.py
Remote login tokens are generated from only 4 bytes of randomness (32 bits = ~4
billion possibilities, 8 hex characters). The /ajax/verify_token endpoint at
remotelogin.py:98 has no rate limiting. The token is valid for 10 minutes.

At even modest request rates (10,000 req/sec), an attacker can test ~6 million
tokens during the 10-minute window , which isn't enough to exhaust the full
space, sure, but combined with multiple concurrent login sessions (each
generating a new token), or if the attacker can trigger the victim to initiate
remote login, the attack becomes more feasible. Compare with the Kobo auth
token which uses urandom(16) (128 bits).
2026-04-15 22:47:35 +02:00
Ozzie Isaacs fca580505c Fix typo 2026-04-15 20:31:40 +02:00
Ozzie Isaacs 19da54a7ae Merge remote-tracking branch 'fixes/escape_ldap' 2026-04-15 19:37:31 +02:00
Ozzie Isaacs 9c87f0fbde Merge remote-tracking branch 'fixes/fix_gdrive_md5' 2026-04-15 19:36:56 +02:00
Ozzie Isaacs e21f943712 Merge remote-tracking branch 'fixes/oauth_relink' 2026-04-15 19:36:48 +02:00
Ozzie Isaacs 4bb553f51d Merge remote-tracking branch 'fixes/noxxe' 2026-04-15 19:35:40 +02:00
Ozzie Isaacs 0615637f58 Merge remote-tracking branch 'fixes/nodebugleak' 2026-04-15 19:33:06 +02:00
Ozzie Isaacs a6c55deac6 Merge remote-tracking branch 'fixes/accessbp' 2026-04-15 19:31:29 +02:00
Ozzie Isaacs 2de8df7e0d Merge remote-tracking branch 'fixes/fix_kobo' 2026-04-15 19:28:55 +02:00
Ozzie Isaacs 4c1d7a9f6b Merge remote-tracking branch 'fixes/permshelf' 2026-04-15 19:28:12 +02:00
Ozzie Isaacs 9489905602 Merge remote-tracking branch 'fixes/chm' 2026-04-15 19:24:12 +02:00
Ozzie Isaacs 5e7c0c9d34 Merge remote-tracking branch 'fixes/sqlidb' 2026-04-15 19:23:01 +02:00
jvoisin b5da0df42a Prevent SQLI via dbpath
This is reachable only by the admin users, but is still a straightforward RCE
vector, if only via SQLite's `ATTACH DATABASE` trick
2026-04-14 23:23:12 +02:00
jvoisin 0959f84fd5 Use a sane permission for the encryption key file
The typical Linux umask of 0022, meaning the encrypted file is world-readable
(-rw-r--r--). Any OS-level user on the same system can read the key and decrypt
the encrypted credentials from app.db.
2026-04-14 23:08:35 +02:00
jvoisin 84777319d7 Fix access bypass on /show/ (serve_book)
The `serve_book` function uses `get_book()` which performs no access filtering:
it simply fetches by ID. Compare with `read_book` at web.py:1562 which
correctly uses `get_filtered_book()`. The `common_filters()` function enforces
per-user tag restrictions, language restrictions, and hidden-book rules.
2026-04-14 23:05:18 +02:00
jvoisin d85bef6c38 Don't leak credentials in debug_info
No need to dump Gmail OAuth client_secret, refresh_token, and
access_token in the debug ZIP in plaintext.
2026-04-14 22:55:08 +02:00
jvoisin 8ad9f4e3b7 Fix a dumb type condition in gdrive.py
hashlib.md5(dbpath) returns a hash object, not a hex string. Comparing a string
(md5Checksum) to a hash object with != always returns True. This means the
DB-replacement code path is always entered, allowing an attacker who sends a
forged notification (with the known static token) to trigger an arbitrary
metadata.db download from GDrive, replacing the live database.
2026-04-14 22:51:36 +02:00
jvoisin cde3888e17 Prevent LDAP injection in bind_user>get_object_details 2026-04-14 22:35:13 +02:00
jvoisin 387678a771 Prevent OAuth relinking.
When an OAuth provider_user_id is already linked to User A, and User B
authenticates with the same OAuth identity, User B is silently logged in as
User A. This is by design for single-user OAuth, but in a multi-user
environment it means: if an attacker gains access to the same OAuth provider
account (e.g., a shared GitHub org account, or by compromising the OAuth
provider), they can log in as the linked Calibre-Web user with no password
needed.
2026-04-14 22:28:06 +02:00
jvoisin c451daad3c Don't allow users to edit shelves they don't have permission to edit. 2026-04-14 22:25:36 +02:00
jvoisin 224915bba1 Prevent XXE in epub/fb2/goodreads API
The lxml.etree.fromstring() function use the default XML parser, which resolves
external entities because XML handling defaults in Python sucks. There is no
need for such dangerous misfeatures in calibre-web, so let's disable it.

A user able to upload epub/fb2 could add something like this to the file:

```xml
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<container><rootfiles><rootfile full-path="&xxe;"/></rootfiles></container>
```

and obtain the content of the `/etc/passwd` file, which is bad™.
2026-04-14 22:12:57 +02:00
jvoisin d598054195 Fix an IDOR in kobo tokens
Authenticated users shouldn't be able to generate/delete kobo auth tokens for
other users if they're not admin.
2026-04-14 22:10:30 +02:00
Ozzie Isaacs 088778969d Better error handling on series display in basic view 2026-04-06 15:28:10 +02:00
Ozzie Isaacs 0ed4d81da2 Better error handling on order parameter for sorting of user and books table 2026-04-06 15:12:09 +02:00
Ozzie Isaacs 9478325c23 Merge branch 'master' into Develop 2026-04-03 09:09:43 +02:00
Ozzie Isaacs 7c715f34dc Clean strings in comments before displaying 2026-04-03 09:01:01 +02:00
Ozzie Isaacs 00686ec1f2 Merge remote-tracking branch 'page_count/fix-series-list-view-sorts-by-name-instead-of-sort-field' 2026-03-07 10:24:58 +01:00
Ozzie Isaacs 83d331c89b Merge remote-tracking branch 'page_count/fix-kobo-prioritytimestamp-put-response' 2026-03-07 09:55:45 +01:00
Ozzie Isaacs f6338a9fda Merge remote-tracking branch 'filename/master' 2026-03-07 09:46:16 +01:00
Ozzie Isaacs 73efef17bd Merge branch 'master' into Develop 2026-03-07 09:36:19 +01:00
Ozzie Isaacs 74725b68b0 Merge remote-tracking branch 'it/patch-1' 2026-03-07 09:28:14 +01:00
mapi68 efbdd0b6e3 Update italian messages.po 2026-03-05 06:44:48 +01:00
jarynclouatre 1134f54be4 fix: series list view sorts by name instead of sort field
In series_list(), the SQLite query correctly orders results by
Series.sort, but a subsequent Python sorted() call (needed to
re-order after appending the "None" category entry) was using
Series.name as the sort key instead of Series.sort.

This caused series titles with leading articles (A, An, The) to
sort strictly alphabetically by the article rather than by the
meaningful word, e.g. "A Collins-Burke Mystery" appeared under
"A" instead of "C".

Fix by using Series.sort (with a fallback to Series.name if sort
is NULL) as the key in the Python re-sort, consistent with the
intent of the existing DB query.

Fixes #3583
2026-03-01 20:44:05 -06:00
jarynclouatre 2d4ca23d0c Kobo: include PriorityTimestamp in PUT /state response
Kobo: include PriorityTimestamp in PUT /state response
2026-03-01 18:19:40 -06:00
jarynclouatre 6157f5027c Update search_metadata.py
Fix TypeError when metadata provider returns None
2026-03-01 15:44:29 +01:00
Ozzie Isaacs 5e48a64b15 Merge remote-tracking branch 'opds-500-unauthenticated-request/fix/opds-500-unauthenticated-request' 2026-02-22 10:43:11 +01:00
Rafik Farhad 36a7ff19bc Fix AttributeError on unauthenticated OPDS requests
request_username() is used as flask-limiter's key_func for the OPDS
blueprint. The limiter evaluates key_func in a before_request handler,
before the route's auth decorator runs. When no Authorization header is
present, request.authorization is None, causing an AttributeError and
a 500 response instead of the expected 401.

Guard against None so unauthenticated requests fall back to an empty
string key, allowing the auth decorator to handle the 401 correctly.

Fixes #3592

Disclaimer: AI assisted—humans supervised.
2026-02-21 16:54:14 -06:00
Ozzie Isaacs 1b4f0d0967 Merge remote-tracking branch 'add-to-shelves-dropdown-menu/add-to-shelves-dropdown-menu' 2026-02-21 09:43:50 +01:00
Ozzie Isaacs 181117f916 Merge remote-tracking branch 'Kobo_sync/fix/kobo-library-sync-url-rewrite' 2026-02-21 09:42:09 +01:00
Ozzie Isaacs 32741be6c8 Merge remote-tracking branch 'kobo_sync/pr/fix-kobo-progress' 2026-02-21 09:31:56 +01:00
Ozzie Isaacs f64fe5d835 Merge remote-tracking branch 'Exclamationmark/chore_fix_error_messages' 2026-02-21 09:29:02 +01:00
Ozzie Isaacs 0db93a6036 Merge remote-tracking branch 'sort/master' 2026-02-21 09:27:04 +01:00
leahjessie a4bf028537 Fix Kobo popup when ProgressPercent is 0 by using is-not-None check instead of truthiness 2026-02-20 15:34:25 -08:00
AsukaVuuyn 47625a616d fix: support native UTF-8 downloaded filenames for browsers 2026-02-20 23:07:06 +08:00
Ozzie Isaacs 87635a5ace Merge branch 'Develop'
Update Requirements
Update Teststatus
2026-02-20 11:50:43 +01:00
Noé Sierra-Velasquez a9713bd497 Fix Kobo sync failure behind reverse proxy: rewrite library_sync URL in init response
The HandleInitRequest() function rewrites image URLs to point to the local
Calibre-Web instance, but library_sync was left pointing to storeapi.kobo.com.
This caused Kobo e-readers to call Kobo's servers for sync instead of the local
server, resulting in no books being synced when running behind a reverse proxy.
2026-02-19 20:34:50 +01:00
Ozzie Isaacs 8cef72b847 Updated requirements 2026-02-18 19:24:23 +01:00
Ozzie Isaacs fbed304df8 Better Fix for flask limiter 4.0 2026-02-18 19:11:38 +01:00
Ozzie Isaacs e3bf369ad6 Fix for flask limiter 4.0
Fix reseting flask_limiter keys
2026-02-18 18:59:29 +01:00
leahjessie d229f71151 Fix Kobo "Return to last page read" popup caused by float/int mismatch 2026-02-17 16:34:46 -08:00
Courville Software 3aef161cb8 fix title sort with "l'" in french
regex should not impose a space after "l'" otherwise detection for "L'arbre" fails.
2026-02-14 17:06:44 +01:00
Ozzie Isaacs cabcace3f0 Update translation
Fixes for flask_limiter version >4
2026-02-14 11:30:50 +01:00
Ozzie Isaacs 424e493946 Lower version of flask_limiter again 2026-02-14 11:30:15 +01:00
Ozzie Isaacs 848302f69e Merge branch 'master' into Develop 2026-02-14 10:21:05 +01:00
Ozzie Isaacs 8bdd95fc45 Fixes for flask_limiter > 4 2026-02-14 10:08:32 +01:00
Ozzie Isaacs 9c7a834cec Fix italian translation error 2026-02-13 19:18:00 +01:00
Ozzie Isaacs 51a2f36966 Revert flask_limiter version 2026-02-13 19:16:18 +01:00
Ozzie Isaacs b64645e5f5 Cover path is now selected based on correct setting and not if split_path is present or not (fix for #3527)
Version bump
Updated requirements
2026-02-08 12:12:34 +01:00
Ozzie Fernandez Isaacs bed8957eae Update pyproject.toml 2026-02-07 09:22:39 +01:00
Ozzie Fernandez Isaacs 0ce8c19e88 Update pyproject.toml 2026-02-06 21:59:49 +01:00
Ozzie Isaacs 5a1f3d8eec Version bump 2026-02-06 20:40:07 +01:00
lb803 315584690d remove exclamation marks 2026-02-05 21:40:45 +00:00
Ozzie Isaacs f14faff308 Merge remote-tracking branch 'cn/patch_cn' 2026-01-31 12:43:58 +01:00
Ozzie Isaacs 6f782c5687 Merge remote-tracking branch 'pt-br/translate-ptbr' 2026-01-31 12:40:01 +01:00
Ozzie Isaacs 7f7fc884c2 Fix for calibre 9.0 2026-01-31 12:31:44 +01:00
Webysther Sperandio 92084bab71 🌐 i18n(translations): typo 2026-01-31 05:51:04 +01:00
Webysther Sperandio f00471baf5 🌐 i18n(translations): add missing pt_BR translations in messages.po 2026-01-31 05:44:18 +01:00
Webysther Sperandio 9ed0774d19 doc: pt-br translation 2026-01-31 05:19:48 +01:00
Kamille eb20db55a4 Update Simplified Chinese 2026-01-31 11:25:50 +08:00
Webysther Sperandio a91724edb0 💄 style(css): include remove menu in dropdown scroll styling 2026-01-28 21:09:47 +01:00
Webysther Sperandio 5ad6b7fa64 💄 style(ui): limit add-to-shelves dropdown height
- 【style】 constrain add-to-shelves dropdown height and enable scroll
  - 【style】 prevent overly tall menus from overflowing the page
2026-01-26 19:39:36 +01:00
Webysther Sperandio c1be944a48 💄 style(ui): limit add-to-shelves dropdown height
- 【style】 constrain add-to-shelves dropdown height and enable scroll
  - 【style】 prevent overly tall menus from overflowing the page
2026-01-26 19:36:47 +01:00
Ozzie Isaacs e2c3c08d65 Merge remote-tracking branch 'origin/master' 2026-01-25 10:45:09 +01:00
Ozzie Isaacs 4fece545e4 Merge remote-tracking branch 'de/master' 2026-01-25 10:42:15 +01:00
Ozzie Isaacs 876a11dc58 Merge branch 'Develop'
Remove Get Parameter sort_param, where it is not valid (#3447)
Generate valid session-cookie-path(s) (fix for #3459)
Mass remove books from shelf (Fix for #3520)
2026-01-25 10:35:59 +01:00
Ozzie Isaacs 486914905e Merge branch 'Develop'
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2026-01-24 20:50:59 +01:00
Ozzie Isaacs d47af66499 Bugfix after testrun 2026-01-24 13:01:12 +01:00
Tom Chiverton 832ec80f1a Update README.md
Highlight how to enable uploads, as it's a FAQ
2026-01-21 18:46:47 +01:00
Kugeleis 85d1aab6a8 Add German translations for several UI strings 2026-01-18 18:07:51 +01:00
Ozzie Isaacs 8db0d004b6 Remove Get Parameter sort_param, where it is not valid (#3447) 2026-01-18 16:07:04 +01:00
Ozzie Isaacs c13eac91c8 Generate valid session-cookie-path(s) (fix for #3459) 2026-01-18 15:38:44 +01:00
Ozzie Isaacs cf3b619c73 Fix for #3520 (Mass remove books from shelf) 2026-01-18 12:59:40 +01:00
Ozzie Isaacs 269ab0e04e Fix for #3522 (Public registration does not apply default Allowed/Denied tags to new users ) 2026-01-17 12:27:16 +01:00
Ozzie Isaacs 47168f5ce9 Merge remote-tracking branch 'typo-epub-reader/webysther-deprecated-apple-meta' 2026-01-17 12:18:17 +01:00
Ozzie Isaacs 73d7306627 Merge remote-tracking branch 'typo-epub-reader/webysther-typo-epub-reader' 2026-01-17 12:16:45 +01:00
Ozzie Isaacs dd1cbb1018 Merge remote-tracking branch 'fr/patch-1' 2026-01-17 12:15:55 +01:00
Ozzie Isaacs 91fdf57b46 Update Teststatus 2026-01-17 12:14:13 +01:00
Webysther Sperandio d77d682ee0 Replace apple-mobile-web-app-capable with mobile-web-app-capable
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:13:03 +01:00
Webysther Sperandio e2eed3d311 Update layout.html
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:12:45 +01:00
Webysther Sperandio 54a47cfc18 Change meta tag for mobile web app capability
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:12:21 +01:00
Webysther Sperandio b5dac9e1a7 Change meta tag for mobile web app capability
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:12:02 +01:00
Webysther Sperandio b00abd283a Update readtxt.html
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:11:29 +01:00
Webysther Sperandio 4821b2b34c Change meta tag from apple-mobile to mobile-web app
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:10:54 +01:00
Webysther Sperandio 0e31be5357 Change meta tag for mobile web app capability
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:10:36 +01:00
Webysther Sperandio dc19d40db1 Change meta tag for mobile web app capability
https://web.dev/learn/pwa/web-app-manifest?hl=pt-br#designing_your_pwa_experience
2026-01-16 06:10:05 +01:00
Webysther Sperandio 02b5409b95 TYPO
Fix theme selection prompt in read.html
2026-01-16 05:57:31 +01:00
Edouard Rouch 63cebcd849 Completed french translation
Added missing translations.
Corrected grammatical or spelling errors.
Harmonized recurring translations (Success !, Oops !, Show...)
2026-01-15 12:34:47 +01:00
Ozzie Isaacs 99e91e622e Change quoting of downloaded filename 2026-01-10 12:53:16 +01:00
Ozzie Isaacs 36ef81dd91 Make kobo resources editable (#3472) 2026-01-10 12:40:19 +01:00
Ozzie Isaacs 2700b4f0d1 Support kobo/auth/refresh endpoint 2026-01-10 12:20:52 +01:00
Ozzie Isaacs d73f9fdd6f Fix for #3515 (Exclude shelves with Advanced search not working correct) 2026-01-08 20:01:39 +01:00
Ozzie Isaacs be89c25a90 Merge branch 'master' into Develop 2026-01-08 19:58:48 +01:00
Ozzie Isaacs c5e9526fc8 Bugfix Exclude shelfs 2026-01-07 20:35:26 +01:00
Ozzie Isaacs a99960e6c5 Merge remote-tracking branch 'it/patch-1' 2026-01-05 15:06:41 +01:00
mapi68 8105f54ebb Update Italian language 2026-01-04 02:24:15 +01:00
Ozzie Isaacs d01010812b Merge remote-tracking branch 'fix_missing_progress_bar_for_cbz_file/fix_missing_progress_bar_for_cbz_file' 2025-12-29 14:37:11 +01:00
Julian_Chu 02fbef3dba Move progress css style to dedicated file
In kthoom implementation, there is also a DOM

    `<div id="progress"....>`

If this part of css style stays in main.css, it also effects the kthoom
implementation, therefore the progress bar will disappear when reading
comic files (such as cbz).
2025-12-29 18:49:44 +09:00
Ozzie Isaacs eb8b0096a1 Update Teststatus 2025-12-23 14:25:21 +01:00
Ozzie Isaacs 38b0af9ea2 Remove debug code 2025-12-20 13:22:20 +01:00
Ozzie Isaacs 83b5d0f22b Merge remote-tracking branch 'replace_archive_icon/chore_replace_archive_icon' 2025-12-14 14:45:46 +01:00
Ozzie Isaacs 38c58bd7c9 Merge remote-tracking branch 'pl/patch-1' 2025-12-14 14:44:46 +01:00
Ozzie Isaacs 6276806c0e Merge remote-tracking branch '401/patch-1' 2025-12-14 14:40:49 +01:00
Evan Goldenberg 97a8fc661f Send WWW-Authenticate header with 401 responses 2025-12-10 00:06:32 -08:00
lb803 0569c644a3 change archive button icon from glyphicon-trash to glyphicon-folder-open 2025-12-09 21:44:36 +00:00
szepix 3565e5f068 Updated polish translations
Did all missing translations
2025-12-08 12:53:57 +01:00
Ozzie Isaacs 8b8bc1b688 Merge remote-tracking branch 'show-shelf-size/show-shelf-size'
Added some missing texts to caliblur theme
Caliblur theme fix language icon
2025-12-07 19:32:40 +01:00
Ozzie Isaacs ff9dabadf1 Merge remote-tracking branch 'cache/master' 2025-12-07 18:27:58 +01:00
Ozzie Isaacs a75d0caa02 Merge remote-tracking branch 'it/patch-1' 2025-12-07 14:32:58 +01:00
Ozzie Isaacs eb872865a5 Merge Reader improvements 2025-12-07 14:31:16 +01:00
Ozzie Isaacs 8031fff5af Merge branch 'Develop' 2025-12-07 10:55:04 +01:00
Ozzie Isaacs ac1ae66246 Merge branch 'master' into Develop 2025-12-07 10:54:40 +01:00
mapi68 310443afc5 Update italian 2025-12-07 04:08:44 +01:00
Ozzie Isaacs 25c9aba469 Fix vertical scrollbar in list.html 2025-12-06 19:26:30 +01:00
Ozzie Isaacs 3a3c8ad2a4 Merge remote-tracking branch 'lock-header-sidebar' 2025-12-06 13:21:23 +01:00
Ozzie Isaacs 10816c7f6d Merge remote-tracking branch 'performance_optimize-search-queries/performance/optimize-search-queries' into Develop 2025-12-06 11:54:28 +01:00
Ozzie Isaacs adcfd8afe8 Merge remote-tracking branch 'cn/patch-1'
Updated requirements
2025-12-06 11:52:18 +01:00
Ozzie Isaacs 74a386a284 Merge branch 'master' into Develop 2025-12-06 11:50:00 +01:00
Ozzie Isaacs fceca5cc08 Merge remote-tracking branch 'performance_optimize-search-queries/performance/optimize-search-queries' into Develop
Updated Requirements
2025-12-06 11:36:02 +01:00
Arlen Cole 13b515d3ab Update Chinese translations in messages.po 2025-12-06 13:54:25 +08:00
Ozzie Isaacs d2015dd998 Optimized search query 2025-12-05 16:33:30 +01:00
Ozzie Isaacs 3959a8eddd Caliblur theme display fix
Rename Gooble Books Metadata provider
2025-12-04 18:52:30 +01:00
Ozzie Isaacs ca63d343bd Merge remote-tracking branch 'resolution/Develop' into Develop 2025-12-04 18:51:37 +01:00
Ozzie Isaacs ecc6e00bfc Sort names before trying to extract coverfile of comics 2025-12-04 14:19:58 +01:00
root c1114cd786 Fix display issue on small width resolution with fetch metadata form on caliblur theme
Renamed Google metadata provider to Google Books
2025-11-27 19:07:16 +00:00
Ozzie Isaacs cf01bd7ad4 Merge remote-tracking branch 'kobo_tree/fix/kobo-store-proxy-routing' 2025-11-22 15:32:30 +01:00
Ozzie Isaacs cf1b3bb37f Merge remote-tracking branch 'de/patch-1' 2025-11-22 15:29:39 +01:00
Ozzie Isaacs 70fefa2f11 Merge remote-tracking branch 'de/patch-1' 2025-11-22 15:17:49 +01:00
Ozzie Isaacs 80df8b1fa6 Merge remote-tracking branch 'ko/patch-1' 2025-11-22 15:16:13 +01:00
Ozzie Isaacs cac33165dc Merge remote-tracking branch 'fr/patch-1' 2025-11-22 15:14:59 +01:00
Ozzie Isaacs bf2ab66362 Merge remote-tracking branch 'ga/patch-1' 2025-11-22 15:13:49 +01:00
Ozzie Isaacs 5e14b4bf41 Merge remote-tracking branch 'ga/patch-2' 2025-11-22 15:13:13 +01:00
Ozzie Isaacs a6fdd8ab36 Updated testresults
Fix goodreads
Remove unused import
Limit number of routes for book pages
2025-11-22 15:06:40 +01:00
Félix Pardal González d091b9d8b9 Update Galician translations in messages.po
Edited msgstr entried adding translation to Galician.
2025-11-22 13:43:28 +01:00
Félix Pardal González 2eb7c0d3f0 Update Spanish translations for various messages
Reviewed msgid entries and added spanish translation in msgstr entries.
2025-11-22 13:30:53 +01:00
Nico31240 54eb30e2b9 Update messages.po French 2025-11-12 10:30:42 +01:00
shsm0520 47f44d71cf Update Korean translations in messages.po
updated translate
2025-11-05 21:54:59 -05:00
Aljosa Asanovic 654095054f fix(kobo): add catch-all route for proxying unknown product endpoints
The Kobo store proxy feature was failing with 404 errors for certain
store endpoints (e.g., /v1/products/featuredforkoboplus/) because
Flask's routing couldn't match these paths. This prevented the Discover
tab from working when "Proxy unknown requests to Kobo Store" was enabled.

Added catch-all routes using Flask's path converter to match any
/v1/products/* paths not handled by more specific routes, allowing
the proxy logic to properly redirect these requests to the official
Kobo store API.
2025-11-02 13:10:25 -05:00
alcibiadesc 8544702bb2 Add security fixes and improvements to search optimization
- Add FTS5 table existence check to avoid log spam on non-FTS databases
- Escape FTS5 special characters (quotes) to prevent query errors
- Wrap FTS5 search terms in quotes for phrase matching accuracy
- Improve logging: change author ordering debug to warning for visibility
- Add comment explaining author_sort data issues

These changes improve robustness and security without affecting performance.
2025-11-01 20:05:53 +01:00
alcibiadesc a3b5d2eac7 Optimize search performance for large libraries
This commit introduces several performance optimizations to the search
system, significantly reducing query times for large Calibre libraries.

Key improvements:

1. FTS5 Integration
   - Added FTS5 full-text search support with automatic fallback
   - Uses indexed search when available, providing sub-second results
   - Gracefully degrades to traditional search if FTS5 is unavailable

2. Query Optimization
   - Replaced expensive .any() subqueries with efficient JOIN-based
     subqueries for tags, series, authors, and publishers
   - Reduced SQL complexity and improved query planning
   - Added selectinload() for authors to prevent N+1 query problems

3. LIMIT+1 Pattern
   - Implemented LIMIT+1 estimation pattern in get_search_results()
   - Avoids expensive COUNT(*) operations on large result sets
   - Provides fast pagination without sacrificing accuracy

4. Author Ordering Optimization
   - Replaced nested database queries with O(1) dictionary lookups
   - Eliminated N+1 query anti-pattern in order_authors()
   - Reduced author sorting from O(n²) to O(n) complexity

Performance Impact:
In testing with a library of 129,000+ books, these optimizations reduced
search times from 3-9 seconds to 85-330ms, achieving 89-97% improvement
across different search types.

The changes maintain backward compatibility and include fallbacks for
environments without FTS5 support.
2025-11-01 19:48:16 +01:00
IgorKurkov bba7812a07 feat(epub.js): add autosaving of current book reading position in localStorage to open this book on the last read point 2025-10-29 22:20:08 +01:00
IgorKurkov 458d0d1e3d feat(read.html, epub): add custom theme selection with color picker, used pickr.js 2025-10-29 22:10:08 +01:00
IgorKurkov 6d6d5ac097 add books button, add page count on bottom and in settings, add swipe for closing sidebar, fix settings modal, fix few styles 2025-10-29 22:06:09 +01:00
IgorKurkov 560e8e132d add swipes by gestures in settings as an option, hammer.js for mobile 2025-10-29 20:37:15 +01:00
schnabelewobski 3433b62864 Update messages.po
Added some missing German translations.
2025-10-29 14:03:04 +01:00
IgorKurkov d8fbf967b3 add console process interruption by ctrl C 2025-10-26 21:28:58 +01:00
IgorKurkov 902368efa7 feat(epub.js): add saving for settings of the font family to localstorage 2025-10-26 18:47:56 +01:00
IgorKurkov c5571fc359 feat(epub.js): add font size selector with precised + and - controls 2025-10-26 18:40:52 +01:00
IgorKurkov 132c17151a feat(epub.js): add font size saving in localstorage like theme saving 2025-10-26 18:24:44 +01:00
IgorKurkov 1ce1aee35e fix(js reader): adjust height to dynamic viewport height, add amber theme 2025-10-26 18:23:07 +01:00
Sebastian Holzer 4dc8948199 Some more fixes for some German translations in messages.po
Added some more translations that I stumbled over. Also added my name to it.
Please note however, the translation in line 3120 (book will be deleted from database) and the next translation (from hard disk) don't work well that way in german. Please consider defining two separate translations (one with and one without the hard disk). The german translation grammatically works the way I committed it here, but it's really not how one would write it if they had a choice. The "gelöscht" should always be at the end in german.
2025-10-22 20:02:04 +02:00
Sebastian Holzer 7db6481704 Added missing and fixed some existing German translations in messages.po
Updated German translations in messages.po, correcting various phrases and ensuring consistency. But most importantly added missing translations. Didn't check all translations though, just those that catched my attention
2025-10-22 19:47:25 +02:00
Ozzie Isaacs 6b11d0b4f3 Removed fuzzy translated strings which cause error during language compiling 2025-09-12 08:50:03 +02:00
Ozzie Isaacs c30d550ee1 Merge remote-tracking branch 'zh_Hant_TW/master' 2025-09-12 08:32:05 +02:00
Ozzie Isaacs c902fe1c5a Merge remote-tracking branch 'fr/master' 2025-09-12 08:28:27 +02:00
Ozzie Isaacs a6fe639e2f Merge remote-tracking branch 'it/patch-1' 2025-09-12 08:27:55 +02:00
mapi68 e1bce5d5fe Update italian messages.po 2025-09-11 01:36:07 +02:00
Ganfoud 5057e68b84 Add files via upload
FR translation update
2025-09-10 21:26:01 +02:00
Finrod Chen f88e776b1f Update zh_Hant_TW translations. 2025-09-10 11:56:43 +08:00
Ganfoud 957c906e53 Add files via upload
added some fr translations
2025-09-09 22:21:02 +02:00
Ozzie Isaacs cfda7f109a Merge remote-tracking branch 'books_api/cjwin2000-google_books_apikey' 2025-09-09 09:59:49 +02:00
Ozzie Isaacs 9fcd636533 Omit exporting of api strings 2025-09-09 09:40:37 +02:00
Ozzie Isaacs 0e50794d9b Testrun 2025-09-09 09:08:37 +02:00
Ozzie Isaacs 714c19f558 Some code cosmetics 2025-09-08 17:31:43 +02:00
Ozzie Isaacs c2d2db913e Merge remote-tracking branch 'opds/opds_improvements' 2025-09-08 15:59:33 +02:00
Ozzie Isaacs e3537235ca Merge remote-tracking branch 'fix_booklist_read_status/fix_booklist_read_status' 2025-09-08 12:52:25 +02:00
Ozzie Isaacs dc5663f21f Merge remote-tracking branch 'nl/patch-1' 2025-09-08 12:22:27 +02:00
Ozzie Isaacs dcc091e354 Merge remote-tracking branch 'instapaper/patch-1' 2025-09-08 12:07:32 +02:00
Ozzie Isaacs 4ad747decf Merge branch 'Develop' 2025-09-08 12:06:11 +02:00
Ozzie Isaacs 0d07e445de Bugfix mass edit books 2025-09-08 12:05:59 +02:00
Hot Garbage 9e7efb187b kobo: add Instapaper config
Latest firmware version writes empty values for Instapaper parameters if missing from the /initialization response.
Add default values to `NATIVE_KOBO_RESOURCES`
2025-09-01 12:15:06 -07:00
Ozzie Isaacs d480c55bcb Uodate optional-requirements.txt 2025-08-25 15:52:41 +02:00
Ozzie Isaacs 0ff1feaef0 Update optional-requirements.txt (#3446) 2025-08-25 14:43:39 +02:00
Ozzie Isaacs 0bfe6de62a Add certifi to requirements for Mac computer (fix for #3385) 2025-08-24 11:02:50 +02:00
Ozzie Isaacs 776eeccc67 Version bump 2025-08-24 09:50:41 +02:00
rjaakke b632f1b9a2 Update get_filtered_book call to include archived books. 2025-08-18 20:31:31 +02:00
Syer10 9aab6d90f7 Keep tags since its now in html 2025-08-15 11:51:14 -04:00
Syer10 5cd914f007 Fix newlines 2025-08-15 11:41:38 -04:00
Syer10 3a899afcce Opds Improvements 2025-08-15 11:10:14 -04:00
Peter Kraus 7418c2c0b4 Update messages.po
Dutch translations almost completely finished
2025-08-15 15:39:07 +02:00
Ozzie Isaacs 91e36b2c7e Testrun
Use getPath more often
2025-08-09 12:11:11 +02:00
Ozzie Isaacs e86833a2a9 Updated requirements 2025-08-05 20:10:44 +02:00
Ozzie Isaacs f3a4df1759 Merge branch 'master' into Develop 2025-07-04 20:22:11 +02:00
Ozzie Isaacs 4fe28028ea Merge remote-tracking branch 'arabic/add-arabic-translation' 2025-06-29 09:46:23 +02:00
Ozzie Isaacs e2c88b3a33 Merge remote-tracking branch 'lazy/patch-1' 2025-06-29 09:29:43 +02:00
Ozzie Isaacs f58c8eb916 Merge remote-tracking branch 'lazy/master' 2025-06-29 09:28:34 +02:00
Ozzie Isaacs 8938598aab Merge remote-tracking branch 'caliblur_fixes/book-table-layout' into Develop 2025-06-29 09:01:11 +02:00
Ozzie Isaacs 7472cdf43f Merge remote-tracking branch 'caliblur_fixes/css-speedup' into Develop 2025-06-29 08:54:31 +02:00
Ozzie Isaacs 6f5af36ecf Merge remote-tracking branch 'identifiers/ebooks_identifier' 2025-06-28 14:01:41 +02:00
Ozzie Isaacs 7c1d372086 Updated translation 2025-06-28 13:59:55 +02:00
Ozzie Isaacs 37b9ee9b0d Merge remote-tracking branch 'updated_translation/patch-1' 2025-06-28 13:58:46 +02:00
Ozzie Isaacs bcfdeacf15 Decode calibre traceback to str 2025-06-28 13:54:03 +02:00
Usama Khalil cb588c2c82 Update messages.po
Correcting typo, separate line 9 & 10.
Poedit (v2.2) compined line 9 & 10 into one line.
2025-06-07 15:04:05 +03:00
Usama Khalil 0b777c47bb Add Arabic translation (messages.po & messages.mo) files. 100% done 2025-06-07 14:47:04 +03:00
Ozzie Isaacs 4dce8abb1d Merge remote-tracking branch '403/fix/thumnbails_route_sso' 2025-06-01 10:37:32 +02:00
Ozzie Isaacs f27d62e22c Merge remote-tracking branch 'overdrive/master' 2025-06-01 10:33:09 +02:00
Landon Cheek 0eb8583dce Allow POST requests issued during Overdrive book returns to be proxied
Addresses janeczku/calibre-web#3398
2025-05-23 12:37:45 -06:00
Kamil Markowicz 6f969ed6a6 fix: fixes 403 when using proxy auth
Resolves a 403 response when accessing /ajax/updateThumbnails from a
session authenticated by reverse proxy.

For some reason, the decorators are swapped on this route specifically,
but not the others. This brings the ordering to be consistent with other
routes. The login check should happen before the auth check now.
2025-05-09 22:31:06 -04:00
Oskar Manhart 707c41c48a feat: lazy-load images 2025-05-04 18:22:46 +02:00
Oskar Manhart 054256067a Merge remote-tracking branch 'upstream' 2025-05-04 11:46:07 +02:00
Asher Max Schweigart 8c0891a078 Updating translation files 2025-04-29 14:33:19 -04:00
Asher Max Schweigart cc0fbd7ce8 Adding Smashwords identifier 2025-04-29 14:31:19 -04:00
Asher Max Schweigart 4e245def8f Moving scrollbar to main panel instead of the body to lock header and sidebar in place 2025-04-29 14:20:17 -04:00
Asher Max Schweigart 50be750460 Updating translation files 2025-04-28 12:42:16 -04:00
Asher Max Schweigart 5127061775 Updating translation template 2025-04-28 12:22:28 -04:00
mapi68 0d2611c8a0 Update messages.po 2025-04-15 00:51:31 +02:00
Asher Max Schweigart 521b46e60d Adding eBooks.com identifier type 2025-04-14 14:05:26 -04:00
Missing Link 36a99b0b5c Fix traceback encoding 2025-04-12 03:59:32 +08:00
David Champion d32ad1d5ca Show shelf sizes in layout 2025-04-03 13:14:15 -07:00
quarz12 7029fa8b60 fix 2025-04-02 21:56:11 +02:00
quarz12 8c98b91a25 hide desktop navbar when in mobile resolution 2025-04-02 20:53:22 +02:00
Kevin Rauwolf feac864e73 Rename CACHE_DIR env var to CACHE_DIRECTORY (#3343) 2025-04-01 01:13:45 -07:00
Ozzie Isaacs d75adfe85e Improved testability 2025-03-31 19:06:53 +02:00
Ozzie Isaacs 4e67a56ed2 Merge remote-tracking branch 'ger/fix/de-translation' 2025-03-30 15:55:57 +02:00
Ozzie Isaacs dbdaa0884e Merge branch 'master' into Develop 2025-03-30 13:48:08 +02:00
Ozzie Isaacs 2b2ccb1710 Refactored epub-reader progress
Progress in epubs now visible also in dark mode(s)
2025-03-30 12:23:13 +02:00
Ozzie Isaacs 4583fc8023 Python 3.13 compatibility
Code cosmetics
2025-03-30 12:03:12 +02:00
Ozzie Isaacs 1b1b7455d2 Merge branch 'master' into Develop 2025-03-30 09:42:58 +02:00
Ozzie Isaacs daaf7337ab Update Testresults 2025-03-30 09:42:31 +02:00
Ozzie Isaacs 2c9d4d2487 Merge remote-tracking branch 'djvu_mimetype/new_djvu_mimetyp' 2025-03-30 09:17:59 +02:00
Ozzie Isaacs af09b50153 Merge remote-tracking branch 'modal/master' 2025-03-30 09:05:24 +02:00
Ozzie Isaacs b8c3674be9 Merge remote-tracking branch 'storygraph/storygraph-id' 2025-03-30 09:00:01 +02:00
Ozzie Isaacs 65a29ed4c9 Log exception for unhandled errors in background tasks 2025-03-30 08:58:56 +02:00
Ozzie Isaacs 854a7d020d Better error output for "empty" Author sort not working
Update bleach requirement
2025-03-28 17:23:45 +01:00
Ozzie Isaacs a215b73e39 Lower requirement of cryptography 2025-03-28 16:52:10 +01:00
Ozzie Isaacs 6bfe022de0 Update Teststatus 2025-03-26 06:56:53 +01:00
Ozzie Isaacs c6cde961b6 Revert Change to reenable change database function 2025-03-25 06:08:08 +01:00
Ozzie Isaacs d08acdc3cc Merge branch 'master' into Develop 2025-03-24 18:59:12 +01:00
Ozzie Isaacs 86e6161b89 Remove testing code 2025-03-23 15:29:03 +01:00
Ozzie Isaacs 646937272a Updates dependencies
Update logger ->filename is logged instead of responsible logger
2025-03-23 15:12:06 +01:00
Ozzie Isaacs 6dd7a023f3 Improved error handling for invalid recipient address 2025-03-23 14:13:02 +01:00
Ozzie Isaacs f0077e73a8 Merge remote-tracking branch 'mail_id/patch-1' 2025-03-23 09:22:14 +01:00
Ozzie Isaacs e485f10e9e List locale alphabetically 2025-03-23 09:18:48 +01:00
Ozzie Isaacs 81a0b15a3d Merge remote-tracking branch 'button/fix-setup-form' 2025-03-23 08:49:29 +01:00
Ozzie Isaacs 89577914ba Merge remote-tracking branch 'fix-js-content-type/fix-js-content-type' 2025-03-23 08:46:27 +01:00
Ozzie Isaacs c289cb5d4b Fix sync fails with error when "Synchronize Notebooks" enabled 2025-03-23 08:44:55 +01:00
Ozzie Isaacs fd88dd2383 Merge remote-tracking branch 'Kobo_pen/Develop' into Develop 2025-03-23 08:44:11 +01:00
Ozzie Isaacs 80bcfb12de Add UniDecodeError to catched errors during kobo sync check layout 2025-03-23 08:41:18 +01:00
Ozzie Isaacs 46f111cd6e Better error handling during proxying kobo requests (fix for #3334) 2025-03-23 07:56:14 +01:00
Ozzie Isaacs bcb72d780f Testupdate 2025-03-23 07:24:46 +01:00
antar37 3d9594cfaa Update mail.py
using make_msgid to create a well-formed  message-ID. The lack of brackets causes gmail to reject the message-ID
2025-03-20 12:43:56 -06:00
Jayson Rhynas bc0e218e73 Added support for storgraph: identifiers 2025-03-18 11:51:43 -04:00
Theo Psallidas f9267ba391 Fix modal for smaller devices [smartphone] 2025-03-17 19:13:25 +02:00
Louis eb02c9e5d4 Fix to use common djvu mimetype 2025-02-25 12:49:31 +01:00
Jørgen V 2f6322b0ee Fix #2849 2025-02-23 22:17:01 +01:00
Ross Williams 22a93a28e2 Refactor epub reader progress calculation
The code in epub-progress.js was causing the epub file to be loaded
twice over the network, because it created a second instance of the
epub.js library. I moved all the progress percentage display code into
epub.js. Only the locationchange polyfill code was left in the separate
file, renamed to locationchange-polyfill.js.

I rewrote the percentage progress code to use epub.js events,
giving more succinct and readable code. Also, I added localStorage
caching of the epub location calculations, required for displaying
percentage progress, which can take up to 60-90 seconds to calculate on
longer ebooks. This localStorage caching approach is recommended by
epub.js in the `locations.html` example in their repo.

Signed-off-by: Ross Williams <ross@ross-williams.net>
2025-02-21 04:15:37 +00:00
Juan Orti Alcaine 1ee49791f4 Remove duplicated 'charset' in Content-Type header for Javascript files
Javascript files are served over HTTP with an incorrect Content-Type
header because the 'charset' option is duplicated:

    $ curl -I http://127.0.0.1:8083/static/js/table.js
    HTTP/1.1 200 OK
    Content-Disposition: inline; filename=table.js
    Content-Type: text/javascript; charset=UTF-8; charset=utf-8
    Content-Length: 35967
    Last-Modified: Sat, 16 Nov 2024 06:21:28 GMT
    Cache-Control: no-cache
    ETag: "1731738088.0-35967-471731799"
    Date: Sat, 08 Feb 2025 16:30:42 GMT
    Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; img-src 'self' data:; object-src 'none';
    X-Content-Type-Options: nosniff
    X-Frame-Options: SAMEORIGIN
    X-XSS-Protection: 1; mode=block
    Strict-Transport-Security: max-age=31536000

This header is not RFC 1521 compliant and it's breaking some reverse
proxy setups that perform compression. See this bug for reference:

https://github.com/crocodilestick/Calibre-Web-Automated/issues/217
2025-02-08 17:29:02 +01:00
Robbinsch 90bccb86ef fix: right html in config_db 2025-01-25 17:52:01 +01:00
Tobias Bayer ff6b341c36 Fix more German translation strings 2025-01-13 10:21:33 +01:00
Tobias Bayer 9d3b2b007b Fix German translation 2025-01-12 14:23:23 +01:00
cjwin2000 dbf104787c Update google.py 2025-01-10 20:31:17 -05:00
cjwin2000 bf31e675db Update config_edit.html 2025-01-10 20:28:09 -05:00
cjwin2000 7e24890991 Update admin.py 2025-01-10 20:20:08 -05:00
cjwin2000 4cf4c4dfe6 Update config_sql.py 2025-01-10 20:14:17 -05:00
Ozzie Isaacs efc7ce31d2 Fix kindle download handling 2025-01-06 17:38:25 +01:00
Ozzie Isaacs 4ccc2c0aec Handle kindle browser download filename can only contain ASCII characters (#3266) 2025-01-06 17:36:17 +01:00
Ozzie Isaacs 4512ab4524 Renamed pdf reader javascript files (#3277) 2025-01-06 14:54:09 +01:00
Ozzie Isaacs 0ef9e46133 Merge remote-tracking branch 'colorspace/fix-colorspace-transformation' 2025-01-06 07:03:23 +01:00
Ozzie Isaacs 88a06deb8c Merge remote-tracking branch 'caliblur/fix-upload-styling' 2025-01-06 07:01:50 +01:00
Wouter Koch e4f1ce9c5a Transform color space to srgb instead of rgb to prevent dark images 2025-01-05 01:49:46 +01:00
James McNee f5048ee1a8 Fix UI bug when book conversion enabled on mobile
On mobile, when book conversion is enabled and using the cabliBlur theme, the upload menu appears behind the description text area making it difficult (though not impossible with some careful guesswork around where the buttons should be).

This commit fixes the styling of this section so that it no longer appears behind the description text area.
2025-01-04 11:51:35 +00:00
Ozzie Isaacs e531bc0f0c Fixes from Test 2025-01-04 06:54:34 +01:00
Ozzie Isaacs f78a2367a2 Fix title sort, author sort setting for multi book table editing 2024-12-31 11:54:32 +01:00
Ozzie Isaacs 928ed10c2e Improved error handling on mass edit list 2024-12-29 20:12:27 +01:00
Ozzie Isaacs e7c2a85fd4 Fix for uploading azw files (#3263) 2024-12-22 13:11:59 +01:00
Ozzie Isaacs 32d120bd39 Fix for (#3261 handle permission error on generating new folders during rename of authors) 2024-12-21 11:36:18 +01:00
Ozzie Isaacs be5247a2ef Fix for (#3261 handle permission error on generating new folders during rename of authors) 2024-12-21 11:30:02 +01:00
Ozzie Isaacs ee7f393e67 enable mobi and rtf files for upload (fix for #3258) 2024-12-15 19:58:29 +01:00
Ozzie Isaacs 14bb01010d Bugfix edit books list (delete value) 2024-12-15 16:09:11 +01:00
Ozzie Isaacs 71686e4490 Merge remote-tracking branch 'it/patch-1' 2024-12-15 07:20:37 +01:00
mapi68 ea1ba54a9d Update messages.po 2024-12-15 06:40:46 +01:00
Ozzie Isaacs 0afba50953 Merge branch 'master' into Develop 2024-12-14 14:54:00 +01:00
Ozzie Isaacs 659535f5de update Galician translation 2024-12-14 14:52:06 +01:00
Ozzie Isaacs d977d1b81b Add title tag to opds feed
Enable Kepub format for epub Reader
2024-12-14 14:49:28 +01:00
Ozzie Isaacs 24fdadbeb6 Update error handling book table edit 2024-12-14 14:39:17 +01:00
Ozzie Isaacs 08527ae3ce Update mass edit
Refactored delete User function
Updated testresults
2024-12-13 18:03:14 +01:00
Aleksei Besogonov 01215aaa68 Allow the web reader to use KEPUB files and print file sizes
This commit allows the web reader to read the KEPUB (Kobo EPUB) files,
and also shows the file sizes. This is especially useful if you have
giant EPUB files with built-in audiobooks and media overlays.
2024-12-08 23:30:05 -08:00
Aleksei Besogonov ed600a9aeb Add a title to OPDS links
This allows the OPDS consumers to distinguish `epub` and `kepub` files
as they have the same MIME-type.
2024-12-08 17:29:03 -08:00
pollitor e542151652 Update Galician translation 2024-12-08 17:02:27 +01:00
Ozzie Isaacs e49402649d Order of elements on the book editing page changed 2024-12-07 19:13:28 +01:00
Ozzie Isaacs 89e9958222 Merge remote-tracking branch 'massedit/bulk-delete' into Develop 2024-12-07 10:32:17 +01:00
Ozzie Isaacs 21989c3ccf Added logging of ip address (#3237)
Refactored Response(json.dumps -> make_response(jsonify..)
Update mimetypes - Allow different mimetypes for download and file upload check (#3245, #3243)
Bugfixes from tests
Updated optional-requirements
2024-12-07 07:27:57 +01:00
Ozzie Isaacs 42924d9508 Added logging of ip address (#3237)
Refactored Response(json.dumps -> make_response(jsonify..)
Update mimetypes - Allow different mimetypes for download and file upload check (#3245, #3243)
Bugfixes from tests
Updated optional-requirements
2024-12-07 07:27:00 +01:00
Ozzie Isaacs f9390ceea5 reenable hide console window on windows 2024-12-01 16:22:08 +01:00
Ozzie Isaacs dc7a0575dd Fix typo on security settings 2024-12-01 16:19:53 +01:00
leyan a972447dbc Correct a typo on config_password_character 2024-11-30 23:21:14 +01:00
Ozzie Isaacs 87b200cc13 Connection to metadata.db is now on per request base, should solve upload issues with multiple files at once 2024-11-30 10:02:53 +01:00
Ozzie Isaacs 14e57e3714 Fix save invalid database 2024-11-30 10:01:03 +01:00
Ozzie Isaacs d45ed1c921 Fixes for parallel upload (coverfiles get individual temporary names)
Updated teststatus
2024-11-30 09:45:46 +01:00
Ozzie Isaacs 344c0c7bc3 Bugfixes after testrun 2024-11-28 14:47:52 +01:00
Ozzie Isaacs 190ee09e60 update Tests
Per request db connection
2024-11-26 06:13:18 +01:00
Ozzie Isaacs e6031844d5 Update Testresults 2024-11-24 08:42:13 +01:00
Ozzie Isaacs 7c6347442f Merge branch 'master' into Develop 2024-11-23 15:53:34 +01:00
Ozzie Isaacs 16255870a8 Merge simple view 2024-11-23 15:46:20 +01:00
Ozzie Isaacs 65bcffc129 Fix layout of identifiers on book edit page if starting with no identifiers 2024-11-23 10:49:54 +01:00
Ozzie Isaacs b28547b248 Update kobo standard resources links 2024-11-23 10:39:55 +01:00
Ozzie Isaacs 8873381290 Merge remote-tracking branch 'amazon_links/patch-2' 2024-11-23 10:19:18 +01:00
Ozzie Isaacs ebb7ddec44 Merge remote-tracking branch 'whatsapp/patch-4' 2024-11-23 10:06:22 +01:00
Ozzie Isaacs b07541a251 Merge remote-tracking branch 'whatsapp/patch-5' 2024-11-23 09:54:57 +01:00
Ozzie Isaacs 85a66a3bfe Merge remote-tracking branch 'it/patch-1' 2024-11-23 09:37:15 +01:00
Ozzie Isaacs f493d4e4e5 Merge remote-tracking branch 'bsd_paths/freebsd-binary-paths' 2024-11-23 09:34:34 +01:00
Ozzie Isaacs 5d8908db49 Update Spanish translation 2024-11-23 09:32:10 +01:00
Matthew Wener 14b044d055 Add Binary Paths for FreeBSD 2024-11-22 21:35:21 -05:00
mapi68 9b0b82ec1b Update messages.po 2024-11-22 04:42:08 +01:00
Ozzie Isaacs 881b48729c Improved handling of docker database errors 2024-11-21 17:13:20 +01:00
Ozzie Isaacs 812265324d Merge remote-tracking branch 'cn/master' 2024-11-21 16:37:52 +01:00
Ozzie Isaacs 621f8debea Update Testresults 2024-11-21 16:36:23 +01:00
qx100 43040f6162 Update Chinese Simplified Ttanslation 2024-11-21 22:06:16 +08:00
Ozzie Isaacs 0bf3bb381f Invisible books are no longer counted in archived page (Fix for #3221) 2024-11-20 18:20:33 +01:00
Ozzie Isaacs 2bfbbfd4ba Possible fix for #3230 2024-11-20 18:08:25 +01:00
Ozzie Isaacs 6cb1449ff3 Fix version detection to work with sqlalchemy version 2.0.36.dev0 2024-11-20 17:55:17 +01:00
Ozzie Fernandez Isaacs 92fbc9c792 Improved dep check for windows exe version and further fixes for windows exe 2024-11-18 19:47:33 +01:00
Ozzie Isaacs f9cfb38ad3 Updated german translation 2024-11-16 20:42:11 +01:00
Ozzie Isaacs e5dc7e2834 Update readme and version 2024-11-16 13:39:04 +01:00
Ozzie Isaacs 0af52f2053 Update Version
Update teststatus
2024-11-16 07:21:28 +01:00
Ozzie Isaacs 2fff002699 Fix for stable update version 2024-11-11 18:46:07 +01:00
Ozzie Isaacs 5a87c1c4b3 Add libmagic for windows installations 2024-11-10 14:52:55 +01:00
Webysther Sperandio 38a7454e7a Add share details metadata 2024-11-08 13:23:44 +01:00
Ozzie Isaacs f0b770224a Fix updater nightly version check 2024-11-05 18:35:21 +01:00
Ozzie Isaacs 4c3fc90830 Fix for dependency check on python versions <3.10 2024-11-05 16:48:03 +01:00
Ozzie Isaacs d6798fb286 Fix error in requirements.txt 2024-11-04 21:20:34 +01:00
Ozzie Isaacs 6315f40800 Dependency check respects now python version restrictions 2024-11-04 21:10:18 +01:00
Ozzie Isaacs 1b73d8a7e4 Specify python versions in requirements file 2024-11-04 18:50:45 +01:00
Ozzie Isaacs 54a0ddaab5 Merge remote-tracking branch 'ko/master' 2024-11-02 20:21:09 +01:00
Ozzie Isaacs a84c316e3d Merge branch 'Develop' - Improved compability for python 3.12+ 2024-11-02 16:59:19 +01:00
Ozzie Isaacs 985e59ad68 Merge branch 'master' into Develop 2024-11-02 08:00:05 +01:00
Ozzie Isaacs 5334acac37 Merge branch 'Develop' 2024-11-02 07:29:32 +01:00
limeade23 0a53719394 Fix : refine korean translation 2024-11-01 20:04:58 +09:00
limeade23 3cc89d4ca0 Fix : refine korean translation 2024-11-01 19:07:14 +09:00
limeade23 2eee5fbdd8 Fix : some fix korean (remove file) 2024-11-01 19:01:14 +09:00
limeade23 e0b2734d66 Fix : some fix korean 2024-11-01 18:59:30 +09:00
limeade23 9926e03009 Fix : some fix korean 2024-11-01 18:42:23 +09:00
limeade23 3aa0c82795 Fix : retranslate korean 2024-11-01 18:01:41 +09:00
Ozzie Isaacs 85368c89dd Merge branch 'master' into Develop 2024-11-01 09:44:10 +01:00
Ozzie Isaacs 75499b989c Merge branch 'Develop' 2024-11-01 09:43:43 +01:00
Ozzie Isaacs bf89bbc8bc Fix series_index 0 isn't correctly synced to kobo (#3204)
Update Teststatus
2024-11-01 06:59:46 +01:00
Ozzie Isaacs deb1ff37b2 Escape characters in filename for kepub conversion 2024-10-31 19:09:00 +01:00
Ozzie Isaacs 2927b5d544 Updated requirements 2024-10-28 16:25:46 +01:00
Ozzie Isaacs bf9313ddeb Update test status
Updates after test run
2024-10-28 16:16:28 +01:00
Ozzie Isaacs c2ffa940cd Add cryptography to requirements 2024-10-27 07:05:50 +01:00
Ozzie Isaacs b94e1df518 Delete custom column related values on database change 2024-10-26 16:08:50 +02:00
Ozzie Isaacs 2eb4f77a23 Display error message if visiblity resticting custom column is no available (Fix for #3190) 2024-10-26 15:34:57 +02:00
Ozzie Isaacs d5059979d5 Marked 2 strings as raw strings to suppress warnings 2024-10-25 14:18:22 +02:00
Ozzie Isaacs 4fa7520598 Final fix for #3189 (also working on windows) 2024-10-25 12:26:35 +02:00
Ozzie Isaacs e8cb84136b Fix quotes for calibre convert on windows 2024-10-25 10:29:41 +02:00
Ozzie Isaacs 8a3409e911 Fix for caliber-convert parameter recognition bug with spaces in parameter (fix for #3189) 2024-10-25 10:21:44 +02:00
Ozzie Isaacs abedb13473 Improved ldap import user routine (fix for #3186) 2024-10-25 10:20:38 +02:00
Webysther Sperandio 366a65f99f Increase sqlite calibre cache 2024-10-23 22:22:50 +02:00
Ozzie Isaacs cd01aaa69e Merge remote-tracking branch 'readme/patch-1' 2024-10-23 19:03:34 +02:00
Ozzie Isaacs d4502713ff Merge remote-tracking branch 'ios_bookmark/fix-impossible-to-press-bookmarks-button-in-safari' 2024-10-23 18:57:24 +02:00
Ozzie Isaacs c523463591 Merge remote-tracking branch 'sl/master' 2024-10-23 18:44:10 +02:00
Andrej Kralj cca8158bdf Updates Slovenian translation 2024-10-22 08:21:00 +02:00
Ozzie Isaacs b9085129a7 Merge branch 'pdf' 2024-10-19 06:47:32 +02:00
Ozzie Isaacs cb073daca3 Changed response code to opds download forbidden to 401 instead of 404 2024-10-19 06:36:03 +02:00
Ozzie Isaacs c40c8a2255 Format seriesindex everywhere to 2 digits 2024-10-18 20:24:49 +02:00
lucaparsani e42e11e2a4 fix bookmarks button for safari browser 2024-10-09 00:24:06 +02:00
contributor 07bf9047b0 pdf metadata - ignore NullObject /Keywords to fix http 500 upload error
this fixes http 500 error while uploading valid PDF document with empty /Keywords
2024-10-04 19:53:54 +03:00
Lucinda Quintal 230573dc7a Enhance README: Adds Troubleshooting section, expands Contribution section
- Adds Troubleshooting Section which has common issues, configuration errors, and user management issues to help users diagnose problems effectively.
- Expanded the Contributing Section to include guidelines for contributions, a code of conduct, bug reporting, and feature requests.
- In the Additional Resources section , provided links to documentation and community contributions for ease of reference
2024-09-29 07:32:50 -07:00
Ozzie Isaacs 94df767c28 Reupdate slovenian translation 2024-09-26 19:46:35 +02:00
Webysther Sperandio fea8a9d40d Update with all amazon site that diverge in domain style 2024-09-25 01:07:22 +02:00
James Armstrong 54d9d33e53 Revert "Add shift-click to select multiple books at once"
This reverts commit fe78222142.
2024-09-23 09:42:04 -07:00
Ozzie Isaacs 1eb9cd44dc Improve message on error cover downlaod 2024-09-21 13:12:48 +02:00
Ozzie Isaacs eb6563480f Fix #3153 (In Firefox, Github OAuth login goes into infinite redirect loops) 2024-09-21 13:01:48 +02:00
Ozzie Isaacs 2f39e51e1c Fix for #3151 (Invalid amazon link for uk) 2024-09-21 12:58:12 +02:00
Ozzie Isaacs 24252e09b4 Merge branch 'master' into Develop 2024-09-21 12:43:19 +02:00
Ozzie Isaacs 0e7eed4d3c Update generated pyproject.toml 2024-09-21 12:39:05 +02:00
Ozzie Isaacs 884f7666fa Integrate advocate and change netiface requirement 2024-09-21 12:38:41 +02:00
Ozzie Isaacs c54e9fe588 Updated slovenian language 2024-09-21 12:35:26 +02:00
Andrej Kralj d82944d08c Updated Slovenian translation
After checking the nightly build I have made some improvements.
2024-09-19 09:15:04 +02:00
Ozzie Isaacs 97764acbcd Added missing locale for datepicker in Slovenian language 2024-09-18 19:56:54 +02:00
Ozzie Isaacs 53f0647c5d New Slovenian translation 2024-09-18 19:50:57 +02:00
Ozzie Isaacs b5ee07eba8 Revert "Update Slovenian translation"
This reverts commit 5fe13f53bf.
2024-09-18 19:43:44 +02:00
Ozzie Isaacs 5fe13f53bf Update Slovenian translation 2024-09-18 19:13:24 +02:00
James Armstrong 338441f116 fix author sort not updating when bulk editing 2024-09-17 12:26:35 -07:00
James Armstrong 31380f2af9 fix typo 2024-09-17 03:19:03 -07:00
Ozzie Isaacs 13dc0b9c8e Updated requirements 2024-09-16 17:28:43 +02:00
Ozzie Isaacs 78d45f0208 Remove tzlocal warning
Updated requirements
2024-09-16 17:25:37 +02:00
Ozzie Isaacs d0e166140f Fix import pycountry on python 3.12 without warning
Fix shadowed "babel" name
2024-09-16 17:22:27 +02:00
Ozzie Isaacs 2617fa8af4 Improved Dependency check 2024-09-16 15:53:29 +02:00
Ozzie Isaacs 89a1dcfbf6 Fix for #3145 (improved contrast for epub reader headline) 2024-09-14 18:55:35 +02:00
Ozzie Isaacs 9d619ae5c7 Merge branch 'master' into Develop 2024-09-14 14:11:32 +02:00
Ozzie Isaacs 10d0542e05 Removed obsolete code 2024-09-14 11:15:04 +02:00
Ozzie Isaacs 111c93c391 Merge remote-tracking branch 'it/patch-1' 2024-09-14 10:53:30 +02:00
Ozzie Isaacs 1d9471b252 Possible fix for #3150 (Invalid cross-device link) without coping metadata 2024-09-14 10:46:02 +02:00
Ozzie Isaacs 88cb58c285 Fix for load metadata from amazon 2024-09-14 10:23:11 +02:00
James Armstrong 7e5d8978ae Merge branch 'master' into bulk-delete 2024-09-11 11:08:01 -07:00
Ozzie Isaacs eea01b0ff5 Simplified version string 2024-09-07 20:07:45 +02:00
Ozzie Isaacs d755878bd2 Migrate to pyproject.toml build 2024-09-07 19:59:02 +02:00
mapi68 2d7cf1e6d3 Update italian 2024-08-24 05:33:36 +02:00
Ozzie Isaacs b8a770a470 Merge branch 'master' into Develop 2024-08-23 11:13:48 +02:00
Ozzie Isaacs 481237dc77 Fix for #3131 (Enable/Disable "Separate Book Files from Library" issue) 2024-08-23 10:51:37 +02:00
Ozzie Isaacs 5e9dfc0724 Test update 2024-08-23 09:44:11 +02:00
Ozzie Isaacs ce720f40fe Shelf sort algorithm is now saved (#3003) 2024-08-21 19:30:33 +02:00
Ozzie Isaacs c92115faf3 Update save shelf sort algorithm 2024-08-21 19:25:50 +02:00
Ozzie Isaacs a1f9f03fc1 Fix for #3130 (only last e-mail address is saved when specifying multiple e-reader mail addresses in user settings) 2024-08-21 18:49:20 +02:00
Ozzie Isaacs 6a504673e5 Merge branch 'Develop':
Better handling of incompatible iso-639 module on python3.12
Music icon only visible once if more than one audio format available
Upload (multiple) book formats with progress and merge the corresponding metadata into the book (also via drag'n drop #2252)
2024-08-21 18:44:48 +02:00
Ozzie Isaacs 839118e4f1 Bugfixes from testrun 2024-08-21 18:21:19 +02:00
Ozzie Isaacs fd730f6609 Bugfix generate thumbnails on gdrive 2024-08-18 20:14:29 +02:00
Ozzie Isaacs 1be0ff9620 Bugfix load metadata
Bugfix xchange author and title
Better handling of incompatible iso-639 module on python3.12
2024-08-18 18:52:13 +02:00
Ozzie Isaacs 6717683ac3 Improvements merge metadata upload format
Music icon only visible once if more than one audio format available
2024-08-18 16:45:42 +02:00
Ozzie Isaacs 0f9b5d08e9 Fix cover handling on upload formats 2024-08-18 10:59:39 +02:00
Ozzie Isaacs 1eb77c9cd4 Upload (multiple) book formats with progress and merge the corresponding metadata into the book 2024-08-17 15:34:50 +02:00
Ozzie Isaacs ca92dbb168 Update german translation 2024-08-17 13:50:33 +02:00
Ozzie Isaacs 8bc1f03164 Bugfix generate thumbnails on gdrive 2024-08-17 09:30:52 +02:00
Ozzie Isaacs d8d9d405e9 First step format upload with progress 2024-08-16 21:14:41 +02:00
Ozzie Isaacs 8f2f6f5c91 Merge branch 'master' into Develop 2024-08-16 11:21:34 +02:00
Ozzie Isaacs 4c01af340f Update Testresults 2024-08-16 09:31:59 +02:00
Ozzie Isaacs 88783257b4 Update readme 2024-08-15 19:08:08 +02:00
Ozzie Isaacs c2474aa91d Fix for #2990 (Kobo browser not downloading kepub files)
Final fix for cover size during kobo sync (fix for #2523)
2024-08-15 19:02:11 +02:00
Ozzie Isaacs d2860fc374 Final fix for cover size during kobo sync (fix for #2523)
Update Teststatus
2024-08-15 18:35:48 +02:00
Ozzie Isaacs 87a1971d74 Merge remote-tracking branch 'spelling/master' 2024-08-14 19:29:40 +02:00
Ozzie Isaacs a9359bc1db Removed accidentally added translation file 2024-08-14 18:23:30 +02:00
Ozzie Isaacs c7f0293568 Implementation of sorting according to shelf added 2024-08-14 18:21:21 +02:00
Daniel M. Drucker, Ph.D. 22affedafc fix spelling of seperate to separate 2024-08-13 14:10:03 -04:00
Ozzie Isaacs 4d79c593f7 Fix for #2990 (Kobo browser not downloading kepub files) 2024-08-13 19:40:32 +02:00
Ozzie Isaacs 87b82424d5 Improved whitespace removal
Testresults
2024-08-13 07:07:54 +02:00
Ozzie Isaacs 5f81084e66 Updated testresults 2024-08-12 18:32:47 +02:00
Ozzie Isaacs 095074f977 Fix metadata search results 2024-08-12 18:32:35 +02:00
Ozzie Isaacs acd3c8d103 Merge branch 'master' into Develop 2024-08-11 19:56:20 +02:00
Ozzie Isaacs 3733fd442f Improved mp4, m4a, m4b import (coverformat png and multiple coverfiles)
Added support for metadata extraction in opus, ogv files
2024-08-11 19:45:12 +02:00
Ozzie Isaacs 4eb9845ff7 Improved asf, aac metadata import 2024-08-11 17:27:41 +02:00
Ozzie Isaacs ec6efafbed Added m4a, m4b files to parsable audio metadata 2024-08-11 12:20:16 +02:00
Ozzie Isaacs 10668571e8 Fixes for audio metadata extraction of oggvorbis and flac files (handle more than one cover and png covers as well) 2024-08-11 11:50:03 +02:00
Ozzie Isaacs 878fd85c63 Fixes for audio metadata extraction of oggvorbis and flac files (handle more than one cover and png covers as well) 2024-08-11 11:49:43 +02:00
Ozzie Isaacs 3827d46d66 Merge remote-tracking branch 'it/patch-1' 2024-08-11 08:41:46 +02:00
Ozzie Isaacs 7feff3b55c Merge branch 'Develop'
Extract metadata of audiofiles during upload
Updated pdf.js
2024-08-11 08:35:37 +02:00
Ozzie Isaacs 4523a12708 Fixes after testrun 2024-08-11 08:21:01 +02:00
Ozzie Isaacs 7a8d054b28 Make datetime utcnow compatible to older python versions 2024-08-10 21:05:29 +02:00
Ozzie Isaacs a56d1c80ae Remove deprecated utcnow method
Bugfix mp3 cover metadata extraction
2024-08-10 18:55:24 +02:00
Ozzie Isaacs 074aed6997 Fix deprecation warning datetime
Started metadata parsing for audio files
2024-08-07 17:23:41 +02:00
Ozzie Isaacs 2f8db1f7f0 Update pdf.js to 4.5.136 2024-08-07 15:16:38 +02:00
Ozzie Isaacs b3d878bae8 Fix deprecation warning datetime 2024-08-07 13:53:27 +02:00
mapi68 9b12852544 Update messages.po 2024-08-06 06:37:05 +02:00
Ozzie Isaacs a7d58386eb Get version info from importlib.metadata 2024-08-05 20:27:34 +02:00
James Armstrong fe78222142 Add shift-click to select multiple books at once 2024-08-05 10:43:51 -07:00
Ozzie Isaacs 68531a44d8 Changed formating of float values. IF value ends not with "0" tailing "0" are printed
Version update
2024-08-05 18:58:19 +02:00
Ozzie Isaacs d3233b4b9c Prepare release 2024-08-05 18:42:13 +02:00
Oskar Manhart 0e9ad0f831 Merge remote-tracking branch 'upstream/master' 2024-08-05 12:03:28 +02:00
Oskar Manhart c53a516887 feat: revert all the resizing stuff 2024-08-05 12:02:43 +02:00
Ozzie Isaacs bdac7a3257 Changed formating of float values. IF value ends not with "0" tailing "0" are printed 2024-08-04 16:39:05 +02:00
Ozzie Isaacs 0d359e9e57 Add cookie prefix environment variable to config 2024-08-04 15:52:19 +02:00
Ozzie Isaacs c4fad3cd20 Fix archiving bookm for kobo sync
Fix back function without search parameter
Updated Testresults
2024-08-04 07:12:44 +02:00
Ozzie Isaacs 9b520b1941 Fix archiving bookm for kobo sync
Fix back function without search parameter
Updated Testresults
2024-08-04 07:12:22 +02:00
James Armstong de3f883992 Add change_archived_books() description 2024-08-03 11:52:28 -07:00
James Armstong 2afce66486 Fix change_archived so state=none is a toggle. Fixes /togglearchived endpoint 2024-08-03 11:50:28 -07:00
James Armstong 2ae80d3007 Fix book_read_status marking as read even though read_status is passed as false 2024-08-03 11:40:11 -07:00
James Armstong e31763d388 Fix kobo sync status marking as archived even though state = false 2024-08-03 11:27:58 -07:00
Oskar Manhart 9955e348a0 Revert "feat: resize images directly at cover endpoint"
This reverts commit ba383643f5.
2024-08-03 16:28:13 +02:00
Ozzie Isaacs bcf4a479ac Merge branch 'master' into Develop 2024-08-03 16:03:52 +02:00
Oskar Manhart 26fb9921b2 fix: typo 2024-08-03 15:49:55 +02:00
Oskar Manhart ba383643f5 feat: resize images directly at cover endpoint 2024-08-03 15:49:15 +02:00
Oskar Manhart 6bd01c2d2b Merge branch 'master' of github.com:janeczku/calibre-web 2024-08-03 14:59:32 +02:00
Oskar Manhart 26c8e15436 feat: resize and convert images to webp using Flask-Image-Resizer 2024-08-03 14:59:16 +02:00
Oskar Manhart fd993b5063 feat: use lazy loading for images 2024-08-03 14:25:15 +02:00
Ozzie Isaacs abc923ade4 Fix for back function on simple search 2024-08-03 12:57:55 +02:00
Ozzie Isaacs 3aa72c8afb Added error message to task list 2024-08-03 11:25:07 +02:00
James Armstong bee6a3593d remove spacing 2024-08-02 23:31:04 -07:00
James Armstong a335dd75a1 Make edit metadata pass all data in a single REST call. Modularize editbooks 2024-08-02 23:29:34 -07:00
James Armstong 96fb2c1439 Auto disable author/title sort input in metadata edit form 2024-08-02 21:39:01 -07:00
James Armstong 9def910924 switch title_sort to sort in api edit request 2024-08-02 21:16:34 -07:00
James Armstong ab3d4e4f21 Fix emptying metadata form after submit 2024-08-02 20:10:43 -07:00
James Armstong ecda717bed Add bulk metadata edit button 2024-08-02 16:05:11 -07:00
James Armstrong 4ed0633edf Add bulk read/unread buttons; Fix buttons not working when moved into table 2024-08-02 14:57:20 -07:00
James Armstrong 34fec0ed97 Move buttons into table 2024-08-02 10:19:25 -07:00
Ozzie Isaacs 249017a4fa Added error message to task list 2024-08-02 18:08:18 +02:00
Ozzie Isaacs 84f432830c Fix for #3109 (search for custom int and float values within ranges) 2024-08-02 18:07:22 +02:00
James Armstrong ca9fc74541 typo fix 2024-08-02 00:40:47 -07:00
James Armstong d9a2a7a1e8 Add bulk archive/unarchive buttons 2024-08-01 21:34:55 -07:00
James Armstong d0d9985370 Add bulk delete button 2024-08-01 19:19:14 -07:00
Ozzie Isaacs e6ccd42f89 Fixed read statu search with linked column in advanced search 2024-07-31 19:25:01 +02:00
Ozzie Isaacs 12d46d33d9 Search custom integer, custom_float in range
Search custom bool like read status (unfinished)
2024-07-30 19:20:13 +02:00
Ozzie Isaacs 8d9f301aea Merge branch 'master' into Develop
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2024-07-30 18:52:22 +02:00
Ozzie Isaacs 0ff3f0f426 Fix for (#3111) exception on python 3.8/3.9 during storing of remember me cookie 2024-07-30 18:51:52 +02:00
Ozzie Isaacs b2f5853b12 Removed unused imports
Rearranged imports to output all missing libs
Update Testresults
Updated requirements
2024-07-29 20:08:17 +02:00
Ozzie Isaacs f8adcbb691 Make light theme default theme on epub reader
Fix csp Header for reader and google drive (#3108)
Testrun
2024-07-25 21:42:32 +02:00
Ozzie Isaacs a0728b07d0 Fix wrong quotes on calibre binary call for windows 2024-07-21 13:46:36 +02:00
Ozzie Isaacs e3b895755d Update test results 2024-07-21 13:46:36 +02:00
Ozzie Isaacs e33e4ac4ac Bugfix upload book format with language to book without language 2024-07-21 09:44:32 +02:00
Ozzie Isaacs 2f515b725b Schedules reconnect is now working even if reconnect route is not available ( #3107) 2024-07-20 18:14:54 +02:00
Ozzie Isaacs a1d7dc5c28 Merge branch 'master' into Develop 2024-07-20 14:01:36 +02:00
Ozzie Isaacs 1c5d1e24f7 Update test results 2024-07-20 13:58:53 +02:00
Ozzie Isaacs 8df45d150d Added hints for libmagic on Windows (fix #3104) 2024-07-20 13:51:45 +02:00
Ozzie Isaacs a59d20bd0f Output error message in case import of libmagic fails (#3104) 2024-07-20 11:31:31 +02:00
Ozzie Isaacs e3be7595e2 Attention flask-httpAuth is additionally needed to run calibre-web
Updated pdf.js viewer
Refactored login routine (#2623)
2024-07-18 20:42:25 +02:00
Ozzie Isaacs e33a7a3f08 Testupdate 2024-07-18 20:39:46 +02:00
Ozzie Isaacs 8c02ffb1cb Test update 2024-07-18 20:39:35 +02:00
Ozzie Isaacs 7367e96d2b Fix hiding console window on windows 2024-07-18 20:32:47 +02:00
Ozzie Isaacs 2d80187faa Clean expired sessions (also old sessions without expiry date) 2024-07-17 20:05:51 +02:00
Ozzie Isaacs 1d3a768dfe Update login routine (remember me working) 2024-07-17 19:42:12 +02:00
Ozzie Isaacs c09e1ed203 Update after testrun 2024-07-15 19:38:54 +02:00
Ozzie Isaacs 99e50bafc4 Fixes refactored user login from tests 2024-07-14 21:20:46 +02:00
Ozzie Isaacs 155cf04536 Added migration of user_session table, removed several old migration routes 2024-07-14 17:10:13 +02:00
Ozzie Isaacs ebe7cd7ba4 proxy login is now no longer saving cookies,
Cookies are saved in database for better Invalidation
Cookies expiry date is saved in database for further deletion (missing)
Database conversion is missing
2024-07-14 16:24:07 +02:00
Ozzie Isaacs 2d470e0ce1 Refactored opds login (proxy header login missing) 2024-07-13 12:17:48 +02:00
Ozzie Isaacs 87c0b15f5f Update pdf.js to version 4.4.168 2024-07-13 10:49:47 +02:00
Ozzie Isaacs 02d2c2bbf8 Better error message on failed convert because of running calibre instance (#3100) 2024-07-11 20:30:35 +02:00
Ozzie Isaacs 6996e813b5 Fixed error during debug logging on upload 2024-07-11 20:29:49 +02:00
Ozzie Isaacs 840f4d2010 Update to make upload of m4a and m4b files work (#3102) 2024-07-11 20:10:15 +02:00
Ozzie Isaacs 3cda0a0f32 Testupdate 2024-07-11 19:39:33 +02:00
Ozzie Isaacs a6931b4936 Merge branch 'master' into Develop:
Logging correct ip address behind reverse proxy,
add br to not sanitized tag for comments
code cleaning
2024-07-10 20:10:05 +02:00
Ozzie Isaacs 9536b2c322 Fix for renaming tags etc. upon upload (#3058) 2024-07-10 20:09:03 +02:00
Ozzie Isaacs 099dbf95e7 Fix error 500 on sending test email 2024-07-10 19:59:21 +02:00
Ozzie Isaacs 23d5463213 Fixes from testrun 2024-07-10 19:58:30 +02:00
Ozzie Isaacs a2ceeb678f Fix for similar names (upper case, umlauts, emphasis mark) in tags and so on 2024-07-09 19:37:24 +02:00
Ozzie Isaacs 092329c9e6 Merge remote-tracking branch 'origin/master' 2024-07-08 20:14:24 +02:00
Ozzie Isaacs de989783e8 Log Forwarded for address in accesslog instead of client address for tornado 2024-07-08 15:04:53 +02:00
Ozzie Isaacs 1d7dcce98d Log Forwarded for address in accesslog instead of client address for gevent 2024-07-08 14:42:48 +02:00
Ozzie Isaacs 551828f8cf Added "br" to not sanitized comments tag 2024-07-08 14:09:53 +02:00
Ozzie Isaacs d26964ab08 Added log message for missing python magic 2024-07-08 12:46:23 +02:00
Ozzie Isaacs 0fe12fe598 Added log message for missing python magic 2024-07-08 12:45:35 +02:00
Ozzie Isaacs b0e8024f08 Updated readme for setup calibre binaries
Added h tags to recognized tags for sanitizing comments
Avoid depreciation warning in lubimyczytac
Removed some unsed imports
2024-07-08 12:34:52 +02:00
Ozzie Isaacs bc1c097271 Updated readme for setup calibre binaries
Added h tags to recognized tags for sanitizing comments
Avoid depreciation warning in lubimyczytac
Removed some unsed imports
2024-07-08 12:34:10 +02:00
Ozzie Isaacs f98cb3d8fb Merge branch 'master' into Develop 2024-07-08 11:50:32 +02:00
Ozzie Isaacs 488005962f Update testresults 2024-07-08 11:49:53 +02:00
Ozzie Isaacs 6d6f8c1b1f Update redirect for login/logout in case of anonymous browsing enabled 2024-07-07 15:53:26 +02:00
Ozzie Isaacs c4a38087d9 Merge remote-tracking branch 'csv_kindle/master' 2024-07-07 14:55:53 +02:00
Ozzie Isaacs f468b9e6e3 Merge branch 'master' into Develop -> Epub progress indicator 2024-07-07 10:40:46 +02:00
Ozzie Isaacs 53a16c5a6f Merge remote-tracking branch 'epub_progress/progress_indicator' 2024-07-07 10:39:06 +02:00
Ozzie Isaacs 108caa0898 Merge branch 'Develop' 2024-07-07 10:32:02 +02:00
Ozzie Isaacs a0b776cbd1 Merge remote-tracking branch 'epub_reader_font/feat_font' into Develop 2024-07-07 10:31:49 +02:00
Ozzie Isaacs 4719797aff Merge branch 'Develop' epub reader themes 2024-07-07 10:22:56 +02:00
Ozzie Isaacs 60cc0c9395 Merge remote-tracking branch 'epub_theme/Develop' into Develop 2024-07-07 10:22:32 +02:00
Ozzie Isaacs ded4cb7a19 Merge remote-tracking branch 'it/patch-1' 2024-07-07 10:18:31 +02:00
Ozzie Isaacs f5f16a3dd2 Merge branch 'master' into Develop 2024-07-06 14:34:45 +02:00
Ozzie Isaacs c5f2323c8a Version bump 2024-07-06 14:21:49 +02:00
Ozzie Isaacs e9bccc83c5 Version bump 2024-07-06 13:29:17 +02:00
Ozzie Isaacs 53ba352ed9 Improved some things in caliblur theme (hide back button) 2024-07-05 20:43:50 +02:00
Ozzie Isaacs bbc3219d31 Update lubimycztac metadata provider (fixes #3090) 2024-07-05 19:16:22 +02:00
mapi68 ea89fe07d8 Update messages.po 2024-07-05 04:35:07 +02:00
Ozzie Isaacs ab2620a265 Merge branch 'Develop' (Fix for #3058, refactored rename author and improved duplicate check for tags/publishers, series, ...) 2024-07-03 15:46:19 +02:00
Ozzie Isaacs a20a4a201e Test update 2024-07-03 15:38:47 +02:00
Ozzie Isaacs ba0718f3d0 Enabled covers with gdrive 2024-07-02 20:53:08 +02:00
Ozzie Isaacs 60dd3df861 Fix after testrun for G-Drive 2024-07-02 20:02:11 +02:00
Ozzie Isaacs 762074596d Fix after testrun on Windows 2024-06-30 17:08:41 +02:00
Ozzie Isaacs 789912d0d4 Merge branch 'Develop'
Update translations
2024-06-30 12:20:42 +02:00
Ozzie Isaacs daf8dd6f90 Fix author rename on google drive
Bugfixes after testrun
2024-06-30 12:18:52 +02:00
Ozzie Isaacs e02610e2f0 Bugfixes change author 2024-06-29 20:45:56 +02:00
Ozzie Isaacs 897188ff18 Merge remote-tracking branch 'origin/Develop' into Develop 2024-06-29 09:52:05 +02:00
Ozzie Isaacs 09c0794f1a Refactored rename_authors
Bugfixes from tests
2024-06-29 09:50:00 +02:00
Ozzie Isaacs b28a3635f8 Merge branch 'master' into Develop
# Conflicts:
#	cps/editbooks.py
#	requirements.txt
#	setup.cfg
#	test/Calibre-Web TestSummary_Linux.html
2024-06-24 14:42:00 +02:00
Ozzie Isaacs 1a1f1498a5 Bugfixes from Tests 2024-06-24 14:39:28 +02:00
Ozzie Isaacs 92ffce1dc8 Rename Archived in Book Detail page 2024-06-23 13:47:54 +02:00
Ozzie Isaacs 6441f5f96b Prevent non unique authors on upload 2024-06-23 13:07:28 +02:00
Ozzie Isaacs d5a57e3b07 Merge remote-tracking branch 'mimetype/python_magic_poc' into Develop 2024-06-23 12:16:40 +02:00
Ozzie Isaacs fef20c831a Changed limiter message 2024-06-22 16:23:04 +02:00
Ozzie Isaacs e52e8c6fdf Merge branch 'master' into Develop 2024-06-22 16:07:01 +02:00
Ozzie Isaacs 89ac9ff498 Updated requirements 2024-06-22 13:35:53 +02:00
Ozzie Isaacs 059bdf1649 Bugfix from code cosmetics 2024-06-22 13:31:18 +02:00
Ozzie Isaacs 266ef11940 Bugfix for gevent as server 2024-06-22 13:30:32 +02:00
Ozzie Isaacs 64e4a1155c Code cosmetics
Bugfix missing bleach tags (#3080)
Bugfix pdfreader
2024-06-20 19:12:46 +02:00
Ozzie Isaacs f7d5480811 Make bleach mandatory 2024-06-15 08:18:38 +02:00
aptalca 7aa122e6cd Attempt pywin32 install only on Windows
Currently, doing a `pip install -r optional-requirements.txt` on a non-Windows platform results in an error due to pywin32 being a Windows only package.
2024-06-11 18:06:06 +02:00
Ozzie Isaacs cfdc07e6b2 Merge remote-tracking branch 'loglevel/handle_regions_in_locales' 2024-06-09 12:57:42 +02:00
Ozzie Isaacs 1ca1ac973a Merge remote-tracking branch 'loglevel/handle_regions_in_locales' into Develop 2024-06-09 12:57:30 +02:00
Ozzie Isaacs ba4235a39e Merge branch 'master' into Develop 2024-06-09 12:33:25 +02:00
Ozzie Isaacs 7295ebaee1 Merge remote-tracking branch 'loglevel/log_exceptions_for_own_modules_when_debug_logging_is_enabled' 2024-06-09 12:32:43 +02:00
Ozzie Isaacs d74ea7dcf2 Merge Text corrections 2024-06-09 12:30:44 +02:00
Ozzie Isaacs ee23ee97d9 Update pdf Reader to 4.3.136 2024-06-09 12:00:10 +02:00
Ozzie Isaacs c73ec52842 Update pdf Reader to 4.3.136 2024-06-09 11:59:38 +02:00
Ozzie Isaacs 0e3d3876f3 Merge branch 'master' into Develop 2024-06-09 11:10:20 +02:00
Ozzie Isaacs 76d086f536 Finish hinde_console_windows 2024-06-09 11:10:01 +02:00
Ozzie Isaacs 6022f48040 Hide console on Windows after start 2024-06-09 11:09:29 +02:00
Ozzie Isaacs e0be168779 Fix 2024-06-09 10:34:20 +02:00
Ozzie Isaacs d3118c0aa9 Merge remote-tracking branch 'it/master' 2024-06-03 20:41:52 +02:00
mapi68 04370944b9 Fix many italian strings 2024-06-01 05:55:41 +02:00
yunimoo af52748aca Update requirements to add python-magic 2024-05-31 18:08:04 -04:00
yunimoo 7eece7603b Add mime_type checks on file uploads 2024-05-31 18:08:04 -04:00
yunimoo d6aa29b095 Fix typos and text consistency 2024-05-31 14:30:58 -04:00
Purbayan Chowdhury 5d86bb0ba0 Merge branch 'janeczku:Develop' into Develop 2024-05-23 01:45:36 +05:30
Ozzie Isaacs ab11919c0b Merge remote-tracking branch 'Synctoken/fix/kobo-sync-token' 2024-05-12 17:56:49 +02:00
Ozzie Isaacs 014a247847 Merge remote-tracking branch 'Synctoken/fix/kobo-sync-token' into Develop 2024-05-12 17:56:19 +02:00
Ozzie Isaacs 58c269881f Merge remote-tracking branch 'barnesnnoble/db-barnesnoble' 2024-05-12 09:03:40 +02:00
Ozzie Isaacs e91dd5729a Finish OPDS-access 2024-05-12 09:01:00 +02:00
Ozzie Isaacs 6760d6971c Fix for #2983 (visible OPDS categories reflect settings in web UI)
Merge branch 'feature/OPDS-access'
2024-05-12 08:57:55 +02:00
Kreeblah ad05534ed2 Add support for barnesnoble identifier 2024-05-11 23:56:37 -07:00
Ozzie Isaacs ee451fb236 Testrun 2024-05-12 08:56:20 +02:00
Ozzie Isaacs ab13fcf60c Merge remote-tracking branch 'origin/feature/OPDS-access' into feature/OPDS-access 2024-05-11 18:37:10 +02:00
Ozzie Isaacs c7fa2ac71a Merge branch 'master' into Develop 2024-05-11 18:27:51 +02:00
Ozzie Isaacs 6f60ec7b99 Change order of imports for goodreads to make import error message clear agan 2024-05-11 18:27:35 +02:00
Ozzie Isaacs 93f1ccbea9 Merge branch 'master' into Develop 2024-05-11 18:21:29 +02:00
Ozzie Isaacs 894fd9d30a get rid of apscheduler timezone warning 2024-05-11 18:21:03 +02:00
Ozzie Isaacs 2b1efdb50e Bugfixes opds feed 2024-05-11 14:26:43 +02:00
Ozzie Isaacs fc9a9cb9ac Fix for #3016 (Parsing lubimyczytac: Tags instead of categories are taken, translator is appended to description) 2024-05-11 09:03:22 +02:00
Ozzie Isaacs e99be72ff7 Merge branch 'Develop' into feature/OPDS-access
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2024-05-11 07:12:46 +02:00
Ozzie Isaacs f1ceff2b52 Merge branch 'master' into Develop (goodreads) 2024-05-11 07:11:22 +02:00
Ozzie Isaacs 7e85894b3a Bugfix for goodreads (html formated info for authors now visible) 2024-05-11 07:10:41 +02:00
Ozzie Isaacs 5c49c8cdd7 Fix for Flask-SimpleLDAP 2.0.0 2024-05-10 20:23:41 +02:00
Ozzie Isaacs c8c3b3cba3 Fix for goodreads not working anymore (due to blocked requests calls by goodreads.com) 2024-05-10 15:24:24 +02:00
Ozzie Isaacs 4911843146 Bugfix for flask-ldap 2.0.0 2024-05-10 11:30:47 +02:00
Ozzie Isaacs 2c37546598 Code cosmetics 2024-05-10 10:41:04 +02:00
Ozzie Isaacs 25a875b628 Fix for goodreads blocking "requests" 2024-05-10 09:42:44 +02:00
Ozzie Isaacs 506f0a33cf Merge branch 'master' into Develop (apply fix for #3050) 2024-05-10 09:06:11 +02:00
Ozzie Isaacs 921caf6716 Fix for #3050 (metadata extraction for cb7 files not working) 2024-05-10 09:05:31 +02:00
Ozzie Isaacs 60ed1904f5 testrun 2024-05-10 09:04:50 +02:00
Ozzie Isaacs cb62d36e44 Bugfix for visibility opds feed 2024-05-07 08:27:55 +02:00
Ozzie Isaacs 737d758362 Return 404 if current element is not visible 2024-05-07 07:30:57 +02:00
Ozzie Isaacs 8e27912ff5 Merge branch 'master' into Develop
Bugfix for too new lxml
2024-05-07 07:16:54 +02:00
Ozzie Isaacs 3a603cec22 Handle error on uploading a book with lxml too new and no bleach, nh3 installation 2024-05-05 11:18:31 +02:00
eggy b1d7badef4 fix: change b64-encoded token to unicode string 2024-05-02 03:06:53 -04:00
Ozzie Isaacs e591211b57 Fix for #3037 (catch OError on detect epub layout) 2024-04-27 07:22:58 +02:00
Ozzie Isaacs a305c35de4 Merge remote-tracking branch 'it/patch-1' 2024-04-27 07:22:23 +02:00
growfrow 51d306b11d chore: fix some typos in comments
Signed-off-by: growfrow <growfrow@outlook.com>
2024-04-20 20:49:56 +02:00
mapi68 abb418fe86 Update messages.po 2024-04-17 07:45:40 +02:00
Ozzie Isaacs 0925f34557 Merge remote-tracking branch 'caliblur/issue/caliBlur-3002' 2024-04-07 18:51:00 +02:00
Ozzie Isaacs 15952a764c Fix for #3021 (speed of calculating number of pages) 2024-04-07 18:49:18 +02:00
Daniel Edwards 28c810b514 feat: base is_debug on effective level vs root log level 2024-03-19 11:05:04 +11:00
Daniel Edwards cfe61a26d0 Refactor get_language_name function to handle missing language names 2024-03-19 10:54:48 +11:00
Ozzie Isaacs fcc95bd895 Improvements for password verify (addresses: https://github.com/iiab/calibre-web/pull/138) 2024-03-10 16:39:51 +01:00
Ghighi Eftimie 964e7de920 fixes 3002 2024-03-06 15:42:10 +02:00
Ozzie Isaacs 14b578dd3a Implement correct password verification of Umlaunts, kyrillic, greek.. charactersets, CJK-Characters, and special characters (#2964) 2024-02-29 12:08:58 +01:00
Ozzie Isaacs becb84a73d Finish Fix_Umlauts 2024-02-29 12:06:48 +01:00
Ozzie Isaacs c901ccbb01 Improved js password strength check
Improved check of CJK-Characters
2024-02-29 11:48:07 +01:00
Ozzie Isaacs f987fb0aba Fixed recognition of capital umlauts 2024-02-27 19:48:33 +01:00
Ozzie Isaacs c30460d76b Merge branch 'Develop'
- Back function for delete and edit books
- configure ratelimiter backend possible
- embed metadata during send to ereader
- bugfixes split library
- updated requirements
2024-02-27 06:03:54 +01:00
Ozzie Isaacs 97380b4b3f Updated teststatus 2024-02-27 06:01:11 +01:00
Ozzie Isaacs 4fbd064b85 Merge remote-tracking branch 'origin/back' into Develop 2024-02-26 18:42:48 +01:00
Ozzie Isaacs abbd9a5888 Revert logging line termination feature 2024-02-26 18:07:24 +01:00
Ozzie Isaacs e860b4e097 Back function implemented for delete book and edit book 2024-02-26 15:07:59 +01:00
Ozzie Isaacs 23a8a4657d Merge branch 'master' into Develop
(Fix for #3005 and #2993)
2024-02-25 20:07:40 +01:00
Ozzie Isaacs b38a1b2298 Admin can now force full sync for users (fix for #2993 2024-02-25 20:03:38 +01:00
Ozzie Isaacs 0ebfba8d05 Added blobs to csp for reader page (fix for #3005) 2024-02-25 19:32:04 +01:00
Ozzie Isaacs 990ad8d72d Update Requirements 2024-02-25 16:11:57 +01:00
Ozzie Isaacs c3fc125501 Added command line option or overwriting limiter backend
Added logger functions to remove newlines in messages
CalibreTask has now a default name
2024-02-25 16:02:01 +01:00
Ozzie Isaacs 3c4ed0de1a Added ratelimiterbackends 2024-02-25 09:00:49 +01:00
Ozzie Isaacs 117c92233d Added sending email to embed metadata text
Updated test result
2024-02-25 06:54:35 +01:00
Ozzie Isaacs 2ba14acf4f Merge branch 'master' into Develop
# Conflicts:
#	cps/helper.py
2024-02-24 14:51:53 +01:00
Ozzie Isaacs 80a2d07009 fix embed metadata, split library and download file / convert files 2024-02-24 14:48:56 +01:00
Ozzie Isaacs ff9e1ed7c8 Implemented embed metadata on send to ereader 2024-02-24 13:59:49 +01:00
Ozzie Isaacs 8e5bee5352 Merge remote-tracking branch 'embed_metadata/embed_metadata_on_send' into Develop 2024-02-18 14:48:04 +01:00
Ozzie Isaacs d659430116 Bugfix view None ratings caused error 500 in certain cases 2024-02-18 14:17:43 +01:00
Ozzie Isaacs 859dac462b Update Teststatus 2024-02-18 13:29:03 +01:00
Ozzie Isaacs 2bea4dbd06 Show List with no file formats available
Bugfix save order of format list view
2024-02-17 11:13:49 +01:00
Ozzie Isaacs 0180b4b6b5 Better error handling on next parameter 2024-02-12 20:58:26 +01:00
Ozzie Isaacs 2bfb02c448 Updated requirements
Updated testresults
2024-02-11 07:31:57 +01:00
Ozzie Isaacs 4864254e37 Merge branch 'Develop'
# Conflicts:
#	cps/config_sql.py
2024-02-10 10:55:02 +01:00
Ozzie Isaacs 09dce28a0e Merge remote-tracking branch 'embed_metadata/embed_metadata_on_convert' into Develop 2024-02-10 10:54:29 +01:00
Ozzie Isaacs e55d09d8bb Merge branch 'Develop' into embed_metadata_on_convert 2024-02-10 10:52:16 +01:00
Ozzie Isaacs 92c162b2fd Merge remote-tracking branch 'embed_metadata/embed_metadata_on_download'
# Conflicts:
#	cps/helper.py
#	cps/tasks/convert.py
#	cps/templates/config_edit.html
2024-02-10 10:49:09 +01:00
Ozzie Isaacs 57fb5001e2 Merge branch 'Develop' 2024-02-10 10:43:58 +01:00
Ozzie Isaacs 64e5314148 Merge remote-tracking branch 'identifier/master' into Develop 2024-02-10 10:31:57 +01:00
Ozzie Isaacs 873602a5c9 Deleted unused code 2024-02-10 10:30:41 +01:00
Ozzie Isaacs 09e966e18a Merge remote-tracking branch 'optimize_details_html/only-render-cc-with-data' into Develop 2024-02-10 10:28:36 +01:00
Ozzie Isaacs f7718cae0c Fix typo in send password message 2024-02-10 10:07:10 +01:00
Ozzie Isaacs 90e728516c Merge remote-tracking branch 'it/master' 2024-02-10 10:04:01 +01:00
Ozzie Isaacs 7c04b68c88 Merge remote-tracking branch 'icon/master' 2024-02-10 10:01:16 +01:00
Ozzie Isaacs 8549689a0f Merge branch 'Develop'
Separate library from metadata file
embed metadata on download
2024-02-10 09:57:28 +01:00
Ozzie Isaacs d8f5c17518 Mark separate library as highly experimental 2024-02-10 09:56:32 +01:00
mapi68 05367d2df5 Update messages.po 2024-01-25 07:46:31 +01:00
Webysther Sperandio eb6fbfc90c HiDPI icons 2024-01-23 04:24:21 +01:00
Ozzie Isaacs c2267b6902 Bugfix add to shelf 2024-01-22 13:17:27 +01:00
Ozzie Isaacs 0e5520a261 Bugfix add to shelf 2024-01-22 13:16:58 +01:00
Ozzie Isaacs 6f5e9f167e Fix typo 2024-01-21 14:42:39 +01:00
Ozzie Isaacs ce83fb6816 Update bug_report.md 2024-01-21 14:37:12 +01:00
Ozzie Isaacs fbfb7adef6 Update issue templates 2024-01-21 14:33:50 +01:00
Ozzie Isaacs cc52ad5d27 Added Notice 2024-01-21 14:27:38 +01:00
Ozzie Isaacs 706b9c4013 Improved errorhandling for adding invalid book_id to shelf 2024-01-21 08:12:49 +01:00
Ozzie Isaacs 6972c1b841 Improved errorhandling for adding invalid book_id to shelf 2024-01-21 08:12:28 +01:00
Ozzie Isaacs b9c329535d Fix for #2980 (invalid seriesindex causes no longer a crash) 2024-01-18 19:41:46 +01:00
Ozzie Isaacs 8fdf7a94ab Updated testresult 2024-01-18 19:37:33 +01:00
Ozzie Isaacs 31a344b410 Bugfix from testrun 2024-01-17 20:29:47 +01:00
Ozzie Isaacs 3814fbf08f Bugfixes from testrun 2024-01-14 14:28:08 +01:00
Ozzie Isaacs ffc13a5565 Merge branch 'master' into Develop 2024-01-13 14:48:09 +01:00
Ozzie Isaacs 74c61d9685 Merge branch 'metadata' into Develop 2024-01-13 14:47:48 +01:00
Ozzie Isaacs b8031cd53f Add possibility to replace kepub metadata on download 2024-01-13 14:31:42 +01:00
Ozzie Isaacs 898e76fc37 re enable gevent 2024-01-13 12:32:54 +01:00
Ozzie Isaacs af71a1a2ed Fix for #2973 (No unix sockets on windows available) 2024-01-13 12:26:53 +01:00
Ozzie Isaacs e0327db08f Enable embedd metadata of kepub file during conversion 2024-01-08 20:51:38 +01:00
Purbayan Chowdhury 05032aed3c Updating theme logic and persisting theme change 2024-01-08 16:56:13 +05:30
Ozzie Isaacs bf2ac97c47 Update setup.cfg 2024-01-07 13:28:31 +01:00
Ozzie Isaacs 902fa254b0 Merge branch 'master' into Develop 2024-01-07 08:03:40 +01:00
Ozzie Isaacs f0cc93abd3 Sanitze username for logging 2024-01-06 16:08:14 +01:00
Ozzie Isaacs 977f07364b Fix for #2961 (empty comment with newline causes error 500 on upload)
Language of error message for kobo sync improved
2024-01-06 16:08:14 +01:00
Ozzie Isaacs 00acd745f4 Fix tornado deprecation warning 2024-01-06 16:08:14 +01:00
Ozzie Isaacs d272f43424 Show error message if user is missing download permission for kobo sync 2024-01-06 16:08:14 +01:00
Whatever Cloud 7a8d8375d0 Refactor detail.html to optimize custom_column rendering
The div with class "real_custom_columns" is now only rendered if the custom_column data exists, removing blank space in details view.
2024-01-01 20:38:05 +01:00
Johannes H 3aa75ef4a7 fix: link to Linux Mind installation in README.md
the link the wiki page had an typo (missing '-') https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-in-Linux-Mint-19-or-20
2023-12-30 14:19:39 +01:00
Ozzie Isaacs 25fb8d934f Merge remote-tracking branch 'nl/master' 2023-12-21 13:47:02 +01:00
GONCALVES Nelson (T0025615) f08c8faaff Generate identifiers in opf metadata file 2023-12-21 11:02:45 +01:00
Michiel Cornelissen bc0ebdb78d Updated Dutch translation. 2023-12-20 21:52:09 +01:00
Ozzie Isaacs 2a4b3cb7af Update Teststatus
Add hint for calibre binary
2023-12-17 08:04:16 +01:00
Ozzie Isaacs 4401cf66d1 Remove remains of ie8 support 2023-12-16 17:23:20 +01:00
Ozzie Isaacs d353c9b6d3 Fix #2945 (handle illegal multibyte sequence) 2023-12-16 10:48:49 +01:00
Ozzie Isaacs 0aba96c032 Update Teststatus 2023-12-16 10:31:36 +01:00
Ozzie Isaacs c60b7e9192 Update teststatus 2023-12-14 07:09:50 +01:00
Ozzie Isaacs 23033255b8 Bugfix updater 2023-12-12 18:15:48 +01:00
Ozzie Isaacs 31c8909dea Merge branch 'Develop' into metadata -> Now all tests should run "pass" now
# Conflicts:
#	cps/constants.py
#	cps/helper.py
#	cps/tasks/convert.py
#	test/Calibre-Web TestSummary_Linux.html
2023-12-11 14:23:12 +01:00
Ozzie Isaacs 9ef89dbcc3 Bugfix convert file from gdrive without cover
Update Teststatus
2023-12-10 15:23:26 +01:00
Ozzie Isaacs 1086296d1d Make embed metadata configurable 2023-12-09 11:29:11 +01:00
Ozzie Isaacs d341faf204 Make Version setuptools compatible and still have the "Beta" in the User interface 2023-12-09 09:36:28 +01:00
Ozzie Isaacs 2334e8f9c9 Refactored calibre executable detection for better error messages 2023-12-07 16:47:10 +01:00
Ozzie Isaacs 90ad570578 Show only folders for selecting converter binaries 2023-12-07 16:22:34 +01:00
Ozzie Isaacs fd90d6e375 Delete temp dir not shown in tasks overview
Handle case no cover during convert ebook
2023-12-06 19:33:22 +01:00
Ozzie Isaacs 7fbbb85f47 Temp folder is deleted on regular base 2023-12-04 19:26:43 +01:00
Ozzie Isaacs 52c7557878 Merge remote-tracking branch 'caliblur/issue/caliBlur2931'
Updated optional requirements
2023-12-04 19:05:39 +01:00
Ozzie Isaacs 794cd354ca Added ToDo's 2023-12-04 18:57:50 +01:00
Ghighi Eftimie 389e3f09f5 small fix for mobile resolution - add to shelf button 2023-12-04 16:40:56 +02:00
Ghighi Eftimie 285979b68d fix for 2931 2023-12-04 16:00:00 +02:00
Ozzie Isaacs 3a012c900e Merge branch 'master' into Develop 2023-11-28 18:00:26 +01:00
Ozzie Isaacs ec45de3212 Merge remote-tracking branch 'douban/master' 2023-11-28 17:59:48 +01:00
Ozzie Isaacs f644a2a136 Merge remote-tracking branch 'kobo_cover/patch-1' 2023-11-28 17:56:36 +01:00
Russell 01108aac42 Remove trailing slash
Signed-off-by: Russell <russell@troxel.io>
2023-11-26 22:01:26 -08:00
Russell Troxel 400c745692 Update KOBO_IMAGEHOST_URL to new CDN endpoint
Kobo has introduced a vanity URL to their Akamai CDN config - "https://https://cdn.kobo.com". As a result, requests sent to the old raw Akamai endpoint now through 404s. This is a simple PR that updates to the new URL.

This is visible in the config of a newly bought Kobo Sage (mine):
```
image_host=https://cdn.kobo.com/book-images/
image_url_quality_template=https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg
image_url_template=https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg
```

Example:

OLD:
https://kbimages1-a.akamaihd.net/1abfb307-457b-4597-b2ea-74beb2eb1478/149/223/false/image.jpg
(Throws 404)

New:
https://cdn.kobo.com/book-images/1abfb307-457b-4597-b2ea-74beb2eb1478/149/223/false/image.jpg
(Renders Properly)

fixes #2371 
ref #2731
2023-11-26 14:31:24 -08:00
ye 9841a4d068 fix the the problem that metadata_provider/douban.py can't get tags info 2023-11-26 09:06:25 +08:00
Ozzie Isaacs 7fd1d10fca Implement gdrive metadata on download 2023-11-11 15:26:05 +01:00
Ozzie Isaacs 4f6bbfa8b8 Little refactoring do_download 2023-11-11 15:00:12 +01:00
Ozzie Isaacs cf6810db87 Refactored get_temp_dir
bugfixes export_metadata
2023-11-11 14:48:59 +01:00
Ozzie Isaacs 5afff2231e Fix for #2743 (read status is no longer implicitly added to advanced search query) 2023-11-11 10:43:50 +01:00
Ozzie Isaacs d611582b78 Fix for #2743 (read status is no longer implicitly added to advanced search query) 2023-11-11 10:43:23 +01:00
Ozzie Isaacs 3bbd8ee27e Use belach or nh3 for cleaning html (fix for #2874) 2023-11-09 17:59:20 +01:00
Ozzie Isaacs f78e0ff938 Use belach or nh3 for cleaning html (fix for #2874) 2023-11-09 17:59:06 +01:00
Ozzie Isaacs bd71391bfb Possible fix for #2849 (404 error during sync request) 2023-11-09 17:43:02 +01:00
Ozzie Isaacs 20b2936cc1 Merge branch 'master' into Develop 2023-11-09 17:41:39 +01:00
Ozzie Isaacs 19825a635a Possible fix for 2849 (404 error during sync request) 2023-11-09 17:41:20 +01:00
Ozzie Isaacs 0d611d35de Updated Testresult
Updated requirements
2023-11-09 06:39:25 +01:00
Ozzie Isaacs effd026fe2 Merge branch 'master' into Develop 2023-11-08 20:36:49 +01:00
Ozzie Isaacs d68e57c4fc Implement split library and books
Bugfix arrows in comic reader
Fix kobo download link
Updated requirement
2023-11-08 20:11:03 +01:00
Ozzie Isaacs 184ce23351 Catch error with tornado on restart 2023-11-06 18:05:36 +01:00
Ozzie Isaacs 2fbc3da451 Added Slovak translation 2023-11-06 16:51:08 +01:00
Ozzie Isaacs fad6550ff1 Show "all" opds feed entries only if there is at least one entry 2023-11-06 16:35:39 +01:00
Ozzie Isaacs b7aaa0f24d Add metadata change code 2023-11-02 17:05:02 +01:00
Ozzie Isaacs 5040bb762c Merge remote-tracking branch 'vi/master' 2023-11-02 16:35:54 +01:00
Ozzie Isaacs 55deca1ec8 Catch additional errors on logfile
Fix error on exception in session management
2023-11-02 16:34:04 +01:00
Ozzie Isaacs 40a16f4717 Reenabled gevent as server 2023-11-01 07:47:29 +01:00
Ozzie Isaacs d26e60724a Socket listening and socket activation enabled on tornado
Refactoring of server code
Updates requirements
Improved requirements parsing for versions containing letters
2023-10-30 14:39:22 +01:00
Ozzie Isaacs d877fa1c68 Merge remote-tracking branch 'socket-activation/socket-activated' 2023-10-29 08:20:03 +01:00
Ozzie Isaacs d55bafdfa9 Added hint for beta release format 2023-10-28 19:10:44 +02:00
Ozzie Isaacs a2a431802a Added prc to supported convert from formats (#2801)
Improved error message for not able to listen to ipv6 address with gevent
2023-10-28 13:49:16 +02:00
Ozzie Isaacs b2e4907165 Merge branch 'master' into Develop 2023-10-28 09:06:51 +02:00
Ozzie Isaacs 6c2e40f544 Added favicon in opds feed
Merge remote-tracking branch 'favicon/master'
2023-10-28 09:05:24 +02:00
Ozzie Isaacs 5e3d0ec2ad Fix location of config files for executable files (#2917) 2023-10-28 08:48:13 +02:00
Ozzie Isaacs c550d6c90d Fix comic reader arrow visiblilty for one paged comic files
Changed logging during kobo sync
2023-10-28 08:48:13 +02:00
bacpd 3b1d0b4013 Update messages.po 2023-10-25 15:08:10 +07:00
Ozzie Isaacs 3d07efbb4f Update Italian and German translation 2023-10-21 15:47:35 +02:00
mapi68 c0ae5bb381 Update messages.po 2023-10-21 15:34:09 +02:00
Ozzie Isaacs 6e755a26f9 Version update
Updated security file
2023-10-21 14:37:21 +02:00
Ozzie Isaacs c45188beb2 Version update 2023-10-21 12:40:32 +02:00
Ozzie Isaacs 0736c53d7b Update teststatus 2023-10-18 18:14:00 +02:00
Ozzie Isaacs f0f8011d24 Modified build script to avoid warnings 2023-10-16 18:17:54 +02:00
Ozzie Isaacs 65f3ecb924 Updated test results 2023-10-15 15:43:50 +02:00
Ozzie Isaacs 87b3999ec8 Merge remote-tracking branch 'ko/master' 2023-10-14 15:55:27 +02:00
Ozzie Isaacs e32312b54a Merge branch 'master' into Develop 2023-10-14 15:53:10 +02:00
Ozzie Isaacs d7ea569e5d Merge remote-tracking branch 'upload_text/patch-1' into Develop 2023-10-14 15:52:37 +02:00
Ozzie Isaacs 96958e7266 Merge remote-tracking branch 'upload_text/patch-1' 2023-10-14 15:48:06 +02:00
Ozzie Isaacs 2c339ed10c Merge branch 'Develop':
- Fix for new tornado version
- bookmark for comic viewer
- Bugfix for showing series containing only one book in list view containing having this book no series_index value set
- updated requirements
2023-10-14 15:30:44 +02:00
Ozzie Isaacs dc2c30f508 Added additional debug output for download links during kobo sync 2023-10-14 09:51:54 +02:00
Ozzie Isaacs 0c43d80163 Added additional debug output for download links
Removed unused code
2023-10-14 09:51:19 +02:00
Ozzie Isaacs df71a86f94 New testrun 2023-10-14 09:34:34 +02:00
Ozzie Isaacs 7ed56b4397 Better fix for new tornado version 2023-10-08 15:46:51 +02:00
Ozzie Isaacs 5ceb2b6d83 Merge branch 'master' into Develop 2023-10-04 20:11:34 +02:00
Ozzie Isaacs 8abea1ddd0 Fix for #2904, werkzeug breaks master branch 2023-10-04 19:46:14 +02:00
Ozzie Isaacs 11816d3405 Fix for tornado 6.3 no response in restart moment
Update test results (many tests fail due to above problem)
2023-10-03 08:12:33 +02:00
Ozzie Isaacs 198bff928f Merge remote-tracking branch 'cn/master' 2023-10-02 20:03:27 +02:00
lawsssscat cac200ba61 Fix messages.po in zh_Hans_CN 2023-09-25 10:48:41 +08:00
David K 8cc36ab081 Clarify Enable Uploads text.
This change updates the help text next to the `Enable Uploads` setting.

- Current: `Please ensure users having also upload rights`
- Update: `Please ensure that users also have upload permissions`

---

The current help text is a little odd in terms of sentence structure for an English speaker.
The updated version clarifies the meaning.
2023-09-11 10:35:01 +01:00
databoy2k b3d1558df8 Add Favicon to OPDS Feed
<icon> is a standard of the OPDS spec, and this commit adds the favicon to the feed. Certain OPDS readers, e.g. FBReader on Windows, respect this tag and will show the same icon as the main page.
2023-09-08 13:09:40 -06:00
byword77 a045b6f467 Update messages.po
It is final commit. 
I made a mistake in the previous update. 
So, Please disregard the previous file.
2023-09-05 17:01:38 +09:00
Ozzie Isaacs 7a961c9011 Fix for click on scrollbar in long strip view
Fix for tornado version
2023-09-01 07:35:48 +02:00
Ozzie Isaacs 444ac181f8 more bookmark feature
tornado fix for tornado <6.2
2023-08-31 17:03:56 +02:00
Ozzie Isaacs 4bbcec21e4 Update test results 2023-08-31 11:42:55 +02:00
Ozzie Isaacs 5509d4598b Bugfix for showing series coatining only one book in list view containing having this book no series_index value set 2023-08-30 20:23:40 +02:00
Ozzie Isaacs fab35e69ec Update Teststatus
Little code refactoring
2023-08-29 20:05:57 +02:00
Ozzie Isaacs 4f0f5b1495 Merge remote-tracking branch 'origin/Develop' into Develop
# Conflicts:
#	optional-requirements.txt
2023-08-28 19:38:51 +02:00
Ozzie Isaacs cfa309f0d1 Merge branch 'master' into Develop
# Conflicts:
#	cps/services/SyncToken.py
#	cps/static/js/kthoom.js
#	cps/templates/readcbr.html
2023-08-28 18:08:19 +02:00
Ozzie Isaacs 885d914f18 Update tornado to 6.2
Remove unneeded imports from jsonschema for synctoken
Update optional requirements
Remove invalid direction arrows in comic reader
2023-08-28 18:06:32 +02:00
Ozzie Isaacs b580f418f7 Merge branch 'master' into Develop 2023-08-27 15:12:26 +02:00
Ozzie Isaacs 6a14e2cf68 Next try showing last book of series in grid view 2023-08-27 12:00:34 +02:00
Ozzie Isaacs 8535bb5821 Fix "got an unexpected keyword argument 'rarExecutable'" during format upload 2023-08-27 11:20:53 +02:00
Ozzie Isaacs b2a26a421c Removed deprecation warning for sqlalchemy2.0 2023-08-26 20:26:01 +02:00
Ozzie Isaacs 7aea7fc0bb Merge remote-tracking branch 'comic-bookmark/comics-save-position'
# Conflicts:
#	cps/static/js/kthoom.js
#	cps/templates/readcbr.html
#	cps/web.py
2023-08-26 20:13:42 +02:00
Ozzie Isaacs 52172044e6 Fix deprecation warnings 2023-08-26 20:12:52 +02:00
Ozzie Isaacs 9b99427c84 Merged comic save position 2023-08-26 20:12:34 +02:00
Ozzie Isaacs 0499e578cd Added /opds/stats route 2023-08-24 13:49:22 +02:00
Ozzie Isaacs b3a85ffcbb Added CB7 to supported upload formats 2023-08-24 10:51:16 +02:00
PhracturedBlue 074e611705 Add support for systemd socket-activation 2023-08-23 20:52:05 -07:00
Ozzie Isaacs a1899bf582 Fix for #2603 (Kobo UserKey in request missing due to no kobo account) 2023-08-23 21:13:17 +02:00
Ozzie Isaacs f7ff3e7cba Added py7zr to setup.cfg 2023-08-23 20:50:39 +02:00
Ozzie Isaacs 3a08b91ffa Added cb7 to supported comic files for upload and metadata extraction 2023-08-16 18:44:15 +02:00
Ozzie Isaacs d253804a50 fix for #2865 (Kobo sync fails during cover request) 2023-08-16 18:04:45 +02:00
Ozzie Isaacs ba0e5399d6 Fixes Uploading pdf file fails with whitespace title (#2824)
Merge remote-tracking branch 'pdf_title/fix/title-parsing-of-pdf-metadata'
2023-08-12 14:09:14 +02:00
lucas 7e46f9c0b1 Add font/spread customize for epub reader 2023-08-09 17:10:09 +08:00
Ozzie Isaacs 7818c4a7b0 Adapt cover size to kobo sync requested sze 2023-08-08 19:21:47 +02:00
Ozzie Isaacs 3f6a12898b Merge branch 'Develop' 2023-07-31 19:02:41 +02:00
Ozzie Isaacs caf69669cb Removed encode utf-8 in dodownload 2023-07-31 19:01:15 +02:00
Ozzie Isaacs de59181be7 Merge remote-tracking branch 'home/link_fixes' into Develop 2023-07-31 18:49:53 +02:00
Ozzie Isaacs 966c9236b9 Merge remote-tracking branch 'home/link_fixes' 2023-07-30 20:00:18 +02:00
Ozzie Isaacs 34c6010ad0 Catch additional error during kobo detect layout 2023-07-30 19:57:42 +02:00
Ozzie Isaacs 60e904967b Fix deprecated text parameter 2023-07-30 19:47:29 +02:00
Ozzie Isaacs 3efcbcc679 Fix deprecated text parameter 2023-07-30 07:44:41 +02:00
Ozzie Isaacs 7bb4bc934c Merge remote-tracking branch 'caliblur/issues/caliblur2838' 2023-07-29 15:36:44 +02:00
Ozzie Isaacs dcb8a0f77b Merge remote-tracking branch 'lubimyczytac/fix-lubimyczytac-metadata' 2023-07-29 15:35:53 +02:00
Ozzie Isaacs 2f12b2e315 Handle invalid or missing container.xml during kobo sync (Fix for #2840) 2023-07-29 15:32:35 +02:00
Ozzie Isaacs 279f0569e4 Fix visibility for sending to reader without download permissions (fix for #2847) 2023-07-29 15:15:38 +02:00
Ozzie Isaacs 6723369d65 Merge branch 'master' into Develop
# Conflicts:
#	cps/admin.py
2023-07-29 12:12:03 +02:00
Ozzie Isaacs 4b93ac034f Do not show password in smtp settings 2023-07-29 12:11:04 +02:00
Ozzie Isaacs fda62dde1d Fix for not changing password in email settings
Improved ssl certificate check on sending emails
2023-07-29 12:03:45 +02:00
Ozzie Isaacs df74fdb4d1 Update edit identifiers 2023-07-29 11:10:54 +02:00
Ozzie Isaacs cce538d5a7 Update Testresult 2023-07-28 13:44:56 +02:00
Ozzie Isaacs e8b0051b31 Merge development into master 2023-07-26 21:43:18 +02:00
Ozzie Isaacs fe55958ecc Update Teststatus 2023-07-26 21:42:19 +02:00
Ozzie Isaacs 7b321d63c1 update testresults 2023-07-26 21:42:19 +02:00
Ozzie Isaacs 986eaf9f02 Merge remote-tracking branch 'br/horus68-pt-translation' 2023-07-26 20:49:13 +02:00
Ozzie Isaacs caf8ed77d7 Merge remote-tracking branch 'br/horus68-ptbr-fix' 2023-07-26 20:30:19 +02:00
Ghighi Eftimie ee5cfa1f36 fix for #2838 2023-07-26 15:11:04 +03:00
Horus68 5eef476135 Update messages.po 2023-07-25 13:48:57 +01:00
Horus68 b5e4a88357 Update messages.po 2023-07-25 13:11:08 +01:00
Horus68 256f4bb428 Update messages.po
fix typo
2023-07-25 11:43:03 +01:00
Horus68 a4d45512ee pt-BR Update messages.po
fixing pt-BR major errors
2023-07-25 11:40:26 +01:00
Horus68 074687c330 Create pt-PT translation
Translation to european portuguese - pt-PT portuguese (Portugal)
Note: messages.mo not created as file was translated directly in browser
2023-07-25 11:29:20 +01:00
archont 2f7b175dda Fix tags xpath 2023-07-16 18:50:54 +02:00
Ozzie Isaacs a256bd5260 Password is not returned in smtp settings 2023-07-09 10:23:56 +02:00
Ozzie Isaacs fdd1410b06 Improved pathchooser 2023-07-09 10:01:48 +02:00
Ozzie Isaacs 3f5583017f Improved pathchooser 2023-07-09 10:00:54 +02:00
Ozzie Isaacs 63b7d70f33 Update optional requirements
Update to be compatible with comicapi 3.2
2023-06-25 14:39:45 +02:00
Ozzie Isaacs 500758050c Fixed typo in German translation 2023-06-25 11:32:23 +02:00
Ozzie Isaacs 4b4c0daab0 Update testresults 2023-06-18 18:30:01 +02:00
Ozzie Isaacs 709a4e51ba Testrun 2023-06-18 11:17:06 +02:00
Ozzie Isaacs eff0750d77 Fix for renaming uppercase/lowercase author, tag, series, publisher entries (fix for #2787)
Update epub.js
2023-06-18 10:17:55 +02:00
Ozzie Isaacs e63a04093c Bugfix rename author in book list 2023-06-18 10:14:53 +02:00
Ozzie Isaacs 07d97d18d0 Merge remote-tracking branch 'epubreader/update_epub.min.js' into Develop 2023-06-17 15:50:51 +02:00
Ozzie Isaacs d8f30983d5 Merge remote-tracking branch 'readme/patch-1' 2023-06-17 15:48:18 +02:00
Ozzie Isaacs 062efc4e78 Bugfix change uppercase/lowercase on edit authors
Remove autocomplete on several elements to make typeahead work without problems
2023-06-17 10:47:23 +02:00
boosh 4e6c9c2703 Update README.md
Link to the raw download URL of the metadata.db file
2023-06-13 06:23:53 +00:00
Ozzie Isaacs 4dc5885723 Bugfix for upper/lowercase rename if not before part of book 2023-06-10 10:37:22 +02:00
quarz12 5e15cac598 deleted empty lines 2023-06-09 18:32:32 +02:00
quarz12 d0551274dc switched to using epub.min.js, deleted now unnecessary dependencies 2023-06-09 18:32:30 +02:00
quarz12 99c05650a1 -added pako and epub-cfi-resolver as dependencies
-cfi resolving is now done by that module, therefore removed validateChildNodes and adjusted cfiToXmlNode
2023-06-09 18:32:28 +02:00
quarz12 617ee3d781 now shows correct numbers, still needs bugfixes 2023-06-09 18:32:26 +02:00
quarz12 47aa3254c7 better css positioning 2023-06-09 18:32:24 +02:00
quarz12 0717b9493c added progress display, better rounding
TODO: getPrev gives wrong values,
design, testing
2023-06-09 18:32:22 +02:00
quarz12 824ebc4d76 added event on change position 2023-06-09 18:32:22 +02:00
quarz12 cbaa61bbb7 EpubParser now gets automatically loaded
TODO:detect page change, display number
2023-06-09 18:32:15 +02:00
quarz12 dbac685985 implemented current file process, needs testing but looks good 2023-06-09 18:32:11 +02:00
quarz12 b25c024b66 added getCurrentFileProgress,
TODO: resolve CFI to xml node
2023-06-09 18:32:10 +02:00
quarz12 bc65180c32 restructured and fixed a bug in getprevfilesize 2023-06-09 18:32:06 +02:00
quarz12 10c66b6e63 wrote a class to handle epub parsing, implemented bytesize of all previous files, total bytesize. still needs implementation of bytesize of current file until CFI point. 2023-06-09 18:32:05 +02:00
quarz12 c29b1696f7 added function to get bytelength of book text 2023-06-09 18:31:55 +02:00
quarz12 39638d3c9c updated epub.min.js to version 0.3.93 2023-06-09 16:11:53 +02:00
Ozzie Isaacs 3ef34c8f15 Bugfixes rename upper-lowercase after testrun 2023-06-08 10:28:42 +02:00
Ozzie Isaacs 932abbf090 Merge remote-tracking branch 'error_scroll/error_page' 2023-06-05 14:54:10 +02:00
Ozzie Isaacs 860443079d Fix for (#2802) search request fails with error after browser session closed and currently returns all results 2023-06-05 14:41:03 +02:00
Ozzie Isaacs bd4b7ffaba Fix for upper lower change of non ascii values in series, tags, ... 2023-05-30 20:02:52 +02:00
Ozzie Isaacs 33e35eeb52 Added option -o to define logfile via command line (fixes #2792) 2023-05-29 11:29:14 +02:00
Ozzie Isaacs ed09814460 Prevent log_message on startup
logfile Parameter
2023-05-28 20:42:18 +02:00
Ozzie Isaacs e52eb74121 Merge branch 'Develop' 2023-05-28 19:58:33 +02:00
Daniel dc7fbce4f7 changed style property of container 2023-05-28 13:21:26 +02:00
Ozzie Isaacs dad0fd5a1c Update test results 2023-05-28 10:24:40 +02:00
Ozzie Isaacs 8a87c152b4 Bugfix opds search 2023-05-27 16:25:06 +02:00
Ozzie Isaacs 16baa306c5 Update test results 2023-05-27 15:32:48 +02:00
Daniel 2eb334fb3d fixed a typo in http_error.html which broke the github link
changed the behavior of the home button so it directs to the application root, not the server root .
2023-05-08 21:48:04 +02:00
Ozzie Isaacs cb7356a04d Fixes to word with new version of comicapi 2023-05-04 20:23:02 +03:00
Ozzie Isaacs 63a561bf9b Merge remote-tracking branch 'chinese/translation/Simplified_Chinese' 2023-05-04 19:54:06 +03:00
Ozzie Isaacs cc733454b2 Merge remote-tracking branch 'chinese/translation/Simplified_Chinese' 2023-04-30 19:38:59 +03:00
whilenot 940544577a don't mutate meta into a str, keep it a namedtuple 2023-04-30 13:37:08 +02:00
xlivevil 9e0fc320cb Update Simplified Chinese translation 2023-04-25 16:03:28 +08:00
xlivevil bf3ca20fb2 Update Simplified Chinese translation 2023-04-25 15:38:40 +08:00
Ozzie Isaacs c7e1736ade Merge branch 'master' into Develop 2023-04-23 11:05:19 +02:00
Ozzie Isaacs bc6a50550e Merge branch 'master' into Develop 2023-04-23 11:03:12 +02:00
Ozzie Isaacs fe4dc1bb8f Fix #2757 (Sqlalchemy >1.30 <1.4.24 wasn't supported anymore) 2023-04-22 09:25:54 +02:00
Ozzie Isaacs f2369609e8 Bugfix for non existent rating, language, and user downloaded books 2023-04-18 20:53:55 +02:00
Ozzie Isaacs de4d6ec7df Merge remote-tracking branch 'it/patch-1' 2023-04-18 20:06:46 +02:00
mapi68 7754f4aa5d Update messages.po 2023-04-18 09:05:19 +02:00
Ozzie Isaacs 524751ea51 Removed superfluous space 2023-04-17 18:52:52 +02:00
Ozzie Isaacs 8111d0dd51 Update translation status 2023-04-16 15:06:34 +02:00
Ozzie Isaacs fad5929253 Bugfix getPath for logfile viewer 2023-04-16 13:08:50 +02:00
Ozzie Isaacs 9f28144779 Fix for #2756 (Home button in caliblur is leading to "/" instead of calibre-web home) 2023-04-16 09:43:13 +02:00
Ozzie Isaacs 42fd6973a0 Merge remote-tracking branch 'readme/patch-1' 2023-04-15 19:15:21 +02:00
driz b2e20ff50c update readme due to my mistake on link
i fubarred the optional calibre layer somehow, my apologies

thanks for all you guys do, myself and my entire family love calibre-web :)
2023-04-15 13:02:55 -04:00
Ozzie Isaacs 6075b3dd1d Merge remote-tracking branch 'readme/patch-2' 2023-04-15 18:46:45 +02:00
driz 37871ea8cb Update README.md
this is built against the formatting changes of https://github.com/janeczku/calibre-web/pull/2742
it corrects the docker-mods section which is incorrect, fixes a dead link, and removes armhf from the listed of supported arches.
2023-04-15 11:38:04 -04:00
Ozzie Isaacs e2785c3985 Added djv file format to djvu reader 2023-04-15 15:25:46 +02:00
Ozzie Isaacs dba83a2900 Fixes for sqlalachemy2 2023-04-15 13:22:17 +02:00
Ozzie Isaacs 33c19b20f4 Merge remote-tracking branch 'readme/update-readme' 2023-04-15 11:20:52 +02:00
Ozzie Isaacs d2f39d3dce Update Teststatus 2023-04-15 11:00:38 +02:00
Ozzie Isaacs 1c8bc78b48 Improvements for sqlalchemy 2 2023-04-13 19:01:53 +02:00
Ozzie Isaacs 6c6841f8b0 Further fixes for sqlalchemy 2.0 2023-04-12 18:56:21 +02:00
Ozzie Isaacs 592216588c Revert "post" as search request. Search request is now get again (fix for #2741)
Revert "Auxiliary commit to revert individual files from 275675b48add79d2bbce06426cc1224c5e2c1bfb"

This reverts commit 6c920bc49f133d5c7451230448121f1f8b3cd9f2.
2023-04-12 18:29:15 +02:00
Wladimir Kirianov f4db0f04d2 add table of contents and contributor recognition section 2023-04-09 13:07:33 +02:00
Wladimir Kirianov b16e3a6e2c Update README.md for clarity, formatting, and improved readability 2023-04-09 12:36:19 +02:00
Ozzie Isaacs 13c0d30a8f Update login
Removed not needed parameters for task status
2023-04-03 20:00:40 +02:00
Ozzie Isaacs b9c942befc Fix for 'NoneType' object has no attribute 'author_sort' while trying to read a book (#2733) 2023-03-31 07:48:18 +02:00
Ozzie Isaacs a68a0dd037 Fixed typo 2023-03-30 11:25:34 +02:00
Thomas de Ruiter a952c36ab7 Remove unused fallback cover handling 2023-03-28 16:13:10 +02:00
Thomas de Ruiter 5f0c7737fe Fix proxy cover images to Kobo store 2023-03-28 15:56:02 +02:00
Ozzie Isaacs 38484624e9 Version update 2023-03-27 20:20:18 +02:00
Ozzie Isaacs 1451a67912 Version bump 2023-03-27 19:49:57 +02:00
Ozzie Isaacs a72f0a160b Bugfix for python3.11, table gdrive_ids2 already exists (fix for #2729) 2023-03-27 17:58:07 +02:00
Ozzie Isaacs 253386b0a5 Bugfix parse ldap server config 2023-03-26 18:27:43 +02:00
Ozzie Isaacs 6c8ffb3e7e Bugfix path selection for reverse proxy 2023-03-26 14:31:19 +02:00
Ozzie Isaacs 7d26e6fc85 Bugfix get updater status 2023-03-26 14:19:12 +02:00
Ozzie Isaacs 085a6b88a3 Fix for #2713 (strip scheme from ldap server name) 2023-03-26 14:08:09 +02:00
Ozzie Isaacs bde36e3cd4 Bugfix for logging ldap debug messages with non stream logfile 2023-03-26 13:17:02 +02:00
Ozzie Isaacs 9646b6e2dd Enable debug output for ldap login 2023-03-26 11:29:54 +02:00
Ozzie Isaacs d35e781d41 Bugfixes after testrun
Catch StaleDataError
2023-03-26 08:19:36 +02:00
Ozzie Isaacs 321db4d712 Refactored send email by make use of ajax calls instead of posting the page
Always use getPath instead of pathname
2023-03-25 12:34:16 +01:00
Ozzie Isaacs 2b9f920454 Metadata backup configurable 2023-03-25 10:42:36 +01:00
Ozzie Isaacs 45acd3febe Merge branch jvonau/patch-1
Updated requirements
Updated test results
2023-03-22 17:50:38 +01:00
Ozzie Isaacs cbd7ca2f3e Activate metadata backup 2023-03-21 20:03:45 +01:00
Ozzie Isaacs ba7fee3918 Update metadatabackup 2023-03-21 20:02:57 +01:00
Ozzie Isaacs 1210ccb43f Update teststatus 2023-03-21 18:51:56 +01:00
Jerry Vonau 04f1f6493b Update optional-requirements.txt
faust-cchardet has the latest code for cchardet https://pypi.org/project/faust-cchardet/#history
2023-03-21 09:52:38 -05:00
Ozzie Isaacs 46d2d217ee Bugfix metadata backup with custom columns 2023-03-20 20:11:37 +01:00
Ozzie Isaacs e3fffa8a8f Bugfix backup metadata for custom date, custom categories 2023-03-20 19:57:05 +01:00
Ozzie Isaacs dfb49bfca9 Merge branch 'master' into Develop (handle case of cover smaller than thumbnail) 2023-03-20 19:03:15 +01:00
Ozzie Isaacs 224777f5e3 Handle case that cover size is smaller than thumbnail size
Update teststatus
2023-03-20 19:00:35 +01:00
Ozzie Isaacs 7ade4615a4 Bugfix write metadata
Bugfix change of custom column now sets updates the book timestamp
2023-03-19 17:23:05 +01:00
Ozzie Isaacs cbd679eb24 Bugfix write metadata
Bugfix change of custom column now sets updates the book timestamp
2023-03-19 17:21:30 +01:00
Ozzie Isaacs b277ed3359 Update teststatus 2023-03-19 15:29:23 +01:00
Ozzie Isaacs fa95b07a95 Merge branch 'master' into Develop
# Conflicts:
#	requirements.txt
2023-03-05 16:06:49 +01:00
Ozzie Isaacs 87bc8c6d96 Updated requirements 2023-03-05 16:06:14 +01:00
Ozzie Isaacs db2bc6a2c2 Changed requirements 2023-03-05 16:03:54 +01:00
Ozzie Isaacs cf850c6ed5 Bugfix backup metadata 2023-03-05 16:03:07 +01:00
Ozzie Isaacs a6b54e398b Bugfix backup metadata 2023-03-05 16:02:48 +01:00
Ozzie Isaacs 28eeb9eec3 Merge branch 'master' into Develop 2023-03-04 11:13:57 +01:00
Ozzie Isaacs 7d76f2ae33 Updated requirements 2023-03-04 11:13:41 +01:00
Ozzie Isaacs 49e4f540c9 ** Be careful, after updating, there is no way back **
** Please install flask-limiter after updating **

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

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

Before this commit, in Chrome response header you can find 

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

After :

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

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

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

Calibre-Web does not start again and the container logfile does not allow me to have more information.
I proceeded to verify the translation file (I did not find anything strange) and I made some updates of the text.
2022-10-02 21:07:24 +02:00
Ozzie Isaacs 44f6655dd2 Catch one additional database error on edit book 2022-10-02 15:21:53 +02:00
Ozzie Isaacs bd52f08a30 Fix for #2547 (None isn't iterable, so in case scholary request fails, empty list has to be returned) 2022-10-02 15:05:07 +02:00
Ozzie Isaacs edc9703716 Merge remote-tracking branch 'vi/add-translation' 2022-10-02 11:45:00 +02:00
Ozzie Isaacs 56d697122c Merge remote-tracking branch 'it/patch-28'
# Conflicts:
#	cps/translations/it/LC_MESSAGES/messages.po
2022-10-02 11:39:20 +02:00
Ozzie Isaacs d39a43e838 Merge remote-tracking branch 'cn/Translation/Simplifield_Chinese' 2022-10-02 11:36:47 +02:00
ElQuimm 9df3a2558d update message.po
updated italian message.po translation
2022-09-29 11:07:31 +02:00
xlivevil 7339c804a3 fix typo 2022-09-29 13:18:21 +08:00
xlivevil 4d61c5535e Update Simplifield Chinese translation 2022-09-28 20:14:58 +08:00
xlivevil 09e1ec3d08 fix typo 2022-09-28 20:12:21 +08:00
Ozzie Isaacs 8421a017f4 Updated Test result 2022-09-26 16:54:15 +02:00
Ozzie Isaacs 27eb514ca4 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	cps/static/js/table.js
2022-09-25 19:57:09 +02:00
Ozzie Isaacs b4d9e400d9 Handle None as identifier value during upload 2022-09-25 19:39:38 +02:00
Ozzie Isaacs 67bc23ee0c Fix for #2537 (Impossible to set Denied Column Value from user table) 2022-09-25 19:39:38 +02:00
Ozzie Isaacs b898b37e29 Fix for #2545 (Max task duration double entry "1hour") 2022-09-25 19:39:38 +02:00
Ozzie Isaacs 10dcf39d50 Merge branch '1' 2022-09-25 19:37:55 +02:00
Ozzie Isaacs e676e1685b Handle None as identifier value during upload 2022-09-25 19:37:38 +02:00
Ozzie Isaacs 59a5ccd05c Bugfixes after testrun 2022-09-25 19:36:40 +02:00
Ozzie Isaacs 04908e22fe backup metadata 5th step 2022-09-23 20:45:30 +02:00
Ozzie Isaacs 0f67e57be4 Merge branch 'master' 2022-09-20 19:12:36 +02:00
Ozzie Isaacs 071d19b8b3 Fix for #2537 (Impossible to set Denied Column Value from user table) 2022-09-20 19:10:45 +02:00
halink0803 1ffa190938 add vietnamese translation 2022-09-20 21:38:47 +07:00
Ozzie Isaacs c10708ed07 Backup metadata 4th step 2022-09-19 22:39:40 +02:00
Ozzie Isaacs b4851e1d70 Fix for #2545 (Max task duration double entry "1hour") 2022-09-19 18:57:55 +02:00
Ozzie Isaacs 26be5ee237 Backup metadata 3rd step 2022-09-19 18:56:31 +02:00
Ozzieisaacs 241aa77d41 backup metadata second step 2022-09-14 17:03:48 +02:00
Ozzieisaacs ca0ee5d391 backup metadata first step 2022-09-10 18:26:52 +02:00
Ozzieisaacs 110d283a50 Enable series type custom column 2022-09-10 17:17:20 +02:00
Giulio De Pasquale f6a9030c33 Removed extra space 2022-09-08 17:26:34 +02:00
Giulio De Pasquale 452093db47 Google Covers: strip curl in thumbnail and request higher resolution image 2022-09-08 17:23:53 +02:00
Ozzieisaacs 9fa56a2323 Merge remote-tracking branch 'kobolanguage/language' 2022-09-06 18:09:07 +02:00
Ozzieisaacs 3a133901e4 Fix: ignore special files originating from Apple devices 2022-09-06 18:06:59 +02:00
Ozzieisaacs 7750ebde0f Update pdf Reader 2022-09-05 19:42:02 +02:00
Ozzieisaacs 2472e03a69 Bugfix ratelimiter kobo 2022-09-05 18:45:24 +02:00
Ozzieisaacs 6598c4d259 Add rate limit for opds 2022-09-04 19:47:04 +02:00
Ozzie Isaacs a9b20ca136 Fix for big database not showing tags 2022-08-29 19:08:04 +02:00
Ozzie Isaacs bf0375d51d Bugfix change emails 2022-08-28 15:59:25 +02:00
Ozzie Isaacs 89d226e36b Allow deletion of kindle email address and force e-mail address to be valid 2022-08-28 15:54:43 +02:00
Ozzie Isaacs ec8844c7d4 Make pyPDF2 again to the favorite pdf metadata extractor 2022-08-27 15:44:21 +02:00
Ozzie Isaacs e5c8a7ce50 Change landing page for issues, to reduce number of empty issues 2022-08-27 10:20:53 +02:00
Ozzie Isaacs dc3cafd23d Debug message improved (fix for #2516) 2022-08-27 10:13:38 +02:00
Ozzie Isaacs 9de474e665 Add galician language to available translations (#2510) 2022-08-27 10:03:01 +02:00
Martin Brodbeck cd143b7ef4 Use part1 instead of part3 language codes 2022-08-12 15:18:50 +02:00
Martin Brodbeck 8a5112502d Considers the language of the ebook instead of always specifying "English". 2022-08-12 11:51:26 +02:00
gjutras c974cd048e minor formatting 2022-08-02 19:51:31 -04:00
gjutras 24e9bf2728 Merge branch 'janeczku:master' into master 2022-08-01 16:38:46 -04:00
Ozzie Isaacs b5d5660d04 Update version 2022-07-31 16:14:03 +02:00
Ozzie Isaacs fc9c641e55 Updated brazilian translation (fix #2496) 2022-07-31 11:30:56 +02:00
Ozzie Isaacs 68e21e1098 Fix #2495, Fix #2494 (Convert for not existent file no longer fails) 2022-07-31 11:28:11 +02:00
Ozzie Isaacs 828be29a80 Update teststatus 2022-07-30 18:13:09 +02:00
viljasenville 46e5305f23 Comic reader: ignore special files originating from Apple devices 2022-07-27 11:17:20 +03:00
Ozzie Isaacs a3f7dc2a5a Bugfix search with wrong custom column configured 2022-07-23 07:23:13 +02:00
Thore Schillmann 9bcbe523d7 (draft) metadata embedding when sending to device 2022-07-22 08:58:28 +00:00
Ozzie Isaacs ae3e3559b8 Rate limit prepared for feedback on login route 2022-07-18 10:59:54 +02:00
Ozzie Isaacs a72f16fd3a Fix missing or_ import 2022-07-16 19:27:44 +02:00
Ozzie Isaacs c2545315e1 Fix Ratings with 0 stars are counted as None 2022-07-16 19:09:19 +02:00
Ozzie Isaacs 61a0c72f8e Merge remote-tracking branch 'typo/fix-typos' 2022-07-16 17:52:45 +02:00
Ozzie Isaacs 1e44cb3b6c Merge remote-tracking branch 'cn/Translation/Simplifield_Chinese'
# Conflicts:
#	cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
#	cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po
2022-07-16 17:49:27 +02:00
Ozzie Isaacs 462aa47ed6 Merge remote-tracking branch 'jp/japanese' 2022-07-16 17:47:54 +02:00
Thore Schillmann e176d63ca6 Merge branch 'embed_metadata_on_convert' into embed_metadata_on_download 2022-07-14 13:39:58 +00:00
Thore Schillmann 80b0e88650 fix for GDrive integration 2022-07-14 13:34:42 +00:00
Thore Schillmann 0b4731913e created do_calibre_export function 2022-07-14 09:25:37 +00:00
Thore Schillmann fc7ce8da2d cleanup 2022-07-13 15:39:01 +00:00
gjutras ff46af9c49 fix indentation and spaces 2022-07-10 17:23:20 -04:00
gjutras 0a7345186d Merge branch 'janeczku-master'
# Conflicts:
#	cps/helper.py
#	cps/tasks/convert.py
#	cps/templates/user_edit.html
2022-07-08 16:42:14 -04:00
Thore Schillmann c89bc12c9b Merge branch 'embed_metadata_on_convert' into embed_metadata_on_download 2022-07-07 14:09:15 +00:00
Thore Schillmann 4913673e8f added subprocess.wait() when getting metadata 2022-07-07 11:47:10 +00:00
Thore Schillmann fc004f4f0c moved get_calibre_binarypath() to helper.py 2022-07-07 11:41:51 +00:00
Ozzie Isaacs 7344ef353c Rate limited login 2022-07-02 19:46:58 +02:00
Ozzie Isaacs 3bde8a5d95 Encrypt passwords 2022-07-02 17:45:24 +02:00
Thore Schillmann c5c3874243 first implementation 2022-07-01 16:04:25 +00:00
Kian-Meng Ang c4104ddaf4 Fix typos 2022-07-01 21:26:06 +08:00
Thore Schillmann 0d34f41a48 cleanup of autodetect_converter_binary 2022-07-01 12:06:33 +00:00
xlivevil b47c1d2431 Update Simplifield Chinese translation 2022-06-30 21:24:22 +08:00
Thore Schillmann a77aef83c6 automatically set config_converterpath 2022-06-30 13:05:36 +00:00
Thore Schillmann e39c6130c3 cleanup and better error handling 2022-06-29 19:54:53 +00:00
subdiox 12071f3e64 Fix author & series ID translation 2022-06-26 10:26:45 +09:00
subdiox 92b6dbf26f Fix one Japanese translation 2022-06-26 01:29:08 +09:00
subdiox 98b554a3a0 Fix Japanese translation 2022-06-26 01:24:44 +09:00
Thore Schillmann 03359599ed input validation for calibre binary directory 2022-06-23 20:02:54 +00:00
Thore Schillmann 3c4330ba51 refactoring of calibre binary detection 2022-06-21 17:04:44 +00:00
Thore Schillmann 8c781ad4a4 code cleanup and implements cover change 2022-06-19 23:20:59 +00:00
Thore Schillmann 5e9ec706c5 first draft of embedding metadata on conversion 2022-06-19 22:59:54 +00:00
Ozzie Isaacs 07c67b09db Merge remote-tracking branch 'uk/master' 2022-06-18 18:45:12 +02:00
Ozzie Isaacs b1c70d5b4a Update Teststatus 2022-06-18 18:44:02 +02:00
Ozzieisaacs c5fc30a1be Bugfix error message missing custom read status column
Bugfix password validation
2022-06-17 14:49:42 +02:00
Ozzie Isaacs 29fd4ae4a2 Bugfixes create users
Update Teststatus
2022-06-17 10:14:33 +02:00
Ozzieisaacs 4ef8c35fb7 Bugfies password validation from testrun 2022-06-16 14:16:00 +02:00
Ozzieisaacs 04326af2da password validation working 2022-06-16 11:15:17 +02:00
Ozzieisaacs d6a31e5db8 config verify password working 2022-06-16 10:44:42 +02:00
Ozzie Isaacs 73d48e4ac1 Frontend for password strength 2022-06-16 08:33:39 +02:00
Ozzie Isaacs b206b7a5d8 Merge branch 'master' into Develop 2022-06-14 18:45:50 +02:00
Ozzie Isaacs 02e1be09df Catch StaleDataError
Update requirements jsonschema
2022-06-13 17:54:35 +02:00
Ozzie Isaacs f85b587d0a Prevent converting of kepub on every new user (#2446)
Added error logging message if convert fails
If convert only task, only convert book message is logged
2022-06-13 17:05:00 +02:00
Ozzie Isaacs 89d522e389 Fix for #2445 (book read status can't be set if custom column is linked and read status was set before) 2022-06-11 11:35:02 +02:00
Thore Schillmann 2816a75c3e changed datetime format of published tag 2022-06-09 20:35:44 +00:00
Ozzie Isaacs 78b45f716a Bugfix for #2433 (LazyString is not JSON serializable if one of kepubify/ebook-convert/unrar is not installed) 2022-06-08 20:25:35 +02:00
Ozzie Isaacs 91df265d40 Fix for #2437 (advanced search for read status crashes calibre-web) 2022-06-08 17:17:07 +02:00
Illia Maier 7e7f54cfa7 Update Ukrainian translation
Update Ukrainian translation
2022-06-06 16:28:03 +03:00
Illia Maier 80bc14c0cf Merge pull request #1 from Illia-M/Develop
Develop
2022-06-06 16:25:49 +03:00
Illia Maier 7685818b16 Update Ukrainian translation
Update Ukrainian translation
2022-06-06 14:37:07 +03:00
Ozzie Isaacs f44d42f834 Merge remote-tracking branch 'opds/bugfix_2419'
Bugfix send emails
2022-06-06 09:00:09 +02:00
GarcaMan bf12542df5 Updated Rotate Left/Right shortcut funtions to update inmediatly
Minor fixes
2022-06-05 19:56:34 +00:00
Ozzie Isaacs 25f2af3f03 Whitespaces are striped from email address (fixes #2432) 2022-06-05 10:06:43 +02:00
Ozzie Isaacs 909797dc49 Merge branch 'master' into Develop 2022-06-04 12:05:34 +02:00
Ozzie Isaacs 07d4e60655 Merge remote-tracking branch 'epub_theme/epub_themes' into Develop 2022-06-04 12:05:24 +02:00
Ozzie Isaacs d90cfce97f Merge remote-tracking branch 'ldap/master' 2022-06-04 12:03:16 +02:00
Ozzie Isaacs 543fe12862 Merge remote-tracking branch 'epub_theme/epub_themes' 2022-06-04 09:26:01 +02:00
Ozzie Isaacs 4f66d6b3b1 Update ukranian translation 2022-06-04 08:49:48 +02:00
Illia Maier c36138b144 Update Ukrainian translations 2022-06-03 08:59:18 +03:00
Thore Schillmann 7f6e88ce5e fixes bug 2419 2022-06-01 22:06:28 +02:00
Ozzie Isaacs aa442d8c51 Update flask-login requirement 2022-05-31 19:22:57 +02:00
Aisha Tammy a3cd217cea allow @ in auto username detection
Signed-off-by: Aisha Tammy <aisha@bsd.ac>
2022-05-25 12:14:09 -04:00
Thore Schillmann 0f3f918153 multiple authors and publication date in opds feed 2022-05-22 17:40:21 +00:00
Ozzieisaacs 790080f2a0 Fix Cache Buster 2022-05-22 12:49:00 +01:00
Ozzieisaacs 4ea80e9810 Code cosmetics 2022-05-21 21:52:59 +01:00
Ozzieisaacs 034d57134d Merge remote-tracking branch 'convert/master' 2022-05-21 22:27:53 +02:00
Ozzieisaacs f6101fd462 Merge remote-tracking branch 'links/patch-1' 2022-05-21 22:25:49 +02:00
Ozzieisaacs 1fa7de397a Merge remote-tracking branch 'cve/patch-1' 2022-05-21 22:22:02 +02:00
leexia 3b7cd38d5e Fix grammar mistake 2022-05-16 13:59:35 +08:00
ImanSharaf 78fb7a9756 Update SECURITY.md
I am the person who has reported the SQLi problem. I contacted the MITRE and they have given me this CVE number: CVE-2022-30765
2022-05-15 19:00:16 -07:00
Evan Peterson 7ae9f89bbf Merge branch 'Develop' into kobo-sync-detect-fixed-layout 2022-05-14 10:02:31 -04:00
Ozzie Isaacs 8a6a8dcbe8 Transfer gevent errors to log file
Transfer warnings from warnings module to logfile (#2394)
2022-05-09 19:47:43 +02:00
Ozzie Isaacs fbac3e38ac Eenabled send epubs to E-Reader devices 2022-05-08 12:55:54 +02:00
Ozzie Isaacs c1f1952b04 Updated jsonschema requirement
Testupdate
Fixed file mode of main.js
2022-05-06 17:49:12 +02:00
Ozzie Isaacs 5e4cf839bc Replace "ast" by "json.loads" to handle enums with default values/colors (fix for #2398) 2022-05-05 18:04:34 +02:00
Ozzie Isaacs 056ecf0d90 Fix for #2394 2022-05-04 19:23:08 +02:00
Chris Thurber 0c2f67bc7b Updates Links in README
Update links for optional pip features and GDrive integration.
2022-05-04 12:42:23 -04:00
Ozzie Isaacs 1bcb714fac Remove mature tags config option 2022-05-03 18:43:07 +02:00
Ozzie Isaacs 8cb3fe32a5 Try to prevent error from #2390 2022-05-02 17:42:26 +02:00
Ozzieisaacs ae5053e072 Added missing translation strings
Update German Translation
2022-05-01 13:24:11 +02:00
Ozzie Isaacs cde51e743a Removed duplicate Werkzeug entry in about page
Code cosmetics
2022-05-01 12:36:35 +02:00
Ozzie Isaacs 3233b357f8 Changed Readme 2022-05-01 11:13:36 +02:00
Ozzie Isaacs 49655e9f2d More bugfixes for time and datetime.time 2022-05-01 10:38:31 +02:00
Ozzie Isaacs 7b45324149 Bugfix time and datetime.time 2022-05-01 10:35:00 +02:00
Ozzie Isaacs 858d099509 Merge branch 'Develop' 2022-05-01 10:26:10 +02:00
Ozzie Isaacs 12f3a13d1d Fix typo
Fix missing maim.py file in update pypi package
Bugfix after last testrun
2022-05-01 10:25:38 +02:00
Ozzieisaacs 813d303ea7 Merge branch 'cover_thumbnail' into Develop 2022-04-30 17:05:36 +02:00
Ozzieisaacs c1ca18f7dc Merge remote-tracking branch 'origin/cover_thumbnail' into cover_thumbnail 2022-04-30 17:04:39 +02:00
Ozzie Isaacs e8e4d87d39 Bugfix session commit
Bugfix get_locale
Bugfix reconnect database schedule
Bugfix no permission error message logging
Bugfix updater
2022-04-30 10:17:41 +02:00
Ozzie Isaacs 5d5a94c9e5 Bugfix start as program 2022-04-29 18:26:09 +02:00
Ozzie Isaacs 258b4a6767 Bugfix OAuth Login 2022-04-28 21:43:26 +02:00
Ozzie Isaacs ef4b5e2881 Bugfix audio player
Bugfix Debugfile export
2022-04-28 21:40:48 +02:00
Ozzie Isaacs a968ddaef2 Bugfixes after testrun 2022-04-28 21:09:03 +02:00
Ozzie Isaacs aaa749933d Further migration to flask_babel
Bugfix sort order
Bugfix tasklist
2022-04-26 20:24:40 +02:00
Ozzie Isaacs 2e007a160e reenable startup logging
Bugfixes from refactoring and merge
2022-04-26 14:45:06 +02:00
Ozzie Isaacs e7464f2694 Refactored web.py to shrink size of file 2022-04-26 11:49:06 +02:00
Ozzie Isaacs 47414ada69 Merge branch 'master' into Develop 2022-04-26 11:11:00 +02:00
Ozzie Isaacs 9410b47144 Refactored startup for compatibility with pyinstaller 5.0 2022-04-26 11:10:44 +02:00
Ozzie Isaacs db03fb3edd Merge branch 'cover_thumbnail' into Develop 2022-04-26 10:59:04 +02:00
Ozzie Isaacs 2b03cae017 Bugfix scheduler end task 2022-04-26 10:55:11 +02:00
Ozzie Isaacs 21ebdc0130 Bugfixes from testrun 2022-04-26 10:49:06 +02:00
Ozzie Isaacs 6e8445fed5 Changed schedule start- and end-time to schedule start and duration
Localized display of schedule start-time and duration
Removed displaying scheduling settings if "APScheduler" is missing
Input check for start-time and duration
2022-04-25 17:00:07 +02:00
Ozzie Isaacs d83c731030 Reconnect only if reconnect is enabled 2022-04-25 08:28:29 +02:00
Ozzie Isaacs ae9a970782 Add button to update cover cache (for usecase sideloaded changed cover)
Bugfix logig start background schedue
2022-04-25 08:24:14 +02:00
Ozzie Isaacs 1e723dff3a Make texts in Background thread translatable 2022-04-24 18:40:50 +02:00
Ozzie Isaacs 8421a17a82 Always catch sqlite create_function error 2022-04-24 13:15:41 +02:00
Ozzie Isaacs bc96ff9a39 Enable scheduled side deleting of thumbnails of deleted books 2022-04-24 11:14:39 +02:00
Ozzie Isaacs bf049d8240 Make series cover cache invisible 2022-04-24 11:05:22 +02:00
Ozzie Isaacs 6e783cd7ee Make Task Stop Action green (marked as clickable) 2022-04-23 20:08:26 +02:00
Ozzie Isaacs 069dc2766f Update optional-requirements
Bugfix with serializing tasks
Bugfix order of tasks (id was used instead of task_id)
Code cosmetics
2022-04-23 20:03:59 +02:00
Ozzie Isaacs 2f5b9e41ac Reduce number visible System tasks in Tasks list 2022-04-22 20:31:03 +02:00
Ozzie Isaacs 9a8093db31 Thumbnail is generated directly after a book is added 2022-04-22 16:13:51 +02:00
Ozzie Isaacs 5c342d4e7c use get for dicts 2022-04-22 09:06:37 +02:00
Ozzie Isaacs 3c98cd1b9a Merge branch 'master' into cover_thumbnail
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2022-04-20 07:25:37 +02:00
Ozzie Isaacs 2303fc0814 Updated requirements
Update after testrun
2022-04-20 07:05:22 +02:00
Ozzie Isaacs a8680a45ca Bugfixes from Testrun
Update teststatus
2022-04-19 20:37:27 +02:00
Ozzie Isaacs d75d95f401 Merge remote-tracking branch 'epub_meta/master' 2022-04-18 20:07:01 +02:00
Ozzie Isaacs fbb6de7195 Merge remote-tracking branch 'translation/translation/Simplified_Chinese' 2022-04-18 20:02:09 +02:00
xlivevil 3cbbf6fa86 Update Simplifield Chinese translation 2022-04-18 14:52:31 +08:00
Ozzieisaacs c92d65aad3 Catch more errors on import metadata provider 2022-04-17 19:05:56 +02:00
Ozzieisaacs c61e5d6ac0 Fix amazon metadata-provider 2022-04-17 11:54:46 +02:00
Ozzieisaacs 130af069aa Merge remote-tracking branch 'douban/metadata_provider/douban'
# Conflicts:
#	cps/metadata_provider/amazon.py
#	cps/metadata_provider/lubimyczytac.py
2022-04-17 10:33:52 +02:00
Ozzieisaacs 09b381101b Added "None" to list of file formats, tags, series, languages
Unified languages.html and list.html template
2022-04-16 17:01:41 +02:00
Ozzieisaacs 35bb899879 Merge branch 'master' into cover_thumbnail 2022-04-14 20:03:43 +02:00
Ozzie Isaacs 6184e2b7bc Updated cve numbers 2022-04-14 19:58:15 +02:00
Ozzie Isaacs 2f3e5eadeb Bug fix after merge (renamed variable) 2022-04-13 19:34:14 +02:00
Ozzie Isaacs fe5d684d2c Merge branch 'master' into cover_thumbnail 2022-04-13 18:37:44 +02:00
Ozzie Isaacs df53a5d8c9 Prevent none comment while upload pdf documents 2022-04-13 18:37:23 +02:00
Ozzie Isaacs 83b99fcb1a Fix cover upload url with spaces at the end
Support image/jpg as upload format mimetype, remove redundant check of mimetype
2022-04-12 19:33:00 +02:00
Ozzie Isaacs 028e6855a7 Pagination button disapears in standard theme once infinite scroll is triggered 2022-04-12 18:45:06 +02:00
Wulf Rajek adf6728f14 Gracefully deal with incorrect dates 2022-04-12 00:22:05 +01:00
Ozzie Isaacs 652d0fd86f Update google-api-python-client version 2022-04-11 19:40:43 +02:00
Ozzie Isaacs 1136383b9a Bugfix for cli folder names as -p or -g parameters 2022-04-11 19:15:50 +02:00
Ozzie Isaacs d770e5392e Further fix for #2363 2022-04-06 11:12:57 +02:00
Ozzie Isaacs 3d2e7e847e Merge branch 'master' into cover_thumbnail
# Conflicts:
#	setup.cfg
#	test/Calibre-Web TestSummary_Linux.html
2022-04-05 19:11:11 +02:00
Ozzie Isaacs a63af5882e Fix for #2363 (Handle empty response from lubimyczytac metadata provider) 2022-04-05 19:04:42 +02:00
Wulf Rajek 2d0af0ab49 Add pubdate, publisher and identifiers metadata #2163 2022-04-05 01:26:35 +01:00
Ozzie Isaacs d912c1c476 Bugfix Quick start section 2022-04-04 14:37:39 +02:00
Ozzie Isaacs 42b0226f1a Fix for missing "query" entry in flask_session 2022-04-04 13:58:47 +02:00
Ozzie Isaacs 8adae6ed0c Handle permission errors for static files (Fix for #2358)
Version bump
2022-04-03 20:26:43 +02:00
Ozzie Isaacs fee76741a0 Update Testresult 2022-04-03 20:17:34 +02:00
Ozzie Isaacs f36d3a76be Return false if custom columns generate an error during connect database
Replaced existing with valid in readme to make it more clear what database is needed
2022-04-02 17:29:30 +02:00
Ozzie Isaacs afaf496fbe Merge branch 'master' into cover_thumbnail
# Conflicts:
#	cps/db.py
#	cps/templates/author.html
#	cps/templates/discover.html
#	cps/templates/index.html
#	cps/templates/search.html
#	cps/templates/shelf.html
#	cps/web.py
#	requirements.txt
#	test/Calibre-Web TestSummary_Linux.html
2022-04-02 11:57:18 +02:00
Ozzie Isaacs c06754975e Added bookeen to the reader for "simple theming" (#1861) 2022-04-02 11:37:02 +02:00
Ozzie Isaacs 834edadc28 Possible fix for #2350 and #2351 (databse locked)
Fix for #2309 (long unicode filenames could get to long)
2022-03-29 18:30:05 +02:00
gjutras cb0d9ec95c Allow kindle_email to be CSV to allow sending to multiple kindles 2022-03-28 19:59:59 -04:00
Ozzie Isaacs 7861f8a89a Prevent problems with werkzeug and flask_login on new installations 2022-03-28 21:43:23 +02:00
Ozzie Isaacs 73d359af05 Bugfix logging with gdrive
Update optional-requirements.txt
2022-03-28 19:02:12 +02:00
Ozzie Isaacs 036cd7be48 Added cloud provider installation link 2022-03-28 14:22:18 +02:00
Ozzie Isaacs baffe1f537 Plus ("+" vs. "%2B") encoded search strings for opds search feeds are now working in request string (fix for #2175) 2022-03-28 14:09:28 +02:00
Ozzie Isaacs 2f949ce1dd Enabled search for text based custom column content in simple search (fix for #2279) 2022-03-28 14:09:28 +02:00
Ozzie Isaacs 32a3c45ee0 Refactored load read status for web access and opds access
Refactored and removed discover html page
Bugfix show author
Bugfix open dialog in author page
Fix for #2341 (advanced search with linked read column and read column having a higher number than number of available custom columns)
2022-03-27 12:21:19 +02:00
Ozzie Isaacs 14a6e7c42c Deactivated several functions for kindle tolino and kobo. Opening books now working for ebook readers 2022-03-25 18:30:12 +01:00
Ozzie Isaacs 2a5e9a97bb Fix #2349 (import error on python <3.7 dataclasses solved) 2022-03-24 18:19:41 +01:00
Nicolas Ferrari 504e58abdb Installation on a Cloud Provider wiki link 2022-03-22 06:49:51 +01:00
Ozzie Isaacs a6a8f7eb43 Series Link in series view no longer clickable 2022-03-21 19:33:57 +01:00
Ozzie Isaacs 5070cc4c23 Merge branch 'master' into cover_thumbnail 2022-03-21 19:02:14 +01:00
Ozzie Isaacs 0d49b56883 Update gmail.json location 2022-03-21 18:50:02 +01:00
Ozzie Isaacs f5b79930ad Bugfix remember sort order of series and authors if entered from link below book cover (#2340) 2022-03-20 20:02:57 +01:00
Ozzie Isaacs c0d0660986 Added names for jobs to make log more readable
Bugfix logging delete thumbnail
2022-03-20 19:55:46 +01:00
Ozzie Isaacs ec53570118 Merge branch 'master' into cover_thumbnail
# Conflicts:
#	cps/editbooks.py
#	test/Calibre-Web TestSummary_Linux.html
2022-03-20 17:15:40 +01:00
Ozzie Isaacs 8cb5989c97 Catch additional error on not existing custom column linked to read column (#2341)
Prevent metadata changes are lost on edit books with errors (#2326)
Better log output
Renamed log message on database delete
2022-03-20 11:55:12 +01:00
Ozzie Isaacs 39459603d4 Update SECURITY.md 2022-03-19 18:01:51 +01:00
Ozzie Isaacs f34fc002da Implement missing cache delete Task identifier 2022-03-16 20:33:11 +01:00
Ozzie Isaacs 06e8845641 Implement delete thumbnail entry
Implement delete cache_dir on database change
2022-03-16 20:31:25 +01:00
Ozzie Isaacs 034ab73ccc Added environment variables for reconnect 2022-03-16 16:57:18 +01:00
Ozzie Isaacs 57cd8160a0 Removed accidently added line regarding "InstrumentedAttribute" 2022-03-16 16:28:58 +01:00
Ozzie Isaacs 399ddc5d6f Update testresults 2022-03-15 18:45:14 +01:00
Ozzie Isaacs d9a83e0638 Merge branch 'master' into cover_thumbnail
# Conflicts:
#	cps/editbooks.py
#	cps/helper.py
#	cps/web.py
#	test/Calibre-Web TestSummary_Linux.html
2022-03-14 19:41:47 +01:00
Ozzie Isaacs 8f3bb2e338 Bugfixes from testrun 2022-03-14 17:12:35 +01:00
Ozzie Isaacs 4545f4a20d Better epub cover parsing with multiple cover-image items
Code cosmetics
renamed variables
refactored xml page generation
refactored prepare author
2022-03-13 19:00:37 +01:00
Ozzie Isaacs 296f76b5fb Fixes after testrun
Code cosmetics
2022-03-13 10:23:13 +01:00
Ozzie Isaacs 3b5e5f9b90 Undo check of read checkbox in case of error
Display error message in details modal dialog
Bugfix set archive bit in booktable
Translate error message readstatus change
2022-03-12 22:16:33 +01:00
Ozzie Isaacs 8e2536c53b Improved cover extraction for epub files 2022-03-12 18:01:11 +01:00
Ozzie Isaacs 4379669cf8 Database error is more detailed
renamed debug_or_exception to error_or_exception
2022-03-12 17:14:54 +01:00
Ozzie Isaacs 2b31b6a306 Fix for #2325 (author sort order differs from authors order with readonly database) 2022-03-12 16:51:50 +01:00
Ozzie Isaacs 3a0dacc6a6 log message on not found author 2022-03-12 14:27:41 +01:00
Ozzie Isaacs 547ea93dc9 First fix for #2325 (edit book table with readonly database) 2022-03-12 10:19:21 +01:00
Ozzie Isaacs d80297e1a8 Bugfix sorting user table 2022-03-12 10:00:38 +01:00
Ozzie Isaacs 49692b4a45 Update catch errors for load metadata from amazon (#2333) 2022-03-12 08:27:42 +01:00
xlivevil b54a170a00 Add clean_date method in douban metadata_provider 2022-03-12 13:54:37 +08:00
Ozzie Isaacs 34478079d8 Prevent local variable 'from_book' referenced before assignment during merge of books
Merge books source book: Each book in own row
Merge books, sources are deleted before dialog shows up again
2022-03-09 14:45:51 +01:00
Ozzie Isaacs 753319c8b6 Version bump 2022-03-06 16:30:50 +01:00
Ozzie Isaacs c53817859a Version update
Updated testresult
2022-03-06 16:10:41 +01:00
Ozzie Isaacs 153a443fca Update setup config 2022-03-06 14:02:06 +01:00
Bharat KNV 9efd644360 Change dark theme from black to dark gray and add a black theme 2022-03-06 15:56:54 +05:30
Ozzie Isaacs 598618e428 Fix rename_files_on_change return handling
Updated test result
2022-03-01 16:36:46 +01:00
Ozzie Isaacs 965352c8d9 Don't allow redirects on cover uploads, catch more addresses which resolve to localhost 2022-02-26 08:05:35 +01:00
xlivevil 97cf20764b Add exception handling and logger in metadata provider 2022-02-25 12:18:07 +08:00
xlivevil 695ce83681 Fix Uncaught RangeError 2022-02-25 01:12:22 +08:00
xlivevil 86b779f39b Add douban metadate provider 2022-02-25 01:01:12 +08:00
Ozzie Isaacs 8007e450b3 Bugfix for cbr support without comicapi 2022-02-19 10:04:21 +01:00
Ozzie Isaacs 0aac961cde Update readme
Bugfix debug logging during update
unrar-free is now also recognized for displaying unrar version in about section, removed unused not configured string
2022-02-19 09:54:09 +01:00
Ozzie Isaacs ef7c6731bc Possible fix for #2301 (sending emails from custom domain name server) 2022-02-14 17:35:20 +01:00
Ozzie Isaacs e9b674f46e Fix a problem with sending emails from custom domain name server (#2301) 2022-02-13 16:17:04 +01:00
Ozzie Isaacs 8f665ebd58 Update Teststatus 2022-02-12 18:41:17 +01:00
Ozzie Isaacs 7bb3cac7fb Avoid problems with percent encoded utf-8 abstracts on certain chinese papers 2022-02-12 12:41:29 +01:00
Ozzie Isaacs 9c5970bbfc Bugfix for empty search results from google 2022-02-12 12:34:54 +01:00
Ozzie Isaacs ba23ada1fe Reenable showing of academic cover in case no cover was found from scholary 2022-02-12 12:32:35 +01:00
Ozzie Isaacs 86b621e768 refactored about
Update dependencies in setup.cfg
Improved detection of library change
2022-02-12 12:19:21 +01:00
Ozzie Isaacs 5f70406b30 Improved dependency check for executables 2022-02-12 12:14:02 +01:00
Ozzie Isaacs 6b026513cb refactored about 2022-02-12 12:12:30 +01:00
Ozzie Isaacs 0436f0f9b2 Improved dependency check for executables 2022-02-12 12:00:12 +01:00
Ozzie Isaacs 295888c654 Find imports in executables 2022-02-12 11:20:09 +01:00
Ozzie Isaacs 7317084a4e Update requirements 2022-02-12 11:11:08 +01:00
Ozzie Isaacs 0981337cdf Update optional_requirements 2022-02-09 20:57:39 +01:00
Ozzie Isaacs 461dd05e2f Readd cache directory to the excluded files during update 2022-02-08 20:18:32 +01:00
Ozzie Isaacs 764389ea2a small change 2022-02-08 20:15:01 +01:00
Ozzie Isaacs 4a0dde0371 Merge remote-tracking branch 'cover_images/thumbnails' into cover_thumbnail
# Conflicts:
#	cps/admin.py
#	cps/config_sql.py
#	cps/helper.py
#	cps/tasks/upload.py
#	cps/updater.py
#	cps/web.py
2022-02-08 19:55:20 +01:00
Ozzie Isaacs e22b3da601 Merge branch 'master' into Develop
# Conflicts:
#	cps/tasks/convert.py
#	test/Calibre-Web TestSummary_Linux.html
2022-02-07 13:57:50 +01:00
Ozzie Isaacs 7c623941de Bugfixes after testrun
Enabled re-encode of bookformats
2022-02-07 13:55:18 +01:00
Ozzie Isaacs 3bb41aca6d Bugfix for already present mobi file during convert (fixes #1900) 2022-02-06 18:57:58 +01:00
Ozzie Isaacs 0c3c0c0664 Improved logging of book title on upload 2022-02-06 16:33:12 +01:00
Ozzie Isaacs 411c13977f Fix for #2294 (cover files in epubs with property as property in manifest are now found) 2022-02-06 16:22:28 +01:00
Ozzie Isaacs 41f89af959 Log error in case gdrive.db folder can't be found 2022-02-06 14:23:35 +01:00
Ozzie Isaacs 895f68033f Improved detection of changed database 2022-02-06 14:22:55 +01:00
Ozzie Isaacs 2d49589e4b Merge branch 'Develop' 2022-02-06 10:43:15 +01:00
Ozzie Isaacs 89877835b3 Detect missing database file also in gdrive mode 2022-02-05 19:05:26 +01:00
Ozzie Isaacs 0ce41aef56 Detect missing database file also in gdrive mode 2022-02-05 19:05:14 +01:00
Ozzie Isaacs 5b3015619d Save book read status on edit in books table 2022-02-05 15:36:18 +01:00
Ozzie Isaacs 6ca08a7cc1 Prevent delete of everything if database wasn't really changed 2022-02-05 15:12:23 +01:00
Ozzie Isaacs 7254ce6c81 Prevent delete of everything if database wasn't really changed 2022-02-05 15:11:50 +01:00
Ozzie Isaacs cfa6b405da Upload files to gdrive with author rename working 2022-02-05 13:21:06 +01:00
Ozzie Isaacs 26a8ac1425 Mass rename author before last stage 2022-02-05 09:06:14 +01:00
Ozzie Isaacs 1ce45f3253 Update teststatus 2022-02-02 17:42:48 +01:00
Ozzie Isaacs a03c95329c Merge branch 'master' into Develop 2022-02-01 20:31:03 +01:00
Ozzie Isaacs e0bf829def Bugfix parsing /Keywords' in doc_info of pdf file with type bytes (fixes #2302) 2022-02-01 20:19:14 +01:00
Ozzie Isaacs 0bc15636f2 Bugfixes for renaming authors on gdrive 2022-02-01 20:08:42 +01:00
Ozzie Isaacs 61bfeae936 Merge branch 'master' into Develop 2022-01-31 19:20:02 +01:00
Ozzie Isaacs 95e0255aa1 Code cosmetics, removed scholarly from about section 2022-01-31 19:19:25 +01:00
Ozzie Isaacs 23e47ba4e6 Fix for #2299 (scholarly requires Internet connection at startup) 2022-01-31 18:09:23 +01:00
Ozzie Isaacs 4dcc44803c Update teststatus 2022-01-31 18:03:28 +01:00
Ozzie Isaacs 111ab121b1 Bugfix upload file with existing author 2022-01-30 21:27:06 +01:00
Ozzie Isaacs 1e04b51148 Renaming of co-authors which are authors in another book 2022-01-30 15:16:42 +01:00
Ozzie Isaacs 3ae1b97d72 Fix renaming title and author in the same request 2022-01-30 13:44:53 +01:00
Ozzie Isaacs 3123a914a4 Updated test results
Fix updater
Added comment regarding code taken from calibre source
2022-01-30 11:15:14 +01:00
Ozzie Isaacs f6b46bb170 Code cosmetics 2022-01-29 21:22:39 +01:00
Ozzie Isaacs bb7f4cf74e Added optional requirements for metadata amazon
Better logging of errors in metadata source files
2022-01-29 21:03:06 +01:00
Ozzie Isaacs 39ac37861f Added option to enable reconnect
Added option to perform dry run of updater
Added possibility to exclude files from updater
2022-01-29 14:47:45 +01:00
mmonkey 62ff6f7e8a Fixed tasks not running if scheduled time starts and ends on different days. 2022-01-28 23:48:44 -06:00
mmonkey 032fced9c7 Merge branch 'develop' into thumbnails 2022-01-28 23:15:50 -06:00
Ozzie Isaacs ae9c5da777 Merge branch 'master' into Develop:
Access to shelfs
2022-01-28 20:04:04 +01:00
Ozzie Isaacs 42f8209a4a Fixed access to shelfs 2022-01-28 20:01:33 +01:00
Ozzie Isaacs e757be6953 Merge remote-tracking branch 'amazon/master' into Develop 2022-01-27 19:13:59 +01:00
Ozzie Isaacs 4f3c396450 Merge remote-tracking branch 'lubimyczytac/add_lubimyczytac.pl_meta_provider' into Develop
# Conflicts:
#	optional-requirements.txt
2022-01-27 18:37:02 +01:00
mmonkey 50bb74d748 Add CSRF support for schedule task settings, fixed details page not loading 2022-01-27 00:35:45 -06:00
mmonkey 3416323767 Removed title tags from cover images 2022-01-26 23:56:10 -06:00
mmonkey 18ce310b30 Merge branch Develop into thumbnails 2022-01-26 23:51:50 -06:00
Ozzie Isaacs 6339d25af0 Bugfixes from testrun 2022-01-26 18:38:39 +01:00
quarz12 477b202c38 import try catch 2022-01-26 10:41:42 +01:00
quarz12 326d6e7b9d Merge branch 'janeczku:master' into master 2022-01-26 10:38:39 +01:00
Ozzie Isaacs baf32f9045 Update Teststatus 2022-01-26 07:31:01 +01:00
Ozzie Isaacs 3c4cd22d9e Renaming files on gdrive seem to work 2022-01-25 21:14:21 +01:00
Ozzie Isaacs d9d6fb33ba Merge branch 'master' into Develop
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2022-01-25 19:33:21 +01:00
Ozzie Isaacs 17b4643b7c fix #2285 (Fix for shelf handling in caliblur theme) 2022-01-25 19:31:52 +01:00
Daniel 239f389c5c Merge remote-tracking branch 'origin/master' 2022-01-25 14:42:37 +01:00
Daniel 8362c82d54 amazon metadata 2022-01-25 14:42:28 +01:00
Daniel 62e7aca0fb amazon metadata 2022-01-25 14:33:34 +01:00
Ozzie Isaacs 6a37c7ca9d Version bump 2022-01-24 19:27:09 +01:00
Ozzie Isaacs e0e0422010 Upates for new release 2022-01-24 19:18:40 +01:00
Ozzie Isaacs 128db26301 Resolved merge conflicts 2022-01-24 18:49:21 +01:00
Ozzie Isaacs 01ab75a158 Updated requirements for gdrive and gmail
Updated teststatus
2022-01-24 18:47:34 +01:00
Ozzie Isaacs d9c10b830a Added variable to allow loading cover from localhost 2022-01-24 18:47:34 +01:00
Ozzie Isaacs 35f6f4c727 Deleted book formats remove book from synced to kobo table
updated teststatus
2022-01-24 18:47:34 +01:00
Ozzie Isaacs 3b216bfa07 Kobo sync token is now also created if accessed from localhost(fixes #1990)
Create kobo sync token button is now "unclicked" after closing dialog
Additional localhost route is catched
If book format is deleted this also deletes the book synced to kobo status
2022-01-24 18:47:34 +01:00
Ozzieisaacs e8e2f789e5 Fix #2287 (books can't be added to shelf from search) 2022-01-24 12:37:15 +01:00
Ozzie Isaacs d8f5bdea6d Refactor rename author/title on gdrive 2022-01-23 19:31:56 +01:00
Ozzie Isaacs 127bf98aac Merge branch 'master' into Develop
# Conflicts:
#	cps/templates/detail.html
#	test/Calibre-Web TestSummary_Linux.html
2022-01-23 17:51:54 +01:00
Ozzie Isaacs ede273a8f9 Added variable to allow loading cover from localhost 2022-01-23 13:43:35 +01:00
Ozzie Isaacs bc7a305285 Deleted book formats remove book from synced to kobo table
updated teststatus
2022-01-23 13:11:02 +01:00
Ozzie Isaacs d759df0df6 Kobo sync token is now also created if accessed from localhost(fixes #1990)
Create kobo sync token button is now "unclicked" after closing dialog
Additional localhost route is catched
If book format is deleted this also deletes the book synced to kobo status
2022-01-22 18:23:43 +01:00
Ozzie Isaacs bbef41290f Fix delete book format wasn't working 2022-01-22 14:01:32 +01:00
Ozzie Isaacs 81b85445d8 Added missing unique marker on comments table (#2278) 2022-01-22 11:51:34 +01:00
Ozzie Isaacs 35209ede67 Update cover extraction with comicapi for webp files (fixes #2280) 2022-01-22 10:31:18 +01:00
Ozzie Isaacs 0c0313f375 Prevent creating a public shelf without permission 2022-01-18 17:55:10 +01:00
Ozzie Isaacs 6bf0753978 Prevent wrong use of safe statement 2022-01-18 17:39:54 +01:00
Ozzie Isaacs a02f621f08 Added id for testability 2022-01-17 18:38:37 +01:00
Ozzie Isaacs de1bc3f9af Fix #2265 (add book to shelf on popup for book details)
Updated testresults
2022-01-17 17:46:57 +01:00
Ozzie Isaacs 01090169a7 Version bump
update dependencies in setup.cfg
Update security bug list
2022-01-16 12:55:15 +01:00
Ozzie Isaacs b564a97cdf Update version
Update testresult
2022-01-16 12:06:02 +01:00
Ozzie Isaacs a118fffc99 Added missing chardet (used by requests in older versions) 2022-01-15 19:58:13 +01:00
Ozzie Isaacs 5b59aab81a Udpate requirements and config.cfg 2022-01-15 18:34:21 +01:00
Ozzie Isaacs 7d3d0c661e Merge remote-tracking branch 'fr/master' 2022-01-15 09:19:55 +01:00
Ozzie Isaacs f6f20ebc77 Update teststatus 2022-01-15 09:18:51 +01:00
Thomas 58cb54c76f small typo 2022-01-13 22:47:47 +01:00
Thomas 3fc326fa48 Update French translation 2022-01-13 22:45:16 +01:00
collerek 20b5a9a2c0 Merge branch 'master' into add_lubimyczytac.pl_meta_provider 2022-01-13 10:49:51 +01:00
Evan Peterson 4eaa9413f9 Kobo metadata return correct layout format for fixed layout 2022-01-10 15:15:19 -05:00
Ozzie Isaacs a50aff67a2 Merge remote-tracking branch 'korean/master' 2022-01-10 17:38:09 +01:00
Ozzie Isaacs 3c1f5fd37f Update Test results 2022-01-10 17:35:18 +01:00
byword77 8ae066c387 Add files via upload 2022-01-10 11:59:16 +09:00
Ozzie Isaacs 0feb62c142 Improvements delete user (delete also depending entries in tables)
Fixes kobo sync with sync only selected shelfs
2022-01-09 17:43:20 +01:00
Ozzie Isaacs 96b1e8960b Bugfix send to kindle with multiple formats 2022-01-09 12:55:38 +01:00
Ozzie Isaacs df67079573 Fixes for kobosync with multiple users (#2230) 2022-01-08 19:03:59 +01:00
Ozzie Isaacs e3dbf7a88d Bugfixes load metadata
Improvements for testing load metadata
2022-01-07 12:40:03 +01:00
byword77 8dd0585919 Add files via upload 2022-01-07 11:07:19 +09:00
Ozzie Isaacs c830a5936e Added dse to supported languages 2022-01-06 14:10:06 +01:00
Ozzie Isaacs 405f3c181f Added korean locale 2022-01-06 14:04:49 +01:00
Ozzie Isaacs 6d839d5cc7 Bugfixes from testrun 2022-01-04 21:12:59 +01:00
Ozzieisaacs bbadfa2251 bugfixes load metadata 2022-01-04 21:11:52 +01:00
Ozzieisaacs 7b8b2f93a0 Merge remote-tracking branch 'scholary/master' 2021-12-30 14:55:39 +01:00
Ozzieisaacs c1030dfd13 Update dependency scholary 2021-12-30 14:45:31 +01:00
Ozzieisaacs a90177afa0 Better version output in about page (exe file, pyPi, git commit string removed if empty) 2021-12-30 14:45:31 +01:00
Ozzieisaacs c095ee3c14 Fix #2243 (whitespaces are trimmed also for normal search) 2021-12-30 14:45:31 +01:00
Ozzieisaacs ae1f515446 Bugfix uncheck all ekements in books list and user list
Improved testability for books list
2021-12-30 14:45:31 +01:00
Ozzieisaacs 0548fbb685 Bugfix post commands without updater 2021-12-30 14:45:31 +01:00
Ozzieisaacs f22e4d996c Proxy kobo library sync at the end of local sync 2021-12-30 14:45:31 +01:00
Ozzieisaacs 3e0d8763c3 Prevent 2 public shelfs with same names due to changing public property 2021-12-30 14:45:31 +01:00
Ozzieisaacs 47f5e2ffb4 Remove python2 urllib imports
Fix for "javascript:" script links in identifier
2021-12-30 14:45:31 +01:00
Ozzieisaacs f39dc100b4 Migrated some routes to POST
- shelf massadd
- resetpassword
- delete shelf
- send to kindle
2021-12-30 14:45:31 +01:00
Ozzieisaacs 573c9f9fb4 Improved logging (right stacklevel on mail exceptions)
Updated jsonschema requirements
2021-12-30 14:45:31 +01:00
Ozzieisaacs 785726deee Migrated some routes to POST
- delete shelf, import ldap users
- delete_kobo token, kobo force full sync
- shutdown, reconnect, shutdown
2021-12-30 14:45:30 +01:00
Ozzieisaacs 7eb875f388 Improvement for gdrive rename authors 2021-12-28 17:13:18 +01:00
cbartondock 4edd1914b4 Fixed google scholar issues 2021-12-23 23:16:41 -05:00
cbartondock 222929e741 Fixed extra s in scholar metadata 2021-12-23 18:05:20 -05:00
cbartondock 70b67077cc Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-23 15:58:45 -05:00
Ozzie Isaacs ec73558b03 Bugfix advanced search for language
Update results from testrun
2021-12-23 19:18:09 +01:00
Ozzieisaacs bdedec90dd Catch more Gdrive errors (#2233) 2021-12-23 11:13:08 +01:00
Ozzieisaacs d45085215f Update Translation 2021-12-22 19:08:15 +01:00
Ozzie Isaacs 1a6579312f Add monkey.patch for gevent 2021-12-20 20:43:28 +01:00
Ozzie Isaacs b85627da5c Fix for adding/deleting visibility restrictions on custom columns/tags 2021-12-19 11:29:54 +01:00
Ozzie Isaacs 2e815147fb Merge branch 'master' into Develop
# Conflicts:
#	cps/kobo_sync_status.py
#	test/Calibre-Web TestSummary_Linux.html
2021-12-19 10:29:56 +01:00
Ozzie Isaacs 592288cb22 Fix inverted "convert to English characters" setting 2021-12-19 08:04:13 +01:00
Ozzie Isaacs 2e3a3ee460 Update dependencies in setup.cfg 2021-12-19 07:28:46 +01:00
Ozzie Isaacs b7927a0df1 Bugfix handling of stacktrace from calibre 2021-12-18 18:38:17 +01:00
Ozzie Isaacs 021298374e Improved debug output for task added
Improved handling of calibre output on windows
2021-12-18 17:31:33 +01:00
Ozzie Isaacs 92f65882b2 Added log4j statement 2021-12-16 06:21:16 +01:00
Bharat KNV 0693cb1ddb Added basic theming to the epub reader 2021-12-15 21:35:10 +05:30
collerek bea14d1784 fix locale for lubimyczytac languages 2021-12-15 15:20:01 +01:00
Ozzie Isaacs f0399d04b7 Merge remote-tracking branch 'bookmark_csrf/master' 2021-12-14 17:59:32 +01:00
Ozzie Isaacs 9b57fa25de Merge remote-tracking branch 'zh/master' 2021-12-14 17:52:12 +01:00
Ozzie Isaacs afbe77de3d Merge remote-tracking branch 'border/patch-1' 2021-12-14 17:50:02 +01:00
xlivevil d26d357151 Merge branch 'master' into master 2021-12-15 00:17:26 +08:00
Laurin Neff 4db9691cfc Remove white border in full-screen cover preview 2021-12-13 19:36:21 +01:00
Ozzie Isaacs 45c433caab Added "hmj" to supported languages. Fix for "bit" language support (Fix #2217) 2021-12-13 18:26:47 +01:00
collerek 51bf35c2e4 unify scholar 2021-12-13 17:21:41 +01:00
collerek d64589914f add series, languages and isbn to google provider 2021-12-13 15:14:19 +01:00
collerek 362fdc5716 run lubimyczytac detail pages in threadpool 2021-12-13 02:14:53 +01:00
collerek d55626d445 refactor and cleaning 2021-12-13 01:23:03 +01:00
Ozzie Isaacs 25422b3411 Fix for #2195 Sync token only updated after complete library sync and sync token content is discarded on first sync to enable real forced full sync 2021-12-12 20:31:45 +01:00
Ozzie Isaacs 42bf40d7bb Change 2 timestamps to utctime 2021-12-12 19:54:17 +01:00
collerek 920acaca99 everything working to refactor 2021-12-11 01:06:04 +01:00
xlivevil a184f4e71a Merge branch 'master' into master 2021-12-08 22:58:30 +08:00
cbartondock 4569188008 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-07 08:37:27 -05:00
Ozzie Isaacs 9d9acb058d Add button to force full kobo sync 2021-12-06 21:02:06 +01:00
Ozzie Isaacs 7d67168a4a Update test result 2021-12-06 20:27:25 +01:00
cbartondock 09751d8b87 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-05 19:00:15 -05:00
Ozzie Isaacs 6e15280fac Start renaming author names on gdrive 2021-12-05 19:01:23 +01:00
Ozzie Isaacs f78d2245aa Fixes from testrun 2021-12-05 18:48:21 +01:00
Ozzie Isaacs fd5ab0ef53 Bugfix handle archive bit 2021-12-05 18:01:56 +01:00
Ozzie Isaacs d217676350 Upated testresult
Bugfix book table
2021-12-05 13:09:41 +01:00
Ozzie Isaacs cd5711e651 Merge branch 'master' into Develop
# Conflicts:
#	cps/web.py
2021-12-04 20:56:04 +01:00
Ozzie Isaacs 3bf173d958 Added response for kobo-benefits route and kobo-gettest route 2021-12-04 15:44:41 +01:00
cbartondock 98d630d453 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-04 09:21:01 -05:00
Ozzie Isaacs eb2e816bfd Switch encoding in kobo metadata to ensure utf-8 characters to show up properly (finally) 2021-12-04 14:58:28 +01:00
Ozzie Isaacs bd01e840ca Delete books in shelfs, downloaded books, kobo sync status, etc on database change (fixes #620) 2021-12-04 11:50:25 +01:00
Ozzie Isaacs 91a21ababe Allow download of archived books 2021-12-04 11:16:33 +01:00
Ozzie Isaacs f4096b136e Added chinese sign language (csl) to supported languages 2021-12-04 10:57:53 +01:00
GarcaMan e2eab808c0 Merge branch 'long_strip_cbrreader' of github.com:GarckaMan/calibre-web into long_strip_cbrreader 2021-12-02 18:20:27 +00:00
GarcaMan 3ac08a8c0d After toggling fullscreen, focus on main container 2021-12-02 18:20:14 +00:00
cbartondock 7598dfe952 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-01 16:17:14 -05:00
Ozzie Isaacs 5ed3b1cf53 On master: Order of language count in /language (fixes #2200) 2021-12-01 21:38:43 +01:00
Ozzie Isaacs 7640ac1b3b Books are removed from synced books upon archiving (from kobo or calibre-web side)
unicode texts (title, author) are showing up right on kobo reader
Added some missing kobo routes (prevents 404 response)
Added a lot of debug output on kobo sync requests
2021-12-01 20:29:51 +01:00
Jonathan Fenske 66874f8163 Update read.html
include the CSR token input
2021-11-30 22:25:45 -06:00
Jonathan Fenske 3f91313303 Update epub.js
send the CSRF token when adding bookmarks
2021-11-30 22:24:34 -06:00
xlivevil 8438b2a07b Update Simplified Chinese translation 2021-12-01 12:14:57 +08:00
xlivevil 67e3721530 fix(tinymce Simplified Chinese locale):rename zh_CN to zh_Hans_CN 2021-12-01 11:28:03 +08:00
cbartondock 2252d661c0 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-27 14:06:36 -05:00
Ozzie Isaacs 87e526642c Bugfix edit series_index
Bugfix invalid languages
2021-11-27 18:23:06 +01:00
Ozzie Isaacs 7f9da94a18 Tie visibility of upload buttons to upload right only 2021-11-27 17:43:51 +01:00
Ozzie Isaacs ec7c2db971 Added package variable for generating "exe" file with pyinstaller 2021-11-27 12:26:35 +01:00
Denis Rodríguez 3f56f0dca7 Removed parameter that was wrongly added 2021-11-25 01:30:20 -03:00
GarcaMan 7fc04b353b Selecting Position will not scroll the current image up 2021-11-24 20:22:10 +00:00
GarcaMan a8689ae26b first commit 2021-11-24 19:41:07 +00:00
cbartondock fcd2b68359 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-23 13:59:43 -05:00
Ozzie Isaacs a1d372630d Added missing language bit (fixes #2187)
Added handling of wrong .jpg cover file content
Guest sorting options are now stored in the session
Updated teststatus
2021-11-23 19:32:48 +01:00
cbartondock fc859afb92 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-22 09:59:36 -05:00
Ozzie Isaacs 2e0d0a2429 Update requirements 2021-11-21 18:31:35 +01:00
Ozzie Isaacs d084a06e63 Bugfix create shelf for users not allowed to create public shelfs 2021-11-21 16:21:27 +01:00
Ozzie Isaacs e880238cb9 Add missing filter for current user in KoboSyncedBooks queries 2021-11-21 16:19:32 +01:00
cbartondock f58c5bee1c Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-21 09:23:52 -05:00
Ozzie Isaacs cbb9edac19 Bugfix search in books list
Fix renaming upper to lowercase letters
Update test results
2021-11-21 13:23:34 +01:00
Ozzie Isaacs 1b8bd27b3c Added cve number for csrf bug 2021-11-21 12:35:53 +01:00
Ozzieisaacs 1e9d88fa98 Merge branch 'master' into Develop 2021-11-21 10:22:44 +01:00
Ozzieisaacs 6cb713d62c Added filtering of languages
Bugfix show all allowed languages in user settings in case restrictions currently apply
2021-11-21 10:21:45 +01:00
Ozzieisaacs 642af2f973 Added support for missing locale Enu 2021-11-21 09:28:03 +01:00
Ozzieisaacs 6deb527769 Merge branch 'master' into Develop 2021-11-21 09:14:36 +01:00
Ozzieisaacs 9273843062 Removed double declared functions 2021-11-21 09:14:16 +01:00
cbartondock 2989586c6d Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-20 18:54:38 -05:00
Ozzie Isaacs 5ede079401 Handling of invalid cover files on upload 2021-11-20 13:45:41 +01:00
Ozzie Isaacs 7ad419dc8c Fix upload of cover and book formats containing html characters 2021-11-20 13:40:23 +01:00
Ozzie Isaacs bcdc976414 Added missing check for creating public shelfs 2021-11-20 13:00:54 +01:00
Ozzie Isaacs 6aad9378b8 Fix visiblility upload right on enabled upload feature 2021-11-20 12:44:34 +01:00
Ozzie Isaacs 6f5390ead5 Changed error message in case of trying to delete a shelf unauthorized
Removed outcommented text
2021-11-20 12:17:03 +01:00
Ozzie Isaacs d624b67e93 Fix #2174 (default sorting order now is timestamp again) 2021-11-20 11:57:51 +01:00
Ozzie Isaacs 01cc97c1b2 Added langugae support for Cholón (fixes #2183) 2021-11-20 11:32:51 +01:00
Ozzieisaacs 8e5bb02a28 Merge author rename 2021-11-20 13:28:45 +04:00
Ozzieisaacs 4fd4cf4355 Merge branch 'master' into Develop
# Conflicts:
#	cps/static/css/style.css
#	cps/web.py
2021-11-13 17:41:16 +04:00
Ozzieisaacs baba205bce Rename everything on rename authors 2021-11-11 18:46:32 +04:00
cbartondock 7c016265d2 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-10 08:59:23 -05:00
Ozzie Isaacs 974549b1af Removed technosoft2000 docker container 2021-11-08 20:36:41 +04:00
Ozzieisaacs add502d236 Fix opds search and opds list of read books 2021-11-07 20:18:33 +04:00
cbartondock 294c594cbb Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-06 14:19:37 -04:00
Ozzieisaacs 60aa016734 Handling of missing required dependency during dependency check 2021-11-06 21:17:48 +04:00
Ozzieisaacs 27e8fbd248 Show dependencies in about section from automatic dependency check
Bugfix and refactored dependency check
Added output of googledrive dependencies as fallback option
2021-11-06 20:58:51 +04:00
cbartondock d856c4d78e Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-04 14:00:51 -04:00
Ozzieisaacs 25b09a532f Fixed missing handle_error in convert calibre task (database readonly case) 2021-11-04 21:23:02 +04:00
Ozzieisaacs c1f4ca36b6 Current sorting order visible in all sidebar selectors, and search results
Sorting "hot" books only ascending and descending according to download numbers
Downloaded books sorting according to authors name working
2021-11-04 20:51:48 +04:00
Ozzieisaacs 58379159fb Fix shown cover in series grid view (#979) 2021-11-04 17:32:31 +04:00
Ozzieisaacs 17470b3b56 Check versions of dependencies at startup and generate logfile output if not fitting (#2157) 2021-11-04 16:38:55 +04:00
Ozzieisaacs 42cc13d1e2 Mark which functions are selected on list pages 2021-11-03 21:07:16 +04:00
Ozzieisaacs ecc5cb167e Upload setting in user template only visible if upload enabled
Delete book setting only visible if edit book setting is ticked
2021-11-03 20:32:17 +04:00
Ozzieisaacs d72210c6ae Exclude upload rights visibility if upload is not activated 2021-11-03 19:05:24 +04:00
Ozzieisaacs 61deda1076 Updated security history 2021-11-03 18:29:30 +04:00
Ozzieisaacs 1e0ff0f9c2 Fix #2045 ("fetch metadata" update appends to the existing tags instead or replacing them) 2021-11-03 18:16:15 +04:00
cbartondock 00bf1f5ec9 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-02 14:25:46 -04:00
Ozzieisaacs 2b6f5b1565 Fix csrf check in list pages 2021-11-02 21:32:29 +04:00
cbartondock e5abc5f281 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-02 13:09:59 -04:00
Ozzieisaacs 95371d0d7f Fix shelf grid ordering for inverted order at page load
Ordering buttons now visible on medium size screens
2021-11-02 20:27:50 +04:00
Ozzieisaacs c7df8a1a34 Fix missing csrf token in series grid view 2021-11-02 18:57:28 +04:00
Ozzieisaacs b414d91964 Fix for #2162 (Epub viewer isn't displaying images) 2021-11-02 18:55:56 +04:00
cbartondock 82164fc8e7 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-01 10:00:18 -04:00
Ozzieisaacs a5415e00d5 Fix grid to list in series view
Fix sort asc, desc in author and series list
2021-11-01 13:11:49 +01:00
cbartondock 434029270b Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-31 20:42:21 -04:00
Ozzie Isaacs b3b85bf692 Fix position read mark in standard theme 2021-10-31 16:33:35 +01:00
Ozzie Isaacs a36c6da3ae Fix position read mark in standard theme 2021-10-31 16:30:51 +01:00
Ozzie Isaacs 9e72c3b40d Fix position read mark in standard theme 2021-10-31 16:18:27 +01:00
cbartondock a0cebfbfb1 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-31 08:29:27 -04:00
Ozzie Isaacs 64e833f5d6 Updated readme
Version Bump
2021-10-31 12:00:20 +01:00
Ozzie Isaacs 4da64ceb23 Update to version 0.6.14 2021-10-31 11:31:53 +01:00
cbartondock 99ec99b539 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-30 13:28:56 -04:00
Ozzie Isaacs 3077b854d7 Merge remote-tracking branch 'de/patch-1' 2021-10-30 16:37:53 +02:00
Ozzie Isaacs a8317d900b Bugfix for edit custom ratings in list edit mode
Bugfix for deleting custom enum in list edit mode
2021-10-30 15:50:17 +02:00
KN4CK3R 668bf3a15e Fix typo in german translation 2021-10-30 13:41:33 +02:00
Ozzie Isaacs 481e52b503 Removed technosoft2000 docker reference 2021-10-30 09:56:39 +02:00
Ozzie Isaacs bbb65ec804 Merge remote-tracking branch 'trustedhost/master' 2021-10-30 09:40:04 +02:00
Ozzie Isaacs 4b7b646692 Merge remote-tracking branch 'autocap/patch-1' 2021-10-30 09:36:48 +02:00
Ozzie Isaacs 65cfb1ccbc Merge remote-tracking branch 'it/patch-26' 2021-10-30 09:34:56 +02:00
Ozzie Isaacs 9e9d7b3642 Merge remote-tracking branch 'mime/fix_mime_type_not_executable' 2021-10-30 09:33:37 +02:00
Ozzie Isaacs 1294672809 Bugfix search with excluded shelfs 2021-10-30 07:31:12 +02:00
i7-8700 857584a929 fix "MIME type ('text/plain') is not executable" on windows 2021-10-27 11:33:56 +08:00
cbartondock 75c68d92ec Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-26 09:58:36 -04:00
ElQuimm b4772d9b66 Update italian version of message.po 2021-10-26 11:10:46 +02:00
Ozzie Isaacs 3cfffa1487 Fix for iOS covers are not displayed during infinite scroll on standard theme 2021-10-25 20:06:22 +02:00
Ozzie Isaacs 382cd9458f Fix for #2126 (iOS covers are not displayed during infinite scroll on standard theme) 2021-10-25 20:03:48 +02:00
Ozzie Isaacs be7ac7e163 Update infinite scrolling javascript 2021-10-25 19:17:10 +02:00
Ozzie Isaacs 3a1a32f053 Update infinite scrolling javascript 2021-10-25 19:16:52 +02:00
cbartondock 27f71d910b Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-24 18:59:10 -04:00
Ozzie Isaacs f6a2b8a9ef Read and archive bit visible in book table 2021-10-24 21:32:19 +02:00
Bernat 9f260128cf Prevent virtual keyboard login auto capitalization
Stops virtual keyboards, such as the ones on mobile devices, to default to uppercase for the first letter of the login user input field.
2021-10-24 20:14:47 +02:00
Ozzie Isaacs cdd38350fe Updated tinymce editor, enabled raw html edit 2021-10-24 17:14:50 +02:00
Ozzie Isaacs 516e76de4f Updated tinymce editor, enabled raw html edit 2021-10-24 17:13:57 +02:00
Ozzie Isaacs 4c7b5999f7 Archived Flag available in book list array 2021-10-24 10:57:29 +02:00
Ozzie Isaacs bb20979c71 Merge branch 'master' into Develop 2021-10-24 09:49:42 +02:00
Ozzie Isaacs 917909cfdb Refactored books detail page 2021-10-24 09:48:29 +02:00
Ozzie Isaacs aefed40a2f Fix for missing message "flask-wtf not installed" 2021-10-23 18:44:29 +02:00
cbartondock 88278672d8 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-23 09:45:11 -04:00
Ozzie Isaacs bd0071354c Remove include subdomwains from hsts protection (#2114) 2021-10-23 09:45:04 +02:00
cbartondock c6bf62a6eb The recent changes to content security policy broke custom theming. I fixed it by adding a trusted hosts field to Server Configuration 2021-10-22 15:35:10 -04:00
cbartondock 95544ef885 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-22 09:20:11 -04:00
Ozzie Isaacs fe4db16a7e html editor in books list 2021-10-21 20:13:04 +02:00
cbartondock 9b127a9f97 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-21 11:09:47 -04:00
Ozzie Isaacs 9e4aeac16d Remove WTF_CSRF_SSL_STRICT workaround for missing referrers 2021-10-17 16:00:02 +02:00
cbartondock 9653308300 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-17 09:50:11 -04:00
Ozzie Isaacs 7671a1d5c8 Fix errormessage after update 2021-10-17 15:36:19 +02:00
Ozzie Isaacs bd6b5ac873 Fix load covers in metadata load screen 2021-10-17 15:06:37 +02:00
Ozzie Isaacs 00dc60da79 Added translation for traditional chinese 2021-10-17 15:02:16 +02:00
Ozzie Isaacs 708861bcd5 Enabed edit of custom texts (including categories) in books list 2021-10-17 14:29:13 +02:00
Ozzie Isaacs 24a2c0a5cf Enable edit of custom text in book table 2021-10-17 14:11:52 +02:00
Ozzie Isaacs 1e1d3a7c81 Enable edit of enum column in books list 2021-10-17 14:05:18 +02:00
Ozzie Isaacs 2ebddcfee3 Lowered version of needed flask-wtf dependency (#2137) 2021-10-17 10:05:50 +02:00
Ozzie Isaacs 4517f5b0cb Fix #2138 (Icons not showing in caliblur theme) 2021-10-17 10:00:04 +02:00
Ozzie Isaacs 666015e867 Fix datepicker localized month names
Removed redundant parenthesis in kobo.py response
2021-10-16 20:53:44 +02:00
Ozzie Isaacs 9d5e9b28ae Enabled editing of "number" custom_columns in books list 2021-10-16 20:46:16 +02:00
Ozzie Isaacs 6f1e78b9a3 Bugfix book list with language restriction or archived books 2021-10-16 20:43:22 +02:00
cbartondock d5aa345da6 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-16 14:40:56 -04:00
Ozzie Isaacs e060c62742 Fix exit on missing flask-wtf 2021-10-16 18:42:26 +02:00
cbartondock 5695268d1b Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-16 09:27:39 -04:00
Ozzie Isaacs cea10d3945 Merge remote-tracking branch 'comic_sort/comic_reader_sort' 2021-10-16 11:50:46 +02:00
Ozzie Isaacs fe21d15194 Merge remote-tracking branch 'cn/fix/bootstrap-table-locale'
# Conflicts:
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-af.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ar.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-bg.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ca.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-cs.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-da.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-de.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-el.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-et.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-eu.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fa.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fi.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-he.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-hr.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-hu.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-id.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-it.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ja.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ka.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ko.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nl.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-pl.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-pt_BR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ro.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ru.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sk.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sv.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-tr.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-uk.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-zh_Hans_CN.min.js
2021-10-16 11:48:00 +02:00
Ozzie Isaacs 369d5059c3 Merge remote-tracking branch 'br/patch-2' 2021-10-16 11:43:27 +02:00
Ozzie Isaacs 56a9c62421 Merge remote-tracking branch 'cn/chinese/master'
# Conflicts:
#	cps/templates/search_form.html
2021-10-16 11:41:09 +02:00
Ozzie Isaacs b4262b1317 Merge branch 'master' into Develop 2021-10-16 11:37:57 +02:00
Ozzie Isaacs 50d703e2d8 Chunked reading status on kobo sync
Bugfix loading text on shelf delete
Bugfix sort books list according to selection without selection
2021-10-16 11:28:35 +02:00
cbartondock 5aefc893de Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-15 15:58:11 -04:00
Ozzie Isaacs 6e5d9d7657 Bugfix toggle readstatus in modal dialog 2021-10-15 19:26:20 +02:00
Ozzie Isaacs 1be7dfcdca Merge remote-tracking branch 'origin/master' 2021-10-15 18:57:18 +02:00
Ozzie Isaacs e58eb8dac1 Show checkbox from boolean custom columns on details page with caliblur theme 2021-10-15 18:57:00 +02:00
Ozzie Isaacs d02eb842c7 Update testresults 2021-10-12 18:40:04 +02:00
Ozzie Isaacs f3efef1f60 Fix stored css bug in all typeahead functions (update typeahead -> bugfix typeahead) 2021-10-11 19:56:54 +02:00
cbartondock 659481c83b Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-11 09:55:09 -04:00
Ozzie Isaacs bad4c01474 Fix stored css bug in all typeahead functions (update typeahead -> bugfix typeahead) 2021-10-10 18:02:18 +02:00
Ozzie Isaacs cd53d57516 Fix for remote login 2021-10-10 10:26:01 +02:00
Ozzie Isaacs ac54899415 Bugfixes kobo sync 2021-10-10 10:23:58 +02:00
Ozzie Isaacs abe46e1862 Show checkbox from boolean custom columns on details page with caliblur theme 2021-10-09 15:01:38 +02:00
Ozzie Isaacs 3be47d6e57 edit_books prepared for kobo sync 2021-10-09 14:57:23 +02:00
Ozzie Isaacs 25f608d109 Bugfixes from testrun (load images from gdrive)
Fix locale for new users
2021-10-09 09:37:10 +02:00
Ozzie Isaacs aca5324914 Bugfix arrange shelf
Bugfix remote login
Bugfix perform Update
2021-10-06 22:06:33 +02:00
Ozzie Isaacs d8f9e2feb2 Bugfixes for user import with default locale and default language
Bugfixes from testrun
2021-10-06 18:41:01 +02:00
Ozzie Isaacs ed26d34961 Code cleaning (remove python2) 2021-10-04 19:23:24 +02:00
Ozzie Isaacs 50919d4721 Added handling for missing flask-wtf dependency
Added CSRF protection (via flask-wtf)
Moved upload function to js file
Fixed error page in case of csrf failure
2021-10-04 19:23:20 +02:00
Ozzie Isaacs 5edde53fed Improved sync for kobo with additional table 2021-10-03 09:58:45 +02:00
Ozzie Isaacs 1c15e10ac0 Improved csrf protection for remote login 2021-10-03 09:58:45 +02:00
Ozzie Isaacs 52be2ad4a2 Improved DJVU Reader 2021-10-03 09:32:29 +02:00
Ozzie Isaacs 43fdef5e53 Improved textreader 2021-10-03 09:31:24 +02:00
Ozzie Isaacs b699796236 Improved CSRF protection 2021-10-03 08:03:14 +02:00
Ozzie Isaacs b82d03c12c Removed js from search_form, tasks
Added translation of bootstrap-select in user table
2021-10-03 08:01:58 +02:00
Ozzie Isaacs b8eb557761 Corrected translation german -> Anzeigesprache 2021-10-03 08:00:53 +02:00
Ozzie Isaacs ba40c6693e Merge remote-tracking branch 'filepicker/fix-filemodal' 2021-10-02 08:32:44 +02:00
Ozzie Isaacs e064a3ec2b Removed unnecessary spaces in book convert texts 2021-10-02 07:53:36 +02:00
GarcaMan f6561456f7 New sort for Comic Reader 2021-10-01 05:47:52 +00:00
xlivevil deb91996a8 fix:bootstrap-table locale
Add data-locale in table tag
Add bootstrap-table-locale-all-min-js
Remove bootstrap-table locale dir
2021-09-30 20:09:02 +08:00
mmonkey cd3791f5f4 Always use full-sized image for book edit and details pages 2021-09-30 01:43:31 -05:00
Ozzie Isaacs cd1fe6dde0 Fix #2085 (Misaligned shelf names after glyphicon) 2021-09-29 19:26:21 +02:00
Ozzie Isaacs 3f6a466ca7 Further language support fixes (displaying "non locale" languages translated) 2021-09-29 19:00:02 +02:00
cbartondock 50f4fe6546 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-09-29 09:34:11 -04:00
mmonkey 9e7f69e38a Updated series cache timeout to one day 2021-09-29 03:01:28 -05:00
mmonkey 46205a1f83 Made long running tasks cancellable. Added cancel button to cancellable tasks in the task list. Added APP_MODE env variable for determining if the app is running in development, test, or production. 2021-09-29 02:40:12 -05:00
Ozzie Isaacs 861277460d Merge remote-tracking branch 'origin/master' 2021-09-28 18:50:11 +02:00
Fernando Mesquita 83babf88fc msgid "Read Books" --> msgstr "Livros Lidos" 2021-09-28 12:03:07 -03:00
Ozzieisaacs 58735caff3 Enhancements for displaying languages on non english locale 2021-09-26 17:04:53 +02:00
mmonkey 26071d4e7a Added Scheduled Tasks Settings 2021-09-26 02:02:48 -05:00
cbartondock 844eb6c379 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-09-25 15:33:56 -04:00
xlivevil 1b8410e786 Update Simplified Chinese translation 2021-09-25 23:15:44 +08:00
xlivevil fed9eff7b8 Fix the publishstart option in advsearch 2021-09-25 22:10:46 +08:00
xlivevil bf5de95fdc Update Simplified Chinese translation 2021-09-25 22:08:05 +08:00
Ozzie Isaacs 2e25e797dd Fix #2002 (Added missing default locale and default language view upon importing ldap user) 2021-09-25 14:08:49 +02:00
Ozzie Isaacs c1ac68e2ae Fix #2053 (Hovering on cover, now shows tooltip also in Caliblur! Theme) 2021-09-25 14:01:17 +02:00
Ozzie Isaacs 623a92ebec Version Bump
Merge branch 'Develop'
2021-09-25 10:49:20 +02:00
mmonkey 0bd544704d Added series cover thumbnail generation. Better cache file handling. 2021-09-25 03:04:38 -05:00
Ozzie Isaacs 2edcd16119 Update version to 0.6.13 for release 2021-09-25 08:34:20 +02:00
Ozzie Isaacs af202bd6d1 Merge remote-tracking branch 'pt_br/patch-1' 2021-09-25 08:25:06 +02:00
Ozzie Isaacs d5a332a84e Update test results 2021-09-25 08:23:14 +02:00
Fernando Mesquita d6884164a5 Update messages.po
Read = Leia > Lido
2021-09-25 01:07:31 -03:00
mmonkey be28a91315 Simplified all of the thumbnail generation and loading. 2021-09-24 03:11:14 -05:00
cbartondock 707556a36d Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-09-23 09:14:51 -04:00
Ozzie Isaacs 356a4f588e Merge remote-tracking branch 'nl/master' 2021-09-22 20:43:29 +02:00
Ozzie Isaacs 241b31458d Added ancient and modern Greek translation to greek language 2021-09-22 20:34:18 +02:00
Ozzie Isaacs efb04ddd8f Fix #2061 (Defective utf-16 txt files) 2021-09-22 19:58:20 +02:00
mmonkey 524ed07a6c Handle read only access to cache dir gracefully. minor cleanup 2021-09-21 23:39:00 -05:00
mmonkey 8bee2b9552 Added CACHE_DIR env variable, graceful handling when APScheduler is not installed 2021-09-19 22:45:19 -05:00
Anatolii Fetisov 781ca7a3f3 Fix filemodal file selection dialog 2021-09-18 09:49:28 +03:00
mmonkey d648785471 Review feedback fixes 2021-09-17 01:42:56 -05:00
mmonkey 9a08bcd2bc Started addressing some PR comments 2021-09-16 23:20:11 -05:00
mmonkey 04a5db5c1d Resolve merge conflicts 2021-09-16 22:58:54 -05:00
Jeroen Hellingman 6f6d8df431 Update messages.po
Review of Dutch translations (native speaker); improving consistency in vocabulary still needed.
2021-09-02 11:25:58 +02:00
cbartondock 9ebde57fc3 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-31 18:44:18 -04:00
Ozzie Isaacs 8dc1e9bfa4 Updated test result 2021-08-31 08:33:09 +02:00
Ozzie Isaacs e3db2796c9 Merge branch 'master' into Develop
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-08-30 20:18:21 +02:00
Ozzie Isaacs 42ef049b63 Catch error on ldap import 2021-08-30 20:17:56 +02:00
cbartondock 8930426f2a Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-30 11:35:10 -04:00
Ozzie Isaacs ceffa3a108 Removed xss on shelf add/remove action 2021-08-30 17:06:11 +02:00
Ozzie Isaacs c0a06eec46 Removed xss on shelf add/remove action 2021-08-30 17:05:53 +02:00
Ozzie Isaacs 86ef1d47e8 Bugfixes from testrun 2021-08-30 08:02:21 +02:00
cbartondock b835aa660a Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-29 14:05:21 -04:00
Ozzie Isaacs 4d92d7da3a Removed timelogging on metadata search 2021-08-29 14:37:15 +02:00
Ozzie Isaacs d1e6a85803 Parallel requests of metadata provider 2021-08-29 14:36:05 +02:00
Ozzie Isaacs d8bad7394a Merge remote-tracking branch 'caliblur/master' 2021-08-29 13:31:46 +02:00
Ozzie Isaacs f5062d1354 Merge remote-tracking branch 'it/patch-1' 2021-08-29 13:31:21 +02:00
Ozzie Isaacs 1d79d9ded2 Bugfix from tests (displaying of book languages in user list) 2021-08-29 11:31:10 +02:00
Ozzieisaacs 10e212fcde Merge branch 'master' into Develop 2021-08-29 10:44:13 +02:00
Ozzie Isaacs 1fa267ce1b Final fix for escaping html special chars in tables 2021-08-28 15:47:57 +02:00
cbartondock abcf9a1808 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-28 09:35:12 -04:00
Ozzie Isaacs d4cfad6363 Fixed several text problems from tasks
Fixed task update
2021-08-28 11:32:13 +02:00
Ozzie Isaacs 3a24561ca2 Removed additional " from convert message 2021-08-28 10:32:40 +02:00
Ozzie Isaacs 5c19a8aacc Bugfix edit comments
Testupdate
2021-08-28 08:24:38 +02:00
Ozzie Isaacs 3946ef8f0d Merge branch 'master' into Develop
# Conflicts:
#	cps.py
#	cps/web.py
2021-08-27 16:16:24 +02:00
cbartondock 6a2868c68d Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-27 10:13:05 -04:00
Ozzie Isaacs 7ae3255ea9 Revert Content-Security-Policy header 2021-08-27 14:28:30 +02:00
Ozzie Isaacs 91e6d94c83 Improved displaying of username and task title in tasks 2021-08-27 14:27:35 +02:00
Ozzie Isaacs e615073893 Add all html special characters to the escaped characters in bootstrap table 2021-08-27 12:56:08 +02:00
Ozzie Isaacs 32e27712f0 Added lxml to needed requirements
Improved displaying of series title, book of series, comments and custom comments
2021-08-27 09:43:32 +02:00
cbartondock ff39fac50f Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-26 17:58:53 -04:00
Daniela Mazza 80d3ba42b4 Update messages.po
Changed translate of "Read Books": the meaning is exactly "Libri letti", not "Libri da leggere"
2021-08-26 19:16:38 +02:00
Ozzie Isaacs d25cfb7499 Fix for #2097 (Quotes are now handled properly in UI-editable tables) 2021-08-26 18:03:59 +02:00
cbartondock 24ad15be11 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-19 08:30:13 -04:00
Ozzie Isaacs a35c635987 Update python metadata search 2021-08-18 21:38:20 +02:00
Ozzie Isaacs aa9fdd2ada Merge branch 'master' into Develop 2021-08-18 20:32:33 +02:00
Ozzie Isaacs afa585eb65 Added -f flag for compatibility reasons 2021-08-18 20:32:13 +02:00
cbartondock 03acb77162 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-08-15 12:06:55 -04:00
Ozzie Isaacs 28ca39ca13 Add logging of user sessions 2021-08-15 12:43:19 +02:00
cbartondock b05c17b8bb Fixed book covers being too wide in search/shelves/discover on mobile 2021-08-14 21:50:05 -04:00
cbartondock 3839af65c4 add python-version to gitignore for managed pyenv compatibility 2021-08-14 15:57:09 -04:00
Ozzie Isaacs 275e073c42 Fix #2085 (Missing spaces between glyphicon and text on sidebar) 2021-08-10 20:17:30 +02:00
Ozzie Isaacs c4876010e9 Merge remote-tracking branch 'pdf/master' 2021-08-09 16:46:19 +02:00
xlivevil bce69b2dfc Add pdf.js font file 2021-08-07 20:51:10 +08:00
xlivevil fb97e39d9f Merge branch 'master' of https://github.com/xlivevil/calibre-web 2021-08-05 21:39:04 +08:00
xlivevil 913690df78 Fix #1913(set lazyload in pdf reader)
Add favicon.ico in pdf reader
2021-08-05 21:37:39 +08:00
Ozzie Isaacs e68a3f18fa Merge branch 'master' into Develop 2021-08-01 20:23:19 +02:00
Ozzie Isaacs 1534827ad7 With merge button disabled it's no longer possible to open merge dialog 2021-08-01 20:23:04 +02:00
Ozzie Isaacs b36422bc05 Update list/grid button action (reverse proxy problem) 2021-08-01 20:06:48 +02:00
Ozzie Isaacs 1e2335dea0 Merge remote-tracking branch 'caliblur/master' 2021-08-01 17:35:30 +02:00
Ozzie Isaacs e052590270 Merge remote-tracking branch 'chinese/master'
# Conflicts:
#	cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
#	cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po
#	messages.pot
2021-08-01 17:31:47 +02:00
Ozzie Isaacs 41ca85268f Update translation files 2021-08-01 17:27:18 +02:00
Ozzie Isaacs 0e9709f304 Make Book-No. of book-range to translatable text 2021-08-01 17:09:29 +02:00
Ozzie Isaacs ceec1051d5 Added option to save books with no nenglish characters
Imporvements on metadata search
2021-08-01 13:50:17 +02:00
cbartondock 81ccc11c9c fixed dropdowns going off screen in caliblur 2021-07-31 12:51:51 -04:00
Ozzie Isaacs 702e96ddd6 Update load metadata 2021-07-31 09:18:05 +02:00
Ozzie Isaacs 53603f79bd Get_meta button unfocused on return to edit books 2021-07-31 09:00:34 +02:00
Ozzie Isaacs 250cafe814 Merge branch 'master' into Develop 2021-07-30 21:00:01 +02:00
Ozzie Isaacs 16633ef1d3 Fix #2029 (Import LDAP User with "." in the name) 2021-07-30 20:45:33 +02:00
Ozzie Isaacs f0a5225524 Merge branch 'master' into Develop 2021-07-30 16:58:30 +02:00
Ozzie Isaacs a32b36bf81 Fixed missing joins for sorting according to series in read/unread and rated views 2021-07-30 16:58:20 +02:00
Ozzie Isaacs 302679719d Merge branch 'master' into Develop
# Conflicts:
#	cps/admin.py
#	cps/converter.py
#	cps/subproc_wrapper.py
#	test/Calibre-Web TestSummary_Linux.html
2021-07-30 16:33:06 +02:00
Ozzie Isaacs a63baa1758 better logged in session protection 2021-07-30 16:28:37 +02:00
cbartondock 752192a057 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-30 09:34:50 -04:00
Ozzie Isaacs e245a147d9 Revert accidentally committed store user session 2021-07-30 10:16:52 +02:00
Ozzie Isaacs 0ec2bcd897 Fixes from testrun 2021-07-30 09:25:19 +02:00
xlivevil 19c75d6790 Update Simplified Chinese translation 2021-07-29 14:00:02 +08:00
cbartondock 0654156dd6 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-27 11:24:23 -04:00
Ziding Zhang e4b0434733 Create SECURITY.md
A simple instruction for security researchers in future.
2021-07-27 13:22:24 +02:00
cbartondock b32721c293 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-27 06:39:29 -04:00
Ozzie Isaacs c5e39a7523 Fix divs in user edit page 2021-07-27 08:09:57 +02:00
cbartondock 0db837c0ce Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-26 14:44:16 -04:00
Ozzie Isaacs 53dae32897 Fix multiple messages on windows after change config with reboot 2021-07-26 16:50:21 +02:00
Ozzie Isaacs 018f3ca250 Sort authors additionally to series and series_index (Fix #2001)
Sqlalchemy version2 is now a global flag
2021-07-26 07:52:01 +02:00
Ozzie Isaacs a1a8a0cf29 Logout if logged out and don't allow to get logged in afterwards 2021-07-25 05:24:03 +02:00
Ozzie Isaacs f9c3e751f6 Fixes for handling shelf requests without title 2021-07-24 05:49:16 +02:00
Ozzie Isaacs c7b057ec51 Merge remote-tracking branch 'shelf_edit/security_fixes' into master 2021-07-24 05:34:24 +02:00
Ozzie Isaacs 85ea762054 Handle shelf request without title 2021-07-24 05:33:51 +02:00
Ozzie Isaacs 56cd62ed90 Fix show edit user after exception 2021-07-23 20:12:37 +02:00
Ozzie Isaacs 9a8c342e61 Fix deleting user (deleting shelfs and so on failed before) 2021-07-23 20:03:20 +02:00
Ozzie Isaacs 3b81ea37f4 Fix entries of user table are visible if database has no tags 2021-07-23 19:46:01 +02:00
Ozzie Isaacs 3c8bfc31e4 fix change name allowd as non admin 2021-07-23 19:34:46 +02:00
Ileana Maricel Barrionuevo 59881367fe Security fixes: Report 85176e1f-7920-4824-87ea-8eb5b5e505e0: Exposure of Private Personal Information to an Unauthorized Actor in janeczku/calibre-web 2021-07-22 01:05:11 -03:00
Ileana Maricel Barrionuevo c8ebaee0f7 Security fix improved: user should not edit other shelve's titles 2021-07-22 00:41:07 -03:00
Ileana Maricel Barrionuevo d5d0ad50fa Fixed security issue: a user could edit others' shelves. 2021-07-21 22:08:41 -03:00
cbartondock 6ef792d65d Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-12 12:08:55 -04:00
Ozzie Isaacs 20fa9f5523 Fix encoding errors on windows while downloading logbooks 2021-07-12 17:33:35 +02:00
Ozzie Isaacs 616cc2018a Fix show cover
Update more debug output for kobo sync
2021-07-12 14:58:03 +02:00
Ozzie Isaacs e69b1adccd Fix #2053 (Add tooltip for complete title on hover over cover and title) 2021-07-12 14:17:28 +02:00
Ozzie Isaacs 280efad939 #2052 (wrong series index shown for series_index >=100) 2021-07-12 14:04:23 +02:00
Ozzie Isaacs 15ec6bec95 fix #2014 (User menu dropdown in caliblur is tiny and presents scrollbars)
Fix display of nonexistent series_index
Fix caliblur add-to-shelf
2021-07-11 13:15:13 +02:00
Ozzie Isaacs aae81c3d24 Fix #2048 (Display book title in reader) 2021-07-11 12:52:46 +02:00
Ozzie Isaacs 259ac94b93 Fix error unclosed IO on external binary version query 2021-07-11 09:19:27 +02:00
Ozzie Isaacs a27314464a Merge branch 'master' into Develop 2021-07-11 08:50:14 +02:00
Ozzie Isaacs 87f07003f4 Removed invalid code
Sqlalchemy 2.0 compatibility for kobo sync
2021-07-11 08:49:47 +02:00
Ozzie Isaacs 1bf065fd04 Bugfix with encoding errors windows 2021-07-11 07:38:15 +02:00
Ozzie Isaacs f8de7e75cc Fix error unclosed IO on external binary version query 2021-07-11 09:19:46 +02:00
cbartondock 7d586b3745 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-10 13:21:27 -04:00
Ozzie Isaacs a56e071a19 Fix #2043 (Multiuser kobo sync with restrict to shelfs working)
Sync only selected shelfs is stored correct on creating user
2021-07-10 17:09:04 +02:00
Ozzie Isaacs 480aecb16c Fix #2046 (Deleting book with additional "/" in database path is working) 2021-07-10 08:27:29 +02:00
Ozzie Isaacs ec7803fa76 Update python search Metadata 2021-07-10 07:38:16 +02:00
cbartondock 91938fd18a Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-07-08 15:52:14 -04:00
Ozzie Isaacs aa2d3d2b36 Update python search Metadata 2021-07-07 21:24:29 +02:00
Ozzie Isaacs d5e9cdc5b7 Search Metadata improvements 2021-07-07 21:10:38 +02:00
Ozzie Isaacs 305e75c0ae Clarification for pip command 2021-07-07 16:42:05 +02:00
Ozzie Isaacs 0d247fef6a Fixes for grab metadata in python 2021-07-06 20:24:27 +02:00
Ozzie Isaacs 94da61c57e Basic Metadata mechanism in python 2021-07-05 18:55:54 +02:00
Ozzie Isaacs 430ccd9ab1 Merge branch 'master' into Develop 2021-06-29 18:52:02 +02:00
Ozzie Isaacs 47d94d9bd6 Search metadata without installed google scholar working (fix #2024) 2021-06-29 18:38:02 +02:00
Ozzie Isaacs a6d1f6039d Merge remote-tracking branch 'es/spanish-translation' into master
# Conflicts:
#	cps/translations/es/LC_MESSAGES/messages.po
2021-06-29 18:30:37 +02:00
Ozzie Isaacs 31234a4b98 Merge remote-tracking branch 'fr/Update-French-transations' into master
# Conflicts:
#	cps/translations/fr/LC_MESSAGES/messages.po
2021-06-29 18:25:57 +02:00
Ozzie Isaacs 476275ea53 Merge remote-tracking branch 'po/master' into master
# Conflicts:
#	cps/translations/pl/LC_MESSAGES/messages.po
2021-06-29 18:25:25 +02:00
Ozzie Isaacs 792d4a65bc Fix typo 2021-06-29 18:24:35 +02:00
Ozzie Isaacs 0e2dca5f4d Fix upload of books uppercase extensions (#2038) 2021-06-26 20:14:52 +02:00
Ozzie Isaacs 557296b7be Fix upload of books uppercase extensions 2021-06-26 20:13:56 +02:00
JFernando122 2236191263 Fixing typos on spanish translation 2021-06-22 23:37:19 -05:00
JFernando122 1138c86868 improved spanish translation with adrocampo's review 2021-06-22 14:22:28 -05:00
Thomas 08500c66a8 Update French translations
The French translation is normally completely now
2021-06-19 16:01:25 +02:00
Ozzie Isaacs 67836006c5 Successfull testrun 2021-06-19 14:44:57 +02:00
JFernando122 fa03a9ee25 Updated spanish translation 2021-06-17 21:49:04 -05:00
cbartondock bf4564e365 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-06-13 21:38:25 -04:00
Radoslaw Kierznowski 6a96664381 Update translations 2021-06-12 15:41:48 +02:00
Ozzie Isaacs e6e3032f02 Merge remote-tracking branch 'pl/master' into master 2021-06-12 08:52:14 +02:00
Ozzie Isaacs 6d424f0a30 Merge remote-tracking branch 'nl/patch-1' into master 2021-06-12 08:50:51 +02:00
Ozzie Isaacs a6f0375db3 Merge remote-tracking branch 'it/patch-25' into master 2021-06-12 08:50:13 +02:00
Ozzie Isaacs 1833e8fdb4 Success message after database change 2021-06-12 08:48:55 +02:00
Radoslaw Kierznowski 70151c2e11 Update Translations 2021-06-10 15:23:12 +02:00
JVT038 ab69962e8a Completed Translation for Dutch (NL) 2021-06-09 20:26:09 +02:00
ElQuimm 075fe994af Update italian message.po
Updated version
2021-06-08 21:11:36 +02:00
Ozzie Isaacs 1aca1b9fdd Fixed duplicate ids in logviewer 2021-06-06 16:14:30 +02:00
Ozzie Isaacs 9fea5a55f4 Merge branch 'master' into Develop 2021-06-06 12:18:04 +02:00
Ozzie Isaacs 1a0bf45c34 Check file/folder permissions before update 2021-06-06 12:16:47 +02:00
Ozzie Isaacs 67874e07b6 Fix routes for reverse proxy 2021-06-05 18:41:52 +02:00
Ozzie Isaacs ced0bcfe33 Fix missing " in books table 2021-06-05 18:41:52 +02:00
Ozzie Isaacs 8c0aa79f78 Fix #1915 (more than 10 books at a time are displayed in books table) 2021-06-05 18:41:52 +02:00
Ozzie Isaacs 93e8c5be32 Fixes for flask version 2.1
Fixes for compatibility with sqlalchemy 2.0
2021-06-05 18:41:42 +02:00
Ozzie Isaacs d9f86aecd2 Fix change_permission 2021-06-05 13:51:16 +02:00
Ozzie Isaacs e9552fedef Merge branch 'master' into Development 2021-06-05 10:23:06 +02:00
Ozzie Isaacs 84e1c6e809 Fix for #2004 (pdf viewer buttons missing in safari) 2021-06-05 08:08:08 +02:00
Ozzie Isaacs aadd6fd7e0 Possible fix for #2003 (Calibre theme download icon broken) 2021-06-03 12:35:09 +02:00
Ozzie Isaacs 59987ed359 Merge branch 'master' into Develop 2021-05-28 18:22:43 +02:00
Ozzie Isaacs a79600c81f Mass change book title and author (fix #1996) 2021-05-28 18:22:29 +02:00
Ozzie Isaacs e8a4c3c6b9 Merge remote-tracking branch 'shields/patch-3' into master 2021-05-28 17:46:53 +02:00
Ozzie Isaacs 62fce26651 Bugfix extended search term with excluded series/shelfs 2021-05-28 14:57:35 +02:00
Ozzie Isaacs 25add6511f Catch error on search for new custom column (created after start of calibre-web) 2021-05-28 14:54:18 +02:00
Ozzie Isaacs b2a28cd39a Merge branch 'Develop' into master 2021-05-28 14:23:47 +02:00
Ozzie Isaacs 6de3aebf3a Fix for #1998 (Error 500 with configured oauth on user setup page accessed by admins) 2021-05-28 14:23:30 +02:00
Ozzie Isaacs 1b7e422772 Changed error page to display logout route in case of unconfigured 2021-05-28 13:56:07 +02:00
Ozzie Isaacs b0f48b7f00 Merge branch 'master' into Develop 2021-05-26 19:58:10 +02:00
Ozzie Isaacs 94b07d05c1 First half of #1996 (editing single books) 2021-05-26 19:57:58 +02:00
Ozzie Isaacs 4392fec7f5 Added fullscreen cover viewer 2021-05-26 19:40:41 +02:00
flying-sausages c1d0ec076b Shield cleanup 2021-05-26 17:20:54 +02:00
Ozzie Isaacs a47d6cd937 Fix confirm dialog database change
Gdrive setup basically working again
Moved basicconfig behind login
Database setup separated from other setup
Config page is using ajax (flask >2 and slow computers)
2021-05-26 14:50:44 +02:00
Ozzie Isaacs dcdb5e2a9e Merge branch 'master' into Develop 2021-05-22 19:52:53 +02:00
Ozzie Isaacs 240325dc18 Update installation instructions 2021-05-22 19:52:20 +02:00
Ozzie Isaacs e599d2ec05 Merge remote-tracking branch 'scholar/master' into master 2021-05-22 19:30:57 +02:00
Ozzie Isaacs c62a417cbf Merge branch 'Develop' into master 2021-05-22 19:29:58 +02:00
Ozzie Isaacs 98fc1ee0a7 Comic reader code cleaning 2021-05-22 19:28:30 +02:00
Ozzie Isaacs 78d0fd811b Version bump 2021-05-22 14:57:55 +02:00
Ozzie Isaacs 54d06e580d Release version 0.6.12 2021-05-22 14:08:46 +02:00
Ozzie Isaacs a62fca1e55 Merge branch 'master' into Develop 2021-05-22 11:28:59 +02:00
Ozzie Isaacs 7b1d23eba4 Restored Discord Widget 2021-05-22 10:03:48 +02:00
Ozzie Isaacs 2502c279fc Update discord 2021-05-22 09:57:47 +02:00
Ozzie Isaacs 878fc8c2fd Added download stat badge 2021-05-21 20:58:10 +02:00
Ozzie Isaacs 64902eebcb Merge remote-tracking branch 'batches/patch-2' into master 2021-05-21 20:54:06 +02:00
flying-sausages e520f6afc4 Add Shields to README.md 2021-05-20 22:48:51 +02:00
Ozzie Isaacs 0cc1ae9324 Updated Testresult for Linux 2021-05-20 17:54:25 +02:00
cbartondock 1b2124aa35 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-19 16:47:19 -04:00
Ozzie Isaacs bd4fde9e63 Fix #1992 handle invalid numbers of books seriesindex 2021-05-19 19:54:44 +02:00
Ozzie Isaacs c85cfa90a4 Update testresults on windows
Added encoding on fb2 upload
2021-05-19 19:21:09 +02:00
Ozzie Isaacs a0449b50c8 Downgrade flask -> to slow during reboot 2021-05-17 17:54:43 +02:00
Ozzie Isaacs 010c0bfc7d Improved error handling for content too long 2021-05-16 14:07:16 +02:00
Ozzie Isaacs 394b063b8c Fixes for caliblur theme 2021-05-16 13:25:29 +02:00
Ozzie Isaacs 251780bed6 Added google scholar as metadata provider 2021-05-16 13:24:42 +02:00
Ozzie Isaacs decc2a9c79 Merge branch 'master' into Develop 2021-05-16 13:19:38 +02:00
Ozzie Isaacs b62d84424e Merge remote-tracking branch 'comic/comic_viewer_changes' into master 2021-05-16 13:19:28 +02:00
Ozzie Isaacs e15324d2cd Merge branch 'master' into Develop 2021-05-16 09:38:00 +02:00
Ozzie Isaacs 20b84a9459 Unified translations
Update translations for release
2021-05-16 09:37:45 +02:00
Ozzie Isaacs 109198a374 Merge branch 'master' into Develop 2021-05-16 09:03:58 +02:00
Ozzie Isaacs dafc68f049 Updated requirements 2021-05-16 09:03:47 +02:00
Ozzie Isaacs 3e5c944365 Changed backend for comic reader, now supports rar5 2021-05-16 08:35:20 +02:00
Ozzie Isaacs d95838309e Added config option for shelf sync behavior 2021-05-15 10:45:51 +02:00
Ozzie Isaacs 4745fc0db1 Bugfix search for publish dates and custom column dates
Bugfix save umplauts in comments unescaped (as calibre does)
Improved testability
2021-05-15 07:51:32 +02:00
Ozzie Isaacs b3648187ff Bugfix search for publishdates and custom column dates
Bugfix save umplauts in comments unescaped (as calibre does)
Improved testability
2021-05-15 07:51:12 +02:00
cbartondock b35483edf8 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-14 23:31:05 -04:00
Ozzie Isaacs f29088e854 Merge branch 'master' into Develop
# Conflicts:
#	cps/kobo.py
2021-05-14 19:59:35 +02:00
Ozzie Isaacs b009dfe4ee Always use bootstrap datepicker 2021-05-14 19:58:06 +02:00
Ozzie Isaacs eba94f430c Updated js libs 2021-05-14 09:29:24 +02:00
Ozzie Isaacs 06347b7e3c Bugfixes from testrun 2021-05-14 08:26:56 +02:00
Ozzie Isaacs 6bc426b21d Fix edit_books.js 2021-05-14 07:29:48 +02:00
cbartondock e0fac8d2c0 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-13 11:25:59 -04:00
Ozzie Isaacs 4a849aab7f Merge remote-tracking branch 'requirements/patch-1' into master 2021-05-13 16:28:54 +02:00
Ozzie Isaacs ad2d0c84e4 Update swedish translation 2021-05-13 16:28:02 +02:00
Ozzie Isaacs 6bf360fbfb Added "comments" type to supported custom columns 2021-05-13 14:00:01 +02:00
Ozzie Isaacs 380292a8aa Custom column datetime now searchable (#1984) 2021-05-13 13:31:36 +02:00
jonyxx-alt f9949da745 Update Swedish translation
Upload files that has been updated
2021-05-13 11:04:57 +00:00
Ozzie Isaacs b0cc52e0aa Enable custom column datetime (adv. search not working yet) #1984 2021-05-13 10:39:36 +02:00
Ozzie Isaacs 9ef705650b Access pages with stored order per default (#1987)
Fix for toggle-text of enable-sort button on shelf page
2021-05-13 09:41:27 +02:00
Ozzie Isaacs c6dadbe75e restrictions are now applied to kobo sync requests (Fix #1938) 2021-05-13 09:17:01 +02:00
cbartondock 0a65c05083 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-07 09:05:27 -04:00
Robert Schütz 21026340ad remove singledispatch from requirements
It's not used anywhere.
2021-05-05 14:30:00 +02:00
Ozzieisaacs eb2273247f Changed texts in edit shelf page for Kobo shelf sync 2021-05-02 15:59:40 +02:00
Ozzieisaacs 02fc698f1c Some little code refactoring 2021-05-02 10:05:25 +02:00
Ozzieisaacs 8dc11e89bd Merge remote-tracking branch 'koko_sync/feature/kobo-shelf' into Develop 2021-05-02 09:43:47 +02:00
cbartondock 12c3264000 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-05-01 21:55:09 -04:00
Ozzieisaacs b34672ed19 Merge branch 'master' into Develop 2021-05-01 20:54:01 +02:00
Ozzieisaacs 541c8c4b93 Improved error handling for disapearing custom column linked to visiblility restrictions 2021-05-01 20:52:48 +02:00
Ozzieisaacs b97373bf37 Improved error handling for disapearing custom column linked to read status 2021-05-01 18:42:57 +02:00
Ozzieisaacs c0b561cb5a Better input check for custom_columns 2021-05-01 17:10:29 +02:00
Ozzieisaacs bd3ccfd0a9 Merge remote-tracking branch 'cn/master' into master 2021-05-01 16:33:47 +02:00
Ozzieisaacs 5470acd3af Fix #1964 (RefreshError with unconfigured gdrive) 2021-05-01 12:45:28 +02:00
Ozzie Isaacs 64696fe973 Bugfixes edit user list (Fix #1938) 2021-05-01 08:36:39 +02:00
cbartondock f2cd93dc54 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-29 10:01:18 -04:00
Ozzie Isaacs ed2fa4cdd8 Update error handling for user list (#1938) 2021-04-28 20:49:16 +02:00
cbartondock c2c4636961 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-28 08:43:44 -04:00
dalin 03cca1d1c0 update Simplified Chinese 2021-04-28 16:15:21 +08:00
dalin 354b075885 Merge remote-tracking branch 'upstream/master' 2021-04-28 14:15:42 +08:00
Ozzie Isaacs de7f039b99 Update list user editor (#1938) 2021-04-27 20:01:17 +02:00
cbartondock c2e530af17 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-26 13:49:25 -04:00
Ozzie Isaacs 19797d5d23 Merge remote-tracking branch 'it/patch-24' into master 2021-04-26 19:05:25 +02:00
Ozzie Isaacs 144c2b5fc7 Renaming ipadress - ip_address
Bugfix user table
Result testrun
Updated cn translation
2021-04-26 19:03:01 +02:00
cbartondock 577b525508 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-26 10:23:24 -04:00
GarcaMan 7da40d1c2e Added option to remove Y-Scrollbar and reset page scroll 2021-04-25 19:59:53 +00:00
Ozzie Isaacs 4e3a5ca33b Added additional debug messages, catch additional errors in updater 2021-04-25 11:30:53 +02:00
Ozzie Isaacs 97e4707f72 user list column value and tags editor (#1938)
Bugfixes for massedit in user list (#1938)
Changed no. of debug messages
2021-04-25 10:43:01 +02:00
cbartondock 541dd6b579 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-22 18:12:19 -04:00
Ozzie Isaacs 450ee43677 Additional logging for emails 2021-04-22 19:14:56 +02:00
cbartondock e57229e670 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-22 12:22:45 -04:00
Ozzie Isaacs c0b2e886d2 bugfix books list with selected books and stored on state 2021-04-21 19:28:22 +02:00
Ozzie Isaacs bb4749c65b Result for testrun
Added mass delete of users
refactores user table refresh
Bugfix for sorting with selected users
Bugfix delete books #1938
2021-04-21 19:23:56 +02:00
cbartondock 1ef2a96465 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-19 12:20:54 -04:00
ElQuimm 5aa37c68a2 Updated version of italian.po
Thank you.
2021-04-19 10:11:26 +02:00
Ozzie Isaacs 6e5a1a1f4d Merge remote-tracking branch 'add_Babelio/patch-1' into master 2021-04-18 12:05:00 +02:00
Ozzie Isaacs b624ce16c3 Merge remote-tracking branch 'it/patch-23' into master 2021-04-18 12:04:08 +02:00
Ozzie Isaacs 3be5b5f919 Merge remote-tracking branch 'comic_reader/master' into master 2021-04-18 12:02:21 +02:00
Ozzie Isaacs 04ac5b69ac Further improvements on user list #1938 2021-04-18 12:01:11 +02:00
Ozzie Isaacs 9cc14ac5c7 Fix for #1905 prevent invalid json data in gmail_token 2021-04-18 11:33:14 +02:00
BuildTools 755eb1405b Fix progress bar direction 2021-04-18 04:25:45 +09:00
subdiox 0138294b36 Fix an issue that space key doesn't work intentionally when inverting reading direction 2021-04-18 03:20:41 +09:00
Ozzie Isaacs c0a4addf30 Cancel button in User edit now leads to the right page back #1938 2021-04-17 18:30:55 +02:00
Ozzie Isaacs 39bbee0eeb Books sort with non Books table column working #1938 2021-04-17 10:27:30 +02:00
Ozzie Isaacs 39dda3f534 Fix opds login with colon in password #1952 2021-04-15 18:02:52 +02:00
Ozzie Isaacs 1cb8dbe795 Update migration routine 2021-04-15 17:42:39 +02:00
cbartondock 85f1ca6101 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-15 08:43:28 -04:00
Ozzie Isaacs e13820bbf0 Update exception upon migration of database #1935 2021-04-15 12:05:03 +02:00
malletfils 3973362457 Update db.py
Just adding support for Babelio (french website about books) in the link section
2021-04-14 19:57:02 +02:00
cbartondock 2f3f13afb9 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-14 09:30:05 -04:00
Ozzie Isaacs b38877e193 Document change 2021-04-13 19:47:55 +02:00
Ozzie Isaacs 0e1dbb5377 Copy author names for displaying (#1935) 2021-04-13 19:41:44 +02:00
Ozzie Isaacs f07cc8b103 Update optional-requirements: flask-dance
Catch error for invalid oauth tokens
Fixes for displaying error messages on deleting books from list
Fixes for displaying error messages on deleting bookformats
Removed non working sorting in books list
2021-04-13 19:26:10 +02:00
Ozzie Isaacs 67775bc797 Update requirements
Catch error for invalid oauth tokens
Fixes for displaying error messages on deleting books from list
Fixes for displaying error messages on deleting bookformats
2021-04-13 19:08:02 +02:00
ElQuimm 05933a5f0c Updated italian message.po
I have not removed the #, fuzzy lines, but modified the corresponding values.
2021-04-13 18:51:50 +02:00
cbartondock e15ebd5aac Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-12 19:02:26 -04:00
Ozzie Isaacs d32b2ca524 Prevent traceback after delete user
flash message in case last admin role is removed #1938
2021-04-12 19:04:27 +02:00
Ozzie Isaacs d0a895628e Prevent delete of Guest user #1938 2021-04-12 18:45:44 +02:00
Ozzie Isaacs 90f2b3fb21 Refactored list checkbox sort 2021-04-12 18:39:09 +02:00
Ozzie Isaacs 0f95800dde Update sqlalchemy-utils dependency for oauth
Sort for state checkbox in user list and books list working
2021-04-12 18:23:25 +02:00
cbartondock ff99cd2456 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-11 16:26:55 -04:00
Ozzieisaacs 04971f8672 Bugfix wrong js file in user list 2021-04-11 20:01:40 +02:00
Ozzieisaacs b6177b27f4 Sorting of users in table according to selection possible 2021-04-11 19:59:20 +02:00
Ozzieisaacs ae97e87506 Delete user working from user table (#1938)
Comment in helper
2021-04-10 11:32:25 +02:00
cbartondock df91ca500f Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-08 20:25:02 -04:00
Ozzie Isaacs 2d73f541c0 Bugfix sort books list and user list
Prevent transferring password hash to client
2021-04-08 19:37:08 +02:00
Ozzie Isaacs 7561eabe52 Implement Backend to deny editing Guest rights restriction #1938 2021-04-07 18:56:17 +02:00
Ozzie Isaacs 067fb1b0b7 Prevent delete Guest user and redirect to admin page after user delete 2021-04-07 18:47:48 +02:00
Ozzie Isaacs 78071841cc After Deleting a book the list page is still displayed #1938 2021-04-07 18:19:48 +02:00
Ozzie Isaacs fac232229e Added missing ead/unread category in user list #1938 2021-04-07 17:49:19 +02:00
Ozzie Isaacs a43021e87c Fix for #1938 2021-04-07 17:46:17 +02:00
cbartondock eb599373ee prevented dropdowns from going off screen in caliblur 2021-04-06 13:21:36 -04:00
cbartondock 7b529b5f9e merge remote 2021-04-06 13:05:55 -04:00
Ozzie Isaacs 1a0de3b3cf Merge remote-tracking branch 'de_translation/patch-1' into master 2021-04-06 18:08:32 +02:00
Ozzie Isaacs 947154dc9c Merge branch 'development' into master 2021-04-06 18:06:25 +02:00
Ozzie Isaacs 8acd1f1fe4 Code refactoring and improved error handling for edit user list
Update teststatus
2021-04-06 17:29:42 +02:00
Ozzie Isaacs 665210e506 Fixes for book edit with title sort and author sort 2021-04-04 14:01:06 +02:00
Ozzie Isaacs 91b9370a21 Merge branch 'master' into development
# Conflicts:
#	cps/static/css/libs/bootstrap-table.min.css
#	cps/static/js/libs/bootstrap-table/bootstrap-table-editable.min.js
#	cps/static/js/libs/bootstrap-table/bootstrap-table.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-af-ZA.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ar-SA.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-bg-BG.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ca-ES.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-cs-CZ.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-da-DK.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-de-DE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-el-GR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-en-US.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-AR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-CL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-CR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-ES.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-MX.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-NI.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-es-SP.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-et-EE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-eu-EU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fa-IR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fi-FI.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-BE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-CH.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-FR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-fr-LU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-he-IL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-hr-HR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-hu-HU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-id-ID.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-it-IT.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ja-JP.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ka-GE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ko-KR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ms-MY.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nb-NO.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nl-BE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-nl-NL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-pl-PL.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-pt-BR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-pt-PT.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ro-RO.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ru-RU.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sk-SK.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sr-Cyrl-RS.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sr-Latn-RS.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-sv-SE.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-th-TH.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-tr-TR.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-uk-UA.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-ur-PK.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-uz-Latn-UZ.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-vi-VN.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-zh-CN.min.js
#	cps/static/js/libs/bootstrap-table/locale/bootstrap-table-zh-TW.min.js
2021-04-02 17:13:22 +02:00
Ozzie Isaacs ca0aa600b4 Bugfix german translation 2021-04-02 17:12:26 +02:00
Ozzie Isaacs 1c42c4c969 Update bootstrap-table
Fixes for book edit
2021-04-02 17:11:16 +02:00
cbartondock 389f0ae452 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-04-02 10:41:25 -04:00
Ozzie Isaacs 6e406311c3 Fix #1928 (ldap_import_user_filter missing in to_save) 2021-04-02 08:40:29 +02:00
cbartondock 00a24756f4 get rid of ugly outline on hover over book 2021-04-01 14:06:24 -04:00
cbartondock 9025f312cc Fixed read in browser button in caliblur theme 2021-04-01 13:53:53 -04:00
cbartondock d5e453edd3 added scholarly to libraries in about page 2021-03-31 22:43:23 -04:00
cbartondock 26cc64bdd6 Use google scholar to fetch metadata 2021-03-31 22:16:00 -04:00
robochud 87884c1af2 Update messages.po
very little typo
2021-03-31 00:56:12 +02:00
Ozzie Isaacs c0623a949c Merge remote-tracking branch 'caliblur/master' into master 2021-03-29 20:54:04 +02:00
cbartondock 42a23ea23c removed some dumb graphical inconsistencies 2021-03-29 14:35:48 -04:00
cbartondock 710c31d1ca Fixed book details not loading after infinite scroll 2021-03-29 14:13:22 -04:00
cbartondock f637ba4dad Fixed border display in book details 2021-03-29 13:52:09 -04:00
Ozzie Isaacs 80d2c60cef Update testresults 2021-03-29 18:03:09 +02:00
Ozzie Isaacs b17d71e4c3 Update optional-requirements.txt
Bugfix Mail task after convert task
2021-03-28 20:36:08 +02:00
Ozzie Isaacs 3c5bd3a605 Added gmail.json file to excluded file, added file also to excluded file during update 2021-03-28 15:01:56 +02:00
Ozzie Isaacs 7cc8d1e693 remove file 2021-03-28 14:58:07 +02:00
Ozzie Isaacs 99520d54a5 Added ability to send emails via gmail (#1905)
Gmail email sending
2021-03-28 14:51:01 +02:00
Ozzie Isaacs e10a8c078b Merge branch 'master' into development
# Conflicts:
#	cps/templates/email_edit.html
2021-03-27 18:36:11 +01:00
Ozzie Isaacs 7a196fed7c Prevent empty email port and server address(#1906) 2021-03-27 18:34:57 +01:00
Ozzie Isaacs c418c7e725 Prevent empty email from field (#1906) 2021-03-27 18:31:49 +01:00
Ozzie Isaacs e35748deff Fix wording and tabs 2021-03-27 17:30:54 +01:00
Ozzie Isaacs b57efbe31c Fix wording and tabs 2021-03-27 17:30:10 +01:00
Ozzie Isaacs 8f91437701 Implementation for gmail server with OAuth2 started 2021-03-27 16:36:24 +01:00
Ozzie Isaacs a7eb547ca4 Renamed brazilian datepicker file 2021-03-27 12:40:23 +01:00
Ozzie Isaacs 0ceb12f74f Renamed brazilian datepicker file 2021-03-27 12:40:01 +01:00
Ozzie Isaacs 4960624de7 Merge branch 'master' into development 2021-03-27 12:20:28 +01:00
Ozzie Isaacs 2c92f24d89 Added Brazilian translation 2021-03-27 12:20:07 +01:00
Ozzie Isaacs 0b7c679dba Changed test email message (fix #657) 2021-03-27 09:48:43 +01:00
Ozzie Isaacs aab3cdc58a Changed test email message 2021-03-27 09:47:43 +01:00
Ozzie Isaacs 3c63f5b204 Merge branch 'master' into development 2021-03-27 09:46:12 +01:00
Ozzie Isaacs 0b97bbf827 Merge remote-tracking branch 'div_a/use-kobo-for-download-link' into master 2021-03-27 09:39:44 +01:00
Ozzie Isaacs 970dbb0c59 Merge branch 'master' into development
# Conflicts:
#	cps/opds.py
#	cps/templates/admin.html
2021-03-27 09:38:12 +01:00
Ozzie Isaacs d21d3c2ceb Merge remote-tracking branch 'div_a/use-btn-anchors' into master 2021-03-27 09:33:28 +01:00
Ozzie Isaacs 0f6493b8ce Update Teststatus 2021-03-27 09:30:59 +01:00
Ozzie Isaacs f26beec1d3 Updated pdf reader 2021-03-25 18:57:40 +01:00
Ozzie Isaacs c4225e29ed Updated pdf reader 2021-03-25 18:57:25 +01:00
Ozzie Isaacs 9243326cd3 Updated testresult 2021-03-25 18:30:31 +01:00
Ozzie Isaacs 86beb8023a Downgrade sqlachemy, as it breaks oauth 2021-03-24 18:47:59 +01:00
Ozzie Isaacs f4e134742b Fix updater
Bugfixes from testruns
Update Testresults
2021-03-24 18:46:03 +01:00
Ozzie Isaacs 2b17bf4114 Update tests
Fixes upload book
2021-03-23 18:57:04 +01:00
Ozzie Isaacs c4f0fc8f7b Update tests
Fixes upload book
2021-03-23 17:57:49 +01:00
Ozzie Isaacs 3d6c836e7d Update tests 2021-03-23 17:39:51 +01:00
Gavin Mogan c279055af4 Use the kobo auth'd version for download link when proxied -- Fixes #1908 2021-03-22 18:08:41 -07:00
Gavin Mogan 657cba042a Use btn classes on anchors not div so the entire button is clickable 2021-03-22 17:25:38 -07:00
Ozzie Isaacs 7a58e48cae Bugfixes opds feed 2021-03-22 19:01:18 +01:00
Ozzie Isaacs 670eab62bf Update opds feed with letters for all titles, authors, categories and series 2021-03-22 17:46:15 +01:00
Ozzie Isaacs fc85586809 Bugfixes for sqlalchemy 1.4.0 2021-03-22 16:33:21 +01:00
Ozzie Isaacs 837fc4988d Letterize authors 2021-03-21 20:21:10 +01:00
Ozzie Isaacs 4664b47851 Fixed alphabetical order in list and grid view
Completed download section
2021-03-21 19:31:32 +01:00
Ozzie Isaacs 9864d932e0 Changed user.nickname to user.name
Added function to view downloads of all users for admins
2021-03-21 18:55:02 +01:00
Ozzie Isaacs 436f60caa9 refactored some functions 2021-03-21 14:17:07 +01:00
Ozzie Isaacs e9530eda9d Bugfix from refactoring 2021-03-21 13:46:13 +01:00
Ozzie Isaacs f4ddac16f9 Improved error logging for #1905 2021-03-21 11:58:51 +01:00
Ozzie Isaacs 33bdc07f55 Fix for #1845 (ods not working in Moonreader an Librera)
Fix opds search with wrong parameter no longer causes error 500
2021-03-21 11:57:42 +01:00
Ozzie Isaacs 130a4ed2d3 Fix opds and error logging in mail 2021-03-21 11:54:39 +01:00
Ozzie Isaacs 59ebc1af8a Code refactoring 2021-03-21 08:20:04 +01:00
Ozzie Isaacs 10731696df Merge branch 'development' into cover_thumb
# Conflicts:
#	cps/admin.py
2021-03-21 07:29:35 +01:00
Ozzie Isaacs 82e15d2e98 Merge branch 'master' into development
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-03-21 07:28:52 +01:00
Ozzie Isaacs 9c842f1895 Bugfixes pdf xmp parsing 2021-03-21 07:28:35 +01:00
Ozzie Isaacs 8cc849488b Code cosmetics 2021-03-21 07:15:38 +01:00
Ozzie Isaacs fcaa232967 Merge remote-tracking branch 'origin/master' into master 2021-03-21 07:11:37 +01:00
Ozzie Isaacs d3f8153b90 Merge branch 'master' into development 2021-03-20 19:34:16 +01:00
Ozzie Isaacs 99ecf22790 Merge remote-tracking branch 'origin/master' into master 2021-03-20 18:10:42 +01:00
Ozzie Isaacs b1b7ee65b4 Refactored advanced search 2021-03-20 18:10:27 +01:00
Ozzie Isaacs 6889456662 Merge remote-tracking branch 'adv_search/master' into master 2021-03-20 17:48:21 +01:00
Ozzie Isaacs 081553dc9f Disable blank issue templates 2021-03-20 16:27:47 +01:00
Ozzie Isaacs 1e7a2c400b Fixed misstyping 2021-03-20 14:06:15 +01:00
Ozzie Isaacs 30e897af48 Merge remote-tracking branch 'cover/thumbnails' into cover_thumb
# Conflicts:
#	cps/admin.py
#	cps/templates/author.html
#	cps/templates/layout.html
#	cps/ub.py
#	cps/web.py
#	requirements.txt
2021-03-20 12:52:17 +01:00
Ozzie Isaacs dd30ac4fbd Added thumbnails
Merge remote-tracking branch 'cover/thumbnails' into development

# Conflicts:
#	cps/admin.py
#	cps/templates/layout.html
#	cps/ub.py
#	cps/web.py
Update join for sqlalchemy 1.4
2021-03-20 11:32:50 +01:00
Ozzie Isaacs 5d8d796807 Compatibility for sqlalchemy 2.0 2021-03-20 10:09:08 +01:00
Ozzie Isaacs f3d88fc746 Update sqlalchemy 1.4 working 2021-03-20 09:31:29 +01:00
Ozzie Isaacs d87ccae6c9 Merge branch 'master' into development
# Conflicts:
#	cps/server.py
#	test/Calibre-Web TestSummary_Linux.html
2021-03-19 20:33:38 +01:00
Ozzie Isaacs 2760a7816d Fix metadata recognition fb2 files 2021-03-19 20:32:15 +01:00
Ozzie Isaacs 8f5c649d0f Updated dependency comicapi (removed pypdf2)
Updated dependency pyPDF2 ->pyPDF3
Fixed broken updater in case of http error
Bugfixes from testrun
Bugfix load cover for BasicMetadata
2021-03-19 13:35:52 +01:00
Ozzie Isaacs fcf9e7a1ef Upload pdf fixes:
Handle no title
Handle no author
Fix import of more than one language
Add missing pdf upload publisher handling
2021-03-17 19:06:51 +01:00
Ozzie Isaacs 2be7b6480a Merge remote-tracking branch 'pdf/XMP_Metadata3' into master 2021-03-17 19:05:26 +01:00
cbartondock f5ded86c02 removed extraneous logs 2021-03-16 19:18:57 -04:00
rra 8abfaf0ffd Parse XMP metadata in separate function, add exception, try multiple metadata formats 2021-03-16 17:53:33 +01:00
Ozzie Isaacs b070ba142f Selects are working in user management with generic confirm dialog 2021-03-16 15:52:31 +01:00
Ozzie Isaacs bd7c6828bf Selects are working in user management 2021-03-16 15:03:38 +01:00
cbartondock d0671ec58c Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-03-15 21:09:51 -04:00
cbartondock 1e40ffd1cc merge changes 2021-03-15 21:08:41 -04:00
Ozzie Isaacs da2c3e9ed7 Edit user mass change checkbox working 2021-03-15 16:46:27 +01:00
Ozzie Isaacs f62d6abb69 Merge branch 'master' into development
# Conflicts:
#	cps/server.py
#	test/Calibre-Web TestSummary_Linux.html
2021-03-15 13:53:23 +01:00
Ozzie Isaacs 12ad7a6322 Suppress some errors 2021-03-15 13:52:44 +01:00
Ozzie Isaacs b75247ea3a Suppress some errors 2021-03-15 13:48:05 +01:00
Ozzie Isaacs 9a963bbe79 Refactored code
Testrun
2021-03-15 13:10:17 +01:00
Ozzie Isaacs 994bc8b0e4 Removed dropdown button on single file listen/read (Fix #1840) 2021-03-15 09:06:40 +01:00
Ozzie Isaacs 2451605033 Merge remote-tracking branch 'djvu/read-djvu' into master 2021-03-15 08:43:25 +01:00
Ozzie Isaacs 10942527f3 Merge branch 'master' into development
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-03-15 08:22:12 +01:00
Ozzie Isaacs 4909ed5ccd Merge remote-tracking branch 'cliblur/master' into development
# Conflicts:
#	cps/static/css/style.css
2021-03-15 08:21:44 +01:00
Ozzie Isaacs 5cf5df68dc Merge remote-tracking branch 'cliblur/master' into master 2021-03-15 08:19:56 +01:00
Ozzie Isaacs dd32cc99ea Merge remote-tracking branch 'es/patch-1' into master 2021-03-15 08:17:14 +01:00
Ozzie Isaacs 79092dc8eb Merge remote-tracking branch 'it/patch-21' into master 2021-03-15 08:11:41 +01:00
Ozzie Isaacs 6229e4610a Updated teststatus and bugfix convert ebooks 2021-03-15 08:10:42 +01:00
Northguy bfe36d3f4a change in color definition
Making color the same as in:
.container-fluid .book .meta .author, .container-fluid .book .meta .author > a {
    color: hsla(0, 0%, 100%, .45)
}
2021-03-15 00:12:02 +01:00
Northguy d42bf44fad Added same color for series name and series number 2021-03-14 23:46:33 +01:00
rra 33e352819c Merge remote-tracking branch 'upstream/master' into XMP_Metadata3 2021-03-14 19:58:26 +01:00
Ozzie Isaacs 53ee0aaee1 Some functions refactored 2021-03-14 17:34:47 +01:00
Ozzie Isaacs 42707a19bd Code cosmetics 2021-03-14 16:57:33 +01:00
Ozzie Isaacs 0888706790 Code cosmetics 2021-03-14 16:53:01 +01:00
Ozzie Isaacs 16453a05f8 Code cosmetics 2021-03-14 16:02:37 +01:00
Ozzie Isaacs 2fbb7466d3 Code cosmetics 2021-03-14 15:44:40 +01:00
Ozzie Isaacs f29f94f45f Code cosmetics 2021-03-14 15:41:36 +01:00
Ozzie Isaacs cd973868fc Code cosmetics 2021-03-14 15:29:34 +01:00
Ozzie Isaacs 3c35f02cac Code cosmetics 2021-03-14 15:18:45 +01:00
Ozzie Isaacs 22c93e2389 Merge branch 'master' into development 2021-03-14 15:06:20 +01:00
Ozzie Isaacs 8c751eb532 Some code cosmetics 2021-03-14 15:06:09 +01:00
Ozzie Isaacs 4df443e007 Some code cosmetics 2021-03-14 14:40:04 +01:00
Ozzie Isaacs f52fa41439 Fix restart server
Some code cosmetics
2021-03-14 14:29:40 +01:00
Ozzie Isaacs f77d72fd86 Merge branch 'master' into development
# Conflicts:
#	cps/static/css/style.css
2021-03-14 14:06:33 +01:00
Ozzie Isaacs 9b80c84794 Some code cosmetics 2021-03-14 14:05:36 +01:00
Ozzie Isaacs 725fc658f8 Some code cosmetics 2021-03-14 13:28:52 +01:00
alfred82santa 24bbf226a1 Sync reading state only for books on kobo shelves 2021-03-14 11:29:23 +01:00
Ozzie Isaacs e4e27662f5 Merge remote-tracking branch 'url/patch-1' into master 2021-03-14 08:35:37 +01:00
cbartondock 83474da7b5 fixed being able to click through dropdown in advanced search results 2021-03-07 22:54:34 -05:00
cbartondock 9146e5f287 fixed advanced search mass add to shelves in caliblur 2021-03-07 19:53:54 -05:00
cbartondock ff4502c63a Made shelf search user specific 2021-03-07 15:23:05 -05:00
cbartondock 9711bd8fe1 added actions boxes to drop downs 2021-03-06 22:34:39 -05:00
cbartondock 05139e53be advanced shelf search is working 2021-03-06 22:15:18 -05:00
cbartondock 870b2642a9 advanced shelf search is almost working 2021-03-06 21:54:39 -05:00
ElQuimm d31b26ae7d Updated italian message.po
Have a nice day.
2021-03-04 14:26:36 +01:00
Zaroz 5511925ba2 Removed elif "url" identifier format type
Removed elif "url" identifier format type, since else output is identical
2021-03-03 21:18:08 -06:00
Angel Docampo f96b20717d Updated spanish messages.po
Corrected a grammatical error
2021-03-04 00:01:03 +01:00
Zaroz 940c9c45d7 Updated db.py
changed __repr__ else statement to allow for custom id entries
2021-03-02 23:46:02 -06:00
Ozzie Isaacs 87d6008dfc Fix for #1870 (png was missing in supported picture formats for comic viewer) 2021-02-27 09:23:34 +01:00
Ozzie Isaacs b9c0c8d2dc Update bootstrap table to 1.18.2 2021-02-20 20:03:39 +01:00
Ozzie Isaacs 81c30d5fd5 Add missing files from bootstrap editable
Added user-table single-select header for locale and default-language
2021-02-20 19:55:51 +01:00
Ozzie Isaacs 0aa33d88a5 Merge branch 'master' into development 2021-02-20 14:19:39 +01:00
Ozzie Isaacs e64a504bb1 Working Locale and default language selection in user table edit 2021-02-20 14:18:27 +01:00
jvoisin bc876a159e Declare variables before using them
It should fix the following stacktrace:

```
[2021-02-18 14:46:14,771] ERROR {cps:1891} Exception on / [GET]
Traceback (most recent call last):
  File "/opt/calibre/vendor/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/opt/calibre/vendor/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/opt/calibre/vendor/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/opt/calibre/vendor/flask/_compat.py", line 39, in reraise
    raise value
  File "/opt/calibre/vendor/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/opt/calibre/vendor/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/calibre/cps/usermanagement.py", line 38, in decorated_view
    return login_required(func)(*args, **kwargs)
  File "/opt/calibre/vendor/flask_login/utils.py", line 272, in decorated_view
    return func(*args, **kwargs)
  File "/opt/calibre/cps/web.py", line 719, in index
    return render_books_list("newest", sort_param, 1, page)
  File "/opt/calibre/cps/web.py", line 422, in render_books_list
    entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
  File "/opt/calibre/cps/db.py", line 610, in fill_indexpage
    return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
  File "/opt/calibre/cps/db.py", line 635, in fill_indexpage_with_archived_books
    #    book = self.order_authors(book)
UnboundLocalError: local variable 'entries' referenced before assignment
```
2021-02-18 17:02:58 +01:00
Ozzieisaacs 4aa1a838ed Removed f-strings, making it compatible with python 3.5 2021-02-15 14:17:05 +01:00
Ozzieisaacs 095a51edd0 Convert tabs to spaces in server.py (related to #1862) 2021-02-15 13:33:42 +01:00
Ozzie Isaacs a3a11bdf3f Changed checkbox states are saved on server 2021-02-14 17:49:40 +01:00
Ozzie Isaacs 70b503f3d4 Merge branch 'master' into development
# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
2021-02-13 14:36:42 +01:00
Ozzie Isaacs bbf609b880 Fix #1859 (Upload of jpg files without installed imagemagick doesn't work) 2021-02-13 14:34:41 +01:00
Ozzie Isaacs 0992bafe30 Bulk User management 2021-02-13 13:17:02 +01:00
Ozzie Isaacs c810c5275a Merge branch 'master' into development 2021-02-09 18:05:32 +01:00
Ozzie Isaacs 3c1b06872d Updated testresults
Changed optional-requirements
Prevent redirect 308 on /<data> routes (#1854)
2021-02-09 18:04:56 +01:00
Ozzie Isaacs cefdd2f66c suppress asyncio message always on restart or shutdown 2021-02-08 20:39:21 +01:00
Ozzie Isaacs 5dac13b1da Revert change in ub.py 2021-02-08 20:17:02 +01:00
alfred82santa 6014b04b2a Use BookShelf added_date as date reference 2021-02-08 20:10:12 +01:00
Ozzie Isaacs 8aebf48193 Update handling for package data 2021-02-08 20:09:36 +01:00
Ozzie Isaacs fbb905957b Prevent redirect 308 on /<data> routes 2021-02-08 20:09:36 +01:00
Ozzie Isaacs e0ce135838 Move "create_engine" call 2021-02-08 20:09:36 +01:00
Ozzie Isaacs 60497c60c1 Update creating Home_Config variable for packaging support, added .HOMEDIR to ignored filename during update 2021-02-07 09:20:37 +01:00
Ozzie Isaacs 251a77c8b4 Merge branch 'master' into Development
Improved packaging support
2021-02-07 08:54:29 +01:00
alfred82santa 2b7c1345ee Fix disable shelf kobo sync 2021-02-07 00:21:51 +01:00
alfred82santa 69b7d94774 Fixes and remove shelf kobo sync flag when kobo sync disabled 2021-02-07 00:19:24 +01:00
Ozzie Isaacs 7b7494b8a4 Added executable folder to gitignore 2021-02-06 21:42:29 +01:00
alfred82santa 8fe762709b Fix mistake 2021-02-06 21:15:36 +01:00
alfred82santa a3f17deb17 Added options in order to synchronize only selected shelf on Kobo device 2021-02-06 20:29:43 +01:00
Ozzie Isaacs 9390dcdd43 Update optional requirements 2021-02-06 11:33:20 +01:00
Ozzie Isaacs e6fb460071 New restart routine for executable files
Fix log in updater
New source option updater available
2021-02-06 09:42:27 +01:00
Ozzie Isaacs 6137fdeb33 Version bump 2021-02-02 19:11:02 +01:00
Ozzie Isaacs 4a4d02ea6a Added translations 2021-02-02 19:04:55 +01:00
Ozzie Isaacs be26e5f152 Update Teststatus
Update Version
2021-02-02 18:52:31 +01:00
Ozzie Isaacs 127bfba135 fix wrong path in get_update_status js function 2021-02-01 19:00:10 +01:00
Ozzie Isaacs 7efae3c125 Renamed "Recently Added" to "Books" 2021-01-31 19:20:43 +01:00
Ozzie Isaacs 1e5af21000 Fix #1843 (get path to script via html file and known js file instead of randomly picking last one) 2021-01-31 19:01:34 +01:00
Ozzie Isaacs 33a0a4c173 Changed function for getting path in js file 2021-01-31 18:55:32 +01:00
Ozzie Isaacs eeb7974e05 User table:
Added button for single user edit
Added delete-trash-icon (not working)
Roles are displayed correct per user
Guest user is not visible if anonymous browsing is disabled
2021-01-31 14:54:45 +01:00
Ozzie Isaacs f45ea1a31c Merge branch 'master' into development 2021-01-31 13:21:47 +01:00
Ozzie Isaacs a866dbaa80 Fix mimetype in comic reader 2021-01-31 11:21:09 +01:00
Ozzie Isaacs 62447d6b89 Basic User edit in tables 2021-01-31 10:17:57 +01:00
Ozzie Isaacs 93f0724b83 Merge branch 'development' into master 2021-01-31 09:11:35 +01:00
Ozzie Isaacs 724762843d Update Teststatus 2021-01-31 09:10:42 +01:00
Ozzie Isaacs ff16afbf0b Merge branch 'master' into development 2021-01-30 17:55:45 +01:00
Ozzie Isaacs 9d7ef25062 Fix Kobo sync & Update Teststatus 2021-01-30 17:55:17 +01:00
Ozzie Isaacs 88078d65e9 Fix #1482 (do not shorten title in book detail page) 2021-01-30 14:57:59 +01:00
Ozzie Isaacs b07a97c17e Merge remote-tracking branch 'webp/comic-webp' into master 2021-01-30 14:56:26 +01:00
Ozzie Isaacs 41e7d65e2a Merge remote-tracking branch 'caliblur/master' into master 2021-01-30 14:54:16 +01:00
Ozzie Isaacs 7fa5865cf6 Merge remote-tracking branch 'autoconv/kepub_autoconv' into development
# Conflicts:
#	cps/kobo.py
#	cps/kobo_auth.py
2021-01-30 14:47:18 +01:00
Ozzie Isaacs e0b8fe3b1a Merge branch 'master' into development 2021-01-30 14:41:46 +01:00
Ozzie Isaacs 4a11dd1e16 Fix mistyping "option" 2021-01-28 20:20:59 +01:00
Ozzie Isaacs 34a474101f Fix #1838 (Log message on failed OPDS login) 2021-01-28 20:05:58 +01:00
Ozzie Isaacs e6799e7a04 Improved detection of invalid email addresses (#1831) upon registering 2021-01-27 19:18:40 +01:00
Ozzie Isaacs 0f83f9992c Added swipe support for epub reader (fixes #925)
Cleaned reader js include files
2021-01-24 14:19:46 +01:00
Ozzie Isaacs d2ad78eb40 Added swipe support for comic reader and txt reader (#925)
Bugfix for txt file not present on serving file
2021-01-24 11:28:17 +01:00
Ozzie Isaacs 0b32738f4e Fix Store UI settings in flask session for guest user (#1820)
Updated testresults
Fix Filepicker (absolute instead of abs)
2021-01-24 07:31:40 +01:00
Ozzie Isaacs a9cedb3fca Add logger to db.py 2021-01-23 13:35:30 +01:00
Ozzie Isaacs 51f9cd4bb4 Fix english language names 2021-01-18 21:24:13 +01:00
Ozzie Isaacs a1668e2411 Version Bump 2021-01-17 16:37:54 +01:00
mmonkey 2c8d055ca4 support python2.7 for the mean time 2021-01-04 12:36:40 -06:00
mmonkey 8cc06683df only python3 supported now 2021-01-04 12:28:05 -06:00
mmonkey 774799f316 resolved merge conflicts 2021-01-04 12:26:46 -06:00
mmonkey b4324cd685 Merge remote-tracking branch 'upstream/master' into master 2021-01-04 12:24:11 -06:00
Ozzieisaacs 6fe4ed3e24 Merge branch 'master' into Develop 2021-01-03 19:28:12 +01:00
mmonkey d53daaa387 Merge branch 'Develop' into thumbnails 2021-01-02 20:21:47 -06:00
mmonkey bc8bdfe385 Merge remote-tracking branch 'upstream/master' into master 2021-01-02 20:21:15 -06:00
mmonkey 35c60eaee5 Merged develop, fixed merged conflicts 2020-12-28 23:41:49 -06:00
Ethan Lin ee28e3346b Merge pull request #21 from janeczku/master
merge from janeczku/master
2020-12-29 10:31:41 +08:00
mmonkey af24d4edbe resolved merge conflicts 2020-12-24 03:04:32 -06:00
mmonkey 0cf4c7b7b7 Merge remote-tracking branch 'upstream/master' into master 2020-12-24 03:01:55 -06:00
mmonkey eef21759cd Fix generate thumbnail task messages, don't load thumbnails when the cache file has been deleted 2020-12-24 03:00:26 -06:00
mmonkey 242a2767a1 Added thumbnail urls to book cover srcsets with cache busting ids 2020-12-24 02:35:32 -06:00
mmonkey 626051e489 Added clear cache button to admin settings, updated cache busting for book cover images 2020-12-23 03:25:25 -06:00
mmonkey 541fc7e14e fixed thumbnail generate tasks, added thumbnail cleanup task, added reconnect db scheduled job 2020-12-22 17:49:21 -06:00
mmonkey e48bdf9d5a Display thumbnails on the frontend, generate thumbnails from google drive 2020-12-20 03:11:21 -06:00
mmonkey 21fce9a5b5 Added background scheduler and scheduled thumbnail generation job 2020-12-19 02:58:40 -06:00
mmonkey 774b9ae12d Added thumbnail task and database table 2020-12-19 00:49:36 -06:00
andylizi 123493ee59 Fix #1255 (support webp in comic viewer) 2020-12-17 23:54:06 +08:00
mmonkey 2d498dd138 Preserve publisher links, and fix formatting in custom columns for CaliBlur theme. 2020-12-16 00:58:45 -06:00
verglor 2343c79126 Use djvu_html5 to read djvu format in browser 2020-11-15 13:19:37 +01:00
verglor 09a5a69f86 djvu_html5 0.3.0-beta1 static assets 2020-11-15 13:09:32 +01:00
dickreckard 65929c02bc isolanguage parsing of xmp data 2020-09-11 10:49:45 +00:00
root 22466d6b98 xmp data processing added to the uploader 2020-09-11 10:08:55 +00:00
dickreckard 23fe79c618 Update uploader.py 2020-09-11 03:42:19 +02:00
dickreckard cb7727900c Update uploader.py
default cover changed from none to pdf_preview when metadata parsing fails
2020-09-07 13:30:03 +02:00
norangebit bdf6052388 Conversion to kepub when creating a sync token 2020-09-03 21:57:14 +02:00
norangebit 99d653eece Add automatic conversion in kepub
Automatic conversion from epub to kepub of new book on Kobo sync.
2020-09-01 19:27:14 +02:00
1077 changed files with 274466 additions and 176768 deletions
+1
View File
@@ -1,4 +1,5 @@
constants.py ident export-subst
/test export-ignore
/library export-ignore
cps/static/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored
+1
View File
@@ -0,0 +1 @@
custom: ["https://PayPal.Me/calibreweb",]
+19 -3
View File
@@ -6,12 +6,23 @@ labels: ''
assignees: ''
---
<!-- Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md) -->
**Describe the bug/problem**
## Short Notice from the maintainer
After 6 years of more or less intensive programming on Calibre-Web, I need a break.
The last few months, maintaining Calibre-Web has felt more like work than a hobby. I felt pressured and teased by people to solve "their" problems and merge PRs for "their" Calibre-Web.
I have turned off all notifications from GitHub/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
I will look into the issues and maybe also the PRs from time to time, but don't expect a quick response from me.
Please also have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
**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 '....'
@@ -19,21 +30,26 @@ Steps to reproduce the behavior:
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/Raspberry Pi OS]
- Python version: [e.g. python2.7]
- Calibre-Web version: [e.g. 0.6.8 or 087c4c59 (git rev-parse --short HEAD)]:
- Docker container: [None/Technosoft2000/LinuxServer]:
- Docker container: [None/LinuxServer]:
- Special Hardware: [e.g. Rasperry Pi Zero]
- Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR]
**Additional context**
Add any other context about the problem here. [e.g. access via reverse proxy, database background sync, special database location]
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+8 -1
View File
@@ -7,7 +7,14 @@ assignees: ''
---
<!-- Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md) -->
# Short Notice from the maintainer
After 6 years of more or less intensive programming on Calibre-Web, I need a break.
The last few months, maintaining Calibre-Web has felt more like work than a hobby. I felt pressured and teased by people to solve "their" problems and merge PRs for "their" Calibre-Web.
I have turned off all notifications from GitHub/Discord and will now concentrate undisturbed on the development of “my” Calibre-Web over the next few weeks/months.
I will look into the issues and maybe also the PRs from time to time, but don't expect a quick response from me.
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
**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 [...]
+6 -1
View File
@@ -6,10 +6,12 @@ __pycache__/
# Distribution / packaging
.Python
.python-version
env/
venv/
eggs/
dist/
executable/
build/
vendor/
.eggs/
@@ -21,12 +23,15 @@ vendor/
# calibre-web
*.db
*.log
cps/cache
.idea/
*.bak
*.log.*
.key
settings.yaml
gdrive_credentials
client_secrets.json
gmail.json
/.key
+6 -6
View File
@@ -20,20 +20,20 @@ Some of the user languages in Calibre-Web having missing translations. We are ha
### **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 consistent 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).
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 consistent 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. Instead, please write an email to "ozzie.fernandez.isaacs@googlemail.com".
Ensure the ***bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
Ensure the **bug was not already reported** by searching on GitHub under [Issues](https://github.com/janeczku/calibre-web/issues). Please also check if a solution for your problem can be found in the [wiki](https://github.com/janeczku/calibre-web/wiki).
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new?assignees=&labels=&template=bug_report.md&title=). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you providing the right information. Deleting the form and just pasting the stack trace doesn't speed up fixing the problem. If your issue could be resolved, consider closing the issue.
If you're unable to find an **open issue** addressing the problem, open a [new one](https://github.com/janeczku/calibre-web/issues/new/choose). Be sure to include a **title** and **clear description**, as much relevant information as possible, the **issue form** helps you provide 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 a feature 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 abilities or add further files storages, or file syncing ability. Furthermore Calibre-Web is made for home usage for company in-house usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or web site analytics integration will not be implemented.
We will not extend Calibre-Web with any more login abilities or add further files storages, or file syncing ability. Calibre-Web is made for home usage for company in-house usage, so requests regarding any sorts of social interaction capability, payment routines, search engine or website analytics integration will not be implemented.
### **Contributing code to Calibre-Web**
@@ -41,6 +41,6 @@ Open a new GitHub pull request with the patch. Ensure the PR description clearly
In case your code enhances features of Calibre-Web: Create your pull request for the development branch if your enhancement consists of more than some lines of code in a local section of Calibre-Webs code. This makes it easier to test it and check all implication before it's made public.
Please check if your code runs on Python 2.7 (still necessary in 2020) and mainly on python 3. If possible and the feature is related to operating system functions, try to check it on Windows and Linux.
Calibre-Web is automatically tested on Linux in combination with python 3.7. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on Github. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests.
Please check if your code runs with python 3, python 2 is no longer supported. If possible and the feature is related to operating system functions, try to check it on Windows and Linux.
Calibre-Web is automatically tested on Linux in combination with python 3.8. The code for testing is in a [separate repo](https://github.com/OzzieIsaacs/calibre-web-test) on GitHub. It uses unit tests and performs real system tests with selenium; it would be great if you could consider also writing some tests.
A static code analysis is done by Codacy, but it's partly broken and doesn't run automatically. You could check your code with ESLint before contributing, a configuration file can be found in the projects root folder.
+2
View File
@@ -1 +1,3 @@
graft src/calibreweb
global-exclude __pycache__
global-exclude *.pyc
Regular → Executable
+159 -61
View File
@@ -1,6 +1,30 @@
# About
# Calibre-Web
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
Calibre-Web is a web app that offers a clean and intuitive interface for browsing, reading, and downloading eBooks using a valid [Calibre](https://calibre-ebook.com) database.
[![License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE)
![Commit Activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)
[![All Releases](https://img.shields.io/github/downloads/janeczku/calibre-web/total?logo=github&style=flat-square)](https://github.com/janeczku/calibre-web/releases)
[![PyPI](https://img.shields.io/pypi/v/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/calibreweb?logo=pypi&logoColor=fff&style=flat-square)](https://pypi.org/project/calibreweb/)
[![Discord](https://img.shields.io/discord/838810113564344381?label=Discord&logo=discord&style=flat-square)](https://discord.gg/h2VsJ2NEfB)
<details>
<summary><strong>Table of Contents</strong> (click to expand)</summary>
1. [About](#calibre-web)
2. [Features](#features)
3. [Installation](#installation)
- [Installation via pip (recommended)](#installation-via-pip-recommended)
- [Quick start](#quick-start)
- [Requirements](#requirements)
4. [Docker Images](#docker-images)
5. [Troubleshooting](#troubleshooting)
6. [Contributor Recognition](#contributor-recognition)
7. [Contact](#contact)
8. [Contributing to Calibre-Web](#contributing-to-calibre-web)
</details>
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
@@ -8,82 +32,156 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
## Features
- Bootstrap 3 HTML5 interface
- full graphical setup
- User management with fine-grained per-user permissions
- Modern and responsive Bootstrap 3 HTML5 interface
- Full graphical setup
- Comprehensive user management with fine-grained per-user permissions
- Admin interface
- User Interface in czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian
- OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language
- Create a custom book collection (shelves)
- Support for editing eBook metadata and deleting eBooks from Calibre library
- Support for converting eBooks through Calibre binaries
- 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, including audio formats (.mp3, .m4a, .m4b)
- Support for Calibre Custom Columns
- Ability to hide content based on categories and Custom Column content per user
- Multilingual user interface supporting 20+ languages ([supported languages](https://github.com/janeczku/calibre-web/wiki/Translation-Status))
- OPDS feed for eBook reader apps
- Advanced search and filtering options
- Custom book collection (shelves) creation
- eBook metadata editing and deletion support
- Metadata download from various sources (extensible via plugins)
- eBook conversion through Calibre binaries
- eBook download restriction to logged-in users
- Public user registration support
- Send eBooks to E-Readers with a single click
- Sync Kobo devices with your Calibre library
- In-browser eBook reading support for multiple formats
- Upload new books in various formats, including audio formats
- Calibre Custom Columns support
- Content hiding 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
- "Magic Link" login for easy access on eReaders
- LDAP, Google/GitHub OAuth, and proxy authentication support
## Quick start
## Installation
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
5. Go to Login page
### Installation via pip (recommended)
**Default admin login:**\
*Username:* admin\
*Password:* admin123
1. **Create a virtual environment**: It’s essential to isolate your Calibre-Web installation to avoid dependency conflicts. You can create a virtual environment by running:
```
python3 -m venv calibre-web-env
```
2. **Activate the virtual environment**:
```
source calibre-web-env/bin/activate
```
3. **Install Calibre-Web**: Use pip to install the application:
```
pip install calibreweb
```
4. **Install optional features**: For additional functionality, you may need to install optional features. Refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-and-Windows) for details on what can be installed.
5. **Start Calibre-Web**: After installation, you can start the application with:
```
cps
```
**Issues with Ubuntu:**
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
*Note: Users of Raspberry Pi OS may encounter installation issues. If you do, try upgrading pip and/or installing cargo as follows:*
```
./venv/bin/python3 -m pip install --upgrade pip
sudo apt install cargo
```
### Important Links
- For additional installation examples, check the following:
- [Manual installation](https://github.com/janeczku/calibre-web/wiki/Manual-installation)
- [Linux Mint installation](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-in-Linux-Mint-19-or-20)
- [Cloud Provider setup](https://github.com/janeczku/calibre-web/wiki/How-To:-Install-Calibre-Web-on-a-Cloud-Provider)
## Quick Start
1. **Access Calibre-Web**: Open your browser and navigate to:
```
http://localhost:8083
```
or for the OPDS catalog:
```
http://localhost:8083/opds
```
2. **Log in**: Use the default admin credentials:
- **Username:** admin
- **Password:** admin123
3. **Database Setup**: If you do not have a Calibre database, download a sample from:
```
https://github.com/janeczku/calibre-web/raw/master/library/metadata.db
```
Move it out of the Calibre-Web folder to avoid overwriting during updates.
4. **Configure Calibre Database**: In the admin interface, set the `Location of Calibre database` to the path of the folder containing your Calibre library (where `metadata.db` is located) and click "Save".
5. **Google Drive Integration**: For hosting your Calibre library on Google Drive, refer to the [Google Drive integration guide](https://github.com/janeczku/calibre-web/wiki/G-Drive-Setup#using-google-drive-integration).
6. **Admin Configuration**: Configure your instance via the admin page, referring to the [Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) guides.
## Requirements
python 3.x+
Optionally, to enable on-the-fly conversion from one ebook format to another when using the send-to-kindle feature, or during editing of ebooks metadata:
[Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page.
[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `\opt\kepubify` Windows: `C:\Program Files\kepubify`.
- **Python Version**: Ensure you have Python 3.7 or newer.
- **Imagemagick**: Required for cover extraction from EPUBs. Windows users may also need to install [Ghostscript](https://ghostscript.com/releases/gsdnld.html) for PDF cover extraction.
- **Optional Tools**:
- **Calibre desktop program**: Recommended for on-the-fly conversion and metadata editing. Set the path to Calibre’s converter tool on the setup page.
- **Kepubify tool**: Needed for Kobo device support. Download the tool and place the binary in `/opt/kepubify` on Linux or `C:\Program Files\kepubify` on Windows.
## Docker Images
Pre-built Docker images are available in these Docker Hub repositories:
Pre-built Docker images are available:
#### **Technosoft2000 - x64**
+ Docker Hub - [https://hub.docker.com/r/technosoft2000/calibre-web](https://hub.docker.com/r/technosoft2000/calibre-web)
+ Github - [https://github.com/Technosoft2000/docker-calibre-web](https://github.com/Technosoft2000/docker-calibre-web)
### **LinuxServer - x64, aarch64**
- **Docker Hub**: [linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
- **GitHub**: [linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web)
- **Optional Calibre layer**: [linuxserver/docker-mods](https://github.com/linuxserver/docker-mods/tree/universal-calibre)
Includes the Calibre `ebook-convert` binary.
+ The "path to convertertool" should be set to `/opt/calibre/ebook-convert`
To include the Calibre `ebook-convert` binary (x64 only), add the environment variable:
```
DOCKER_MODS=linuxserver/mods:universal-calibre
```
in your Docker run/compose file. Omit this variable for a lightweight image.
#### **LinuxServer - x64, armhf, aarch64**
+ Docker Hub - [https://hub.docker.com/r/linuxserver/calibre-web](https://hub.docker.com/r/linuxserver/calibre-web)
+ Github - [https://github.com/linuxserver/docker-calibre-web](https://github.com/linuxserver/docker-calibre-web)
+ Github - (Optional Calibre layer) - [https://github.com/linuxserver/docker-calibre-web/tree/calibre](https://github.com/linuxserver/docker-calibre-web/tree/calibre)
- **Paths Configuration**:
- Set **Path to Calibre Binaries** to `/usr/bin`.
- Set **Path to Unrar** to `/usr/bin/unrar`.
This image has the option to pull in an extra docker manifest layer to include the Calibre `ebook-convert` binary. Just include the environmental variable `DOCKER_MODS=linuxserver/calibre-web:calibre` in your docker run/docker compose file. **(x64 only)**
If you do not need this functionality then this can be omitted, keeping the image as lightweight as possible.
Both the Calibre-Web and Calibre-Mod images are rebuilt automatically on new releases of Calibre-Web and Calibre respectively, and on updates to any included base image packages on a weekly basis if required.
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
+ The "path to unrar" should be set to `/usr/bin/unrar`
## Troubleshooting
# Wiki
- **Common Issues**:
- If you experience issues starting the application, check the log files located in the `logs` directory for error messages.
- If eBooks fail to load, verify that the `Location of Calibre database` is correctly set and that the database file is accessible.
- You need to enable uploads under `Basic settings` for this option to appear
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
- **Configuration Errors**: Ensure that your Calibre database is compatible and properly formatted. Refer to the Calibre documentation for guidance on maintaining the database.
# Contributing to Calibre-Web
- **Performance Problems**:
- If the application is slow, consider increasing the allocated resources (CPU/RAM) to your server or optimizing the Calibre database by removing duplicates and unnecessary entries.
- Regularly clear the cache in your web browser to improve loading times.
Please have a look at our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md)
- **User Management Issues**: If users are unable to log in or register, check the user permission settings in the admin interface. Ensure that registration is enabled and that users are being assigned appropriate roles.
- **Support Resources**: For additional help, consider visiting the [FAQ section](https://github.com/janeczku/calibre-web/wiki/FAQ) of the wiki or posting your questions in the [Discord community](https://discord.gg/h2VsJ2NEfB).
## Contributor Recognition
We would like to thank all the [contributors](https://github.com/janeczku/calibre-web/graphs/contributors) and maintainers of Calibre-Web for their valuable input and dedication to the project. Your contributions are greatly appreciated.
## Contact
Join us on [Discord](https://discord.gg/h2VsJ2NEfB)
For more information, How To's, and FAQs, please visit the [Wiki](https://github.com/janeczku/calibre-web/wiki)
## Contributing to Calibre-Web
To contribute, please check our [Contributing Guidelines](https://github.com/janeczku/calibre-web/blob/master/CONTRIBUTING.md). We welcome issues, feature requests, and pull requests from the community.
### Reporting Bugs
If you encounter bugs or issues, please report them in the [issues section](https://github.com/janeczku/calibre-web/issues) of the repository. Be sure to include detailed information about your setup and the problem encountered.
### Feature Requests
We welcome suggestions for new features. Please create a new issue in the repository to discuss your ideas.
## Additional Resources
- **Documentation**: Comprehensive documentation is available on the [Calibre-Web wiki](https://github.com/janeczku/calibre-web/wiki).
- **Community Contributions**: Explore the [community contributions](https://github.com/janeczku/calibre-web/pulls) to see ongoing work and how you can get involved.
---
Thank you for using Calibre-Web! We hope you enjoy managing your eBook library with our tool.
+57
View File
@@ -0,0 +1,57 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to ozzie.fernandez.isaacs@googlemail.com
## Supported Versions
To receive fixes for security vulnerabilities it is required to always upgrade to the latest version of Calibre-Web. See https://github.com/janeczku/calibre-web/releases/latest for the latest release.
## History
| Fixed in | Description | CVE number |
|---------------|----------------------------------------------------------------------------------------------------------------------------------|----------------|
| 3rd July 2018 | Guest access acts as a backdoor | |
| V 0.6.7 | Hardcoded secret key for sessions | CVE-2020-12627 |
| V 0.6.13 | Calibre-Web Metadata cross site scripting | CVE-2021-25964 |
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo | |
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) | |
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field | |
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code | |
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code | |
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title | |
| V 0.6.13 | JavaScript could get executed in the shelf title | |
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo | |
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) | CVE-2021-25965 |
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 | CVE-2021-4164 |
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 | CVE-2021-4170 |
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo | |
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo | |
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo | |
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz | CVE-2022-0352 |
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 | CVE-2022-0339 |
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon | CVE-2022-0273 |
| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon | CVE-2022-0405 |
| V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 | CVE-2022-0767 |
| V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH | CVE-2022-0766 |
| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) | CVE-2022-30765 |
| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 | CVE-2022-0939 |
| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley | CVE-2022-0990 |
| V 0.6.20 | Credentials for emails are now stored encrypted | |
| V 0.6.20 | Login is rate limited | |
| V 0.6.20 | Passwordstrength can be forced | |
| V 0.6.21 | SMTP server credentials are no longer returned to client | |
| V 0.6.21 | Cross-site scripting (XSS) stored in href bypasses filter using data wrapper no longer possible | |
| V 0.6.21 | Cross-site scripting (XSS) is no longer possible via pathchooser | |
| V 0.6.21 | Error Handling at non existent rating, language, and user downloaded books was fixed | |
| V 0.6.22 | Upload mimetype is checked to prevent malicious file content in the books library | |
| V 0.6.22 | Cross-site scripting (XSS) stored in comments section is prevented better (switching from lxml to bleach for sanitizing strings) | |
| V 0.6.23 | Cookies are no longer stored for opds basic authentication and proxy authentication | |
## Statement regarding Log4j (CVE-2021-44228 and related)
Calibre-web is not affected by bugs related to Log4j. Calibre-Web is a python program, therefore not using Java, and not using the Java logging feature log4j.
+2 -1
View File
@@ -1,3 +1,4 @@
[python: **.py]
# has to be executed with jinja2 >=2.9 to have autoescape enabled automatically
[jinja2: **/templates/**.*ml]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
+38 -57
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 OzzieIsaacs
# Copyright (C) 2022 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,70 +17,51 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function, unicode_literals
import sys
import os
import sys
# Insert local directories into path
if sys.version_info < (3, 0):
sys.path.append(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))))
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__.decode('utf-8'))), 'vendor'))
else:
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vendor'))
# Add local path to sys.path, so we can import cps
path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, path)
from cps.main import main
from cps import create_app
from cps import web_server
from cps.opds import opds
from cps.web import web
from cps.jinjia import jinjia
from cps.about import about
from cps.shelf import shelf
from cps.admin import admi
from cps.gdrive import gdrive
from cps.editbooks import editbook
from cps.remotelogin import remotelogin
from cps.error_handler import init_errorhandler
def hide_console_windows():
import ctypes
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
kernel32 = ctypes.WinDLL('kernel32')
user32 = ctypes.WinDLL('user32')
try:
from cps.oauth_bb import oauth
oauth_available = True
except ImportError:
oauth_available = False
SW_HIDE = 0
def main():
app = create_app()
init_errorhandler()
app.register_blueprint(web)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
app.register_blueprint(admi)
app.register_blueprint(remotelogin)
# if config.config_use_google_drive:
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()
sys.exit(0 if success else 1)
hWnd = kernel32.GetConsoleWindow()
if hWnd:
user32.ShowWindow(hWnd, SW_HIDE)
if __name__ == '__main__':
main()
# Only hide the console window on Windows when there is no interactive
# console attached. This preserves the ability to use Ctrl+C from a
# terminal (stdin is a tty) to stop the server.
if os.name == "nt" and not sys.stdin.isatty():
hide_console_windows()
try:
main()
except KeyboardInterrupt:
# Allow stopping the server with Ctrl+C from an interactive console, after reboot of calibre-web
# (initally the SIGINT signal CTRL+C is catched in server.py via the signal callback).
try:
# Try to perform a graceful stop if web_server is available.
# Import here to avoid import-time side-effects when not needed.
from cps import web_server
web_server.stop()
except Exception:
pass
print('\nCalibre-Web: received interrupt, shutting down')
sys.exit(0)
+37
View File
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler, GammaC0de, vuolter
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .cw_login import LoginManager
from flask import session
class MyLoginManager(LoginManager):
def _session_protection_failed(self):
sess = session._get_current_object()
ident = self._session_identifier_generator()
if(sess and not (len(sess) == 1
and sess.get('csrf_token', None))) and ident != sess.get('_id', None):
return super(). _session_protection_failed()
return False
+148 -72
View File
@@ -19,133 +19,209 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__package__ = "cps"
from __future__ import division, print_function, unicode_literals
import sys
import os
import mimetypes
from babel import Locale as LC
from babel import negotiate_locale
from babel.core import UnknownLocaleError
from flask import Flask, request, g
from flask_login import LoginManager
from flask_babel import Babel
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
from .MyLoginManager import MyLoginManager
from flask_principal import Principal
from . import config_sql, logger, cache_buster, cli, ub, db
from . import logger
from .cli import CliParameter
from .reverseproxy import ReverseProxied
from .server import WebServer
from .dep_check import dependency_check
from .updater import Updater
from . import config_sql
from . import cache_buster
from . import ub, db
try:
from flask_limiter import Limiter
limiter_present = True
except ImportError:
limiter_present = False
try:
from flask_wtf.csrf import CSRFProtect
wtf_present = True
except ImportError:
wtf_present = False
mimetypes.init()
mimetypes.add_type('application/xhtml+xml', '.xhtml')
mimetypes.add_type('application/epub+zip', '.epub')
mimetypes.add_type('application/epub+zip', '.kepub')
mimetypes.add_type('application/fb2+zip', '.fb2')
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
mimetypes.add_type('application/vnd.amazon.ebook', '.azw')
mimetypes.add_type('application/x-mobi8-ebook', '.azw3')
mimetypes.add_type('application/octet-stream', '.prc')
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
mimetypes.add_type('application/x-mobipocket-ebook', '.azw3')
mimetypes.add_type('application/x-cbr', '.cbr')
mimetypes.add_type('application/x-cbz', '.cbz')
mimetypes.add_type('application/x-cbt', '.cbt')
mimetypes.add_type('application/x-tar', '.cbt')
mimetypes.add_type('application/x-7z-compressed', '.cb7')
mimetypes.add_type('image/vnd.djvu', '.djv')
mimetypes.add_type('image/vnd.djvu', '.djvu')
mimetypes.add_type('application/mpeg', '.mpeg')
mimetypes.add_type('application/mpeg', '.mp3')
mimetypes.add_type('application/mp4', '.m4a')
mimetypes.add_type('application/mp4', '.m4b')
mimetypes.add_type('application/ogg', '.ogg')
mimetypes.add_type('audio/mpeg', '.mp3')
mimetypes.add_type('audio/x-m4a', '.m4a')
mimetypes.add_type('audio/x-m4a', '.m4b')
mimetypes.add_type('audio/x-hx-aac-adts', '.aac')
mimetypes.add_type('audio/vnd.dolby.dd-raw', '.ac3')
mimetypes.add_type('video/x-ms-asf', '.asf')
mimetypes.add_type('audio/ogg', '.ogg')
mimetypes.add_type('application/ogg', '.oga')
mimetypes.add_type('text/css', '.css')
mimetypes.add_type('application/x-ms-reader', '.lit')
mimetypes.add_type('text/javascript', '.js')
mimetypes.add_type('text/rtf', '.rtf')
log = logger.create()
app = Flask(__name__)
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
REMEMBER_COOKIE_SAMESITE='Lax', # will be available in flask-login 0.5.1 earliest
REMEMBER_COOKIE_SAMESITE='Strict',
WTF_CSRF_SSL_STRICT=False,
SESSION_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "session",
REMEMBER_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "remember_token"
)
lm = MyLoginManager()
lm = LoginManager()
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong'
cli_param = CliParameter()
ub.init_db(cli.settingspath)
# pylint: disable=no-member
config = config_sql.load_configuration(ub.session)
config = config_sql.ConfigSQL()
if wtf_present:
csrf = CSRFProtect()
else:
csrf = None
calibre_db = db.CalibreDB(app)
web_server = WebServer()
babel = Babel()
_BABEL_TRANSLATIONS = set()
updater_thread = Updater()
log = logger.create()
if limiter_present:
limiter = Limiter(key_func=True, headers_enabled=True, in_memory_fallback_enabled=True, default_limits=[],
swallow_errors=True)
else:
limiter = None
from . import services
class ScriptNameSessionInterface(SecureCookieSessionInterface):
def get_cookie_path(self, app):
# Called once per response, after request context exists
return app.wsgi_app.script_name.rstrip("/") or "/"
db.CalibreDB.setup_db(config, cli.settingspath)
calibre_db = db.CalibreDB()
def create_app():
app.wsgi_app = ReverseProxied(app.wsgi_app)
# For python2 convert path to unicode
app.session_interface = ScriptNameSessionInterface()
if csrf:
csrf.init_app(app)
cli_param.init()
ub.init_db(cli_param.settings_path)
# pylint: disable=no-member
encrypt_key, error = config_sql.get_encryption_key(os.path.dirname(cli_param.settings_path))
config_sql.load_configuration(ub.session, encrypt_key)
config.init_config(ub.session, encrypt_key, cli_param)
if error:
log.error(error)
ub.password_change(cli_param.user_credentials)
if sys.version_info < (3, 0):
app.static_folder = app.static_folder.decode('utf-8')
app.root_path = app.root_path.decode('utf-8')
app.instance_path = app.instance_path.decode('utf-8')
log.info(
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
'please update your installation to Python3 ***')
print(
'*** Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2, '
'please update your installation to Python3 ***')
web_server.stop(True)
sys.exit(5)
lm.login_view = 'web.login'
lm.anonymous_user = ub.Anonymous
lm.session_protection = 'strong' if config.config_session == 1 else "basic"
db.CalibreDB.update_config(config, config.config_calibre_dir, cli_param.settings_path)
updater_thread.init_updater(config, web_server)
# Perform dry run of updater and exit afterward
if cli_param.dry_run:
updater_thread.dry_run()
sys.exit(0)
updater_thread.start()
requirements = dependency_check()
for res in requirements:
if res['found'] == "not installed":
message = ('Cannot import {name} module, it is needed to run calibre-web, '
'please install it using "pip install {name}"').format(name=res["name"])
log.info(message)
print("*** " + message + " ***")
web_server.stop(True)
sys.exit(8)
for res in requirements + dependency_check(True):
log.info('*** "{}" version does not meet the requirements. '
'Should: {}, Found: {}, please consider installing required version ***'
.format(res['name'],
res['target'],
res['found']))
app.wsgi_app = ReverseProxied(app.wsgi_app)
if os.environ.get('FLASK_DEBUG'):
cache_buster.init_cache_busting(app)
cache_buster.init_cache_busting(app)
log.info('Starting Calibre Web...')
if sys.version_info < (3, 0):
log.info('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3')
print('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3')
Principal(app)
lm.init_app(app)
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
web_server.init_app(app, config)
from .cw_babel import babel, get_locale
if hasattr(babel, "localeselector"):
babel.init_app(app)
babel.localeselector(get_locale)
else:
babel.init_app(app, locale_selector=get_locale)
babel.init_app(app)
_BABEL_TRANSLATIONS.update(str(item) for item in babel.list_translations())
_BABEL_TRANSLATIONS.add('en')
from . import services
if services.ldap:
services.ldap.init_app(app, config)
if services.goodreads_support:
services.goodreads_support.connect(config.config_goodreads_api_key,
config.config_goodreads_api_secret,
config.config_use_goodreads)
config.store_calibre_uuid(calibre_db, db.Library_Id)
# Configure rate limiter
# https://limits.readthedocs.io/en/stable/storage.html
app.config.update(RATELIMIT_ENABLED=config.config_ratelimiter)
if config.config_limiter_uri != "" and not cli_param.memory_backend:
app.config.update(RATELIMIT_STORAGE_URI=config.config_limiter_uri)
if config.config_limiter_options != "":
app.config.update(RATELIMIT_STORAGE_OPTIONS=config.config_limiter_options)
try:
limiter.init_app(app)
except Exception as e:
log.error('Wrong Flask Limiter configuration, falling back to default: {}'.format(e))
app.config.update(RATELIMIT_STORAGE_URI=None)
limiter.init_app(app)
# Register scheduled tasks
from .schedule import register_scheduled_tasks, register_startup_tasks
register_scheduled_tasks(config.schedule_reconnect)
register_startup_tasks()
return app
@babel.localeselector
def get_locale():
# if a user is logged in, use the locale from the user settings
user = getattr(g, 'user', None)
# user = None
if user is not None and hasattr(user, "locale"):
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
return user.locale
preferred = list()
if request.accept_languages:
for x in request.accept_languages.values():
try:
preferred.append(str(LC.parse(x.replace('-', '_'))))
except (UnknownLocaleError, ValueError) as e:
log.debug('Could not parse locale "%s": %s', x, e)
return negotiate_locale(preferred or ['en'], _BABEL_TRANSLATIONS)
@babel.timezoneselector
def get_timezone():
user = getattr(g, 'user', None)
return user.timezone if user else None
from .updater import Updater
updater_thread = Updater()
updater_thread.start()
+40 -56
View File
@@ -20,82 +20,66 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import platform
import sqlite3
from importlib.metadata import metadata
from collections import OrderedDict
import babel, pytz, requests, sqlalchemy
import werkzeug, flask, flask_login, flask_principal, jinja2
import flask
from flask_babel import gettext as _
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
from . import db, calibre_db, converter, uploader, constants, dep_check
from .render_template import render_title_template
try:
from flask_login import __version__ as flask_loginVersion
except ImportError:
from flask_login.__about__ import __version__ as flask_loginVersion
try:
import unidecode
# _() necessary to make babel aware of string for translation
unidecode_version = _(u'installed')
except ImportError:
unidecode_version = _(u'not installed')
from .usermanagement import user_login_required
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 = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python=sys.version,
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
WebServer=server.VERSION,
Flask=flask.__version__,
Flask_Login=flask_loginVersion,
Flask_Principal=flask_principal.__version__,
Werkzeug=werkzeug.__version__,
Babel=babel.__version__,
Jinja2=jinja2.__version__,
Requests=requests.__version__,
SqlAlchemy=sqlalchemy.__version__,
pySqlite=sqlite3.version,
SQLite=sqlite3.sqlite_version,
iso639=isoLanguages.__version__,
pytz=pytz.__version__,
Unidecode = unidecode_version,
Flask_SimpleLDAP = u'installed' if bool(services.ldap) else None,
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
Goodreads = u'installed' if bool(services.goodreads_support) else None,
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
flask_dance = flask_danceVersion
)
_VERSIONS.update(uploader.get_versions())
modules = dict()
req = dep_check.load_dependencies(False)
opt = dep_check.load_dependencies(True)
for i in (req + opt):
modules[i[1]] = i[0]
modules['Jinja2'] = metadata("jinja2")["Version"]
if sys.version_info < (3, 12):
modules['pySqlite'] = sqlite3.version
modules['SQLite'] = sqlite3.sqlite_version
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))
def collect_stats():
_VERSIONS['ebook converter'] = _(converter.get_calibre_version())
_VERSIONS['unrar'] = _(converter.get_unrar_version())
_VERSIONS['kepubify'] = _(converter.get_kepubify_version())
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
calibre_web_version = constants.STABLE_VERSION.replace("b", " Beta")
else:
calibre_web_version = (constants.STABLE_VERSION.replace("b", " Beta") + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%', '%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%', '%%'))
if getattr(sys, 'frozen', False):
calibre_web_version += " - Exe-Version"
elif constants.HOME_CONFIG:
calibre_web_version += " - pyPi"
_VERSIONS = {'Calibre Web': calibre_web_version}
_VERSIONS.update(OrderedDict(
Python=sys.version,
Platform='{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
))
_VERSIONS.update(uploader.get_magick_version())
_VERSIONS['Unrar'] = converter.get_unrar_version()
_VERSIONS['Ebook converter'] = converter.get_calibre_version()
_VERSIONS['Kepubify'] = converter.get_kepubify_version()
_VERSIONS.update(sorted_modules)
return _VERSIONS
@about.route("/stats")
@flask_login.login_required
@user_login_required
def stats():
counter = calibre_db.session.query(db.Books).count()
authors = calibre_db.session.query(db.Authors).count()
categorys = calibre_db.session.query(db.Tags).count()
categories = calibre_db.session.query(db.Tags).count()
series = calibre_db.session.query(db.Series).count()
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
categorycounter=categories, seriecounter=series, title=_("Statistics"), page="stat")
+1372 -657
View File
File diff suppressed because it is too large Load Diff
+147
View File
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2024 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 mutagen
import base64
from . import cover, logger
from cps.constants import BookMeta
log = logger.create()
def get_audio_file_info(tmp_file_path, original_file_extension, original_file_name, no_cover_processing):
tmp_cover_name = None
audio_file = mutagen.File(tmp_file_path)
comments = None
if original_file_extension in [".mp3", ".wav", ".aiff"]:
cover_data = list()
for key, val in audio_file.tags.items():
if key.startswith("APIC:"):
cover_data.append(val)
if key.startswith("COMM:"):
comments = val.text[0]
title = audio_file.tags.get('TIT2').text[0] if "TIT2" in audio_file.tags else None
author = audio_file.tags.get('TPE1').text[0] if "TPE1" in audio_file.tags else None
if author is None:
author = audio_file.tags.get('TPE2').text[0] if "TPE2" in audio_file.tags else None
tags = audio_file.tags.get('TCON').text[0] if "TCON" in audio_file.tags else None # Genre
series = audio_file.tags.get('TALB').text[0] if "TALB" in audio_file.tags else None# Album
series_id = audio_file.tags.get('TRCK').text[0] if "TRCK" in audio_file.tags else None # track no.
publisher = audio_file.tags.get('TPUB').text[0] if "TPUB" in audio_file.tags else None
pubdate = str(audio_file.tags.get('TDRL').text[0]) if "TDRL" in audio_file.tags else None
if not pubdate:
pubdate = str(audio_file.tags.get('TDRC').text[0]) if "TDRC" in audio_file.tags else None
if not pubdate:
pubdate = str(audio_file.tags.get('TDOR').text[0]) if "TDOR" in audio_file.tags else None
if cover_data and not no_cover_processing:
cover_info = cover_data[0]
for dat in cover_data:
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
cover_info = dat
break
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
elif original_file_extension in [".ogg", ".flac", ".opus", ".ogv"]:
title = audio_file.tags.get('TITLE')[0] if "TITLE" in audio_file else None
author = audio_file.tags.get('ARTIST')[0] if "ARTIST" in audio_file else None
comments = audio_file.tags.get('COMMENTS')[0] if "COMMENTS" in audio_file else None
tags = audio_file.tags.get('GENRE')[0] if "GENRE" in audio_file else None # Genre
series = audio_file.tags.get('ALBUM')[0] if "ALBUM" in audio_file else None
series_id = audio_file.tags.get('TRACKNUMBER')[0] if "TRACKNUMBER" in audio_file else None
publisher = audio_file.tags.get('LABEL')[0] if "LABEL" in audio_file else None
pubdate = audio_file.tags.get('DATE')[0] if "DATE" in audio_file else None
cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE')
if not no_cover_processing:
if cover_data:
cover_info = mutagen.flac.Picture(base64.b64decode(cover_data[0]))
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
if hasattr(audio_file, "pictures"):
cover_info = audio_file.pictures[0]
for dat in audio_file.pictures:
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
cover_info = dat
break
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
elif original_file_extension in [".aac"]:
title = audio_file.tags.get('Title').value if "Title" in audio_file else None
author = audio_file.tags.get('Artist').value if "Artist" in audio_file else None
comments = audio_file.tags.get('Comment').value if "Comment" in audio_file else None
tags = audio_file.tags.get('Genre').value if "Genre" in audio_file else None
series = audio_file.tags.get('Album').value if "Album" in audio_file else None
series_id = audio_file.tags.get('Track').value if "Track" in audio_file else None
publisher = audio_file.tags.get('Label').value if "Label" in audio_file else None
pubdate = audio_file.tags.get('Year').value if "Year" in audio_file else None
cover_data = audio_file.tags['Cover Art (Front)']
if cover_data and not no_cover_processing:
tmp_cover_name = tmp_file_path + '.jpg'
with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(cover_data.value.split(b"\x00",1)[1])
elif original_file_extension in [".asf"]:
title = audio_file.tags.get('Title')[0].value if "Title" in audio_file else None
author = audio_file.tags.get('Artist')[0].value if "Artist" in audio_file else None
comments = audio_file.tags.get('Comments')[0].value if "Comments" in audio_file else None
tags = audio_file.tags.get('Genre')[0].value if "Genre" in audio_file else None
series = audio_file.tags.get('Album')[0].value if "Album" in audio_file else None
series_id = audio_file.tags.get('Track')[0].value if "Track" in audio_file else None
publisher = audio_file.tags.get('Label')[0].value if "Label" in audio_file else None
pubdate = audio_file.tags.get('Year')[0].value if "Year" in audio_file else None
cover_data = audio_file.tags.get('WM/Picture', None)
if cover_data and not no_cover_processing:
tmp_cover_name = tmp_file_path + '.jpg'
with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(cover_data[0].value)
elif original_file_extension in [".mp4", ".m4a", ".m4b"]:
title = audio_file.tags.get('©nam')[0] if "©nam" in audio_file.tags else None
author = audio_file.tags.get('©ART')[0] if "©ART" in audio_file.tags else None
comments = audio_file.tags.get('©cmt')[0] if "©cmt" in audio_file.tags else None
tags = audio_file.tags.get('©gen')[0] if "©gen" in audio_file.tags else None
series = audio_file.tags.get('©alb')[0] if "©alb" in audio_file.tags else None
series_id = str(audio_file.tags.get('trkn')[0][0]) if "trkn" in audio_file.tags else None
publisher = ""
pubdate = audio_file.tags.get('©day')[0] if "©day" in audio_file.tags else None
cover_data = audio_file.tags.get('covr', None)
if cover_data and not no_cover_processing:
cover_type = None
for c in cover_data:
if c.imageformat == mutagen.mp4.AtomDataType.JPEG:
cover_type =".jpg"
cover_bin = c
break
elif c.imageformat == mutagen.mp4.AtomDataType.PNG:
cover_type = ".png"
cover_bin = c
break
if cover_type:
tmp_cover_name = cover.cover_processing(tmp_file_path, cover_bin, cover_type)
else:
logger.error("Unknown covertype in file {} ".format(original_file_name))
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title or original_file_name ,
author="Unknown" if author is None else author,
cover=tmp_cover_name,
description="" if comments is None else comments,
tags="" if tags is None else tags,
series="" if series is None else series,
series_id="1" if series_id is None else series_id.split("/")[0],
languages="",
publisher= "" if publisher is None else publisher,
pubdate="" if pubdate is None else pubdate,
identifiers=[],
)
+89
View File
@@ -0,0 +1,89 @@
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11,
# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh,
# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe,
# ruben-herold, marblepebble, JackED42, SiphonSquirrel,
# apetresc, nanu-c, mutschler, carderne
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import Blueprint
from flask_babel import gettext as _
from flask_babel import get_locale
from flask import request, redirect, url_for
from . import logger, isoLanguages
from . import db, config
from . import calibre_db
from .usermanagement import login_required_if_no_ano
from .render_template import render_title_template
from .web import get_sort_function
try:
from natsort import natsorted as sort
except ImportError:
sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files
basic = Blueprint('basic', __name__)
log = logger.create()
@basic.route("/basic", methods=["GET"])
@login_required_if_no_ano
def index():
term = request.args.get("query", "") # default to showing all books
limit = 15
page = int(request.args.get("page") or 1)
off = (page - 1) * limit
order = get_sort_function("stored", "search")
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
entries, result_count, pagination = calibre_db.get_search_results(term,
config,
off,
order,
limit,
*join)
return render_title_template('basic_index.html',
searchterm=term,
pagination=pagination,
query=term,
adv_searchterm=term,
entries=entries,
result_count=result_count,
title=_("Search"),
page="search",
order=order[1])
@basic.route("/basic_book/<int:book_id>")
@login_required_if_no_ano
def show_book(book_id):
entries = calibre_db.get_book_read_archived(book_id, config.config_read_column, allow_show_archived=True)
if entries:
entry = entries[0]
for lang_index in range(0, len(entry.languages)):
entry.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[
lang_index].lang_code)
entry.ordered_authors = calibre_db.order_authors([entry])
return render_title_template('basic_detail.html',
entry=entry,
is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest',
title=entry.title,
page="book")
else:
log.debug("Selected book is unavailable. File does not exist or is not accessible")
return redirect(url_for("basic.index"))
+14 -11
View File
@@ -19,7 +19,6 @@
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
# Uses query strings so CSS font files are found without having to resort to absolute URLs
from __future__ import division, print_function, unicode_literals
import os
import hashlib
@@ -48,22 +47,26 @@ def init_cache_busting(app):
for filename in filenames:
# compute version component
rooted_filename = os.path.join(dirpath, filename)
with open(rooted_filename, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
try:
with open(rooted_filename, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
file_path = file_path.replace("\\", "/") # Convert Windows path to web path
hash_table[file_path] = file_hash
except PermissionError:
log.error("No permission to access {} file.".format(rooted_filename))
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
file_path = file_path.replace("\\", "/") # Convert Windows path to web path
hash_table[file_path] = file_hash
log.debug('Finished computing cache-busting values')
def bust_filename(filename):
return hash_table.get(filename, "")
def bust_filename(file_name):
return hash_table.get(file_name, "")
def unbust_filename(filename):
return filename.split("?", 1)[0]
def unbust_filename(file_name):
return file_name.split("?", 1)[0]
@app.url_defaults
# pylint: disable=unused-variable
def reverse_to_cache_busted_url(endpoint, values):
"""
Make `url_for` produce busted filenames when using the 'static' endpoint.
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import logger
from lxml.etree import ParserError
log = logger.create()
try:
# at least bleach 6.0 is needed -> incomplatible change from list arguments to set arguments
from bleach import clean as clean_html
from bleach.sanitizer import ALLOWED_TAGS
bleach = True
except ImportError:
from nh3 import clean as clean_html
bleach = False
def clean_string(unsafe_text, book_id=0):
try:
if bleach:
allowed_tags = list(ALLOWED_TAGS)
allowed_tags.extend(["p", "span", "div", "pre", "br", "h1", "h2", "h3", "h4", "h5", "h6", "img"])
allowed_attributes = {
"*": ["class", "style"],
"a": ["href", "title", "rel"],
"img": ["src", "alt", "title", "width", "height"],
}
safe_text = clean_html(unsafe_text, tags=set(allowed_tags), attributes=allowed_attributes)
else:
safe_text = clean_html(unsafe_text)
except ParserError as e:
log.error("Comments of book {} are corrupted: {}".format(book_id, e))
safe_text = ""
except TypeError as e:
log.error("Comments can't be parsed, maybe 'lxml' is too new, try installing 'bleach': {}".format(e))
safe_text = ""
return safe_text
+106 -77
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import os
import argparse
@@ -25,95 +24,125 @@ import socket
from .constants import CONFIG_DIR as _CONFIG_DIR
from .constants import STABLE_VERSION as _STABLE_VERSION
from .constants import NIGHTLY_VERSION as _NIGHTLY_VERSION
from .constants import DEFAULT_SETTINGS_FILE, DEFAULT_GDRIVE_FILE
def version_info():
if _NIGHTLY_VERSION[1].startswith('$Format'):
return "Calibre-Web version: %s - unkown git-clone" % _STABLE_VERSION['version']
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION['version'], _NIGHTLY_VERSION[1])
return "Calibre-Web version: %s - unknown git-clone" % _STABLE_VERSION.replace("b", " Beta")
return "Calibre-Web version: %s -%s" % (_STABLE_VERSION.replace("b", " Beta"), _NIGHTLY_VERSION[1])
parser = argparse.ArgumentParser(description='Calibre Web is a web app'
' 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',
help='path and name to SSL certfile, e.g. /opt/test.cert, works only in combination with keyfile')
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-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
args = parser.parse_args()
class CliParameter(object):
if sys.version_info < (3, 0):
if args.p:
args.p = args.p.decode('utf-8')
if args.g:
args.g = args.g.decode('utf-8')
if args.k:
args.k = args.k.decode('utf-8')
if args.c:
args.c = args.c.decode('utf-8')
if args.s:
args.s = args.s.decode('utf-8')
def __init__(self):
self.user_credentials = None
self.ip_address = None
self.allow_localhost = None
self.reconnect_enable = None
self.memory_backend = None
self.dry_run = None
self.certfilepath = None
self.keyfilepath = None
self.gd_path = None
self.settings_path = None
self.logpath = None
def init(self):
self.arg_parser()
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
gdpath = args.g or os.path.join(_CONFIG_DIR, "gdrive.db")
def arg_parser(self):
parser = argparse.ArgumentParser(description='Calibre Web is a web app 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', help='path and name to SSL certfile, '
'e.g. /opt/test.cert, works only in combination with keyfile')
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('-o', metavar='path', help='path and name Calibre-Web logfile')
parser.add_argument('-v', '--version', action='version', help='Shows version number '
'and exits Calibre-Web',
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-m', action='store_true',
help='Use Memory-backend as limiter backend, use this parameter '
'in case of miss configured backend')
parser.add_argument('-s', metavar='user:pass',
help='Sets specific username to new password and exits Calibre-Web')
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
parser.add_argument('-d', action='store_true', help='Dry run of updater to check file permissions '
'in advance and exits Calibre-Web')
parser.add_argument('-r', action='store_true', help='Enable public database reconnect '
'route under /reconnect')
args = parser.parse_args()
# handle and check parameter for ssl encryption
certfilepath = None
keyfilepath = None
if args.c:
if os.path.isfile(args.c):
certfilepath = args.c
else:
print("Certfilepath is invalid. Exiting...")
sys.exit(1)
self.logpath = args.o or ""
self.settings_path = args.p or os.path.join(_CONFIG_DIR, DEFAULT_SETTINGS_FILE)
self.gd_path = args.g or os.path.join(_CONFIG_DIR, DEFAULT_GDRIVE_FILE)
if args.c == "":
certfilepath = ""
if os.path.isdir(self.settings_path):
self.settings_path = os.path.join(self.settings_path, DEFAULT_SETTINGS_FILE)
if args.k:
if os.path.isfile(args.k):
keyfilepath = args.k
else:
print("Keyfilepath is invalid. Exiting...")
sys.exit(1)
if os.path.isdir(self.gd_path):
self.gd_path = os.path.join(self.gd_path, DEFAULT_GDRIVE_FILE)
if (args.k and not args.c) or (not args.k and args.c):
print("Certfile and Keyfile have to be used together. Exiting...")
sys.exit(1)
if args.k == "":
keyfilepath = ""
# handle and check ipadress argument
ipadress = args.i or None
if ipadress:
try:
# try to parse the given ip address with socket
if hasattr(socket, 'inet_pton'):
if ':' in ipadress:
socket.inet_pton(socket.AF_INET6, ipadress)
# handle and check parameter for ssl encryption
self.certfilepath = None
self.keyfilepath = None
if args.c:
if os.path.isfile(args.c):
self.certfilepath = args.c
else:
socket.inet_pton(socket.AF_INET, ipadress)
else:
# on windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(ipadress)
except socket.error as err:
print(ipadress, ':', err)
sys.exit(1)
print("Certfile path is invalid. Exiting...")
sys.exit(1)
# handle and check user password argument
user_credentials = args.s or None
if user_credentials and ":" not in user_credentials:
print("No valid username:password format")
sys.exit(3)
if args.c == "":
self.certfilepath = ""
# Handles enableing of filepicker
filepicker = args.f or None
if args.k:
if os.path.isfile(args.k):
self.keyfilepath = args.k
else:
print("Keyfile path is invalid. Exiting...")
sys.exit(1)
if (args.k and not args.c) or (not args.k and args.c):
print("Certfile and Keyfile have to be used together. Exiting...")
sys.exit(1)
if args.k == "":
self.keyfilepath = ""
# overwrite limiter backend
self.memory_backend = args.m or None
# dry run updater
self.dry_run = args.d or None
# enable reconnect endpoint for docker database reconnect
self.reconnect_enable = args.r or os.environ.get("CALIBRE_RECONNECT", None)
# load covers from localhost
self.allow_localhost = args.l or os.environ.get("CALIBRE_LOCALHOST", None)
# handle and check ip address argument
self.ip_address = args.i or None
if self.ip_address:
try:
# try to parse the given ip address with socket
if hasattr(socket, 'inet_pton'):
if ':' in self.ip_address:
socket.inet_pton(socket.AF_INET6, self.ip_address)
else:
socket.inet_pton(socket.AF_INET, self.ip_address)
else:
# on Windows python < 3.4, inet_pton is not available
# inet_atom only handles IPv4 addresses
socket.inet_aton(self.ip_address)
except socket.error as err:
print(self.ip_address, ':', err)
sys.exit(1)
# handle and check user password argument
self.user_credentials = args.s or None
if self.user_credentials and ":" not in self.user_credentials:
print("No valid 'username:password' format")
sys.exit(3)
+115 -83
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 OzzieIsaacs
# Copyright (C) 2018-2022 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
@@ -16,22 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
from . import logger, isoLanguages
from . import logger, isoLanguages, cover
from .constants import BookMeta
log = logger.create()
try:
from wand.image import Image
use_IM = True
except (ImportError, RuntimeError) as e:
use_IM = False
log = logger.create()
try:
from comicapi.comicarchive import ComicArchive, MetaDataStyle
@@ -40,6 +36,12 @@ try:
from comicapi import __version__ as comic_version
except ImportError:
comic_version = ''
try:
from comicapi.comicarchive import load_archive_plugins
import comicapi.utils
comicapi.utils.add_rar_paths()
except ImportError:
load_archive_plugins = None
except (ImportError, LookupError) as e:
log.debug('Cannot import comicapi, extracting comic metadata will not work: %s', e)
import zipfile
@@ -50,114 +52,144 @@ except (ImportError, LookupError) as e:
except (ImportError, SyntaxError) as e:
log.debug('Cannot import rarfile, extracting cover files from rar files will not work: %s', e)
use_rarfile = False
try:
import py7zr
use_7zip = True
except (ImportError, SyntaxError) as e:
log.debug('Cannot import py7zr, extracting cover files from CB7 files will not work: %s', e)
use_7zip = False
use_comic_meta = False
NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
def _cover_processing(tmp_file_name, img, extension):
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
if use_IM:
# convert to jpg because calibre only supports jpg
if extension in NO_JPEG_EXTENSIONS:
with Image(filename=tmp_file_name) as imgc:
imgc.format = 'jpeg'
imgc.transform_colorspace('rgb')
imgc.save(tmp_cover_name)
return tmp_cover_name
if not img:
return None
with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_executable):
cover_data = extension = None
if use_comic_meta:
archive = ComicArchive(tmp_file_name, rar_exe_path=rarExecutable)
for index, name in enumerate(archive.getPageNameList()):
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
for name in sorted(cf.namelist()):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = archive.getPage(index)
if extension in cover.COVER_EXTENSIONS:
cover_data = cf.read(name)
break
else:
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist():
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
for name in sorted(cf.getnames()):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rar_executable
cf = rarfile.RarFile(tmp_file_name)
for name in sorted(cf.namelist()):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
if extension in cover.COVER_EXTENSIONS:
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.extractfile(name).read()
break
elif original_file_extension.upper() == '.CBR' and use_rarfile:
try:
rarfile.UNRAR_TOOL = rarExecutable
cf = rarfile.RarFile(tmp_file_name)
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in COVER_EXTENSIONS:
cover_data = cf.read(name)
break
except Exception as e:
log.debug('Rarfile failed with error: %s', e)
return _cover_processing(tmp_file_name, cover_data, extension)
except Exception as ex:
log.error('Rarfile failed with error: {}'.format(ex))
elif original_file_extension.upper() == '.CB7' and use_7zip:
cf = py7zr.SevenZipFile(tmp_file_name)
for name in sorted(cf.getnames()):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
try:
cover_data = cf.read([name])[name].read()
except (py7zr.Bad7zFile, OSError) as ex:
log.error('7Zip file failed with error: {}'.format(ex))
break
return cover_data, extension
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rarExecutable):
def _extract_cover(tmp_file_path, original_file_extension, rar_executable):
cover_data = extension = None
if use_comic_meta:
archive = ComicArchive(tmp_file_path, rar_exe_path=rarExecutable)
if archive.seemsToBeAComicArchive():
if archive.hasMetadata(MetaDataStyle.CIX):
try:
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
except TypeError:
archive = ComicArchive(tmp_file_path)
name_list = archive.getPageNameList if hasattr(archive, "getPageNameList") else archive.get_page_name_list
for index, name in enumerate(name_list()):
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
get_page = archive.getPage if hasattr(archive, "getPageNameList") else archive.get_page
cover_data = get_page(index)
break
else:
cover_data, extension = _extract_cover_from_archive(original_file_extension, tmp_file_path, rar_executable)
return cover.cover_processing(tmp_file_path, cover_data, extension)
def get_comic_info(tmp_file_path, original_file_name, original_file_extension, rar_executable, no_cover_processing):
if use_comic_meta:
try:
archive = ComicArchive(tmp_file_path, rar_exe_path=rar_executable)
except TypeError:
load_archive_plugins(force=True, rar=rar_executable)
archive = ComicArchive(tmp_file_path)
if hasattr(archive, "seemsToBeAComicArchive"):
seems_archive = archive.seemsToBeAComicArchive
else:
seems_archive = archive.seems_to_be_a_comic_archive
if seems_archive():
has_metadata = archive.hasMetadata if hasattr(archive, "hasMetadata") else archive.has_metadata
if has_metadata(MetaDataStyle.CIX):
style = MetaDataStyle.CIX
elif archive.hasMetadata(MetaDataStyle.CBI):
elif has_metadata(MetaDataStyle.CBI):
style = MetaDataStyle.CBI
else:
style = None
# if style is not None:
loadedMetadata = archive.readMetadata(style)
lang = loadedMetadata.language or ""
loadedMetadata.language = isoLanguages.get_lang3(lang)
read_metadata = archive.readMetadata if hasattr(archive, "readMetadata") else archive.read_metadata
loaded_metadata = read_metadata(style)
lang = loaded_metadata.language or ""
loaded_metadata.language = isoLanguages.get_lang3(lang)
if not no_cover_processing:
cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable)
else:
cover_file = None
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, rarExecutable),
description=loadedMetadata.comments or "",
title=loaded_metadata.title or original_file_name,
author=" & ".join([credit["person"]
for credit in loaded_metadata.credits if credit["role"] == "Writer"]) or 'Unknown',
cover=cover_file,
description=loaded_metadata.comments or "",
tags="",
series=loadedMetadata.series or "",
series_id=loadedMetadata.issue or "",
languages=loadedMetadata.language)
series=loaded_metadata.series or "",
series_id=loaded_metadata.issue or "",
languages=loaded_metadata.language,
publisher="",
pubdate="",
identifiers=[])
if not no_cover_processing:
cover_file = _extract_cover(tmp_file_path, original_file_extension, rar_executable)
else:
cover_file = None
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, rarExecutable),
author='Unknown',
cover=cover_file,
description="",
tags="",
series="",
series_id="",
languages="")
languages="",
publisher="",
pubdate="",
identifiers=[])
+248 -97
View File
@@ -16,28 +16,39 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import sys
import json
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
from sqlalchemy.exc import OperationalError
from sqlalchemy.ext.declarative import declarative_base
from . import constants, cli, logger, ub
from sqlalchemy.sql.expression import text
from sqlalchemy import exists
from cryptography.fernet import Fernet
import cryptography.exceptions
from base64 import urlsafe_b64decode
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from . import constants, logger
from .subproc_wrapper import process_wait
from .string_helper import strip_whitespaces
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="")
flask_session_key = Column(BLOB, default=b"")
def __init__(self, key):
super().__init__()
self.flask_session_key = key
@@ -51,29 +62,36 @@ class _Settings(_Base):
mail_port = Column(Integer, default=25)
mail_use_ssl = Column(SmallInteger, default=0)
mail_login = Column(String, default='mail@example.com')
mail_password = Column(String, default='mypassword')
mail_password_e = Column(String)
mail_password = Column(String)
mail_from = Column(String, default='automailer <mail@example.com>')
mail_size = Column(Integer, default=25*1024*1024)
mail_server_type = Column(SmallInteger, default=0)
mail_gmail_token = Column(JSON, default={})
config_calibre_dir = Column(String)
config_calibre_uuid = Column(String)
config_calibre_split = Column(Boolean, default=False)
config_calibre_split_dir = Column(String)
config_port = Column(Integer, default=constants.DEFAULT_PORT)
config_external_port = Column(Integer, default=constants.DEFAULT_PORT)
config_certfile = Column(String)
config_keyfile = Column(String)
config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_trustedhosts = Column(String, default='')
config_calibre_web_title = Column(String, default='Calibre-Web')
config_books_per_page = Column(Integer, default=60)
config_random_books = Column(Integer, default=4)
config_authors_max = Column(Integer, default=0)
config_read_column = Column(Integer, default=0)
config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
config_mature_content_tags = Column(String, default='')
config_title_regex = Column(String,
default=r"^(A|The|An|Der|Die|Das|Den|Ein|Eine"
r"|Einen|Dem|Des|Einem|Eines|Le|La|Les|L'|Un|Une)(\s+|(?<='))")
config_theme = Column(Integer, default=0)
config_log_level = Column(SmallInteger, default=logger.DEFAULT_LOG_LEVEL)
config_logfile = Column(String)
config_logfile = Column(String, default=logger.DEFAULT_LOG_FILE)
config_access_log = Column(SmallInteger, default=0)
config_access_logfile = Column(String)
config_access_logfile = Column(String, default=logger.DEFAULT_ACCESS_LOG)
config_uploading = Column(SmallInteger, default=0)
config_anonbrowse = Column(SmallInteger, default=0)
@@ -83,6 +101,8 @@ class _Settings(_Base):
config_default_role = Column(SmallInteger, default=0)
config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR)
config_default_language = Column(String(3), default="all")
config_default_locale = Column(String(2), default="en")
config_columns_to_ignore = Column(String)
config_denied_tags = Column(String, default="")
@@ -97,7 +117,7 @@ class _Settings(_Base):
config_use_goodreads = Column(Boolean, default=False)
config_goodreads_api_key = Column(String)
config_goodreads_api_secret = Column(String)
config_googlebooks_api_key = Column(String, default='')
config_register_email = Column(Boolean, default=False)
config_login_type = Column(Integer, default=0)
@@ -107,14 +127,15 @@ class _Settings(_Base):
config_ldap_port = Column(SmallInteger, default=389)
config_ldap_authentication = Column(SmallInteger, default=constants.LDAP_AUTH_SIMPLE)
config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org')
config_ldap_serv_password = Column(String, default="")
config_ldap_serv_password_e = Column(String)
config_ldap_serv_password = Column(String)
config_ldap_encryption = Column(SmallInteger, default=0)
config_ldap_cacert_path = Column(String, default="")
config_ldap_cert_path = Column(String, default="")
config_ldap_key_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_member_user_object = Column(String, default='') #
config_ldap_member_user_object = Column(String, default='')
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')
@@ -122,45 +143,74 @@ class _Settings(_Base):
config_kepubifypath = Column(String, default=None)
config_converterpath = Column(String, default=None)
config_binariesdir = Column(String, default=None)
config_calibre = Column(String)
config_rarfile_location = Column(String, default=None)
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
config_unicode_filename = Column(Boolean, default=False)
config_embed_metadata = Column(Boolean, default=True)
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
config_reverse_proxy_login_header_name = Column(String)
config_allow_reverse_proxy_header_login = Column(Boolean, default=False)
schedule_start_time = Column(Integer, default=4)
schedule_duration = Column(Integer, default=10)
schedule_generate_book_covers = Column(Boolean, default=False)
schedule_generate_series_covers = Column(Boolean, default=False)
schedule_reconnect = Column(Boolean, default=False)
schedule_metadata_backup = Column(Boolean, default=False)
config_password_policy = Column(Boolean, default=True)
config_password_min_length = Column(Integer, default=8)
config_password_number = Column(Boolean, default=True)
config_password_lower = Column(Boolean, default=True)
config_password_upper = Column(Boolean, default=True)
config_password_character = Column(Boolean, default=True)
config_password_special = Column(Boolean, default=True)
config_session = Column(Integer, default=1)
config_ratelimiter = Column(Boolean, default=True)
config_limiter_uri = Column(String, default="")
config_limiter_options = Column(String, default="")
config_check_extensions = Column(Boolean, default=True)
def __repr__(self):
return self.__class__.__name__
# Class holds all application specific settings in calibre-web
class _ConfigSQL(object):
class ConfigSQL(object):
# pylint: disable=no-member
def __init__(self, session):
def __init__(self):
self.__dict__["dirty"] = list()
def init_config(self, session, secret_key, cli):
self._session = session
self._settings = None
self.db_configured = None
self.config_calibre_dir = None
self._fernet = Fernet(secret_key)
self.cli = cli
self.load()
change = False
if self.config_converterpath == None:
change = True
self.config_converterpath = autodetect_calibre_binary()
if self.config_kepubifypath == None:
if self.config_binariesdir is None:
change = True
self.config_binariesdir = autodetect_calibre_binaries()
self.config_converterpath = autodetect_converter_binary(self.config_binariesdir)
if self.config_kepubifypath is None:
change = True
self.config_kepubifypath = autodetect_kepubify_binary()
if self.config_rarfile_location == None:
if self.config_rarfile_location is None:
change = True
self.config_rarfile_location = autodetect_unrar_binary()
if change:
self.save()
def _read_from_storage(self):
if self._settings is None:
log.debug("_ConfigSQL._read_from_storage")
@@ -168,21 +218,21 @@ class _ConfigSQL(object):
return self._settings
def get_config_certfile(self):
if cli.certfilepath:
return cli.certfilepath
if cli.certfilepath == "":
if self.cli.certfilepath:
return self.cli.certfilepath
if self.cli.certfilepath == "":
return None
return self.config_certfile
def get_config_keyfile(self):
if cli.keyfilepath:
return cli.keyfilepath
if cli.certfilepath == "":
if self.cli.keyfilepath:
return self.cli.keyfilepath
if self.cli.certfilepath == "":
return None
return self.config_keyfile
def get_config_ipaddress(self):
return cli.ipadress or ""
return self.cli.ip_address or ""
def _has_role(self, role_flag):
return constants.has_flag(self.config_default_role, role_flag)
@@ -219,39 +269,41 @@ class _ConfigSQL(object):
def list_denied_tags(self):
mct = self.config_denied_tags or ""
return [t.strip() for t in mct.split(",")]
return [strip_whitespaces(t) for t in mct.split(",")]
def list_allowed_tags(self):
mct = self.config_allowed_tags or ""
return [t.strip() for t in mct.split(",")]
return [strip_whitespaces(t) for t in mct.split(",")]
def list_denied_column_values(self):
mct = self.config_denied_column_value or ""
return [t.strip() for t in mct.split(",")]
return [strip_whitespaces(t) for t in mct.split(",")]
def list_allowed_column_values(self):
mct = self.config_allowed_column_value or ""
return [t.strip() for t in mct.split(",")]
return [strip_whitespaces(t) for t in mct.split(",")]
def get_log_level(self):
return logger.get_level_name(self.config_log_level)
def get_mail_settings(self):
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
return {k: v for k, v in self.__dict__.items() if k.startswith('mail_')}
def get_mail_server_configured(self):
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
def get_scheduled_task_settings(self):
return {k: v for k, v in self.__dict__.items() if k.startswith('schedule_')}
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
'''Possibly updates a field of this object.
"""Possibly updates a field of this object.
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
:returns: `True` if the field has changed value
'''
"""
new_value = dictionary.get(field, default)
if new_value is None:
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
return False
if field not in self.__dict__:
@@ -268,20 +320,20 @@ class _ConfigSQL(object):
if current_value == new_value:
return False
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
setattr(self, field, new_value)
return True
def toDict(self):
def to_dict(self):
storage = {}
for k, v in self.__dict__.items():
if k[0] != '_' and not k.endswith("password") and not k.endswith("secret"):
if k[0] != '_' and not k.endswith("_e") and not k == "cli" \
and 'api' not in k.lower() and 'token' not in k.lower() \
and 'secret' not in k.lower():
storage[k] = v
return storage
def load(self):
'''Load all configuration values from the underlying storage.'''
"""Load all configuration values from the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k, v in s.__dict__.items():
if k[0] != '_':
@@ -290,35 +342,51 @@ class _ConfigSQL(object):
column = s.__class__.__dict__.get(k)
if column.default is not None:
v = column.default.arg
setattr(self, k, v)
if k.endswith("_e") and v is not None:
try:
setattr(self, k, self._fernet.decrypt(v).decode())
except cryptography.fernet.InvalidToken:
setattr(self, k, "")
else:
setattr(self, k, v)
have_metadata_db = bool(self.config_calibre_dir)
if have_metadata_db:
if not self.config_use_google_drive:
db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
have_metadata_db = os.path.isfile(db_file)
db_file = os.path.join(self.config_calibre_dir, 'metadata.db')
have_metadata_db = os.path.isfile(db_file)
self.db_configured = have_metadata_db
constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')]
logfile = logger.setup(self.config_logfile, self.config_log_level)
if logfile != self.config_logfile:
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
from . import cli_param
if os.environ.get('FLASK_DEBUG'):
logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
else:
# pylint: disable=access-member-before-definition
logfile = logger.setup(cli_param.logpath or self.config_logfile, self.config_log_level)
if logfile != os.path.abspath(self.config_logfile):
if logfile != os.path.abspath(cli_param.logpath):
log.warning("Log path %s not valid, falling back to default", self.config_logfile)
self.config_logfile = logfile
s.config_logfile = logfile
self._session.merge(s)
try:
self._session.commit()
except OperationalError as e:
log.error('Database error: %s', e)
self._session.rollback()
self.__dict__["dirty"] = list()
def save(self):
'''Apply all configuration values to the underlying storage.'''
"""Apply all configuration values to the underlying storage."""
s = self._read_from_storage() # type: _Settings
for k, v in self.__dict__.items():
for k in self.dirty:
if k[0] == '_':
continue
if hasattr(s, k):
setattr(s, k, v)
if k.endswith("_e"):
setattr(s, k, self._fernet.encrypt(self.__dict__[k].encode()))
else:
setattr(s, k, self.__dict__[k])
log.debug("_ConfigSQL updating storage")
self._session.merge(s)
@@ -334,37 +402,78 @@ class _ConfigSQL(object):
log.error(error)
log.warning("invalidating configuration")
self.db_configured = False
self.config_calibre_dir = None
self.save()
def get_book_path(self):
return self.config_calibre_split_dir if self.config_calibre_split else self.config_calibre_dir
def _migrate_table(session, orm_class):
def store_calibre_uuid(self, calibre_db, Library_table):
from . import app
try:
with app.app_context():
calibre_uuid = calibre_db.session.query(Library_table).one_or_none()
if self.config_calibre_uuid != calibre_uuid.uuid:
self.config_calibre_uuid = calibre_uuid.uuid
self.save()
except AttributeError:
pass
def __setattr__(self, attr_name, attr_value):
super().__setattr__(attr_name, attr_value)
self.__dict__["dirty"].append(attr_name)
def _encrypt_fields(session, secret_key):
try:
session.query(exists().where(_Settings.mail_password_e)).scalar()
except OperationalError:
with session.bind.connect() as conn:
conn.execute(text("ALTER TABLE settings ADD column 'mail_password_e' String"))
conn.execute(text("ALTER TABLE settings ADD column 'config_ldap_serv_password_e' String"))
session.commit()
crypter = Fernet(secret_key)
settings = session.query(_Settings.mail_password, _Settings.config_ldap_serv_password).first()
if settings.mail_password:
session.query(_Settings).update(
{_Settings.mail_password_e: crypter.encrypt(settings.mail_password.encode())})
if settings.config_ldap_serv_password:
session.query(_Settings).update(
{_Settings.config_ldap_serv_password_e: crypter.encrypt(settings.config_ldap_serv_password.encode())})
session.commit()
def _migrate_table(session, orm_class, secret_key=None):
if secret_key:
_encrypt_fields(session, secret_key)
changed = False
for column_name, column in orm_class.__dict__.items():
if column_name[0] != '_':
try:
session.query(column).first()
except exc.OperationalError as err:
except OperationalError as err:
log.debug("%s: %s", column_name, err.args[0])
if column.default is not None:
if sys.version_info < (3, 0):
if isinstance(column.default.arg, unicode):
column.default.arg = column.default.arg.encode('utf-8')
if column.default is None:
column_default = ""
else:
if isinstance(column.default.arg, bool):
column_default = ("DEFAULT %r" % int(column.default.arg))
column_default = "DEFAULT {}".format(int(column.default.arg))
else:
column_default = ("DEFAULT %r" % column.default.arg)
alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name,
column.type,
column_default)
column_default = "DEFAULT `{}`".format(column.default.arg)
if isinstance(column.type, JSON):
column_type = "JSON"
else:
column_type = column.type
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
column_name,
column_type,
column_default))
log.debug(alter_table)
session.execute(alter_table)
changed = True
except json.decoder.JSONDecodeError as e:
log.error("Database corrupt column: {}".format(column_name))
log.debug(e)
if changed:
try:
@@ -373,23 +482,46 @@ def _migrate_table(session, orm_class):
session.rollback()
def autodetect_calibre_binary():
def autodetect_calibre_binaries():
if sys.platform == "win32":
calibre_path = ["C:\\program files\\calibre\\ebook-convert.exe",
"C:\\program files(x86)\\calibre\\ebook-convert.exe",
"C:\\program files(x86)\\calibre2\\ebook-convert.exe",
"C:\\program files\\calibre2\\ebook-convert.exe"]
calibre_path = ["C:\\program files\\calibre\\",
"C:\\program files(x86)\\calibre\\",
"C:\\program files(x86)\\calibre2\\",
"C:\\program files\\calibre2\\"]
elif sys.platform.startswith("freebsd"):
calibre_path = ["/usr/local/bin/"]
else:
calibre_path = ["/opt/calibre/ebook-convert"]
calibre_path = ["/opt/calibre/"]
for element in calibre_path:
if os.path.isfile(element) and os.access(element, os.X_OK):
return element
supported_binary_paths = [os.path.join(element, binary)
for binary in constants.SUPPORTED_CALIBRE_BINARIES.values()]
if all(os.path.isfile(binary_path) and os.access(binary_path, os.X_OK)
for binary_path in supported_binary_paths):
values = [process_wait([binary_path, "--version"],
pattern=r'\(calibre (.*)\)') for binary_path in supported_binary_paths]
if all(values):
version = values[0].group(1)
log.debug("calibre version %s", version)
return element
return ""
def autodetect_converter_binary(calibre_path):
if sys.platform == "win32":
converter_path = os.path.join(calibre_path, "ebook-convert.exe")
else:
converter_path = os.path.join(calibre_path, "ebook-convert")
if calibre_path and os.path.isfile(converter_path) and os.access(converter_path, os.X_OK):
return converter_path
return ""
def autodetect_unrar_binary():
if sys.platform == "win32":
calibre_path = ["C:\\program files\\WinRar\\unRAR.exe",
"C:\\program files(x86)\\WinRar\\unRAR.exe"]
elif sys.platform.startswith("freebsd"):
calibre_path = ["/usr/local/bin/unrar"]
else:
calibre_path = ["/usr/bin/unrar"]
for element in calibre_path:
@@ -397,10 +529,13 @@ def autodetect_unrar_binary():
return element
return ""
def autodetect_kepubify_binary():
if sys.platform == "win32":
calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe",
"C:\\program files(x86)\\kepubify\\kepubify-windows-64Bit.exe"]
elif sys.platform.startswith("freebsd"):
calibre_path = ["/usr/local/bin/kepubify"]
else:
calibre_path = ["/opt/kepubify/kepubify-linux-64bit", "/opt/kepubify/kepubify-linux-32bit"]
for element in calibre_path:
@@ -408,33 +543,49 @@ def autodetect_kepubify_binary():
return element
return ""
def _migrate_database(session):
def _migrate_database(session, secret_key):
# make sure the table is created, if it does not exist
_Base.metadata.create_all(session.bind)
_migrate_table(session, _Settings)
_migrate_table(session, _Settings, secret_key)
_migrate_table(session, _Flask_Settings)
def load_configuration(session):
_migrate_database(session)
def load_configuration(session, secret_key):
_migrate_database(session, secret_key)
if not session.query(_Settings).count():
session.add(_Settings())
session.commit()
conf = _ConfigSQL(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
def get_flask_session_key(session):
flask_settings = session.query(_Flask_Settings).one_or_none()
if flask_settings == None:
def get_flask_session_key(_session):
flask_settings = _session.query(_Flask_Settings).one_or_none()
if flask_settings is None:
flask_settings = _Flask_Settings(os.urandom(32))
session.add(flask_settings)
session.commit()
_session.add(flask_settings)
_session.commit()
return flask_settings.flask_session_key
def get_encryption_key(key_path):
key_file = os.path.join(key_path, ".key")
generate = True
error = ""
key = None
if os.path.exists(key_file) and os.path.getsize(key_file) > 32:
with open(key_file, "rb") as f:
key = f.read()
try:
urlsafe_b64decode(key)
generate = False
except ValueError:
pass
if generate:
key = Fernet.generate_key()
try:
with open(key_file, "wb") as f:
f.write(key)
os.chmod(key_file, 0o600)
except PermissionError as e:
error = e
return key, error
+77 -21
View File
@@ -16,33 +16,48 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import os
from collections import namedtuple
HOME_CONFIG = False
# APP_MODE - production, development, or test
APP_MODE = os.environ.get('APP_MODE', 'production')
# if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
# In executables updater is not available, so variable is set to False there
UPDATER_AVAILABLE = True
# Base dir is parent of current file, necessary if called from different folder
if sys.version_info < (3, 0):
BASE_DIR = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),os.pardir)).decode('utf-8')
else:
BASE_DIR = os.path.abspath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),os.pardir))
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
# if executable file the files should be placed in the parent dir (parallel to the exe file)
STATIC_DIR = os.path.join(BASE_DIR, 'cps', 'static')
TEMPLATES_DIR = os.path.join(BASE_DIR, 'cps', 'templates')
TRANSLATIONS_DIR = os.path.join(BASE_DIR, 'cps', 'translations')
# Cache dir - use CACHE_DIRECTORY environment variable, otherwise CACHE_DIR (for backwards compatibility),
# otherwise use the default directory: cps/cache
DEFAULT_CACHE_DIR = os.path.join(BASE_DIR, 'cps', 'cache')
CACHE_DIRECTORY = os.environ.get(
'CACHE_DIRECTORY', os.environ.get('CACHE_DIR', DEFAULT_CACHE_DIR)
)
if HOME_CONFIG:
home_dir = os.path.join(os.path.expanduser("~"),".calibre-web")
home_dir = os.path.join(os.path.expanduser("~"), ".calibre-web")
if not os.path.exists(home_dir):
os.makedirs(home_dir)
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', home_dir)
else:
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR)
CONFIG_DIR = os.environ.get('CALIBRE_DBPATH', BASE_DIR)
if getattr(sys, 'frozen', False):
CONFIG_DIR = os.path.abspath(os.path.join(CONFIG_DIR, os.pardir))
DEFAULT_SETTINGS_FILE = "app.db"
DEFAULT_GDRIVE_FILE = "gdrive.db"
ROLE_USER = 0 << 0
ROLE_ADMIN = 1 << 0
ROLE_DOWNLOAD = 1 << 1
@@ -84,6 +99,26 @@ SIDEBAR_ARCHIVED = 1 << 15
SIDEBAR_DOWNLOAD = 1 << 16
SIDEBAR_LIST = 1 << 17
sidebar_settings = {
"detail_random": DETAIL_RANDOM,
"sidebar_language": SIDEBAR_LANGUAGE,
"sidebar_series": SIDEBAR_SERIES,
"sidebar_category": SIDEBAR_CATEGORY,
"sidebar_random": SIDEBAR_RANDOM,
"sidebar_author": SIDEBAR_AUTHOR,
"sidebar_best_rated": SIDEBAR_BEST_RATED,
"sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD,
"sidebar_recent": SIDEBAR_RECENT,
"sidebar_sorted": SIDEBAR_SORTED,
"sidebar_publisher": SIDEBAR_PUBLISHER,
"sidebar_rating": SIDEBAR_RATING,
"sidebar_format": SIDEBAR_FORMAT,
"sidebar_archived": SIDEBAR_ARCHIVED,
"sidebar_download": SIDEBAR_DOWNLOAD,
"sidebar_list": SIDEBAR_LIST,
}
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
@@ -102,7 +137,7 @@ LDAP_AUTH_SIMPLE = 0
DEFAULT_MAIL_SERVER = "mail.example.org"
DEFAULT_PASSWORD = "admin123"
DEFAULT_PASSWORD = "admin123" # nosec
DEFAULT_PORT = 8083
env_CALIBRE_PORT = os.environ.get("CALIBRE_PORT", DEFAULT_PORT)
try:
@@ -112,32 +147,53 @@ except ValueError:
del env_CALIBRE_PORT
EXTENSIONS_AUDIO = {'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt','cbz','cbr']
EXTENSIONS_CONVERT_TO = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt']
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx',
'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
EXTENSIONS_AUDIO = {'mp3', 'mp4', 'ogg', 'opus', 'wav', 'flac', 'm4a', 'm4b'}
EXTENSIONS_CONVERT_FROM = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf',
'txt', 'htmlz', 'rtf', 'odt', 'cbz', 'cbr', 'prc']
EXTENSIONS_CONVERT_TO = ['pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2',
'lit', 'lrf', 'txt', 'htmlz', 'rtf', 'odt']
EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'kepub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'cb7', 'djvu', 'djv',
'prc', 'doc', 'docx', 'fb2', 'html', 'rtf', 'lit', 'odt', 'mp3', 'mp4', 'ogg',
'opus', 'wav', 'flac', 'm4a', 'm4b'}
_extension = ""
if sys.platform == "win32":
_extension = ".exe"
SUPPORTED_CALIBRE_BINARIES = {binary: binary + _extension for binary in ["ebook-convert", "calibredb"]}
def has_flag(value, bit_flag):
return bit_flag == (bit_flag & (value or 0))
def selected_roles(dictionary):
return sum(v for k, v in ALL_ROLES.items() if k in dictionary)
# :rtype: BookMeta
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages')
'series_id, languages, publisher, pubdate, identifiers')
STABLE_VERSION = {'version': '0.6.10'}
# python build process likes to have x.y.zbw -> b for beta and w a counting number
STABLE_VERSION = '0.6.27b'
NIGHTLY_VERSION = {}
NIGHTLY_VERSION = dict()
NIGHTLY_VERSION[0] = '$Format:%H$'
NIGHTLY_VERSION[1] = '$Format:%cI$'
# NIGHTLY_VERSION[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# NIGHTLY_VERSION[1] = '2018-09-09T10:13:08+02:00'
# CACHE
CACHE_TYPE_THUMBNAILS = 'thumbnails'
# Thumbnail Types
THUMBNAIL_TYPE_COVER = 1
THUMBNAIL_TYPE_SERIES = 2
THUMBNAIL_TYPE_AUTHOR = 3
# Thumbnails Sizes
COVER_THUMBNAIL_ORIGINAL = 0
COVER_THUMBNAIL_SMALL = 1
COVER_THUMBNAIL_MEDIUM = 2
COVER_THUMBNAIL_LARGE = 4
# clean-up the module namespace
del sys, os, namedtuple
+15 -15
View File
@@ -16,10 +16,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import re
from flask_babel import gettext as _
from flask_babel import lazy_gettext as N_
from . import config, logger
from .subproc_wrapper import process_wait
@@ -27,10 +27,9 @@ from .subproc_wrapper import process_wait
log = logger.create()
# _() necessary to make babel aware of string for translation
_NOT_CONFIGURED = _('not configured')
_NOT_INSTALLED = _('not installed')
_EXECUTION_ERROR = _('Execution permissions missing')
# strings getting translated when used
_NOT_INSTALLED = N_('not installed')
_EXECUTION_ERROR = N_('Execution permissions missing')
def _get_command_version(path, pattern, argument=None):
@@ -39,9 +38,9 @@ def _get_command_version(path, pattern, argument=None):
if argument:
command.append(argument)
try:
for line in process_wait(command):
if re.search(pattern, line):
return line
match = process_wait(command, pattern=pattern)
if isinstance(match, re.Match):
return match.string
except Exception as ex:
log.warning("%s: %s", path, ex)
return _EXECUTION_ERROR
@@ -49,14 +48,15 @@ def _get_command_version(path, pattern, argument=None):
def get_calibre_version():
return _get_command_version(config.config_converterpath, r'ebook-convert.*\(calibre', '--version') \
or _NOT_CONFIGURED
return _get_command_version(config.config_converterpath, r'ebook-convert.*\(calibre', '--version')
def get_unrar_version():
return _get_command_version(config.config_rarfile_location, r'UNRAR.*\d') or _NOT_CONFIGURED
unrar_version = _get_command_version(config.config_rarfile_location, r'UNRAR.*\d')
if unrar_version == "not installed":
unrar_version = _get_command_version(config.config_rarfile_location, r'unrar.*\d', '-V')
return unrar_version
def get_kepubify_version():
return _get_command_version(config.config_kepubifypath, r'kepubify\s','--version') or _NOT_CONFIGURED
return _get_command_version(config.config_kepubifypath, r'kepubify\s', '--version')
+49
View File
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2022 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
try:
from wand.image import Image
use_IM = True
except (ImportError, RuntimeError) as e:
use_IM = False
NO_JPEG_EXTENSIONS = ['.png', '.webp', '.bmp']
COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
def cover_processing(tmp_file_path, img, extension):
# tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
tmp_cover_name = tmp_file_path + '.jpg'
if extension in NO_JPEG_EXTENSIONS:
if use_IM:
with Image(blob=img) as imgc:
imgc.format = 'jpeg'
imgc.transform_colorspace('srgb')
imgc.save(filename=tmp_cover_name)
return tmp_cover_name
else:
return None
if img:
with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name
else:
return None
+22
View File
@@ -0,0 +1,22 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
from .adapters import ValidatingHTTPAdapter
from .api import *
from .addrvalidator import AddrValidator
from .exceptions import UnacceptableAddressException
+48
View File
@@ -0,0 +1,48 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK
from .addrvalidator import AddrValidator
from .exceptions import ProxyDisabledException
from .poolmanager import ValidatingPoolManager
class ValidatingHTTPAdapter(HTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + ['_validator']
def __init__(self, *args, **kwargs):
self._validator = kwargs.pop('validator', None)
if not self._validator:
self._validator = AddrValidator()
super().__init__(*args, **kwargs)
def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK,
**pool_kwargs):
self._pool_connections = connections
self._pool_maxsize = maxsize
self._pool_block = block
self.poolmanager = ValidatingPoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
validator=self._validator,
**pool_kwargs
)
def proxy_manager_for(self, proxy, **proxy_kwargs):
raise ProxyDisabledException("Proxies cannot be used with Advocate")
+281
View File
@@ -0,0 +1,281 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
import functools
import fnmatch
import ipaddress
import re
try:
import netifaces
HAVE_NETIFACES = True
except ImportError:
netifaces = None
HAVE_NETIFACES = False
from .exceptions import NameserverException, ConfigException
def canonicalize_hostname(hostname):
"""Lowercase and punycodify a hostname"""
# We do the lowercasing after IDNA encoding because we only want to
# lowercase the *ASCII* chars.
# TODO: The differences between IDNA2003 and IDNA2008 might be relevant
# to us, but both specs are damn confusing.
return str(hostname.encode("idna").lower(), 'utf-8')
def determine_local_addresses():
"""Get all IPs that refer to this machine according to netifaces"""
if not HAVE_NETIFACES:
raise ConfigException("Tried to determine local addresses, "
"but netifaces module was not importable")
ips = []
for interface in netifaces.interfaces():
if_families = netifaces.ifaddresses(interface)
for family_kind in {netifaces.AF_INET, netifaces.AF_INET6}:
addrs = if_families.get(family_kind, [])
for addr in (x.get("addr", "") for x in addrs):
if family_kind == netifaces.AF_INET6:
# We can't do anything sensible with the scope here
addr = addr.split("%")[0]
ips.append(ipaddress.ip_network(addr))
return ips
def add_local_address_arg(func):
"""Add the "_local_addresses" kwarg if it's missing
IMO this information shouldn't be cached between calls (what if one of the
adapters got a new IP at runtime?,) and we don't want each function to
recalculate it. Just recalculate it if the caller didn't provide it for us.
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
if "_local_addresses" not in kwargs:
if self.autodetect_local_addresses:
kwargs["_local_addresses"] = determine_local_addresses()
else:
kwargs["_local_addresses"] = []
return func(self, *args, **kwargs)
return wrapper
class AddrValidator:
_6TO4_RELAY_NET = ipaddress.ip_network("192.88.99.0/24")
# Just the well known prefix, DNS64 servers can set their own
# prefix, but in practice most probably don't.
_DNS64_WK_PREFIX = ipaddress.ip_network("64:ff9b::/96")
DEFAULT_PORT_WHITELIST = {80, 8080, 443, 8443, 8000}
def __init__(
self,
ip_blacklist=None,
ip_whitelist=None,
port_whitelist=None,
port_blacklist=None,
hostname_blacklist=None,
allow_ipv6=False,
allow_teredo=False,
allow_6to4=False,
allow_dns64=False,
# Must be explicitly set to "False" if you don't want to try
# detecting local interface addresses with netifaces.
autodetect_local_addresses=True,
):
if not port_blacklist and not port_whitelist:
# An assortment of common HTTPS? ports.
port_whitelist = self.DEFAULT_PORT_WHITELIST.copy()
self.ip_blacklist = ip_blacklist or set()
self.ip_whitelist = ip_whitelist or set()
self.port_blacklist = port_blacklist or set()
self.port_whitelist = port_whitelist or set()
# TODO: ATM this can contain either regexes or globs that are converted
# to regexes upon every check. Create a collection that automagically
# converts them to regexes on insert?
self.hostname_blacklist = hostname_blacklist or set()
self.allow_ipv6 = allow_ipv6
self.allow_teredo = allow_teredo
self.allow_6to4 = allow_6to4
self.allow_dns64 = allow_dns64
self.autodetect_local_addresses = autodetect_local_addresses
@add_local_address_arg
def is_ip_allowed(self, addr_ip, _local_addresses=None):
if not isinstance(addr_ip,
(ipaddress.IPv4Address, ipaddress.IPv6Address)):
addr_ip = ipaddress.ip_address(addr_ip)
# The whitelist should take precedence over the blacklist so we can
# punch holes in blacklisted ranges
if any(addr_ip in net for net in self.ip_whitelist):
return True
if any(addr_ip in net for net in self.ip_blacklist):
return False
if any(addr_ip in net for net in _local_addresses):
return False
if addr_ip.version == 4:
if not addr_ip.is_private:
# IPs for carrier-grade NAT. Seems weird that it doesn't set
# `is_private`, but we need to check `not is_global`
if not ipaddress.ip_network(addr_ip).is_global:
return False
elif addr_ip.version == 6:
# You'd better have a good reason for enabling IPv6
# because Advocate's techniques don't work well without NAT.
if not self.allow_ipv6:
return False
# v6 addresses can also map to IPv4 addresses! Tricky!
v4_nested = []
if addr_ip.ipv4_mapped:
v4_nested.append(addr_ip.ipv4_mapped)
# WTF IPv6? Why you gotta have a billion tunneling mechanisms?
# XXX: Do we even really care about these? If we're tunneling
# through public servers we shouldn't be able to access
# addresses on our private network, right?
if addr_ip.sixtofour:
if not self.allow_6to4:
return False
v4_nested.append(addr_ip.sixtofour)
if addr_ip.teredo:
if not self.allow_teredo:
return False
# Check both the client *and* server IPs
v4_nested.extend(addr_ip.teredo)
if addr_ip in self._DNS64_WK_PREFIX:
if not self.allow_dns64:
return False
# When using the well-known prefix the last 4 bytes
# are the IPv4 addr
v4_nested.append(ipaddress.ip_address(addr_ip.packed[-4:]))
if not all(self.is_ip_allowed(addr_v4) for addr_v4 in v4_nested):
return False
# fec0::*, apparently deprecated?
if addr_ip.is_site_local:
return False
else:
raise ValueError("Unsupported IP version(?): %r" % addr_ip)
# 169.254.XXX.XXX, AWS uses these for autoconfiguration
if addr_ip.is_link_local:
return False
# 127.0.0.1, ::1, etc.
if addr_ip.is_loopback:
return False
if addr_ip.is_multicast:
return False
# 192.168.XXX.XXX, 10.XXX.XXX.XXX
if addr_ip.is_private:
return False
# 255.255.255.255, ::ffff:XXXX:XXXX (v6->v4) mapping
if addr_ip.is_reserved:
return False
# There's no reason to connect directly to a 6to4 relay
if addr_ip in self._6TO4_RELAY_NET:
return False
# 0.0.0.0
if addr_ip.is_unspecified:
return False
# It doesn't look bad, so... it's must be ok!
return True
def _hostname_matches_pattern(self, hostname, pattern):
# If they specified a string, just assume they only want basic globbing.
# This stops people from not realizing they're dealing in REs and
# not escaping their periods unless they specifically pass in an RE.
# This has the added benefit of letting us sanely handle globbed
# IDNs by default.
if isinstance(pattern, str):
# convert the glob to a punycode glob, then a regex
pattern = fnmatch.translate(canonicalize_hostname(pattern))
hostname = canonicalize_hostname(hostname)
# Down the line the hostname may get treated as a null-terminated string
# (as with `socket.getaddrinfo`.) Try to account for that.
#
# >>> socket.getaddrinfo("example.com\x00aaaa", 80)
# [(2, 1, 6, '', ('93.184.216.34', 80)), [...]
no_null_hostname = hostname.split("\x00")[0]
return any(re.match(pattern, x.strip(".")) for x
in (no_null_hostname, hostname))
def is_hostname_allowed(self, hostname):
# Sometimes (like with "external" services that your IP has privileged
# access to) you might not always know the IP range to blacklist access
# to, or the `A` record might change without you noticing.
# For e.x.: `foocorp.external.org`.
#
# Another option is doing something like:
#
# for addrinfo in socket.getaddrinfo("foocorp.external.org", 80):
# global_validator.ip_blacklist.add(ip_address(addrinfo[4][0]))
#
# but that's not always a good idea if they're behind a third-party lb.
for pattern in self.hostname_blacklist:
if self._hostname_matches_pattern(hostname, pattern):
return False
return True
@add_local_address_arg
def is_addrinfo_allowed(self, addrinfo, _local_addresses=None):
assert(len(addrinfo) == 5)
# XXX: Do we care about any of the other elements? Guessing not.
family, socktype, proto, canonname, sockaddr = addrinfo
# The 4th elem inaddrinfo may either be a touple of two or four items,
# depending on whether we're dealing with IPv4 or v6
if len(sockaddr) == 2:
# v4
ip, port = sockaddr
elif len(sockaddr) == 4:
# v6
# XXX: what *are* `flow_info` and `scope_id`? Anything useful?
# Seems like we can figure out all we need about the scope from
# the `is_<x>` properties.
ip, port, flow_info, scope_id = sockaddr
else:
raise ValueError("Unexpected addrinfo format %r" % sockaddr)
# Probably won't help protect against SSRF, but might prevent our being
# used to attack others' non-HTTP services. See
# http://www.remote.org/jochen/sec/hfpa/
if self.port_whitelist and port not in self.port_whitelist:
return False
if port in self.port_blacklist:
return False
if self.hostname_blacklist:
if not canonname:
raise NameserverException(
"addrinfo must contain the canon name to do blacklisting "
"based on hostname. Make sure you use the "
"`socket.AI_CANONNAME` flag, and that each record contains "
"the canon name. Your DNS server might also be garbage."
)
if not self.is_hostname_allowed(canonname):
return False
return self.is_ip_allowed(ip, _local_addresses=_local_addresses)
+202
View File
@@ -0,0 +1,202 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
"""
advocate.api
~~~~~~~~~~~~
This module implements the Requests API, largely a copy/paste from `requests`
itself.
:copyright: (c) 2015 by Jordan Milne.
:license: Apache2, see LICENSE for more details.
"""
from collections import OrderedDict
import hashlib
import pickle
from requests import Session as RequestsSession
# import cw_advocate
from .adapters import ValidatingHTTPAdapter
from .exceptions import MountDisabledException
class Session(RequestsSession):
"""Convenience wrapper around `requests.Session` set up for `advocate`ing"""
__attrs__ = RequestsSession.__attrs__ + ["validator"]
DEFAULT_VALIDATOR = None
"""
User-replaceable default validator to use for all Advocate sessions,
includes sessions created by advocate.get()
"""
def __init__(self, *args, **kwargs):
self.validator = kwargs.pop("validator", None) or self.DEFAULT_VALIDATOR
adapter_kwargs = kwargs.pop("_adapter_kwargs", {})
# `Session.__init__()` calls `mount()` internally, so we need to allow
# it temporarily
self.__mount_allowed = True
RequestsSession.__init__(self, *args, **kwargs)
# Drop any existing adapters
self.adapters = OrderedDict()
self.mount("http://", ValidatingHTTPAdapter(validator=self.validator, **adapter_kwargs))
self.mount("https://", ValidatingHTTPAdapter(validator=self.validator, **adapter_kwargs))
self.__mount_allowed = False
def mount(self, *args, **kwargs):
"""Wrapper around `mount()` to prevent a protection bypass"""
if self.__mount_allowed:
super().mount(*args, **kwargs)
else:
raise MountDisabledException(
"mount() is disabled to prevent protection bypasses"
)
def session(*args, **kwargs):
return Session(*args, **kwargs)
def request(method, url, **kwargs):
"""Constructs and sends a :class:`Request <Request>`.
:param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': ('filename', fileobj)}``) for multipart encoding upload.
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
:param timeout: (optional) How long to wait for the server to send data
before giving up, as a float, or a (`connect timeout, read timeout
<user/advanced.html#timeouts>`_) tuple.
:type timeout: float or tuple
:param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE redirect following is allowed.
:type allow_redirects: bool
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
validator = kwargs.pop("validator", None)
with Session(validator=validator) as sess:
response = sess.request(method=method, url=url, **kwargs)
return response
def get(url, **kwargs):
"""Sends a GET request.
:param url: URL for the new :class:`Request` object.
:param **kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""
kwargs.setdefault('allow_redirects', True)
return request('get', url, **kwargs)
class RequestsAPIWrapper:
"""Provides a `requests.api`-like interface with a specific validator"""
# Due to how the classes are dynamically constructed pickling may not work
# correctly unless loaded within the same interpreter instance.
# Enable at your peril.
SUPPORT_WRAPPER_PICKLING = False
def __init__(self, validator):
# Do this here to avoid circular import issues
try:
from .futures import FuturesSession
have_requests_futures = True
except ImportError as e:
have_requests_futures = False
self.validator = validator
outer_self = self
class _WrappedSession(Session):
"""An `advocate.Session` that uses the wrapper's blacklist
the wrapper is meant to be a transparent replacement for `requests`,
so people should be able to subclass `wrapper.Session` and still
get the desired validation behaviour
"""
DEFAULT_VALIDATOR = outer_self.validator
self._make_wrapper_cls_global(_WrappedSession)
if have_requests_futures:
class _WrappedFuturesSession(FuturesSession):
"""Like _WrappedSession, but for `FuturesSession`s"""
DEFAULT_VALIDATOR = outer_self.validator
self._make_wrapper_cls_global(_WrappedFuturesSession)
self.FuturesSession = _WrappedFuturesSession
self.request = self._default_arg_wrapper(request)
self.get = self._default_arg_wrapper(get)
self.Session = _WrappedSession
def __getattr__(self, item):
# This class is meant to mimic the requests base module, so if we don't
# have this attribute, it might be on the base module (like the Request
# class, etc.)
try:
return object.__getattribute__(self, item)
except AttributeError:
from . import cw_advocate
return getattr(cw_advocate, item)
def _default_arg_wrapper(self, fun):
def wrapped_func(*args, **kwargs):
kwargs.setdefault("validator", self.validator)
return fun(*args, **kwargs)
return wrapped_func
def _make_wrapper_cls_global(self, cls):
if not self.SUPPORT_WRAPPER_PICKLING:
return
# Gnarly, but necessary to give pickle a consistent module-level
# reference for each wrapper.
wrapper_hash = hashlib.sha256(pickle.dumps(self)).hexdigest()
cls.__name__ = "_".join((cls.__name__, wrapper_hash))
cls.__qualname__ = ".".join((__name__, cls.__name__))
if not globals().get(cls.__name__):
globals()[cls.__name__] = cls
__all__ = (
"get",
"request",
"session",
"Session",
"RequestsAPIWrapper",
)
+201
View File
@@ -0,0 +1,201 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
import ipaddress
import socket
from socket import timeout as SocketTimeout
from urllib3.connection import HTTPSConnection, HTTPConnection
from urllib3.exceptions import ConnectTimeoutError
from urllib3.util.connection import _set_socket_options
from urllib3.util.connection import create_connection as old_create_connection
from . import addrvalidator
from .exceptions import UnacceptableAddressException
def advocate_getaddrinfo(host, port, get_canonname=False):
addrinfo = socket.getaddrinfo(
host,
port,
0,
socket.SOCK_STREAM,
0,
# We need what the DNS client sees the hostname as, correctly handles
# IDNs and tricky things like `private.foocorp.org\x00.google.com`.
# All IDNs will be converted to punycode.
socket.AI_CANONNAME if get_canonname else 0,
)
return fix_addrinfo(addrinfo)
def fix_addrinfo(records):
"""
Propagate the canonname across records and parse IPs
I'm not sure if this is just the behaviour of `getaddrinfo` on Linux, but
it seems like only the first record in the set has the canonname field
populated.
"""
def fix_record(record, canonname):
sa = record[4]
sa = (ipaddress.ip_address(sa[0]),) + sa[1:]
return record[0], record[1], record[2], canonname, sa
canonname = None
if records:
# Apparently the canonical name is only included in the first record?
# Add it to all of them.
assert(len(records[0]) == 5)
canonname = records[0][3]
return tuple(fix_record(x, canonname) for x in records)
# Lifted from requests' urllib3, which in turn lifted it from `socket.py`. Oy!
def validating_create_connection(address,
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None, socket_options=None,
validator=None):
"""Connect to *address* and return the socket object.
Convenience function. Connect to *address* (a 2-tuple ``(host,
port)``) and return the socket object. Passing the optional
*timeout* parameter will set the timeout on the socket instance
before attempting to connect. If no *timeout* is supplied, the
global default timeout setting returned by :func:`getdefaulttimeout`
is used. If *source_address* is set it must be a tuple of (host, port)
for the socket to bind as a source address before making the connection.
An host of '' or port 0 tells the OS to use the default.
"""
host, port = address
# We can skip asking for the canon name if we're not doing hostname-based
# blacklisting.
need_canonname = False
if validator.hostname_blacklist:
need_canonname = True
# We check both the non-canonical and canonical hostnames so we can
# catch both of these:
# CNAME from nonblacklisted.com -> blacklisted.com
# CNAME from blacklisted.com -> nonblacklisted.com
if not validator.is_hostname_allowed(host):
raise UnacceptableAddressException(host)
err = None
addrinfo = advocate_getaddrinfo(host, port, get_canonname=need_canonname)
if addrinfo:
if validator.autodetect_local_addresses:
local_addresses = addrvalidator.determine_local_addresses()
else:
local_addresses = []
for res in addrinfo:
# Are we allowed to connect with this result?
if not validator.is_addrinfo_allowed(
res,
_local_addresses=local_addresses,
):
continue
af, socktype, proto, canonname, sa = res
# Unparse the validated IP
sa = (sa[0].exploded,) + sa[1:]
sock = None
try:
sock = socket.socket(af, socktype, proto)
# If provided, set socket level options before connecting.
# This is the only addition urllib3 makes to this function.
_set_socket_options(sock, socket_options)
if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
return sock
except socket.error as _:
err = _
if sock is not None:
sock.close()
sock = None
if err is None:
# If we got here, none of the results were acceptable
err = UnacceptableAddressException(address)
if err is not None:
raise err
else:
raise socket.error("getaddrinfo returns an empty list")
# TODO: Is there a better way to add this to multiple classes with different
# base classes? I tried a mixin, but it used the base method instead.
def _validating_new_conn(self):
""" Establish a socket connection and set nodelay settings on it.
:return: New socket connection.
"""
extra_kw = {}
if self.source_address:
extra_kw['source_address'] = self.source_address
if self.socket_options:
extra_kw['socket_options'] = self.socket_options
try:
# Hack around HTTPretty's patched sockets
# TODO: some better method of hacking around it that checks if we
# _would have_ connected to a private addr?
conn_func = validating_create_connection
if socket.getaddrinfo.__module__.startswith("httpretty"):
conn_func = old_create_connection
else:
extra_kw["validator"] = self._validator
conn = conn_func(
(self.host, self.port),
self.timeout,
**extra_kw
)
except SocketTimeout:
raise ConnectTimeoutError(
self, "Connection to %s timed out. (connect timeout=%s)" %
(self.host, self.timeout))
return conn
# Don't silently break if the private API changes across urllib3 versions
assert(hasattr(HTTPConnection, '_new_conn'))
assert(hasattr(HTTPSConnection, '_new_conn'))
class ValidatingHTTPConnection(HTTPConnection):
_new_conn = _validating_new_conn
def __init__(self, *args, **kwargs):
self._validator = kwargs.pop("validator")
HTTPConnection.__init__(self, *args, **kwargs)
class ValidatingHTTPSConnection(HTTPSConnection):
_new_conn = _validating_new_conn
def __init__(self, *args, **kwargs):
self._validator = kwargs.pop("validator")
HTTPSConnection.__init__(self, *args, **kwargs)
+39
View File
@@ -0,0 +1,39 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
from urllib3 import HTTPConnectionPool, HTTPSConnectionPool
from .connection import (
ValidatingHTTPConnection,
ValidatingHTTPSConnection,
)
# Don't silently break if the private API changes across urllib3 versions
assert(hasattr(HTTPConnectionPool, 'ConnectionCls'))
assert(hasattr(HTTPSConnectionPool, 'ConnectionCls'))
assert(hasattr(HTTPConnectionPool, 'scheme'))
assert(hasattr(HTTPSConnectionPool, 'scheme'))
class ValidatingHTTPConnectionPool(HTTPConnectionPool):
scheme = 'http'
ConnectionCls = ValidatingHTTPConnection
class ValidatingHTTPSConnectionPool(HTTPSConnectionPool):
scheme = 'https'
ConnectionCls = ValidatingHTTPSConnection
+39
View File
@@ -0,0 +1,39 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
class AdvocateException(Exception):
pass
class UnacceptableAddressException(AdvocateException):
pass
class NameserverException(AdvocateException):
pass
class MountDisabledException(AdvocateException):
pass
class ProxyDisabledException(NotImplementedError, AdvocateException):
pass
class ConfigException(AdvocateException):
pass
+61
View File
@@ -0,0 +1,61 @@
#
# Copyright 2015 Jordan Milne
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Source: https://github.com/JordanMilne/Advocate
import collections
import functools
from urllib3 import PoolManager
from urllib3.poolmanager import _default_key_normalizer, PoolKey
from .connectionpool import (
ValidatingHTTPSConnectionPool,
ValidatingHTTPConnectionPool,
)
pool_classes_by_scheme = {
"http": ValidatingHTTPConnectionPool,
"https": ValidatingHTTPSConnectionPool,
}
AdvocatePoolKey = collections.namedtuple('AdvocatePoolKey',
PoolKey._fields + ('key_validator',))
def key_normalizer(key_class, request_context):
request_context = request_context.copy()
# TODO: add ability to serialize validator rules to dict,
# allowing pool to be shared between sessions with the same
# rules.
request_context["validator"] = id(request_context["validator"])
return _default_key_normalizer(key_class, request_context)
key_fn_by_scheme = {
'http': functools.partial(key_normalizer, AdvocatePoolKey),
'https': functools.partial(key_normalizer, AdvocatePoolKey),
}
class ValidatingPoolManager(PoolManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make sure the API hasn't changed
assert (hasattr(self, 'pool_classes_by_scheme'))
self.pool_classes_by_scheme = pool_classes_by_scheme
self.key_fn_by_scheme = key_fn_by_scheme.copy()
+41
View File
@@ -0,0 +1,41 @@
from babel import negotiate_locale
from flask_babel import Babel, Locale
from babel.core import UnknownLocaleError
from flask import request
from .cw_login import current_user
from . import logger
log = logger.create()
babel = Babel()
def get_locale():
# if a user is logged in, use the locale from the user settings
if current_user is not None and hasattr(current_user, "locale"):
# if the account is the guest account bypass the config lang settings
if current_user.name != 'Guest':
return current_user.locale
preferred = list()
if request.accept_languages:
for x in request.accept_languages.values():
try:
preferred.append(str(Locale.parse(x.replace('-', '_'))))
except (UnknownLocaleError, ValueError) as e:
log.debug('Could not parse locale "%s": %s', x, e)
return negotiate_locale(preferred or ['en'], get_available_translations())
def get_user_locale_language(user_language):
return Locale.parse(user_language).get_language_name(get_locale())
def get_available_locale():
return sorted(babel.list_translations(), key=lambda x: x.display_name.lower())
def get_available_translations():
return set(str(item) for item in get_available_locale())
+98
View File
@@ -0,0 +1,98 @@
# from .__about__ import __version__
from .config import AUTH_HEADER_NAME
from .config import COOKIE_DURATION
from .config import COOKIE_HTTPONLY
from .config import COOKIE_NAME
from .config import COOKIE_SECURE
from .config import ID_ATTRIBUTE
from .config import LOGIN_MESSAGE
from .config import LOGIN_MESSAGE_CATEGORY
from .config import REFRESH_MESSAGE
from .config import REFRESH_MESSAGE_CATEGORY
from .login_manager import LoginManager
from .mixins import AnonymousUserMixin
from .mixins import UserMixin
from .signals import session_protected
from .signals import user_accessed
from .signals import user_loaded_from_cookie
from .signals import user_loaded_from_request
from .signals import user_logged_in
from .signals import user_logged_out
from .signals import user_login_confirmed
from .signals import user_needs_refresh
from .signals import user_unauthorized
# from .test_client import FlaskLoginClient
from .utils import confirm_login
from .utils import current_user
from .utils import decode_cookie
from .utils import encode_cookie
from .utils import fresh_login_required
from .utils import login_fresh
from .utils import login_remembered
from .utils import login_required
from .utils import login_url
from .utils import login_user
from .utils import logout_user
from .utils import make_next_param
from .utils import set_login_view
__version_info__ = ("0", "6", "3")
__version__ = ".".join(__version_info__)
__all__ = [
"__version__",
"AUTH_HEADER_NAME",
"COOKIE_DURATION",
"COOKIE_HTTPONLY",
"COOKIE_NAME",
"COOKIE_SECURE",
"ID_ATTRIBUTE",
"LOGIN_MESSAGE",
"LOGIN_MESSAGE_CATEGORY",
"REFRESH_MESSAGE",
"REFRESH_MESSAGE_CATEGORY",
"LoginManager",
"AnonymousUserMixin",
"UserMixin",
"session_protected",
"user_accessed",
"user_loaded_from_cookie",
"user_loaded_from_request",
"user_logged_in",
"user_logged_out",
"user_login_confirmed",
"user_needs_refresh",
"user_unauthorized",
# "FlaskLoginClient",
"confirm_login",
"current_user",
"decode_cookie",
"encode_cookie",
"fresh_login_required",
"login_fresh",
"login_remembered",
"login_required",
"login_url",
"login_user",
"logout_user",
"make_next_param",
"set_login_view",
]
def __getattr__(name):
if name == "user_loaded_from_header":
import warnings
from .signals import _user_loaded_from_header
warnings.warn(
"'user_loaded_from_header' is deprecated and will be"
" removed in Flask-Login 0.7. Use"
" 'user_loaded_from_request' instead.",
DeprecationWarning,
stacklevel=2,
)
return _user_loaded_from_header
raise AttributeError(name)
+55
View File
@@ -0,0 +1,55 @@
from datetime import timedelta
#: The default name of the "remember me" cookie (``remember_token``)
COOKIE_NAME = "remember_token"
#: The default time before the "remember me" cookie expires (365 days).
COOKIE_DURATION = timedelta(days=365)
#: Whether the "remember me" cookie requires Secure; defaults to ``False``
COOKIE_SECURE = False
#: Whether the "remember me" cookie uses HttpOnly or not; defaults to ``True``
COOKIE_HTTPONLY = True
#: Whether the "remember me" cookie requires same origin; defaults to ``None``
COOKIE_SAMESITE = None
#: The default flash message to display when users need to log in.
LOGIN_MESSAGE = "Please log in to access this page."
#: The default flash message category to display when users need to log in.
LOGIN_MESSAGE_CATEGORY = "message"
#: The default flash message to display when users need to reauthenticate.
REFRESH_MESSAGE = "Please reauthenticate to access this page."
#: The default flash message category to display when users need to
#: reauthenticate.
REFRESH_MESSAGE_CATEGORY = "message"
#: The default attribute to retreive the str id of the user
ID_ATTRIBUTE = "get_id"
#: Default name of the auth header (``Authorization``)
AUTH_HEADER_NAME = "Authorization"
#: A set of session keys that are populated by Flask-Login. Use this set to
#: purge keys safely and accurately.
SESSION_KEYS = {
"_user_id",
"_remember",
"_remember_seconds",
"_id",
"_fresh",
"next",
}
#: A set of HTTP methods which are exempt from `login_required` and
#: `fresh_login_required`. By default, this is just ``OPTIONS``.
EXEMPT_METHODS = {"OPTIONS"}
#: If true, the page the user is attempting to access is stored in the session
#: rather than a url parameter when redirecting to the login view; defaults to
#: ``False``.
USE_SESSION_FOR_NEXT = False
+523
View File
@@ -0,0 +1,523 @@
from datetime import datetime
from datetime import timezone
from datetime import timedelta
import hashlib
from flask import abort
from flask import current_app
from flask import flash
from flask import g
from flask import redirect
from flask import request
from flask import session
from itsdangerous import URLSafeSerializer
from flask.json.tag import TaggedJSONSerializer
from .config import AUTH_HEADER_NAME
from .config import COOKIE_DURATION
from .config import COOKIE_HTTPONLY
from .config import COOKIE_NAME
from .config import COOKIE_SAMESITE
from .config import COOKIE_SECURE
from .config import ID_ATTRIBUTE
from .config import LOGIN_MESSAGE
from .config import LOGIN_MESSAGE_CATEGORY
from .config import REFRESH_MESSAGE
from .config import REFRESH_MESSAGE_CATEGORY
from .config import SESSION_KEYS
from .config import USE_SESSION_FOR_NEXT
from .mixins import AnonymousUserMixin
from .signals import session_protected
from .signals import user_accessed
from .signals import user_loaded_from_cookie
from .signals import user_loaded_from_request
from .signals import user_needs_refresh
from .signals import user_unauthorized
from .utils import _create_identifier
from .utils import _user_context_processor
from .utils import confirm_login
from .utils import expand_login_view
from .utils import login_url as make_login_url
from .utils import make_next_param
class LoginManager:
"""This object is used to hold the settings used for logging in. Instances
of :class:`LoginManager` are *not* bound to specific apps, so you can
create one in the main body of your code and then bind it to your
app in a factory function.
"""
def __init__(self, app=None, add_context_processor=True):
#: A class or factory function that produces an anonymous user, which
#: is used when no one is logged in.
self.anonymous_user = AnonymousUserMixin
#: The name of the view to redirect to when the user needs to log in.
#: (This can be an absolute URL as well, if your authentication
#: machinery is external to your application.)
self.login_view = None
#: Names of views to redirect to when the user needs to log in,
#: per blueprint. If the key value is set to None the value of
#: :attr:`login_view` will be used instead.
self.blueprint_login_views = {}
#: The message to flash when a user is redirected to the login page.
self.login_message = LOGIN_MESSAGE
#: The message category to flash when a user is redirected to the login
#: page.
self.login_message_category = LOGIN_MESSAGE_CATEGORY
#: The name of the view to redirect to when the user needs to
#: reauthenticate.
self.refresh_view = None
#: The message to flash when a user is redirected to the 'needs
#: refresh' page.
self.needs_refresh_message = REFRESH_MESSAGE
#: The message category to flash when a user is redirected to the
#: 'needs refresh' page.
self.needs_refresh_message_category = REFRESH_MESSAGE_CATEGORY
#: The mode to use session protection in. This can be either
#: ``'basic'`` (the default) or ``'strong'``, or ``None`` to disable
#: it.
self.session_protection = "basic"
#: If present, used to translate flash messages ``self.login_message``
#: and ``self.needs_refresh_message``
self.localize_callback = None
self.unauthorized_callback = None
self.needs_refresh_callback = None
self.id_attribute = ID_ATTRIBUTE
self._user_callback = None
self._header_callback = None
self._request_callback = None
self._session_identifier_generator = _create_identifier
if app is not None:
self.init_app(app, add_context_processor)
def setup_app(self, app, add_context_processor=True): # pragma: no cover
"""
This method has been deprecated. Please use
:meth:`LoginManager.init_app` instead.
"""
import warnings
warnings.warn(
"'setup_app' is deprecated and will be removed in"
" Flask-Login 0.7. Use 'init_app' instead.",
DeprecationWarning,
stacklevel=2,
)
self.init_app(app, add_context_processor)
def init_app(self, app, add_context_processor=True):
"""
Configures an application. This registers an `after_request` call, and
attaches this `LoginManager` to it as `app.login_manager`.
:param app: The :class:`flask.Flask` object to configure.
:type app: :class:`flask.Flask`
:param add_context_processor: Whether to add a context processor to
the app that adds a `current_user` variable to the template.
Defaults to ``True``.
:type add_context_processor: bool
"""
app.login_manager = self
app.after_request(self._update_remember_cookie)
if add_context_processor:
app.context_processor(_user_context_processor)
def unauthorized(self):
"""
This is called when the user is required to log in. If you register a
callback with :meth:`LoginManager.unauthorized_handler`, then it will
be called. Otherwise, it will take the following actions:
- Flash :attr:`LoginManager.login_message` to the user.
- If the app is using blueprints find the login view for
the current blueprint using `blueprint_login_views`. If the app
is not using blueprints or the login view for the current
blueprint is not specified use the value of `login_view`.
- Redirect the user to the login view. (The page they were
attempting to access will be passed in the ``next`` query
string variable, so you can redirect there if present instead
of the homepage. Alternatively, it will be added to the session
as ``next`` if USE_SESSION_FOR_NEXT is set.)
If :attr:`LoginManager.login_view` is not defined, then it will simply
raise a HTTP 401 (Unauthorized) error instead.
This should be returned from a view or before/after_request function,
otherwise the redirect will have no effect.
"""
user_unauthorized.send(current_app._get_current_object())
if self.unauthorized_callback:
return self.unauthorized_callback()
if request.blueprint in self.blueprint_login_views:
login_view = self.blueprint_login_views[request.blueprint]
else:
login_view = self.login_view
if not login_view:
abort(401)
if self.login_message:
if self.localize_callback is not None:
flash(
self.localize_callback(self.login_message),
category=self.login_message_category,
)
else:
flash(self.login_message, category=self.login_message_category)
config = current_app.config
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
login_url = expand_login_view(login_view)
session["_id"] = self._session_identifier_generator()
session["next"] = make_next_param(login_url, request.url)
redirect_url = make_login_url(login_view)
else:
redirect_url = make_login_url(login_view, next_url=request.url)
return redirect(redirect_url)
def user_loader(self, callback):
"""
This sets the callback for reloading a user from the session. The
function you set should take a user ID (a ``str``) and return a
user object, or ``None`` if the user does not exist.
:param callback: The callback for retrieving a user object.
:type callback: callable
"""
self._user_callback = callback
return self.user_callback
@property
def user_callback(self):
"""Gets the user_loader callback set by user_loader decorator."""
return self._user_callback
def request_loader(self, callback):
"""
This sets the callback for loading a user from a Flask request.
The function you set should take Flask request object and
return a user object, or `None` if the user does not exist.
:param callback: The callback for retrieving a user object.
:type callback: callable
"""
self._request_callback = callback
return self.request_callback
@property
def request_callback(self):
"""Gets the request_loader callback set by request_loader decorator."""
return self._request_callback
def unauthorized_handler(self, callback):
"""
This will set the callback for the `unauthorized` method, which among
other things is used by `login_required`. It takes no arguments, and
should return a response to be sent to the user instead of their
normal view.
:param callback: The callback for unauthorized users.
:type callback: callable
"""
self.unauthorized_callback = callback
return callback
def needs_refresh_handler(self, callback):
"""
This will set the callback for the `needs_refresh` method, which among
other things is used by `fresh_login_required`. It takes no arguments,
and should return a response to be sent to the user instead of their
normal view.
:param callback: The callback for unauthorized users.
:type callback: callable
"""
self.needs_refresh_callback = callback
return callback
def needs_refresh(self):
"""
This is called when the user is logged in, but they need to be
reauthenticated because their session is stale. If you register a
callback with `needs_refresh_handler`, then it will be called.
Otherwise, it will take the following actions:
- Flash :attr:`LoginManager.needs_refresh_message` to the user.
- Redirect the user to :attr:`LoginManager.refresh_view`. (The page
they were attempting to access will be passed in the ``next``
query string variable, so you can redirect there if present
instead of the homepage.)
If :attr:`LoginManager.refresh_view` is not defined, then it will
simply raise a HTTP 401 (Unauthorized) error instead.
This should be returned from a view or before/after_request function,
otherwise the redirect will have no effect.
"""
user_needs_refresh.send(current_app._get_current_object())
if self.needs_refresh_callback:
return self.needs_refresh_callback()
if not self.refresh_view:
abort(401)
if self.needs_refresh_message:
if self.localize_callback is not None:
flash(
self.localize_callback(self.needs_refresh_message),
category=self.needs_refresh_message_category,
)
else:
flash(
self.needs_refresh_message,
category=self.needs_refresh_message_category,
)
config = current_app.config
if config.get("USE_SESSION_FOR_NEXT", USE_SESSION_FOR_NEXT):
login_url = expand_login_view(self.refresh_view)
session["_id"] = self._session_identifier_generator()
session["next"] = make_next_param(login_url, request.url)
redirect_url = make_login_url(self.refresh_view)
else:
login_url = self.refresh_view
redirect_url = make_login_url(login_url, next_url=request.url)
return redirect(redirect_url)
def _update_request_context_with_user(self, user=None):
"""Store the given user as ctx.user."""
if user is None:
user = self.anonymous_user()
g._login_user = user
def _load_user(self):
"""Loads user from session or remember_me cookie as applicable"""
if self._user_callback is None and self._request_callback is None:
raise Exception(
"Missing user_loader or request_loader. Refer to "
"https://flask-login.readthedocs.io/#how-it-works "
"for more info."
)
user_accessed.send(current_app._get_current_object())
# Check SESSION_PROTECTION
if self._session_protection_failed():
return self._update_request_context_with_user()
user = None
# Load user from Flask Session
user_id = session.get("_user_id")
user_random = session.get("_random")
user_session_key = session.get("_id")
if (user_id is not None
and user_random is not None
and user_session_key is not None
and self._user_callback is not None):
user = self._user_callback(user_id, user_random, user_session_key)
# Load user from Remember Me Cookie or Request Loader
if user is None:
config = current_app.config
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
header_name = config.get("AUTH_HEADER_NAME", AUTH_HEADER_NAME)
has_cookie = (
cookie_name in request.cookies and session.get("_remember") != "clear"
)
if has_cookie:
cookie = request.cookies[cookie_name]
user = self._load_user_from_remember_cookie(cookie)
elif self._request_callback:
user = self._load_user_from_request(request)
elif header_name in request.headers:
header = request.headers[header_name]
user = self._load_user_from_header(header)
if not user:
self._update_request_context_with_user()
return self._update_request_context_with_user(user)
def _session_protection_failed(self):
sess = session._get_current_object()
ident = self._session_identifier_generator()
app = current_app._get_current_object()
mode = app.config.get("SESSION_PROTECTION", self.session_protection)
if not mode or mode not in ["basic", "strong"]:
return False
# if the sess is empty, it's an anonymous user or just logged out
# so we can skip this
if sess and ident != sess.get("_id", None):
if mode == "basic" or sess.permanent:
if sess.get("_fresh") is not False:
sess["_fresh"] = False
session_protected.send(app)
return False
elif mode == "strong":
for k in SESSION_KEYS:
sess.pop(k, None)
sess["_remember"] = "clear"
session_protected.send(app)
return True
return False
def _load_user_from_remember_cookie(self, cookie):
signer_kwargs = dict(
key_derivation="hmac", digest_method=hashlib.sha1
)
try:
remember_dict = URLSafeSerializer(
current_app.secret_key,
salt="remember",
serializer=TaggedJSONSerializer(),
signer_kwargs=signer_kwargs,
).loads(cookie)
except Exception:
return None
if remember_dict['user'] is not None:
session["_user_id"] = remember_dict['user']
if "_random" not in session:
session["_random"] = remember_dict['random']
session["_fresh"] = False
user = None
if self._user_callback:
user = self._user_callback(remember_dict['user'], session["_random"], None)
if user is not None:
app = current_app._get_current_object()
user_loaded_from_cookie.send(app, user=user)
# if session was restored from remember me cookie make login valid
confirm_login()
return user
return None
def _load_user_from_header(self, header):
if self._header_callback:
user = self._header_callback(header)
if user is not None:
app = current_app._get_current_object()
from .signals import _user_loaded_from_header
_user_loaded_from_header.send(app, user=user)
return user
return None
def _load_user_from_request(self, request):
if self._request_callback:
user = self._request_callback(request)
if user is not None:
app = current_app._get_current_object()
user_loaded_from_request.send(app, user=user)
return user
return None
def _update_remember_cookie(self, response):
# Don't modify the session unless there's something to do.
if "_remember" not in session and current_app.config.get(
"REMEMBER_COOKIE_REFRESH_EACH_REQUEST"
):
session["_remember"] = "set"
if "_remember" in session:
operation = session.pop("_remember", None)
if operation == "set" and "_user_id" in session:
self._set_cookie(response)
elif operation == "clear":
self._clear_cookie(response)
return response
def _set_cookie(self, response):
# cookie settings
config = current_app.config
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
domain = config.get("REMEMBER_COOKIE_DOMAIN")
path = config.get("REMEMBER_COOKIE_PATH", current_app.wsgi_app.script_name)
secure = config.get("REMEMBER_COOKIE_SECURE", COOKIE_SECURE)
httponly = config.get("REMEMBER_COOKIE_HTTPONLY", COOKIE_HTTPONLY)
samesite = config.get("REMEMBER_COOKIE_SAMESITE", COOKIE_SAMESITE)
if "_remember_seconds" in session:
duration = timedelta(seconds=session["_remember_seconds"])
else:
duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION)
# prepare data
max_age = int(current_app.permanent_session_lifetime.total_seconds())
signer_kwargs = dict(
key_derivation="hmac", digest_method=hashlib.sha1
)
# save
data = URLSafeSerializer(
current_app.secret_key,
salt="remember",
serializer=TaggedJSONSerializer(),
signer_kwargs=signer_kwargs,
).dumps({"user":session["_user_id"], "random":session["_random"]})
if isinstance(duration, int):
duration = timedelta(seconds=duration)
try:
expires = datetime.now(timezone.utc) + duration
except TypeError as e:
raise Exception(
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
f" instead got: {duration}"
) from e
# actually set it
response.set_cookie(
cookie_name,
value=data,
expires=expires,
domain=domain,
path=path,
secure=secure,
httponly=httponly,
samesite=samesite,
)
def _clear_cookie(self, response):
config = current_app.config
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
domain = config.get("REMEMBER_COOKIE_DOMAIN")
path = config.get("REMEMBER_COOKIE_PATH", current_app.wsgi_app.script_name)
response.delete_cookie(cookie_name, domain=domain, path=path)
+65
View File
@@ -0,0 +1,65 @@
class UserMixin:
"""
This provides default implementations for the methods that Flask-Login
expects user objects to have.
"""
# Python 3 implicitly set __hash__ to None if we override __eq__
# We set it back to its default implementation
__hash__ = object.__hash__
@property
def is_active(self):
return True
@property
def is_authenticated(self):
return self.is_active
@property
def is_anonymous(self):
return False
def get_id(self):
try:
return str(self.id)
except AttributeError:
raise NotImplementedError("No `id` attribute - override `get_id`") from None
def __eq__(self, other):
"""
Checks the equality of two `UserMixin` objects using `get_id`.
"""
if isinstance(other, UserMixin):
return self.get_id() == other.get_id()
return NotImplemented
def __ne__(self, other):
"""
Checks the inequality of two `UserMixin` objects using `get_id`.
"""
equal = self.__eq__(other)
if equal is NotImplemented:
return NotImplemented
return not equal
class AnonymousUserMixin:
"""
This is the default object for representing an anonymous user.
"""
@property
def is_authenticated(self):
return False
@property
def is_active(self):
return False
@property
def is_anonymous(self):
return True
def get_id(self):
return
+61
View File
@@ -0,0 +1,61 @@
from flask.signals import Namespace
_signals = Namespace()
#: Sent when a user is logged in. In addition to the app (which is the
#: sender), it is passed `user`, which is the user being logged in.
user_logged_in = _signals.signal("logged-in")
#: Sent when a user is logged out. In addition to the app (which is the
#: sender), it is passed `user`, which is the user being logged out.
user_logged_out = _signals.signal("logged-out")
#: Sent when the user is loaded from the cookie. In addition to the app (which
#: is the sender), it is passed `user`, which is the user being reloaded.
user_loaded_from_cookie = _signals.signal("loaded-from-cookie")
#: Sent when the user is loaded from the header. In addition to the app (which
#: is the #: sender), it is passed `user`, which is the user being reloaded.
_user_loaded_from_header = _signals.signal("loaded-from-header")
#: Sent when the user is loaded from the request. In addition to the app (which
#: is the #: sender), it is passed `user`, which is the user being reloaded.
user_loaded_from_request = _signals.signal("loaded-from-request")
#: Sent when a user's login is confirmed, marking it as fresh. (It is not
#: called for a normal login.)
#: It receives no additional arguments besides the app.
user_login_confirmed = _signals.signal("login-confirmed")
#: Sent when the `unauthorized` method is called on a `LoginManager`. It
#: receives no additional arguments besides the app.
user_unauthorized = _signals.signal("unauthorized")
#: Sent when the `needs_refresh` method is called on a `LoginManager`. It
#: receives no additional arguments besides the app.
user_needs_refresh = _signals.signal("needs-refresh")
#: Sent whenever the user is accessed/loaded
#: receives no additional arguments besides the app.
user_accessed = _signals.signal("accessed")
#: Sent whenever session protection takes effect, and a session is either
#: marked non-fresh or deleted. It receives no additional arguments besides
#: the app.
session_protected = _signals.signal("session-protected")
def __getattr__(name):
if name == "user_loaded_from_header":
import warnings
warnings.warn(
"'user_loaded_from_header' is deprecated and will be"
" removed in Flask-Login 0.7. Use"
" 'user_loaded_from_request' instead.",
DeprecationWarning,
stacklevel=2,
)
return _user_loaded_from_header
raise AttributeError(name)
+424
View File
@@ -0,0 +1,424 @@
import hmac
import os
from functools import wraps
from hashlib import sha512
from urllib.parse import parse_qs
from urllib.parse import urlencode
from urllib.parse import urlsplit
from urllib.parse import urlunsplit
from flask import current_app
from flask import g
from flask import has_request_context
from flask import request
from flask import session
from flask import url_for
from werkzeug.local import LocalProxy
from .config import COOKIE_NAME
from .config import EXEMPT_METHODS
from .signals import user_logged_in
from .signals import user_logged_out
from .signals import user_login_confirmed
#: A proxy for the current user. If no user is logged in, this will be an
#: anonymous user
current_user = LocalProxy(lambda: _get_user())
def encode_cookie(payload, key=None):
"""
This will encode a ``str`` value into a cookie, and sign that cookie
with the app's secret key.
:param payload: The value to encode, as `str`.
:type payload: str
:param key: The key to use when creating the cookie digest. If not
specified, the SECRET_KEY value from app config will be used.
:type key: str
"""
return f"{payload}|{_cookie_digest(payload, key=key)}"
def decode_cookie(cookie, key=None):
"""
This decodes a cookie given by `encode_cookie`. If verification of the
cookie fails, ``None`` will be implicitly returned.
:param cookie: An encoded cookie.
:type cookie: str
:param key: The key to use when creating the cookie digest. If not
specified, the SECRET_KEY value from app config will be used.
:type key: str
"""
try:
payload, digest = cookie.rsplit("|", 1)
if hasattr(digest, "decode"):
digest = digest.decode("ascii") # pragma: no cover
except ValueError:
return
if hmac.compare_digest(_cookie_digest(payload, key=key), digest):
return payload
def make_next_param(login_url, current_url):
"""
Reduces the scheme and host from a given URL so it can be passed to
the given `login` URL more efficiently.
:param login_url: The login URL being redirected to.
:type login_url: str
:param current_url: The URL to reduce.
:type current_url: str
"""
l_url = urlsplit(login_url)
c_url = urlsplit(current_url)
if (not l_url.scheme or l_url.scheme == c_url.scheme) and (
not l_url.netloc or l_url.netloc == c_url.netloc
):
return urlunsplit(("", "", c_url.path, c_url.query, ""))
return current_url
def expand_login_view(login_view):
"""
Returns the url for the login view, expanding the view name to a url if
needed.
:param login_view: The name of the login view or a URL for the login view.
:type login_view: str
"""
if login_view.startswith(("https://", "http://", "/")):
return login_view
return url_for(login_view)
def login_url(login_view, next_url=None, next_field="next"):
"""
Creates a URL for redirecting to a login page. If only `login_view` is
provided, this will just return the URL for it. If `next_url` is provided,
however, this will append a ``next=URL`` parameter to the query string
so that the login view can redirect back to that URL. Flask-Login's default
unauthorized handler uses this function when redirecting to your login url.
To force the host name used, set `FORCE_HOST_FOR_REDIRECTS` to a host. This
prevents from redirecting to external sites if request headers Host or
X-Forwarded-For are present.
:param login_view: The name of the login view. (Alternately, the actual
URL to the login view.)
:type login_view: str
:param next_url: The URL to give the login view for redirection.
:type next_url: str
:param next_field: What field to store the next URL in. (It defaults to
``next``.)
:type next_field: str
"""
base = expand_login_view(login_view)
if next_url is None:
return base
parsed_result = urlsplit(base)
md = parse_qs(parsed_result.query, keep_blank_values=True)
md[next_field] = make_next_param(base, next_url)
netloc = current_app.config.get("FORCE_HOST_FOR_REDIRECTS") or parsed_result.netloc
parsed_result = parsed_result._replace(
netloc=netloc, query=urlencode(md, doseq=True)
)
return urlunsplit(parsed_result)
def login_fresh():
"""
This returns ``True`` if the current login is fresh.
"""
return session.get("_fresh", False)
def login_remembered():
"""
This returns ``True`` if the current login is remembered across sessions.
"""
config = current_app.config
cookie_name = config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
has_cookie = cookie_name in request.cookies and session.get("_remember") != "clear"
if has_cookie:
cookie = request.cookies[cookie_name]
user_id = decode_cookie(cookie)
return user_id is not None
return False
def login_user(user, remember=False, duration=None, force=False, fresh=True):
"""
Logs a user in. You should pass the actual user object to this. If the
user's `is_active` property is ``False``, they will not be logged in
unless `force` is ``True``.
This will return ``True`` if the log in attempt succeeds, and ``False`` if
it fails (i.e. because the user is inactive).
:param user: The user object to log in.
:type user: object
:param remember: Whether to remember the user after their session expires.
Defaults to ``False``.
:type remember: bool
:param duration: The amount of time before the remember cookie expires. If
``None`` the value set in the settings is used. Defaults to ``None``.
:type duration: :class:`datetime.timedelta`
:param force: If the user is inactive, setting this to ``True`` will log
them in regardless. Defaults to ``False``.
:type force: bool
:param fresh: setting this to ``False`` will log in the user with a session
marked as not "fresh". Defaults to ``True``.
:type fresh: bool
"""
if not force and not user.is_active:
return False
user_id = getattr(user, current_app.login_manager.id_attribute)()
session["_user_id"] = user_id
session["_fresh"] = fresh
session["_id"] = current_app.login_manager._session_identifier_generator()
session["_random"] = os.urandom(10).hex()
if remember:
session["_remember"] = "set"
if duration is not None:
try:
# equal to timedelta.total_seconds() but works with Python 2.6
session["_remember_seconds"] = (
duration.microseconds
+ (duration.seconds + duration.days * 24 * 3600) * 10**6
) / 10.0**6
except AttributeError as e:
raise Exception(
f"duration must be a datetime.timedelta, instead got: {duration}"
) from e
current_app.login_manager._update_request_context_with_user(user)
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True
def logout_user():
"""
Logs a user out. (You do not need to pass the actual user.) This will
also clean up the remember me cookie if it exists.
"""
user = _get_user()
if "_user_id" in session:
session.pop("_user_id")
if "_fresh" in session:
session.pop("_fresh")
if "_id" in session:
session.pop("_id")
if "_random" in session:
session.pop("_random")
cookie_name = current_app.config.get("REMEMBER_COOKIE_NAME", COOKIE_NAME)
if cookie_name in request.cookies:
session["_remember"] = "clear"
if "_remember_seconds" in session:
session.pop("_remember_seconds")
user_logged_out.send(current_app._get_current_object(), user=user)
current_app.login_manager._update_request_context_with_user()
return True
def confirm_login():
"""
This sets the current session as fresh. Sessions become stale when they
are reloaded from a cookie.
"""
session["_fresh"] = True
session["_id"] = current_app.login_manager._session_identifier_generator()
user_login_confirmed.send(current_app._get_current_object())
def login_required(func):
"""
If you decorate a view with this, it will ensure that the current user is
logged in and authenticated before calling the actual view. (If they are
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
example::
@app.route('/post')
@user_login_required
def post():
pass
If there are only certain times you need to require that your user is
logged in, you can do so with::
if not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
...which is essentially the code that this function adds to your views.
It can be convenient to globally turn off authentication when unit testing.
To enable this, if the application configuration variable `LOGIN_DISABLED`
is set to `True`, this decorator will be ignored.
.. Note ::
Per `W3 guidelines for CORS preflight requests
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
HTTP ``OPTIONS`` requests are exempt from login checks.
:param func: The view function to decorate.
:type func: function
"""
@wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
pass
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
# flask 1.x compatibility
# current_app.ensure_sync is only available in Flask >= 2.0
if callable(getattr(current_app, "ensure_sync", None)):
return current_app.ensure_sync(func)(*args, **kwargs)
return func(*args, **kwargs)
return decorated_view
def fresh_login_required(func):
"""
If you decorate a view with this, it will ensure that the current user's
login is fresh - i.e. their session was not restored from a 'remember me'
cookie. Sensitive operations, like changing a password or e-mail, should
be protected with this, to impede the efforts of cookie thieves.
If the user is not authenticated, :meth:`LoginManager.unauthorized` is
called as normal. If they are authenticated, but their session is not
fresh, it will call :meth:`LoginManager.needs_refresh` instead. (In that
case, you will need to provide a :attr:`LoginManager.refresh_view`.)
Behaves identically to the :func:`login_required` decorator with respect
to configuration variables.
.. Note ::
Per `W3 guidelines for CORS preflight requests
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
HTTP ``OPTIONS`` requests are exempt from login checks.
:param func: The view function to decorate.
:type func: function
"""
@wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS or current_app.config.get("LOGIN_DISABLED"):
pass
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
elif not login_fresh():
return current_app.login_manager.needs_refresh()
try:
# current_app.ensure_sync available in Flask >= 2.0
return current_app.ensure_sync(func)(*args, **kwargs)
except AttributeError: # pragma: no cover
return func(*args, **kwargs)
return decorated_view
def set_login_view(login_view, blueprint=None):
"""
Sets the login view for the app or blueprint. If a blueprint is passed,
the login view is set for this blueprint on ``blueprint_login_views``.
:param login_view: The user object to log in.
:type login_view: str
:param blueprint: The blueprint which this login view should be set on.
Defaults to ``None``.
:type blueprint: object
"""
num_login_views = len(current_app.login_manager.blueprint_login_views)
if blueprint is not None or num_login_views != 0:
(current_app.login_manager.blueprint_login_views[blueprint.name]) = login_view
if (
current_app.login_manager.login_view is not None
and None not in current_app.login_manager.blueprint_login_views
):
(
current_app.login_manager.blueprint_login_views[None]
) = current_app.login_manager.login_view
current_app.login_manager.login_view = None
else:
current_app.login_manager.login_view = login_view
def _get_user():
if has_request_context():
if "flask_httpauth_user" in g:
if g.flask_httpauth_user is not None:
return g.flask_httpauth_user
if "_login_user" not in g:
current_app.login_manager._load_user()
return g._login_user
return None
def _cookie_digest(payload, key=None):
key = _secret_key(key)
return hmac.new(key, payload.encode("utf-8"), sha512).hexdigest()
def _get_remote_addr():
address = request.headers.get("X-Forwarded-For", request.remote_addr)
if address is not None:
# An 'X-Forwarded-For' header includes a comma separated list of the
# addresses, the first address being the actual remote address.
address = address.encode("utf-8").split(b",")[0].strip()
return address
def _create_identifier():
user_agent = request.headers.get("User-Agent")
if user_agent is not None:
user_agent = user_agent.encode("utf-8")
base = f"{_get_remote_addr()}|{user_agent}"
if str is bytes:
base = str(base, "utf-8", errors="replace") # pragma: no cover
h = sha512()
h.update(base.encode("utf8"))
return h.hexdigest()
def _user_context_processor():
return dict(current_user=_get_user())
def _secret_key(key=None):
if key is None:
key = current_app.config["SECRET_KEY"]
if isinstance(key, str): # pragma: no cover
key = key.encode("latin1") # ensure bytes
return key
+769 -342
View File
File diff suppressed because it is too large Load Diff
+34 -15
View File
@@ -22,11 +22,8 @@ import glob
import zipfile
import json
from io import BytesIO
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from flask_babel.speaklater import LazyString
from importlib.metadata import metadata
import os
from flask import send_file
@@ -36,16 +33,32 @@ from .about import collect_stats
log = logger.create()
class lazyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, LazyString):
return str(obj)
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, obj)
def assemble_logfiles(file_name):
log_list = sorted(glob.glob(file_name + '*'), reverse=True)
wfd = StringIO()
wfd = BytesIO()
for f in log_list:
with open(f, 'r') as fd:
with open(f, 'rb') as fd:
shutil.copyfileobj(fd, wfd)
wfd.seek(0)
return send_file(wfd,
as_attachment=True,
attachment_filename=os.path.basename(file_name))
version = metadata("flask")["Version"]
if int(version.split('.')[0]) < 2:
return send_file(wfd,
as_attachment=True,
attachment_filename=os.path.basename(file_name))
else:
return send_file(wfd,
as_attachment=True,
download_name=os.path.basename(file_name))
def send_debug():
file_list = glob.glob(logger.get_logfile(config.config_logfile) + '*')
@@ -55,11 +68,17 @@ def send_debug():
file_list.remove(element)
memory_zip = BytesIO()
with zipfile.ZipFile(memory_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr('settings.txt', json.dumps(config.toDict()))
zf.writestr('libs.txt', json.dumps(collect_stats()))
zf.writestr('settings.txt', json.dumps(config.to_dict(), sort_keys=True, indent=2))
zf.writestr('libs.txt', json.dumps(collect_stats(), sort_keys=True, indent=2, cls=lazyEncoder))
for fp in file_list:
zf.write(fp, os.path.basename(fp))
memory_zip.seek(0)
return send_file(memory_zip,
as_attachment=True,
attachment_filename="Calibre-Web-debug-pack.zip")
version = metadata("flask")["Version"]
if int(version.split('.')[0]) < 2:
return send_file(memory_zip,
as_attachment=True,
attachment_filename="Calibre-Web-debug-pack.zip")
else:
return send_file(memory_zip,
as_attachment=True,
download_name="Calibre-Web-debug-pack.zip")
+127
View File
@@ -0,0 +1,127 @@
import os
import re
import sys
import json
from .constants import BASE_DIR
try:
from importlib.metadata import version
importlib = True
ImportNotFound = BaseException
except ImportError:
importlib = False
version = None
if not importlib:
try:
import pkg_resources
from pkg_resources import DistributionNotFound as ImportNotFound
pkgresources = True
except ImportError as e:
pkgresources = False
def load_dependencies(optional=False):
deps = list()
if getattr(sys, 'frozen', False):
pip_installed = os.path.join(BASE_DIR, ".pip_installed")
if os.path.exists(pip_installed):
with open(pip_installed) as f:
exe_deps = json.loads("".join(f.readlines()))
else:
return deps
if importlib or pkgresources:
if optional:
req_path = os.path.join(BASE_DIR, "optional-requirements.txt")
else:
req_path = os.path.join(BASE_DIR, "requirements.txt")
if os.path.exists(req_path):
with open(req_path, 'r') as f:
for line in f:
if not line.startswith('#') and not line == '\n' and not line.startswith('git'):
res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?(?:\s?;\s?'
r'(?:(python_version)\s?([<=>]+)\s?\'([\d\.]+)\'|'
r'(sys_platform)\s?([\!=]+)\s?\'([\w]+)\'))?', line.strip())
try:
if res.group(7) and res.group(8):
val = res.group(8).split(".")
if not eval(str(sys.version_info[0]) + "." + "{:02d}".format(sys.version_info[1]) +
res.group(7) + val[0] + "." + "{:02d}".format(int(val[1]))):
continue
elif res.group(10) and res.group(11):
# only installed if platform is eqal, don't check if platform is not equal
if res.group(10) == "==":
if sys.platform != res.group(11):
continue
# installed if platform is not eqal, don't check if platform is equal
elif res.group(10) == "!=":
if sys.platform == res.group(11):
continue
if getattr(sys, 'frozen', False):
dep_version = exe_deps[res.group(1).lower().replace('_', '-')]
else:
if importlib:
dep_version = version(res.group(1))
else:
dep_version = pkg_resources.get_distribution(res.group(1)).version
except (ImportNotFound, KeyError):
if optional:
continue
dep_version = "not installed"
deps.append([dep_version, res.group(1), res.group(2), res.group(3), res.group(4), res.group(5)])
return deps
def dependency_check(optional=False):
d = list()
dep_version_int = None
low_check = None
deps = load_dependencies(optional)
for dep in deps:
try:
dep_version_int = [int(x) if x.isnumeric() else 0 for x in dep[0].split('.')[:3]]
low_check = [int(x) for x in dep[3].split('.')]
high_check = [int(x) for x in dep[5].split('.')]
except AttributeError:
high_check = []
except ValueError:
d.append({'name': dep[1],
'target': "available",
'found': "Not available"
})
continue
if dep[2].strip() == "==":
if dep_version_int != low_check:
d.append({'name': dep[1],
'found': dep[0],
"target": dep[2] + dep[3]})
continue
elif dep[2].strip() == ">=":
if dep_version_int < low_check:
d.append({'name': dep[1],
'found': dep[0],
"target": dep[2] + dep[3]})
continue
elif dep[2].strip() == ">":
if dep_version_int <= low_check:
d.append({'name': dep[1],
'found': dep[0],
"target": dep[2] + dep[3]})
continue
if dep[4] and dep[5]:
if dep[4].strip() == "<":
if dep_version_int >= high_check:
d.append(
{'name': dep[1],
'found': dep[0],
"target": dep[4] + dep[5]})
continue
elif dep[4].strip() == "<=":
if dep_version_int > high_check:
d.append(
{'name': dep[1],
'found': dep[0],
"target": dep[4] + dep[5]})
continue
return d
+1508 -866
View File
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2024 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from uuid import uuid4
import os
from .file_helper import get_temp_dir
from .subproc_wrapper import process_open
from . import logger, config
from .constants import SUPPORTED_CALIBRE_BINARIES
log = logger.create()
def do_calibre_export(book_id, book_format):
try:
quotes = [4, 6]
tmp_dir = get_temp_dir()
calibredb_binarypath = get_calibre_binarypath("calibredb")
temp_file_name = str(uuid4())
my_env = os.environ.copy()
if config.config_calibre_split:
my_env['CALIBRE_OVERRIDE_DATABASE_PATH'] = os.path.join(config.config_calibre_dir, "metadata.db")
library_path = config.get_book_path()
opf_command = [calibredb_binarypath, 'export', '--dont-write-opf', '--dont-save-cover',
'--with-library', library_path,
'--to-dir', tmp_dir, '--formats', book_format, "--template", "{}".format(temp_file_name),
str(book_id)]
p = process_open(opf_command, quotes, my_env)
_, err = p.communicate()
if err:
log.error('Metadata embedder encountered an error: %s', err)
return tmp_dir, temp_file_name
except OSError as ex:
# ToDo real error handling
log.error_or_exception(ex)
return None, None
def get_calibre_binarypath(binary):
binariesdir = config.config_binariesdir
if binariesdir:
try:
return os.path.join(binariesdir, SUPPORTED_CALIBRE_BINARIES[binary])
except KeyError as ex:
log.error("Binary not supported by Calibre-Web: %s", SUPPORTED_CALIBRE_BINARIES[binary])
pass
return ""
+127 -64
View File
@@ -16,68 +16,95 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import zipfile
from lxml import etree
from . import isoLanguages
from . import isoLanguages, cover
from . import config, logger
from .helper import split_authors
from .epub_helper import get_content_opf, default_ns
from .constants import BookMeta
from .string_helper import strip_whitespaces
log = logger.create()
def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name):
if cover_file is None:
return None
def extractCover(zipFile, coverFile, coverpath, tmp_file_name):
if coverFile is None:
cf = extension = None
zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
prefix = os.path.splitext(tmp_file_name)[0]
tmp_cover_name = prefix + '.' + os.path.basename(zip_cover_path)
ext = os.path.splitext(tmp_cover_name)
if len(ext) > 1:
extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
cf = zip_file.read(zip_cover_path)
return cover.cover_processing(tmp_file_name, cf, extension)
def get_epub_layout(book, book_data):
file_path = os.path.normpath(os.path.join(config.get_book_path(),
book.path, book_data.name + "." + book_data.format.lower()))
try:
tree, __ = get_content_opf(file_path, default_ns)
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=default_ns)[0]
layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=default_ns)
except (etree.XMLSyntaxError, KeyError, IndexError, OSError, UnicodeDecodeError) as e:
log.error("Could not parse epub metadata of book {} during kobo sync: {}".format(book.id, e))
layout = []
if len(layout) == 0:
return None
else:
zipCoverPath = os.path.join(coverpath, coverFile).replace('\\', '/')
cf = zipFile.read(zipCoverPath)
prefix = os.path.splitext(tmp_file_name)[0]
tmp_cover_name = prefix + '.' + os.path.basename(zipCoverPath)
image = open(tmp_cover_name, 'wb')
image.write(cf)
image.close()
return tmp_cover_name
return layout[0]
def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
def get_epub_info(tmp_file_path, original_file_name, original_file_extension, no_cover_processing):
ns = {
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
'pkg': 'http://www.idpf.org/2007/opf',
'dc': 'http://purl.org/dc/elements/1.1/'
}
epubZip = zipfile.ZipFile(tmp_file_path)
tree, cf_name = get_content_opf(tmp_file_path, ns)
txt = epubZip.read('META-INF/container.xml')
tree = etree.fromstring(txt)
cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epubZip.read(cfname)
tree = etree.fromstring(cf)
coverpath = os.path.dirname(cfname)
cover_path = os.path.dirname(cf_name)
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
epub_metadata = {}
for s in ['title', 'description', 'creator', 'language', 'subject']:
for s in ['title', 'description', 'creator', 'language', 'subject', 'publisher', 'date']:
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
if len(tmp) > 0:
if s == 'creator':
epub_metadata[s] = ' & '.join(split_authors(tmp))
elif s == 'subject':
epub_metadata[s] = ', '.join(tmp)
elif s == 'date':
epub_metadata[s] = tmp[0][:10]
else:
epub_metadata[s] = tmp[0]
epub_metadata[s] = strip_whitespaces(tmp[0])
else:
epub_metadata[s] = u'Unknown'
epub_metadata[s] = 'Unknown'
if epub_metadata['subject'] == u'Unknown':
if epub_metadata['subject'] == 'Unknown':
epub_metadata['subject'] = ''
if epub_metadata['description'] == u'Unknown':
if epub_metadata['publisher'] == 'Unknown':
epub_metadata['publisher'] = ''
if epub_metadata['date'] == 'Unknown':
epub_metadata['date'] = ''
if epub_metadata['description'] == 'Unknown':
description = tree.xpath("//*[local-name() = 'description']/text()")
if len(description) > 0:
epub_metadata['description'] = description
@@ -87,45 +114,24 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
lang = epub_metadata['language'].split('-', 1)[0].lower()
epub_metadata['language'] = isoLanguages.get_lang3(lang)
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
if len(series) > 0:
epub_metadata['series'] = series[0]
else:
epub_metadata['series'] = ''
epub_metadata = parse_epub_series(ns, tree, epub_metadata)
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
if len(series_id) > 0:
epub_metadata['series_id'] = series_id[0]
epub_zip = zipfile.ZipFile(tmp_file_path)
if not no_cover_processing:
cover_file = parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path)
else:
epub_metadata['series_id'] = '1'
cover_file = None
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
coverfile = None
if len(coversection) > 0:
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
else:
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0:
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
else:
coversection = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
if len(coversection) > 0:
filetype = coversection[0].rsplit('.', 1)[-1]
if filetype == "xhtml" or filetype == "html": # if cover is (x)html format
markup = epubZip.read(os.path.join(coverpath, coversection[0]))
markupTree = etree.fromstring(markup)
# no matter xhtml or html with no namespace
imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src")
# Alternative image source
if not len(imgsrc):
imgsrc = markupTree.xpath("//attribute::*[contains(local-name(), 'href')]")
if len(imgsrc):
# imgsrc maybe startwith "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])),
imgsrc[0]))
coverfile = extractCover(epubZip, filename, "", tmp_file_path)
else:
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
identifiers = []
for node in p.xpath('dc:identifier', namespaces=ns):
try:
identifier_name = node.attrib.values()[-1]
except IndexError:
continue
identifier_value = node.text
if identifier_name in ('uuid', 'calibre') or identifier_value is None:
continue
identifiers.append([identifier_name, identifier_value])
if not epub_metadata['title']:
title = original_file_name
@@ -137,9 +143,66 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
extension=original_file_extension,
title=title.encode('utf-8').decode('utf-8'),
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
cover=coverfile,
cover=cover_file,
description=epub_metadata['description'],
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
languages=epub_metadata['language'])
languages=epub_metadata['language'],
publisher=epub_metadata['publisher'].encode('utf-8').decode('utf-8'),
pubdate=epub_metadata['date'],
identifiers=identifiers)
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
for cs in cover_section:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file:
return cover_file
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0:
cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
if not cover_section:
cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
else:
cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
cover_file = None
for cs in cover_section:
if cs.endswith('.xhtml') or cs.endswith('.html'):
markup = epub_zip.read(os.path.join(cover_path, cs))
markup_tree = etree.fromstring(markup, parser=etree.XMLParser(resolve_entities=False, no_network=True))
# no matter xhtml or html with no namespace
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
# Alternative image source
if not len(img_src):
img_src = markup_tree.xpath("//attribute::*[contains(local-name(), 'href')]")
if len(img_src):
# img_src maybe start with "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(cover_path, cover_section[0])),
img_src[0]))
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file:
break
return cover_file
def parse_epub_series(ns, tree, epub_metadata):
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
if len(series) > 0:
epub_metadata['series'] = series[0]
else:
epub_metadata['series'] = ''
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
if len(series_id) > 0:
epub_metadata['series_id'] = series_id[0]
else:
epub_metadata['series_id'] = '1'
return epub_metadata
+173
View File
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 lemmsh, Kennyl, Kyosfonica, matthazinski
#
# 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 zipfile
from lxml import etree
from . import isoLanguages
default_ns = {
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
'pkg': 'http://www.idpf.org/2007/opf',
}
OPF_NAMESPACE = "http://www.idpf.org/2007/opf"
PURL_NAMESPACE = "http://purl.org/dc/elements/1.1/"
OPF = "{%s}" % OPF_NAMESPACE
PURL = "{%s}" % PURL_NAMESPACE
etree.register_namespace("opf", OPF_NAMESPACE)
etree.register_namespace("dc", PURL_NAMESPACE)
OPF_NS = {None: OPF_NAMESPACE} # the default namespace (no prefix)
NSMAP = {'dc': PURL_NAMESPACE, 'opf': OPF_NAMESPACE}
def updateEpub(src, dest, filename, data, ):
# create a temp copy of the archive without filename
with zipfile.ZipFile(src, 'r') as zin:
with zipfile.ZipFile(dest, 'w') as zout:
zout.comment = zin.comment # preserve the comment
for item in zin.infolist():
if item.filename != filename:
zout.writestr(item, zin.read(item.filename))
# now add filename with its new data
with zipfile.ZipFile(dest, mode='a', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr(filename, data)
# Safe parser: disable entity resolution and network access to prevent XXE attacks
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
def get_content_opf(file_path, ns=None):
if ns is None:
ns = default_ns
epubZip = zipfile.ZipFile(file_path)
txt = epubZip.read('META-INF/container.xml')
tree = etree.fromstring(txt, parser=_safe_parser)
cf_name = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epubZip.read(cf_name)
return etree.fromstring(cf, parser=_safe_parser), cf_name
def create_new_metadata_backup(book, custom_columns, export_language, translated_cover_name, lang_type=3):
# generate root package element
package = etree.Element(OPF + "package", nsmap=OPF_NS)
package.set("unique-identifier", "uuid_id")
package.set("version", "2.0")
# generate metadata element and all sub elements of it
metadata = etree.SubElement(package, "metadata", nsmap=NSMAP)
identifier = etree.SubElement(metadata, PURL + "identifier", id="calibre_id", nsmap=NSMAP)
identifier.set(OPF + "scheme", "calibre")
identifier.text = str(book.id)
identifier2 = etree.SubElement(metadata, PURL + "identifier", id="uuid_id", nsmap=NSMAP)
identifier2.set(OPF + "scheme", "uuid")
identifier2.text = book.uuid
for i in book.identifiers:
identifier = etree.SubElement(metadata, PURL + "identifier", nsmap=NSMAP)
identifier.set(OPF + "scheme", i.format_type())
identifier.text = str(i.val)
title = etree.SubElement(metadata, PURL + "title", nsmap=NSMAP)
title.text = book.title
for author in book.authors:
creator = etree.SubElement(metadata, PURL + "creator", nsmap=NSMAP)
creator.text = str(author.name)
creator.set(OPF + "file-as", book.author_sort) # ToDo Check
creator.set(OPF + "role", "aut")
contributor = etree.SubElement(metadata, PURL + "contributor", nsmap=NSMAP)
contributor.text = "calibre (5.7.2) [https://calibre-ebook.com]"
contributor.set(OPF + "file-as", "calibre") # ToDo Check
contributor.set(OPF + "role", "bkp")
date = etree.SubElement(metadata, PURL + "date", nsmap=NSMAP)
date.text = '{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(d=book.pubdate)
if book.comments and book.comments[0].text:
for b in book.comments:
description = etree.SubElement(metadata, PURL + "description", nsmap=NSMAP)
description.text = b.text
for b in book.publishers:
publisher = etree.SubElement(metadata, PURL + "publisher", nsmap=NSMAP)
publisher.text = str(b.name)
if not book.languages:
language = etree.SubElement(metadata, PURL + "language", nsmap=NSMAP)
language.text = export_language
else:
for b in book.languages:
language = etree.SubElement(metadata, PURL + "language", nsmap=NSMAP)
language.text = str(b.lang_code) if lang_type == 3 else isoLanguages.get(part3=b.lang_code).part1
for b in book.tags:
subject = etree.SubElement(metadata, PURL + "subject", nsmap=NSMAP)
subject.text = str(b.name)
etree.SubElement(metadata, "meta", name="calibre:author_link_map",
content="{" + ", ".join(['"' + str(a.name) + '": ""' for a in book.authors]) + "}",
nsmap=NSMAP)
for b in book.series:
etree.SubElement(metadata, "meta", name="calibre:series",
content=str(str(b.name)),
nsmap=NSMAP)
if book.series:
etree.SubElement(metadata, "meta", name="calibre:series_index",
content=str(book.series_index),
nsmap=NSMAP)
if len(book.ratings) and book.ratings[0].rating > 0:
etree.SubElement(metadata, "meta", name="calibre:rating",
content=str(book.ratings[0].rating),
nsmap=NSMAP)
etree.SubElement(metadata, "meta", name="calibre:timestamp",
content='{d.year:04}-{d.month:02}-{d.day:02}T{d.hour:02}:{d.minute:02}:{d.second:02}'.format(
d=book.timestamp),
nsmap=NSMAP)
etree.SubElement(metadata, "meta", name="calibre:title_sort",
content=book.sort,
nsmap=NSMAP)
sequence = 0
for cc in custom_columns:
value = None
extra = None
cc_entry = getattr(book, "custom_column_" + str(cc.id))
if cc_entry.__len__():
value = [c.value for c in cc_entry] if cc.is_multiple else cc_entry[0].value
extra = cc_entry[0].extra if hasattr(cc_entry[0], "extra") else None
etree.SubElement(metadata, "meta", name="calibre:user_metadata:#{}".format(cc.label),
content=cc.to_json(value, extra, sequence),
nsmap=NSMAP)
sequence += 1
# generate guide element and all sub elements of it
# Title is translated from default export language
guide = etree.SubElement(package, "guide")
etree.SubElement(guide, "reference", type="cover", title=translated_cover_name, href="cover.jpg")
return package
def replace_metadata(tree, package):
rep_element = tree.xpath('/pkg:package/pkg:metadata', namespaces=default_ns)[0]
new_element = package.xpath('//metadata', namespaces=default_ns)[0]
tree.replace(rep_element, new_element)
return etree.tostring(tree,
xml_declaration=True,
encoding='utf-8',
pretty_print=True).decode('utf-8')
+59 -13
View File
@@ -17,37 +17,72 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import traceback
from flask import render_template
from flask import render_template, request, flash, make_response
from flask_limiter import RateLimitExceeded
from flask_babel import gettext as _
from werkzeug.exceptions import default_exceptions
from .cw_login import current_user
try:
from werkzeug.exceptions import FailedDependency
except ImportError:
from werkzeug.exceptions import UnprocessableEntity as FailedDependency
from . import config, app, logger, services
from .render_template import render_title_template
from .web import render_login
from .usermanagement import auth
from cps.string_helper import strip_whitespaces
log = logger.create()
# custom error page
def error_http(error):
headers = {'WWW-Authenticate': 'Basic realm="calibre-web"'} if error.code == 401 else {}
return render_template('http_error.html',
error_code="Error {0}".format(error.code),
error_name=error.name,
issue=False,
goto_admin=False,
unconfigured=not config.db_configured,
instance=config.config_calibre_web_title
), error.code
), error.code, headers
def internal_error(error):
if (isinstance(error.original_exception, AttributeError) and
error.original_exception.args[0] == "'NoneType' object has no attribute 'query'"
and error.original_exception.name == "query"):
return render_template('http_error.html',
error_code="Database Error",
error_name='The library used is invalid or has permission errors',
issue=False,
goto_admin=True,
unconfigured=False,
error_stack="",
instance=config.config_calibre_web_title
), 500
log.error("500 Internal Server Error: %s", traceback.format_exc())
error_stack = ""
try:
if current_user.is_authenticated and current_user.role_admin():
error_stack = traceback.format_exc().split("\n")
except Exception:
pass
return render_template('http_error.html',
error_code="Internal Server Error",
error_name=str(error),
error_code="500 Internal Server Error",
error_name='The server encountered an internal error and was unable to complete your '
'request. There is an error in the application.',
issue=True,
error_stack=traceback.format_exc().split("\n"),
goto_admin=False,
unconfigured=False,
error_stack=error_stack,
instance=config.config_calibre_web_title
), 500
def init_errorhandler():
# http error handling
for ex in default_exceptions:
@@ -56,18 +91,29 @@ def init_errorhandler():
elif ex == 500:
app.register_error_handler(ex, internal_error)
if services.ldap:
# Only way of catching the LDAPException upon logging in with LDAP server down
@app.errorhandler(services.ldap.LDAPException)
# pylint: disable=unused-variable
def handle_exception(e):
log.debug('LDAP server not accessible while trying to login to opds feed')
return error_http(FailedDependency())
# @app.errorhandler(InvalidRequestError)
#@app.errorhandler(OperationalError)
#def handle_db_exception(e):
# db.session.rollback()
# log.error('Database request error: %s',e)
# return internal_error(InternalServerError(e))
@app.errorhandler(RateLimitExceeded)
def handle_rate_limit(__):
log.error("Rate limit exceeded {}".format(request.endpoint))
if "register" in request.endpoint:
flash(_(u"Please wait one minute to register next user"), category="error")
return render_title_template('register.html', config=config, title=_("Register"), page="register")
elif "login" in request.endpoint:
form = request.form.to_dict()
username = strip_whitespaces(form.get('username', "")).lower().replace("\n", "").replace("\r", "")
flash(_("Please wait one minute before next login"), category="error")
return render_login(username, form.get("password", ""))
elif "opds" in request.endpoint:
return auth.auth_error_callback(429)
else:
return make_response('', 429)
+25 -20
View File
@@ -16,11 +16,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from lxml import etree
from .constants import BookMeta
# Safe parser: disable entity resolution and network access to prevent XXE attacks
_safe_parser = etree.XMLParser(resolve_entities=False, no_network=True)
def get_fb2_info(tmp_file_path, original_file_extension):
@@ -29,52 +31,55 @@ def get_fb2_info(tmp_file_path, original_file_extension):
'l': 'http://www.w3.org/1999/xlink',
}
fb2_file = open(tmp_file_path)
tree = etree.fromstring(fb2_file.read())
fb2_file = open(tmp_file_path, encoding="utf-8")
tree = etree.fromstring(fb2_file.read().encode(), parser=_safe_parser)
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
def get_author(element):
last_name = element.xpath('fb:last-name/text()', namespaces=ns)
if len(last_name):
last_name = last_name[0].encode('utf-8')
last_name = last_name[0]
else:
last_name = u''
last_name = ''
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
if len(middle_name):
middle_name = middle_name[0].encode('utf-8')
middle_name = middle_name[0]
else:
middle_name = u''
middle_name = ''
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
if len(first_name):
first_name = first_name[0].encode('utf-8')
first_name = first_name[0]
else:
first_name = u''
return (first_name.decode('utf-8') + u' '
+ middle_name.decode('utf-8') + u' '
+ last_name.decode('utf-8')).encode('utf-8')
first_name = ''
return (first_name + ' '
+ middle_name + ' '
+ last_name)
author = str(", ".join(map(get_author, authors)))
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
if len(title):
title = str(title[0].encode('utf-8'))
title = str(title[0])
else:
title = u''
title = ''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description):
description = str(description[0].encode('utf-8'))
description = str(description[0])
else:
description = u''
description = ''
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title.decode('utf-8'),
author=author.decode('utf-8'),
title=title,
author=author,
cover=None,
description=description.decode('utf-8'),
description=description,
tags="",
series="",
series_id="",
languages="")
languages="",
publisher="",
pubdate="",
identifiers=[])
+83
View File
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2023 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from tempfile import gettempdir
import os
import shutil
import zipfile
import mimetypes
from io import BytesIO
from . import logger
log = logger.create()
try:
import magic
error = None
except ImportError as e:
error = "Cannot import python-magic, checking uploaded file metadata will not work: {}".format(e)
def get_mimetype(ext):
# overwrite some mimetypes for proper file detection
mimes = {".fb2": "text/xml",
".cbz": "application/zip",
".cbr": "application/x-rar"
}
return mimes.get(ext, mimetypes.types_map[ext])
def get_temp_dir():
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
return tmp_dir
def del_temp_dir():
tmp_dir = os.path.join(gettempdir(), 'calibre_web')
shutil.rmtree(tmp_dir)
def validate_mime_type(file_buffer, allowed_extensions):
if error:
log.error(error)
return False
mime = magic.Magic(mime=True)
allowed_mimetypes = list()
for x in allowed_extensions:
try:
allowed_mimetypes.append(get_mimetype("." + x))
except KeyError:
log.error("Unkown mimetype for Extension: {}".format(x))
tmp_mime_type = mime.from_buffer(file_buffer.read())
file_buffer.seek(0)
if any(mime_type in tmp_mime_type for mime_type in allowed_mimetypes):
return True
# Some epubs show up as zip mimetypes
elif "zip" in tmp_mime_type:
try:
with zipfile.ZipFile(BytesIO(file_buffer.read()), 'r') as epub:
file_buffer.seek(0)
if "mimetype" in epub.namelist():
return True
except:
file_buffer.seek(0)
log.error("Mimetype '{}' not found in allowed types".format(tmp_mime_type))
return False
+95
View File
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 mmonkey
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import logger
from .constants import CACHE_DIRECTORY
from os import makedirs, remove
from os.path import isdir, isfile, join
from shutil import rmtree
class FileSystem:
_instance = None
_cache_dir = CACHE_DIRECTORY
def __new__(cls):
if cls._instance is None:
cls._instance = super(FileSystem, cls).__new__(cls)
cls.log = logger.create()
return cls._instance
def get_cache_dir(self, cache_type=None):
if not isdir(self._cache_dir):
try:
makedirs(self._cache_dir)
except OSError:
self.log.info(f'Failed to create path {self._cache_dir} (Permission denied).')
raise
path = join(self._cache_dir, cache_type)
if cache_type and not isdir(path):
try:
makedirs(path)
except OSError:
self.log.info(f'Failed to create path {path} (Permission denied).')
raise
return path if cache_type else self._cache_dir
def get_cache_file_dir(self, filename, cache_type=None):
path = join(self.get_cache_dir(cache_type), filename[:2])
if not isdir(path):
try:
makedirs(path)
except OSError:
self.log.info(f'Failed to create path {path} (Permission denied).')
raise
return path
def get_cache_file_path(self, filename, cache_type=None):
return join(self.get_cache_file_dir(filename, cache_type), filename) if filename else None
def get_cache_file_exists(self, filename, cache_type=None):
path = self.get_cache_file_path(filename, cache_type)
return isfile(path)
def delete_cache_dir(self, cache_type=None):
if not cache_type and isdir(self._cache_dir):
try:
rmtree(self._cache_dir)
except OSError:
self.log.info(f'Failed to delete path {self._cache_dir} (Permission denied).')
raise
path = join(self._cache_dir, cache_type)
if cache_type and isdir(path):
try:
rmtree(path)
except OSError:
self.log.info(f'Failed to delete path {path} (Permission denied).')
raise
def delete_cache_file(self, filename, cache_type=None):
path = self.get_cache_file_path(filename, cache_type)
if isfile(path):
try:
remove(path)
except OSError:
self.log.info(f'Failed to delete path {path} (Permission denied).')
raise
+53 -55
View File
@@ -20,22 +20,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import sys
import hashlib
import json
import tempfile
from uuid import uuid4
from time import time
from shutil import move, copyfile
from flask import Blueprint, flash, request, redirect, url_for, abort
from flask_babel import gettext as _
from flask_login import login_required
from . import logger, gdriveutils, config, ub, calibre_db
from . import logger, gdriveutils, config, ub, calibre_db, csrf
from .admin import admin_required
from .file_helper import get_temp_dir
from .usermanagement import user_login_required
gdrive = Blueprint('gdrive', __name__, url_prefix='/gdrive')
log = logger.create()
@@ -47,17 +45,17 @@ except ImportError as err:
current_milli_time = lambda: int(round(time() * 1000))
gdrive_watch_callback_token = 'target=calibreweb-watch_files'
gdrive_watch_callback_token = 'target=calibreweb-watch_files' # nosec
@gdrive.route("/authenticate")
@login_required
@user_login_required
@admin_required
def authenticate_google_drive():
try:
authUrl = gdriveutils.Gauth.Instance().auth.GetAuthUrl()
except gdriveutils.InvalidConfigError:
flash(_(u'Google Drive setup not completed, try to deactivate and activate Google Drive again'),
flash(_('Google Drive setup not completed, try to deactivate and activate Google Drive again'),
category="error")
return redirect(url_for('web.index'))
return redirect(authUrl)
@@ -74,11 +72,11 @@ def google_drive_callback():
f.write(credentials.to_json())
except (ValueError, AttributeError) as error:
log.error(error)
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/subscribe")
@login_required
@user_login_required
@admin_required
def watch_gdrive():
if not config.config_google_drive_watch_changes_response:
@@ -88,22 +86,23 @@ def watch_gdrive():
notification_id = str(uuid4())
try:
result = gdriveutils.watchChange(gdriveutils.Gdrive.Instance().drive, notification_id,
'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
'web_hook', address, gdrive_watch_callback_token, current_milli_time() + 604800*1000)
config.config_google_drive_watch_changes_response = result
config.save()
except HttpError as e:
reason=json.loads(e.content)['error']['errors'][0]
if reason['reason'] == u'push.webhookUrlUnauthorized':
flash(_(u'Callback domain is not verified, '
u'please follow steps to verify domain in google developer console'), category="error")
reason = json.loads(e.content)['error']['errors'][0]
if reason['reason'] == 'push.webhookUrlUnauthorized':
flash(_('Callback domain is not verified, '
'please follow steps to verify domain in google developer console'), category="error")
else:
flash(reason['message'], category="error")
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/revoke")
@login_required
@user_login_required
@admin_required
def revoke_watch_gdrive():
last_watch_response = config.config_google_drive_watch_changes_response
@@ -111,50 +110,49 @@ def revoke_watch_gdrive():
try:
gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
last_watch_response['resourceId'])
except HttpError:
except (HttpError, AttributeError):
pass
config.config_google_drive_watch_changes_response = {}
config.save()
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
def on_received_watch_confirmation():
if not config.config_google_drive_watch_changes_response:
return ''
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
or request.headers.get('X-Goog-Resource-State') != 'change' \
or not request.data:
return ''
try:
@csrf.exempt
@gdrive.route("/watch/callback", methods=['GET', 'POST'])
def on_received_watch_confirmation():
if not config.config_google_drive_watch_changes_response:
return ''
if request.headers.get('X-Goog-Channel-Token') != gdrive_watch_callback_token \
or request.headers.get('X-Goog-Resource-State') != 'change' \
or not request.data:
return ''
log.debug('%r', request.headers)
log.debug('%r', request.data)
log.info('Change received from gdrive')
log.debug('%r', request.headers)
log.debug('%r', request.data)
log.info('Change received from gdrive')
try:
j = json.loads(request.data)
log.info('Getting change details')
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
log.debug('%r', response)
if response:
if sys.version_info < (3, 0):
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
else:
try:
j = json.loads(request.data)
log.info('Getting change details')
response = gdriveutils.getChangeById(gdriveutils.Gdrive.Instance().drive, j['id'])
log.debug('%r', response)
if response:
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
and response['file']['md5Checksum'] != hashlib.md5(dbpath).hexdigest(): # nosec
tmp_dir = get_temp_dir()
log.info('Database file updated')
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
log.info('Backing up existing and downloading updated metadata.db')
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
log.info('Setting up new DB')
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
calibre_db.reconnect_db(config, ub.app_DB_path)
except Exception as e:
log.debug_or_exception(e)
return ''
log.info('Database file updated')
copyfile(dbpath, os.path.join(tmp_dir, "metadata.db_" + str(current_milli_time())))
log.info('Backing up existing and downloading updated metadata.db')
gdriveutils.downloadFile(None, "metadata.db", os.path.join(tmp_dir, "tmp_metadata.db"))
log.info('Setting up new DB')
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
calibre_db.reconnect_db(config, ub.app_DB_path)
except Exception as ex:
log.error_or_exception(ex)
return ''
except AttributeError:
pass
+202 -114
View File
@@ -16,20 +16,32 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import json
import shutil
import chardet
import ssl
import sqlite3
import mimetypes
from werkzeug.datastructures import Headers
from flask import Response, stream_with_context
from sqlalchemy import create_engine
from sqlalchemy import Column, UniqueConstraint
from sqlalchemy import String, Integer
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError
try:
# Compatibility with sqlalchemy 2.0
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import OperationalError, InvalidRequestError, IntegrityError
from sqlalchemy.orm.exc import StaleDataError
try:
from httplib2 import __version__ as httplib2_version
except ImportError:
httplib2_version = "not installed"
try:
from apiclient import errors
@@ -43,16 +55,18 @@ try:
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from pydrive2.auth import RefreshError
from pydrive2.files import ApiRequestError
except ImportError as err:
try:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError
from pydrive.files import ApiRequestError
except ImportError as err:
importError = err
gdrive_support = False
from . import logger, cli, config
from . import logger, cli_param, config, db
from .constants import CONFIG_DIR as _CONFIG_DIR
@@ -66,7 +80,7 @@ if gdrive_support:
if not logger.is_debug_enabled():
logger.get('googleapiclient.discovery').setLevel(logger.logging.ERROR)
else:
log.debug("Cannot import pydrive,httplib2, using gdrive will not work: %s", importError)
log.debug("Cannot import pydrive, httplib2, using gdrive will not work: {}".format(importError))
class Singleton:
@@ -126,15 +140,16 @@ class Gdrive:
def __init__(self):
self.drive = getDrive(gauth=Gauth.Instance().auth)
def is_gdrive_ready():
return os.path.exists(SETTINGS_YAML) and os.path.exists(CREDENTIALS)
engine = create_engine('sqlite:///{0}'.format(cli.gdpath), echo=False)
engine = create_engine('sqlite:///{0}'.format(cli_param.gd_path), echo=False)
Base = declarative_base()
# Open session for database connection
Session = sessionmaker()
Session = sessionmaker(autoflush=False)
Session.configure(bind=engine)
session = scoped_session(Session)
@@ -161,29 +176,12 @@ class PermissionAdded(Base):
return str(self.gdrive_id)
def migrate():
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
PermissionAdded.__table__.create(bind = engine)
for sql in session.execute("select sql from sqlite_master where type='table'"):
if 'CREATE TABLE gdrive_ids' in sql[0]:
currUniqueConstraint = 'UNIQUE (gdrive_id)'
if currUniqueConstraint in sql[0]:
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
session.execute(sql)
session.execute("INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, "
"gdrive_id, path FROM gdrive_ids;")
session.commit()
session.execute('DROP TABLE %s' % 'gdrive_ids')
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
break
if not os.path.exists(cli.gdpath):
if not os.path.exists(cli_param.gd_path):
try:
Base.metadata.create_all(engine)
except Exception:
except Exception as ex:
log.error("Error connect to database: {} - {}".format(cli_param.gd_path, ex))
raise
migrate()
def getDrive(drive=None, gauth=None):
@@ -197,9 +195,9 @@ def getDrive(drive=None, gauth=None):
try:
gauth.Refresh()
except RefreshError as e:
log.error("Google Drive error: %s", e)
except Exception as e:
log.debug_or_exception(e)
log.error("Google Drive error: {}".format(e))
except Exception as ex:
log.error_or_exception(ex)
else:
# Initialize the saved creds
gauth.Authorize()
@@ -209,16 +207,17 @@ def getDrive(drive=None, gauth=None):
try:
drive.auth.Refresh()
except RefreshError as e:
log.error("Google Drive error: %s", e)
log.error("Google Drive error: {}".format(e))
return drive
def listRootFolders():
try:
drive = getDrive(Gdrive.Instance().drive)
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
fileList = drive.ListFile({'q': folder}).GetList()
except (ServerNotFoundError, ssl.SSLError) as e:
log.info("GDrive Error %s" % e)
except (ServerNotFoundError, ssl.SSLError, RefreshError) as e:
log.info("GDrive Error {}".format(e))
fileList = []
return fileList
@@ -229,7 +228,7 @@ def getEbooksFolder(drive):
def getFolderInFolder(parentId, folderName, drive):
# drive = getDrive(drive)
query=""
query = ""
if folderName:
query = "title = '%s' and " % folderName.replace("'", r"\'")
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder'" \
@@ -240,6 +239,7 @@ def getFolderInFolder(parentId, folderName, drive):
else:
return fileList[0]
# Search for id of root folder in gdrive database, if not found request from gdrive and store in internal database
def getEbooksFolderId(drive=None):
storedPathName = session.query(GdriveId).filter(GdriveId.path == '/').first()
@@ -253,56 +253,73 @@ def getEbooksFolderId(drive=None):
log.error('Error gDrive, root ID not found')
gDriveId.path = '/'
session.merge(gDriveId)
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
return gDriveId.gdrive_id
def getFile(pathId, fileName, drive):
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", r"\'"))
def getFile(pathId, fileName, drive, nocase):
metaDataFile = "'%s' in parents and trashed = false and title contains '%s'" % (pathId, fileName.replace("'", r"\'"))
fileList = drive.ListFile({'q': metaDataFile}).GetList()
if fileList.__len__() == 0:
return None
else:
return fileList[0]
if nocase:
return fileList[0] if db.lcase(fileList[0]['title']) == db.lcase(fileName) else None
for f in fileList:
if f['title'] == fileName:
return f
return None
def getFolderId(path, drive):
# drive = getDrive(drive)
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
currentFolderId = None
try:
currentFolderId = getEbooksFolderId(drive)
sqlCheckPath = path if path[-1] == '/' else path + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolder = getFolderInFolder(currentFolderId, x, drive)
if currentFolder:
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolder['id']
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
currentFolderId = currentFolder['id']
if not storedPathName:
dbChange = False
s = path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath = "/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId = storedPathName.gdrive_id
else:
currentFolderId = None
break
if dbChange:
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
currentFolder = getFolderInFolder(currentFolderId, x, drive)
if currentFolder:
gDriveId = GdriveId()
gDriveId.gdrive_id = currentFolder['id']
gDriveId.path = currentPath
session.merge(gDriveId)
dbChange = True
currentFolderId = currentFolder['id']
else:
currentFolderId = None
break
if dbChange:
session.commit()
else:
currentFolderId = storedPathName.gdrive_id
except (OperationalError, IntegrityError, StaleDataError, sqlite3.IntegrityError) as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
except ApiRequestError as ex:
log.error('{} {}'.format(ex.error['message'], path))
session.rollback()
except RefreshError as ex:
log.error(ex)
return currentFolderId
def getFileFromEbooksFolder(path, fileName):
def getFileFromEbooksFolder(path, fileName, nocase=False):
drive = getDrive(Gdrive.Instance().drive)
if path:
# sqlCheckPath=path if path[-1] =='/' else path + '/'
@@ -310,13 +327,13 @@ def getFileFromEbooksFolder(path, fileName):
else:
folderId = getEbooksFolderId(drive)
if folderId:
return getFile(folderId, fileName, drive)
return getFile(folderId, fileName, drive, nocase)
else:
return None
def moveGdriveFileRemote(origin_file_id, new_title):
origin_file_id['title']= new_title
origin_file_id['title'] = new_title
origin_file_id.Upload()
@@ -326,43 +343,58 @@ def downloadFile(path, filename, output):
f.GetContentFile(output)
def moveGdriveFolderRemote(origin_file, target_folder):
def moveGdriveFolderRemote(origin_file, target_folder, single_book=False):
drive = getDrive(Gdrive.Instance().drive)
previous_parents = ",".join([parent["id"] for parent in origin_file.get('parents')])
children = drive.auth.service.children().list(folderId=previous_parents).execute()
gFileTargetDir = getFileFromEbooksFolder(None, target_folder)
if not gFileTargetDir:
if single_book:
gFileTargetDir = getFileFromEbooksFolder(None, target_folder, nocase=True)
if gFileTargetDir:
# Move the file to the new folder
drive.auth.service.files().update(fileId=origin_file['id'],
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
else:
gFileTargetDir = drive.CreateFile(
{'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}],
"mimeType": "application/vnd.google-apps.folder"})
gFileTargetDir.Upload()
# Move the file to the new folder
drive.auth.service.files().update(fileId=origin_file['id'],
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
elif origin_file['title'] != target_folder:
#gFileTargetDir = getFileFromEbooksFolder(None, target_folder, nocase=True)
#if gFileTargetDir:
deleteDatabasePath(origin_file['title'])
# Folder is not existing, create, and move folder
gFileTargetDir = drive.CreateFile(
{'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}],
"mimeType": "application/vnd.google-apps.folder"})
gFileTargetDir.Upload()
# Move the file to the new folder
drive.auth.service.files().update(fileId=origin_file['id'],
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
# if previous_parents has no childs anymore, delete original fileparent
drive.auth.service.files().patch(fileId=origin_file['id'],
body={'title': target_folder},
fields='title').execute()
# if previous_parents has no children anymore, delete original fileparent
if len(children['items']) == 1:
deleteDatabaseEntry(previous_parents)
drive.auth.service.files().delete(fileId=previous_parents).execute()
def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
ignoreFiles=None,
parent=None, prevDir=''):
ignoreFiles=None,
parent=None, prevDir=''):
ignoreFiles = ignoreFiles or []
drive = getDrive(drive)
isInitial = not bool(parent)
if not parent:
parent = getEbooksFolder(drive)
if os.path.isdir(os.path.join(prevDir,uploadFile)):
if os.path.isdir(os.path.join(prevDir, uploadFile)):
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(os.path.basename(uploadFile).replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFolder) == 0 and (not isInitial or createRoot):
parent = drive.CreateFile({'title': os.path.basename(uploadFile),
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"})
"mimeType": "application/vnd.google-apps.folder"})
parent.Upload()
else:
if (not isInitial or createRoot) and len(existingFolder) > 0:
@@ -378,39 +410,42 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
driveFile = existingFiles[0]
else:
driveFile = drive.CreateFile({'title': os.path.basename(uploadFile).replace("'", r"\'"),
'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], })
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], })
driveFile.SetContentFile(os.path.join(prevDir, uploadFile))
driveFile.Upload()
def uploadFileToEbooksFolder(destFile, f):
def uploadFileToEbooksFolder(destFile, f, string=False):
drive = getDrive(Gdrive.Instance().drive)
parent = getEbooksFolder(drive)
splitDir = destFile.split('/')
for i, x in enumerate(splitDir):
if i == len(splitDir)-1:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFiles) > 0:
driveFile = existingFiles[0]
existing_Files = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existing_Files) > 0:
driveFile = existing_Files[0]
else:
driveFile = drive.CreateFile({'title': x,
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], })
driveFile.SetContentFile(f)
if not string:
driveFile.SetContentFile(f)
else:
driveFile.SetContentString(f)
driveFile.Upload()
else:
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFolder) == 0:
existing_Folder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existing_Folder) == 0:
parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"})
"mimeType": "application/vnd.google-apps.folder"})
parent.Upload()
else:
parent = existingFolder[0]
parent = existing_Folder[0]
def watchChange(drive, channel_id, channel_type, channel_address,
channel_token=None, expiration=None):
channel_token=None, expiration=None):
# Watch for all changes to a user's Drive.
# Args:
# service: Drive API service instance.
@@ -481,7 +516,7 @@ def stopChannel(drive, channel_id, resource_id):
return drive.auth.service.channels().stop(body=body).execute()
def getChangeById (drive, change_id):
def getChangeById(drive, change_id):
# Print a single Change resource information.
#
# Args:
@@ -493,8 +528,8 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error:
log.error(error)
return None
except Exception as e:
log.error(e)
except Exception as ex:
log.error(ex)
return None
@@ -503,9 +538,10 @@ def deleteDatabaseOnChange():
try:
session.query(GdriveId).delete()
session.commit()
except (OperationalError, InvalidRequestError):
except (OperationalError, InvalidRequestError) as ex:
session.rollback()
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
log.info(u"GDrive DB is not Writeable")
def updateGdriveCalibreFromLocal():
@@ -514,22 +550,40 @@ def updateGdriveCalibreFromLocal():
if os.path.isdir(os.path.join(config.config_calibre_dir, x)):
shutil.rmtree(os.path.join(config.config_calibre_dir, x))
# update gdrive.db on edit of books title
def updateDatabaseOnEdit(ID,newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
def updateDatabaseOnEdit(ID, newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + '/'
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName:
storedPathName.path = sqlCheckPath
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
# Deletes the hashes in database of deleted book
def deleteDatabaseEntry(ID):
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
session.commit()
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
def deleteDatabasePath(Pathname):
session.query(GdriveId).filter(GdriveId.path.contains(Pathname)).delete()
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
# Gets cover file from gdrive
# ToDo: Check is this right everyone get read permissions on cover files?
def get_cover_via_gdrive(cover_path):
df = getFileFromEbooksFolder(cover_path, 'cover.jpg')
if df:
@@ -543,11 +597,43 @@ def get_cover_via_gdrive(cover_path):
permissionAdded = PermissionAdded()
permissionAdded.gdrive_id = df['id']
session.add(permissionAdded)
session.commit()
try:
session.commit()
except (OperationalError, IntegrityError) as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
headers = Headers()
headers["Content-Type"] = 'image/jpeg'
resp, content = df.auth.Get_Http_Object().request(df.metadata.get('downloadUrl'), headers=headers)
return content
else:
return None
# Gets cover file from gdrive
def get_metadata_backup_via_gdrive(metadata_path):
df = getFileFromEbooksFolder(metadata_path, 'metadata.opf')
if df:
if not session.query(PermissionAdded).filter(PermissionAdded.gdrive_id == df['id']).first():
df.GetPermissions()
df.InsertPermission({
'type': 'anyone',
'value': 'anyone',
'role': 'writer', # ToDo needs write access
'withLink': True})
permissionAdded = PermissionAdded()
permissionAdded.gdrive_id = df['id']
session.add(permissionAdded)
try:
session.commit()
except OperationalError as ex:
log.error_or_exception('Database error: {}'.format(ex))
session.rollback()
return df.metadata.get('webContentLink')
else:
return None
# Creates chunks for downloading big files
def partial(total_byte_len, part_size_limit):
s = []
@@ -556,6 +642,7 @@ def partial(total_byte_len, part_size_limit):
s.append([p, last])
return s
# downloads files in chunks from gdrive
def do_gdrive_download(df, headers, convert_encoding=False):
total_size = int(df.metadata.get('fileSize'))
@@ -564,7 +651,7 @@ def do_gdrive_download(df, headers, convert_encoding=False):
def stream(convert_encoding):
for byte in s:
headers = {"Range": 'bytes=%s-%s' % (byte[0], byte[1])}
headers = {"Range": 'bytes={}-{}'.format(byte[0], byte[1])}
resp, content = df.auth.Get_Http_Object().request(download_url, headers=headers)
if resp.status == 206:
if convert_encoding:
@@ -572,7 +659,7 @@ def do_gdrive_download(df, headers, convert_encoding=False):
content = content.decode(result['encoding']).encode('utf-8')
yield content
else:
log.warning('An error occurred: %s', resp)
log.warning('An error occurred: {}'.format(resp))
return
return Response(stream_with_context(stream(convert_encoding)), headers=headers)
@@ -595,6 +682,7 @@ oauth_scope:
- https://www.googleapis.com/auth/drive
"""
def update_settings(client_id, client_secret, redirect_uri):
if redirect_uri.endswith('/'):
redirect_uri = redirect_uri[:-1]
+52
View File
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2022 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from gevent.pywsgi import WSGIHandler
class MyWSGIHandler(WSGIHandler):
def get_environ(self):
env = super().get_environ()
path, __ = self.path.split('?', 1) if '?' in self.path else (self.path, '')
env['RAW_URI'] = path
return env
def format_request(self):
now = datetime.now().replace(microsecond=0)
length = self.response_length or '-'
if self.time_finish:
delta = '%.6f' % (self.time_finish - self.time_start)
else:
delta = '-'
forwarded = self.environ.get('HTTP_X_FORWARDED_FOR', None)
if forwarded:
client_address = forwarded
else:
client_address = self.client_address[0] if isinstance(self.client_address, tuple) else self.client_address
return '%s - - [%s] "%s" %s %s %s' % (
client_address or '-',
now,
self.requestline or '',
# Use the native string version of the status, saved so we don't have to
# decode. But fallback to the encoded 'status' in case of subclasses
# (Is that really necessary? At least there's no overhead.)
(self._orig_status or self.status or '000').split()[0],
length,
delta)
+762 -436
View File
File diff suppressed because it is too large Load Diff
+43 -29
View File
@@ -15,23 +15,17 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
from . import logger
from .string_helper import strip_whitespaces
log = logger.create()
try:
from iso639 import languages, __version__
get = languages.get
except ImportError:
from pycountry import languages as pyc_languages
try:
import pkg_resources
__version__ = pkg_resources.get_distribution('pycountry').version + ' (PyCountry)'
del pkg_resources
except (ImportError, Exception):
__version__ = "? (PyCountry)"
def _copy_fields(l):
l.part1 = getattr(l, 'alpha_2', None)
@@ -45,39 +39,59 @@ except ImportError:
return _copy_fields(pyc_languages.get(alpha_2=part1))
if name is not None:
return _copy_fields(pyc_languages.get(name=name))
except ImportError as ex:
if sys.version_info >= (3, 12):
print("Python 3.12 isn't compatible with iso-639. Please install pycountry.")
from iso639 import languages
get = languages.get
def get_language_names(locale):
return _LANGUAGE_NAMES.get(locale)
names = _LANGUAGE_NAMES.get(str(locale))
if names is None:
names = _LANGUAGE_NAMES.get(locale.language)
return names
def get_language_name(locale, lang_code):
return get_language_names(locale)[lang_code]
UNKNOWN_TRANSLATION = "Unknown"
names = get_language_names(locale)
if names is None:
log.error(f"Missing language names for locale: {str(locale)}/{locale.language}")
return UNKNOWN_TRANSLATION
name = names.get(lang_code, UNKNOWN_TRANSLATION)
if name == UNKNOWN_TRANSLATION:
log.error("Missing translation for language name: {}".format(lang_code))
return name
def get_language_codes(locale, language_names, remainder=None):
language_names = set(x.strip().lower() for x in language_names if x)
languages = list()
for k, v in get_language_names(locale).items():
v = v.lower()
if v in language_names:
languages.append(k)
language_names.remove(v)
if remainder is not None:
def get_language_code_from_name(locale, language_names, remainder=None):
language_names = set(strip_whitespaces(x).lower() for x in language_names if x)
lang = list()
for key, val in get_language_names(locale).items():
val = val.lower()
if val in language_names:
lang.append(key)
language_names.remove(val)
if remainder is not None and language_names:
remainder.extend(language_names)
return languages
return lang
def get_valid_language_codes(locale, language_names, remainder=None):
languages = list()
def get_valid_language_codes_from_code(locale, language_names, remainder=None):
lang = list()
if "" in language_names:
language_names.remove("")
for k, v in get_language_names(locale).items():
for k, __ in get_language_names(locale).items():
if k in language_names:
languages.append(k)
lang.append(k)
language_names.remove(k)
if remainder is not None and len(language_names):
remainder.extend(language_names)
return languages
return lang
def get_lang3(lang):
try:
@@ -87,6 +101,6 @@ def get_lang3(lang):
ret_value = lang
else:
ret_value = ""
except KeyError:
except (KeyError, AttributeError):
ret_value = lang
return ret_value
+4668 -419
View File
File diff suppressed because it is too large Load Diff
+76 -21
View File
@@ -22,18 +22,17 @@
# custom jinja filters
from __future__ import division, print_function, unicode_literals
from markupsafe import escape
import datetime
import mimetypes
from uuid import uuid4
from babel.dates import format_date
from flask import Blueprint, request, url_for
from flask_babel import get_locale
from flask_login import current_user
from . import logger
from flask import Blueprint, request, url_for, g
from flask_babel import format_date
from .cw_login import current_user
from .clean_html import clean_string as html_clean_string
from . import constants, logger
jinjia = Blueprint('jinjia', __name__)
log = logger.create()
@@ -45,6 +44,8 @@ def url_for_other_page(page):
args = request.view_args.copy()
args['page'] = page
for get, val in request.args.items():
if get == "page":
continue
args[get] = val
return url_for(request.endpoint, **args)
@@ -78,11 +79,11 @@ def mimetype_filter(val):
@jinjia.app_template_filter('formatdate')
def formatdate_filter(val):
try:
return format_date(val, format='medium', locale=get_locale())
return format_date(val, format='medium')
except AttributeError as e:
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
current_user.locale,
current_user.nickname
current_user.name
)
return val
@@ -113,23 +114,77 @@ def yesno(value, yes, no):
@jinjia.app_template_filter('formatfloat')
def formatfloat(value, decimals=1):
formatedstring = '%d' % value
if (value % 1) != 0:
formatedstring = ('%s.%d' % (formatedstring, (value % 1) * 10**decimals)).rstrip('0')
return formatedstring
if not value or (isinstance(value, str) and not value.is_numeric()):
return value
formated_value = ('{0:.' + str(decimals) + 'f}').format(value)
if formated_value.endswith('.' + "0" * decimals):
formated_value = formated_value.rstrip('0').rstrip('.')
return formated_value
@jinjia.app_template_filter('formatseriesindex')
def formatseriesindex_filter(series_index):
if series_index:
if int(series_index) - series_index == 0:
return int(series_index)
else:
return series_index
return 0
@jinjia.app_template_filter('escapedlink')
def escapedlink_filter(url, text):
return "<a href='{}'>{}</a>".format(url, escape(text))
@jinjia.app_template_filter('uuidfilter')
def uuidfilter(var):
return uuid4()
@jinjia.app_template_filter('cache_timestamp')
def cache_timestamp(rolling_period='month'):
if rolling_period == 'day':
return str(int(datetime.datetime.today().replace(hour=1, minute=1).timestamp()))
elif rolling_period == 'year':
return str(int(datetime.datetime.today().replace(day=1).timestamp()))
else:
return str(int(datetime.datetime.today().replace(month=1, day=1).timestamp()))
@jinjia.app_template_filter('last_modified')
def book_last_modified(book):
return str(int(book.last_modified.timestamp()))
@jinjia.app_template_filter('get_cover_srcset')
def get_cover_srcset(book):
srcset = list()
resolutions = {
constants.COVER_THUMBNAIL_SMALL: 'sm',
constants.COVER_THUMBNAIL_MEDIUM: 'md',
constants.COVER_THUMBNAIL_LARGE: 'lg'
}
for resolution, shortname in resolutions.items():
url = url_for('web.get_cover', book_id=book.id, resolution=shortname, c=book_last_modified(book))
srcset.append(f'{url} {resolution}x')
return ', '.join(srcset)
@jinjia.app_template_filter('get_series_srcset')
def get_cover_srcset(series):
srcset = list()
resolutions = {
constants.COVER_THUMBNAIL_SMALL: 'sm',
constants.COVER_THUMBNAIL_MEDIUM: 'md',
constants.COVER_THUMBNAIL_LARGE: 'lg'
}
for resolution, shortname in resolutions.items():
url = url_for('web.get_series_cover', series_id=series.id, resolution=shortname, c=cache_timestamp())
srcset.append(f'{url} {resolution}x')
return ', '.join(srcset)
@jinjia.app_template_filter('music')
def contains_music(book_formats):
result = False
for format in book_formats:
if format.format.lower() in g.constants.EXTENSIONS_AUDIO:
result = True
return result
@jinjia.app_template_filter('clean_string')
def clean_string(unsafe_text):
return html_clean_string(unsafe_text)
+443 -252
View File
File diff suppressed because it is too large Load Diff
+73 -69
View File
@@ -22,7 +22,7 @@
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.
When first booting a Kobo device the user must log in to 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:
@@ -41,7 +41,7 @@ issue for a few years now https://www.mobileread.com/forums/showpost.php?p=34768
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
* 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
@@ -62,27 +62,76 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
from binascii import hexlify
from datetime import datetime
from os import urandom
from functools import wraps
from flask import g, Blueprint, url_for, abort, request
from flask_login import login_user, login_required
from flask import g, Blueprint, abort, request
from .cw_login import login_user, current_user
from flask_babel import gettext as _
from . import logger, ub, lm
from . import logger, config, calibre_db, db, helper, ub, lm, limiter
from .render_template import render_title_template
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
from .usermanagement import user_login_required
log = logger.create()
kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor
def pop_auth_token(__, values):
g.auth_token = values.pop("auth_token")
@kobo_auth.route("/generate_auth_token/<int:user_id>")
@user_login_required
def generate_auth_token(user_id):
if current_user.id != user_id and not current_user.role_admin():
abort(403)
warning = False
host_list = request.host.rsplit(':')
if len(host_list) == 1:
host = ':'.join(host_list)
else:
host = ':'.join(host_list[0:-1])
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]":
warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device')
# Generate auth token if none is existing for this user
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == user_id
).filter(ub.RemoteAuthToken.token_type==1).first()
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()
books = calibre_db.session.query(db.Books).join(db.Data).all()
for book in books:
formats = [data.format for data in book.data]
if 'KEPUB' not in formats and config.config_kepubifypath and 'EPUB' in formats:
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
return render_title_template(
"generate_kobo_auth_url.html",
title=_("Kobo Setup"),
auth_token=auth_token.auth_token,
warning=warning
)
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
@user_login_required
def delete_auth_token(user_id):
if current_user.id != user_id and not current_user.role_admin():
abort(403)
# Invalidate any previously generated Kobo Auth token for this user
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
.filter(ub.RemoteAuthToken.token_type==1).delete()
return ub.session_commit()
def disable_failed_auth_redirect_for_blueprint(bp):
@@ -96,6 +145,13 @@ def get_auth_token():
return None
def register_url_value_preprocessor(kobo):
@kobo.url_value_preprocessor
# pylint: disable=unused-variable
def pop_auth_token(__, values):
g.auth_token = values.pop("auth_token")
def requires_kobo_auth(f):
@wraps(f)
def inner(*args, **kwargs):
@@ -109,60 +165,8 @@ def requires_kobo_auth(f):
)
if user is not None:
login_user(user)
[limiter.limiter.clear(limit.limit, *limit.request_args) for limit in limiter.current_limits]
return f(*args, **kwargs)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
log.debug("Received Kobo request without a recognizable auth token.")
return abort(401)
return inner
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_list = request.host.rsplit(':')
if len(host_list) == 1:
host = ':'.join(host_list)
else:
host = ':'.join(host_list[0:-1])
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
return render_title_template(
"generate_kobo_auth_url.html",
title=_(u"Kobo Setup"),
warning = warning
)
else:
# Invalidate any prevously generated Kobo Auth token for this user.
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == user_id
).filter(ub.RemoteAuthToken.token_type==1).first()
if 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()
return ub.session_commit()
+90
View File
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .cw_login import current_user
from . import ub
from datetime import datetime, timezone
from sqlalchemy.sql.expression import or_, and_, true
# from sqlalchemy import exc
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
# do nothing (safety precaution)
def add_synced_books(book_id):
is_present = ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id)\
.filter(ub.KoboSyncedBooks.user_id == current_user.id).count()
if not is_present:
synced_book = ub.KoboSyncedBooks()
synced_book.user_id = current_user.id
synced_book.book_id = book_id
ub.session.add(synced_book)
ub.session_commit()
# Select all entries of current book in kobo_synced_books table, which are from current user and delete them
def remove_synced_book(book_id, all=False, session=None):
if not all:
user = ub.KoboSyncedBooks.user_id == current_user.id
else:
user = true()
if not session:
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
ub.session_commit()
else:
session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
ub.session_commit(_session=session)
# If state == none, it will toggle the archive state of the passed book_id.
# state = true archives it, state = false unarchives it
def change_archived_books(book_id, state=None, message=None):
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
ub.ArchivedBook.book_id == book_id)).first()
if not archived_book: # and (state == True or state == None):
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
archived_book.is_archived = state if state != None else not archived_book.is_archived
archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp
ub.session.merge(archived_book)
ub.session_commit(message)
return archived_book.is_archived
# select all books which are synced by the current user and do not belong to a synced shelf and set them to archive
# select all shelves from current user which are synced and do not belong to the "only sync" shelves
def update_on_sync_shelfs(user_id):
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
.join(ub.Shelf, ub.Shelf.user_id == user_id, isouter=True)
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync==None))
.filter(ub.KoboSyncedBooks.user_id == user_id).all())
for b in books_to_archive:
change_archived_books(b.book_id, True)
ub.session.query(ub.KoboSyncedBooks) \
.filter(ub.KoboSyncedBooks.book_id == b.book_id) \
.filter(ub.KoboSyncedBooks.user_id == user_id).delete()
ub.session_commit()
# Search all shelf which are currently not synced
shelves_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == user_id).filter(
ub.Shelf.kobo_sync == 0).all()
for a in shelves_to_archive:
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=user_id))
ub.session_commit()
+23 -32
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import os
import sys
import inspect
@@ -30,7 +29,7 @@ from .constants import CONFIG_DIR as _CONFIG_DIR
ACCESS_FORMATTER_GEVENT = Formatter("%(message)s")
ACCESS_FORMATTER_TORNADO = Formatter("[%(asctime)s] %(message)s")
FORMATTER = Formatter("[%(asctime)s] %(levelname)5s {%(name)s:%(lineno)d} %(message)s")
FORMATTER = Formatter("[%(asctime)s] %(levelname)5s {%(filename)s:%(lineno)d} %(message)s")
DEFAULT_LOG_LEVEL = logging.INFO
DEFAULT_LOG_FILE = os.path.join(_CONFIG_DIR, "calibre-web.log")
DEFAULT_ACCESS_LOG = os.path.join(_CONFIG_DIR, "access.log")
@@ -43,35 +42,25 @@ logging.addLevelName(logging.CRITICAL, "CRIT")
class _Logger(logging.Logger):
def debug_or_exception(self, message, *args, **kwargs):
if sys.version_info > (3, 7):
if is_debug_enabled():
self.exception(message, stacklevel=2, *args, **kwargs)
else:
self.error(message, stacklevel=2, *args, **kwargs)
elif sys.version_info > (3, 0):
if is_debug_enabled():
self.exception(message, stack_info=True, *args, **kwargs)
else:
self.error(message, *args, **kwargs)
def error_or_exception(self, message, stacklevel=1, *args, **kwargs):
is_debug = self.getEffectiveLevel() <= logging.DEBUG
if not is_debug:
self.exception(message, stacklevel=stacklevel, *args, **kwargs)
else:
if is_debug_enabled():
self.exception(message, *args, **kwargs)
else:
self.error(message, *args, **kwargs)
self.error(message, stacklevel=stacklevel, *args, **kwargs)
def debug_no_auth(self, message, *args, **kwargs):
message = message.strip("\r\n")
if message.startswith("send: AUTH"):
self.debug(message[:16], stacklevel=2, *args, **kwargs)
self.debug(message[:16], *args, **kwargs)
else:
self.debug(message, stacklevel=2, *args, **kwargs)
self.debug(message, *args, **kwargs)
def get(name=None):
return logging.getLogger(name)
def create():
parent_frame = inspect.stack(0)[1]
if hasattr(parent_frame, 'frame'):
@@ -81,9 +70,11 @@ def create():
parent_module = inspect.getmodule(parent_frame)
return get(parent_module.__name__)
def is_debug_enabled():
return logging.root.level <= logging.DEBUG
def is_info_enabled(logger):
return logging.getLogger(logger).level <= logging.INFO
@@ -120,10 +111,10 @@ def get_accesslogfile(log_file):
def setup(log_file, log_level=None):
'''
"""
Configure the logging output.
May be called multiple times.
'''
"""
log_level = log_level or DEFAULT_LOG_LEVEL
logging.setLoggerClass(_Logger)
logging.getLogger(__package__).setLevel(log_level)
@@ -133,7 +124,7 @@ def setup(log_file, log_level=None):
# avoid spamming the log with debug messages from libraries
r.setLevel(log_level)
# Otherwise name get's destroyed on windows
# Otherwise, name gets 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)
@@ -153,11 +144,11 @@ def setup(log_file, log_level=None):
file_handler.baseFilename = log_file
else:
try:
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
except IOError:
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
except (IOError, PermissionError):
if log_file == DEFAULT_LOG_FILE:
raise
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8')
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
log_file = ""
file_handler.setFormatter(FORMATTER)
@@ -165,13 +156,14 @@ def setup(log_file, log_level=None):
r.removeHandler(h)
h.close()
r.addHandler(file_handler)
logging.captureWarnings(True)
return "" if log_file == DEFAULT_LOG_FILE else log_file
def create_access_log(log_file, log_name, formatter):
'''
"""
One-time configuration for the web server's access log.
'''
"""
log_file = _absolute_log_file(log_file, DEFAULT_ACCESS_LOG)
logging.debug("access log: %s", log_file)
@@ -180,7 +172,7 @@ def create_access_log(log_file, log_name, formatter):
access_log.setLevel(logging.INFO)
try:
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
except IOError:
except (IOError, PermissionError):
if log_file == DEFAULT_ACCESS_LOG:
raise
file_handler = RotatingFileHandler(DEFAULT_ACCESS_LOG, maxBytes=50000, backupCount=2, encoding='utf-8')
@@ -188,8 +180,7 @@ def create_access_log(log_file, log_name, formatter):
file_handler.setFormatter(formatter)
access_log.addHandler(file_handler)
return access_log, \
"" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
return access_log, "" if _absolute_log_file(log_file, DEFAULT_ACCESS_LOG) == DEFAULT_ACCESS_LOG else log_file
# Enable logging of smtp lib debug output
+86
View File
@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2022 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 . import create_app, limiter
from .jinjia import jinjia
from flask import request
def request_username():
return request.authorization.username if request.authorization else ""
def main():
app = create_app()
from .web import web
from .basic import basic
from .opds import opds
from .admin import admi
from .gdrive import gdrive
from .editbooks import editbook
from .about import about
from .search import search
from .search_metadata import meta
from .shelf import shelf
from .tasks_status import tasks
from .error_handler import init_errorhandler
from .remotelogin import remotelogin
try:
from .kobo import kobo, get_kobo_activated
from .kobo_auth import kobo_auth
from flask_limiter.util import get_remote_address
kobo_available = get_kobo_activated()
except (ImportError, AttributeError): # Catch also error for not installed flask-WTF (missing csrf decorator)
kobo_available = False
kobo = kobo_auth = get_remote_address = None
try:
from .oauth_bb import oauth
oauth_available = True
except ImportError:
oauth_available = False
oauth = None
from . import web_server
init_errorhandler()
app.register_blueprint(search)
app.register_blueprint(tasks)
app.register_blueprint(web)
app.register_blueprint(basic)
limiter.limit("3/minute", key_func=request_username)(opds)
app.register_blueprint(opds)
app.register_blueprint(jinjia)
app.register_blueprint(about)
app.register_blueprint(shelf)
app.register_blueprint(admi)
app.register_blueprint(remotelogin)
app.register_blueprint(meta)
app.register_blueprint(gdrive)
app.register_blueprint(editbook)
if kobo_available:
limiter.limit("3/minute", key_func=get_remote_address)(kobo)
app.register_blueprint(kobo)
app.register_blueprint(kobo_auth)
if oauth_available:
app.register_blueprint(oauth)
success = web_server.start()
sys.exit(0 if success else 1)
+140
View File
@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2022 quarz12
#
# 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 concurrent.futures
import requests
from bs4 import BeautifulSoup as BS # requirement
from typing import List, Optional
try:
import cchardet #optional for better speed
except ImportError:
pass
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
import cps.logger as logger
#from time import time
from operator import itemgetter
log = logger.create()
class Amazon(Metadata):
__name__ = "Amazon"
__id__ = "amazon"
headers = {'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'Upgrade-Insecure-Requests': '1',
'Alt-Used' : 'www.amazon.com',
'Priority' : 'u=0, i',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'en-US,en;q=0.9'}
session = requests.Session()
session.headers=headers
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
def inner(link, index) -> [dict, int]:
with self.session as session:
try:
r = session.get(f"https://www.amazon.com/{link}")
r.raise_for_status()
except Exception as ex:
log.warning(ex)
return []
long_soup = BS(r.text, "lxml") #~4sec :/
soup2 = long_soup.find("div", attrs={"cel_widget_id": "dpx-ppd_csm_instrumentation_wrapper"})
if soup2 is None:
return []
try:
match = MetaRecord(
title = "",
authors = "",
source=MetaSourceInfo(
id=self.__id__,
description="Amazon Books",
link="https://amazon.com/"
),
url = f"https://www.amazon.com{link}",
#the more searches the slower, these are too hard to find in reasonable time or might not even exist
publisher= "", # very unreliable
publishedDate= "", # very unreliable
id = None, # ?
tags = [] # dont exist on amazon
)
try:
match.description = "\n".join(
soup2.find("div", attrs={"data-feature-name": "bookDescription"}).stripped_strings)\
.replace("\xa0"," ")[:-9].strip().strip("\n")
except (AttributeError, TypeError):
return [] # if there is no description it is not a book and therefore should be ignored
try:
match.title = soup2.find("span", attrs={"id": "productTitle"}).text
except (AttributeError, TypeError):
match.title = ""
try:
match.authors = [next(
filter(lambda i: i != " " and i != "\n" and not i.startswith("{"),
x.findAll(string=True))).strip()
for x in soup2.findAll("span", attrs={"class": "author"})]
except (AttributeError, TypeError, StopIteration):
match.authors = ""
try:
match.rating = int(
soup2.find("span", class_="a-icon-alt").text.split(" ")[0].split(".")[
0]) # first number in string
except (AttributeError, ValueError):
match.rating = 0
try:
match.cover = soup2.find("img", attrs={"class": "a-dynamic-image"})["src"]
except (AttributeError, TypeError):
match.cover = ""
return match, index
except Exception as e:
log.error_or_exception(e)
return []
val = list()
if self.active:
try:
results = self.session.get(
f"https://www.amazon.com/s?k={query.replace(' ', '+')}&i=digital-text&sprefix={query.replace(' ', '+')}"
f"%2Cdigital-text&ref=nb_sb_noss",
headers=self.headers)
results.raise_for_status()
except requests.exceptions.HTTPError as e:
log.error_or_exception(e)
return []
except Exception as e:
log.warning(e)
return []
soup = BS(results.text, 'html.parser')
links_list = [next(filter(lambda i: "digital-text" in i["href"], x.findAll("a")))["href"] for x in
soup.findAll("div", attrs={"data-component-type": "s-search-result"})]
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
fut = {executor.submit(inner, link, index) for index, link in enumerate(links_list[:3])}
val = list(map(lambda x : x.result(), concurrent.futures.as_completed(fut)))
result = list(filter(lambda x: x, val))
return [x[0] for x in sorted(result, key=itemgetter(1))] #sort by amazons listing order for best relevance
+92
View File
@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ComicVine api document: https://comicvine.gamespot.com/api/documentation
from typing import Dict, List, Optional
from urllib.parse import quote
import requests
from cps import logger
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
log = logger.create()
class ComicVine(Metadata):
__name__ = "ComicVine"
__id__ = "comicvine"
DESCRIPTION = "ComicVine Books"
META_URL = "https://comicvine.gamespot.com/"
API_KEY = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
BASE_URL = (
f"https://comicvine.gamespot.com/api/search?api_key={API_KEY}"
f"&resources=issue&query="
)
QUERY_PARAMS = "&sort=name:desc&format=json"
HEADERS = {"User-Agent": "Not Evil Browser"}
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
val = list()
if self.active:
title_tokens = list(self.get_title_tokens(query, strip_joiners=False))
if title_tokens:
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
query = "%20".join(tokens)
try:
result = requests.get(
f"{ComicVine.BASE_URL}{query}{ComicVine.QUERY_PARAMS}",
headers=ComicVine.HEADERS,
)
result.raise_for_status()
except Exception as e:
log.warning(e)
return []
for result in result.json()["results"]:
match = self._parse_search_result(
result=result, generic_cover=generic_cover, locale=locale
)
val.append(match)
return val
def _parse_search_result(
self, result: Dict, generic_cover: str, locale: str
) -> MetaRecord:
series = result["volume"].get("name", "")
series_index = result.get("issue_number", 0)
issue_name = result.get("name", "")
match = MetaRecord(
id=result["id"],
title=f"{series}#{series_index} - {issue_name}",
authors=result.get("authors", []),
url=result.get("site_detail_url", ""),
source=MetaSourceInfo(
id=self.__id__,
description=ComicVine.DESCRIPTION,
link=ComicVine.META_URL,
),
series=series,
)
match.cover = result["image"].get("original_url", generic_cover)
match.description = result.get("description", "")
match.publishedDate = result.get("store_date", result.get("date_added"))
match.series_index = series_index
match.tags = ["Comics", series]
match.identifiers = {"comicvine": match.id}
return match
+260
View File
@@ -0,0 +1,260 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2022 xlivevil
#
# 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 re
from concurrent import futures
from typing import List, Optional
import requests
from html2text import HTML2Text
from lxml import etree
from cps import logger
from cps.services.Metadata import Metadata, MetaRecord, MetaSourceInfo
log = logger.create()
def html2text(html: str) -> str:
h2t = HTML2Text()
h2t.body_width = 0
h2t.single_line_break = True
h2t.emphasis_mark = "*"
return h2t.handle(html)
class Douban(Metadata):
__name__ = "豆瓣"
__id__ = "douban"
DESCRIPTION = "豆瓣"
META_URL = "https://book.douban.com/"
SEARCH_JSON_URL = "https://www.douban.com/j/search"
SEARCH_URL = "https://www.douban.com/search"
ID_PATTERN = re.compile(r"sid: (?P<id>\d+),")
AUTHORS_PATTERN = re.compile(r"作者|译者")
PUBLISHER_PATTERN = re.compile(r"出版社")
SUBTITLE_PATTERN = re.compile(r"副标题")
PUBLISHED_DATE_PATTERN = re.compile(r"出版年")
SERIES_PATTERN = re.compile(r"丛书")
IDENTIFIERS_PATTERN = re.compile(r"ISBN|统一书号")
CRITERIA_PATTERN = re.compile("criteria = '(.+)'")
TITTLE_XPATH = "//span[@property='v:itemreviewed']"
COVER_XPATH = "//a[@class='nbg']"
INFO_XPATH = "//*[@id='info']//span[@class='pl']"
TAGS_XPATH = "//a[contains(@class, 'tag')]"
DESCRIPTION_XPATH = "//div[@id='link-report']//div[@class='intro']"
RATING_XPATH = "//div[@class='rating_self clearfix']/strong"
session = requests.Session()
session.headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.56',
}
def search(self,
query: str,
generic_cover: str = "",
locale: str = "en") -> List[MetaRecord]:
val = []
if self.active:
log.debug(f"start searching {query} on douban")
if title_tokens := list(
self.get_title_tokens(query, strip_joiners=False)):
query = "+".join(title_tokens)
book_id_list = self._get_book_id_list_from_html(query)
if not book_id_list:
log.debug("No search results in Douban")
return []
with futures.ThreadPoolExecutor(
max_workers=5, thread_name_prefix='douban') as executor:
fut = [
executor.submit(self._parse_single_book, book_id,
generic_cover) for book_id in book_id_list
]
val = [
future.result() for future in futures.as_completed(fut)
if future.result()
]
return val
def _get_book_id_list_from_html(self, query: str) -> List[str]:
try:
r = self.session.get(self.SEARCH_URL,
params={
"cat": 1001,
"q": query
})
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
html = etree.HTML(r.content.decode("utf8"))
result_list = html.xpath(self.COVER_XPATH)
return [
self.ID_PATTERN.search(item.get("onclick")).group("id")
for item in result_list[:10]
if self.ID_PATTERN.search(item.get("onclick"))
]
def _get_book_id_list_from_json(self, query: str) -> List[str]:
try:
r = self.session.get(self.SEARCH_JSON_URL,
params={
"cat": 1001,
"q": query
})
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
results = r.json()
if results["total"] == 0:
return []
return [
self.ID_PATTERN.search(item).group("id")
for item in results["items"][:10] if self.ID_PATTERN.search(item)
]
def _parse_single_book(self,
id: str,
generic_cover: str = "") -> Optional[MetaRecord]:
url = f"https://book.douban.com/subject/{id}/"
log.debug(f"start parsing {url}")
try:
r = self.session.get(url)
r.raise_for_status()
except Exception as e:
log.warning(e)
return []
match = MetaRecord(
id=id,
title="",
authors=[],
url=url,
source=MetaSourceInfo(
id=self.__id__,
description=self.DESCRIPTION,
link=self.META_URL,
),
)
decode_content = r.content.decode("utf8")
html = etree.HTML(decode_content)
match.title = html.xpath(self.TITTLE_XPATH)[0].text
match.cover = html.xpath(
self.COVER_XPATH)[0].attrib["href"] or generic_cover
try:
rating_num = float(html.xpath(self.RATING_XPATH)[0].text.strip())
except Exception:
rating_num = 0
match.rating = int(-1 * rating_num // 2 * -1) if rating_num else 0
tag_elements = html.xpath(self.TAGS_XPATH)
if len(tag_elements):
match.tags = [tag_element.text for tag_element in tag_elements]
else:
match.tags = self._get_tags(decode_content)
description_element = html.xpath(self.DESCRIPTION_XPATH)
if len(description_element):
match.description = html2text(
etree.tostring(description_element[-1]).decode("utf8"))
info = html.xpath(self.INFO_XPATH)
for element in info:
text = element.text
if self.AUTHORS_PATTERN.search(text):
next_element = element.getnext()
while next_element is not None and next_element.tag != "br":
match.authors.append(next_element.text)
next_element = next_element.getnext()
elif self.PUBLISHER_PATTERN.search(text):
if publisher := element.tail.strip():
match.publisher = publisher
else:
match.publisher = element.getnext().text
elif self.SUBTITLE_PATTERN.search(text):
match.title = f'{match.title}:{element.tail.strip()}'
elif self.PUBLISHED_DATE_PATTERN.search(text):
match.publishedDate = self._clean_date(element.tail.strip())
elif self.SERIES_PATTERN.search(text):
match.series = element.getnext().text
elif i_type := self.IDENTIFIERS_PATTERN.search(text):
match.identifiers[i_type.group()] = element.tail.strip()
return match
@staticmethod
def _clean_date(date: str) -> str:
"""
Clean up the date string to be in the format YYYY-MM-DD
Examples of possible patterns:
'2014-7-16', '1988年4月', '1995-04', '2021-8', '2020-12-1', '1996年',
'1972', '2004/11/01', '1959年3月北京第1版第1印'
"""
year = date[:4]
moon = "01"
day = "01"
if len(date) > 5:
digit = []
ls = []
for i in range(5, len(date)):
if date[i].isdigit():
digit.append(date[i])
elif digit:
ls.append("".join(digit) if len(digit) ==
2 else f"0{digit[0]}")
digit = []
if digit:
ls.append("".join(digit) if len(digit) ==
2 else f"0{digit[0]}")
moon = ls[0]
if len(ls) > 1:
day = ls[1]
return f"{year}-{moon}-{day}"
def _get_tags(self, text: str) -> List[str]:
tags = []
if criteria := self.CRITERIA_PATTERN.search(text):
tags.extend(
item.replace('7:', '') for item in criteria.group().split('|')
if item.startswith('7:'))
return tags
+130
View File
@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Google Books api document: https://developers.google.com/books/docs/v1/using
from typing import Dict, List, Optional
from urllib.parse import quote
from datetime import datetime
import requests
from cps import logger, config
from cps.isoLanguages import get_lang3, get_language_name
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
log = logger.create()
class Google(Metadata):
__name__ = "Google Books"
__id__ = "google"
DESCRIPTION = "Google Books"
META_URL = "https://books.google.com/"
BOOK_URL = "https://books.google.com/books?id="
SEARCH_URL = "https://www.googleapis.com/books/v1/volumes?q="
ISBN_TYPE = "ISBN_13"
API_KEY = "&key=" + config.config_googlebooks_api_key
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
val = list()
if self.active:
title_tokens = list(self.get_title_tokens(query, strip_joiners=False))
if title_tokens:
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
query = "+".join(tokens)
try:
results = requests.get(Google.SEARCH_URL + query + Google.API_KEY)
results.raise_for_status()
except Exception as e:
log.warning(e)
return []
for result in results.json().get("items", []):
val.append(
self._parse_search_result(
result=result, generic_cover=generic_cover, locale=locale
)
)
return val
def _parse_search_result(
self, result: Dict, generic_cover: str, locale: str
) -> MetaRecord:
match = MetaRecord(
id=result["id"],
title=result["volumeInfo"]["title"],
authors=result["volumeInfo"].get("authors", []),
url=Google.BOOK_URL + result["id"],
source=MetaSourceInfo(
id=self.__id__,
description=Google.DESCRIPTION,
link=Google.META_URL,
),
)
match.cover = self._parse_cover(result=result, generic_cover=generic_cover)
match.description = result["volumeInfo"].get("description", "")
match.languages = self._parse_languages(result=result, locale=locale)
match.publisher = result["volumeInfo"].get("publisher", "")
try:
datetime.strptime(result["volumeInfo"].get("publishedDate", ""), "%Y-%m-%d")
match.publishedDate = result["volumeInfo"].get("publishedDate", "")
except ValueError:
match.publishedDate = ""
match.rating = result["volumeInfo"].get("averageRating", 0)
match.series, match.series_index = "", 1
match.tags = result["volumeInfo"].get("categories", [])
match.identifiers = {"google": match.id}
match = self._parse_isbn(result=result, match=match)
return match
@staticmethod
def _parse_isbn(result: Dict, match: MetaRecord) -> MetaRecord:
identifiers = result["volumeInfo"].get("industryIdentifiers", [])
for identifier in identifiers:
if identifier.get("type") == Google.ISBN_TYPE:
match.identifiers["isbn"] = identifier.get("identifier")
break
return match
@staticmethod
def _parse_cover(result: Dict, generic_cover: str) -> str:
if result["volumeInfo"].get("imageLinks"):
cover_url = result["volumeInfo"]["imageLinks"]["thumbnail"]
# strip curl in cover
cover_url = cover_url.replace("&edge=curl", "")
# request 800x900 cover image (higher resolution)
cover_url += "&fife=w800-h900"
return cover_url.replace("http://", "https://")
return generic_cover
@staticmethod
def _parse_languages(result: Dict, locale: str) -> List[str]:
language_iso2 = result["volumeInfo"].get("language", "")
languages = (
[get_language_name(locale, get_lang3(language_iso2))]
if language_iso2
else []
)
return languages
+359
View File
@@ -0,0 +1,359 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import json
import re
from multiprocessing.pool import ThreadPool
from typing import List, Optional, Tuple, Union
from urllib.parse import quote
import requests
from dateutil import parser
from html2text import HTML2Text
from lxml.html import HtmlElement, fromstring, tostring
from markdown2 import Markdown
from cps import logger
from cps.isoLanguages import get_language_name
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
log = logger.create()
SYMBOLS_TO_TRANSLATE = (
"öÖüÜóÓőŐúÚéÉáÁűŰíÍąĄćĆęĘłŁńŃóÓśŚźŹżŻ",
"oOuUoOoOuUeEaAuUiIaAcCeElLnNoOsSzZzZ",
)
SYMBOL_TRANSLATION_MAP = dict(
[(ord(a), ord(b)) for (a, b) in zip(*SYMBOLS_TO_TRANSLATE)]
)
def get_int_or_float(value: str) -> Union[int, float]:
number_as_float = float(value)
number_as_int = int(number_as_float)
return number_as_int if number_as_float == number_as_int else number_as_float
def strip_accents(s: Optional[str]) -> Optional[str]:
return s.translate(SYMBOL_TRANSLATION_MAP) if s is not None else s
def sanitize_comments_html(html: str) -> str:
text = html2text(html)
md = Markdown()
html = md.convert(text)
return html
def html2text(html: str) -> str:
# replace <u> tags with <span> as <u> becomes emphasis in html2text
if isinstance(html, bytes):
html = html.decode("utf-8")
html = re.sub(
r"<\s*(?P<solidus>/?)\s*[uU]\b(?P<rest>[^>]*)>",
r"<\g<solidus>span\g<rest>>",
html,
)
h2t = HTML2Text()
h2t.body_width = 0
h2t.single_line_break = True
h2t.emphasis_mark = "*"
return h2t.handle(html)
class LubimyCzytac(Metadata):
__name__ = "LubimyCzytac.pl"
__id__ = "lubimyczytac"
BASE_URL = "https://lubimyczytac.pl"
BOOK_SEARCH_RESULT_XPATH = (
"*//div[@class='listSearch']//div[@class='authorAllBooks__single']"
)
SINGLE_BOOK_RESULT_XPATH = ".//div[contains(@class,'authorAllBooks__singleText')]"
TITLE_PATH = "/div/a[contains(@class,'authorAllBooks__singleTextTitle')]"
TITLE_TEXT_PATH = f"{TITLE_PATH}//text()"
URL_PATH = f"{TITLE_PATH}/@href"
AUTHORS_PATH = "/div/a[contains(@href,'autor')]//text()"
SIBLINGS = "/following-sibling::dd"
CONTAINER = "//section[@class='container book']"
PUBLISHER = f"{CONTAINER}//dt[contains(text(),'Wydawnictwo:')]{SIBLINGS}/a/text()"
LANGUAGES = f"{CONTAINER}//dt[contains(text(),'Język:')]{SIBLINGS}/text()"
DESCRIPTION = f"{CONTAINER}//div[@class='collapse-content']"
SERIES = f"{CONTAINER}//span/a[contains(@href,'/cykl/')]/text()"
TRANSLATOR = f"{CONTAINER}//dt[contains(text(),'Tłumacz:')]{SIBLINGS}/a/text()"
DETAILS = "//div[@id='book-details']"
PUBLISH_DATE = "//dt[contains(@title,'Data pierwszego wydania"
FIRST_PUBLISH_DATE = f"{DETAILS}{PUBLISH_DATE} oryginalnego')]{SIBLINGS}[1]/text()"
FIRST_PUBLISH_DATE_PL = f"{DETAILS}{PUBLISH_DATE} polskiego')]{SIBLINGS}[1]/text()"
TAGS = "//a[contains(@href,'/ksiazki/t/')]/text()" # "//nav[@aria-label='breadcrumbs']//a[contains(@href,'/ksiazki/k/')]/span/text()"
RATING = "//meta[@property='books:rating:value']/@content"
COVER = "//meta[@property='og:image']/@content"
ISBN = "//meta[@property='books:isbn']/@content"
META_TITLE = "//meta[@property='og:description']/@content"
SUMMARY = "//script[@type='application/ld+json']//text()"
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
if self.active:
try:
result = requests.get(self._prepare_query(title=query))
result.raise_for_status()
except Exception as e:
log.warning(e)
return None
root = fromstring(result.text)
lc_parser = LubimyCzytacParser(root=root, metadata=self)
matches = lc_parser.parse_search_results()
if matches:
with ThreadPool(processes=10) as pool:
final_matches = pool.starmap(
lc_parser.parse_single_book,
[(match, generic_cover, locale) for match in matches],
)
return final_matches
return matches
def _prepare_query(self, title: str) -> str:
query = ""
characters_to_remove = r"\?()\/"
pattern = "[" + characters_to_remove + "]"
title = re.sub(pattern, "", title)
title = title.replace("_", " ")
if '"' in title or ",," in title:
title = title.split('"')[0].split(",,")[0]
if "/" in title:
title_tokens = [
token for token in title.lower().split(" ") if len(token) > 1
]
else:
title_tokens = list(self.get_title_tokens(title, strip_joiners=False))
if title_tokens:
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
query = query + "%20".join(tokens)
if not query:
return ""
return f"{LubimyCzytac.BASE_URL}/szukaj/ksiazki?phrase={query}"
class LubimyCzytacParser:
PAGES_TEMPLATE = "<p id='strony'>Książka ma {0} stron(y).</p>"
TRANSLATOR_TEMPLATE = "<p id='translator'>Tłumacz: {0}</p>"
PUBLISH_DATE_TEMPLATE = "<p id='pierwsze_wydanie'>Data pierwszego wydania: {0}</p>"
PUBLISH_DATE_PL_TEMPLATE = (
"<p id='pierwsze_wydanie'>Data pierwszego wydania w Polsce: {0}</p>"
)
def __init__(self, root: HtmlElement, metadata: Metadata) -> None:
self.root = root
self.metadata = metadata
def parse_search_results(self) -> List[MetaRecord]:
matches = []
results = self.root.xpath(LubimyCzytac.BOOK_SEARCH_RESULT_XPATH)
for result in results:
title = self._parse_xpath_node(
root=result,
xpath=f"{LubimyCzytac.SINGLE_BOOK_RESULT_XPATH}"
f"{LubimyCzytac.TITLE_TEXT_PATH}",
)
book_url = self._parse_xpath_node(
root=result,
xpath=f"{LubimyCzytac.SINGLE_BOOK_RESULT_XPATH}"
f"{LubimyCzytac.URL_PATH}",
)
authors = self._parse_xpath_node(
root=result,
xpath=f"{LubimyCzytac.SINGLE_BOOK_RESULT_XPATH}"
f"{LubimyCzytac.AUTHORS_PATH}",
take_first=False,
)
if not all([title, book_url, authors]):
continue
matches.append(
MetaRecord(
id=book_url.replace(f"/ksiazka/", "").split("/")[0],
title=title,
authors=[strip_accents(author) for author in authors],
url=LubimyCzytac.BASE_URL + book_url,
source=MetaSourceInfo(
id=self.metadata.__id__,
description=self.metadata.__name__,
link=LubimyCzytac.BASE_URL,
),
)
)
return matches
def parse_single_book(
self, match: MetaRecord, generic_cover: str, locale: str
) -> MetaRecord:
try:
response = requests.get(match.url)
response.raise_for_status()
except Exception as e:
log.warning(e)
return None
self.root = fromstring(response.text)
match.cover = self._parse_cover(generic_cover=generic_cover)
match.description = self._parse_description()
match.languages = self._parse_languages(locale=locale)
match.publisher = self._parse_publisher()
match.publishedDate = self._parse_from_summary(attribute_name="datePublished")
match.rating = self._parse_rating()
match.series, match.series_index = self._parse_series()
match.tags = self._parse_tags()
match.identifiers = {
"isbn": self._parse_isbn(),
"lubimyczytac": match.id,
}
return match
def _parse_xpath_node(
self,
xpath: str,
root: HtmlElement = None,
take_first: bool = True,
strip_element: bool = True,
) -> Optional[Union[str, List[str]]]:
root = root if root is not None else self.root
node = root.xpath(xpath)
if not node:
return None
return (
(node[0].strip() if strip_element else node[0])
if take_first
else [x.strip() for x in node]
)
def _parse_cover(self, generic_cover) -> Optional[str]:
return (
self._parse_xpath_node(xpath=LubimyCzytac.COVER, take_first=True)
or generic_cover
)
def _parse_publisher(self) -> Optional[str]:
return self._parse_xpath_node(xpath=LubimyCzytac.PUBLISHER, take_first=True)
def _parse_languages(self, locale: str) -> List[str]:
languages = list()
lang = self._parse_xpath_node(xpath=LubimyCzytac.LANGUAGES, take_first=True)
if lang:
if "polski" in lang:
languages.append("pol")
if "angielski" in lang:
languages.append("eng")
return [get_language_name(locale, language) for language in languages]
def _parse_series(self) -> Tuple[Optional[str], Optional[Union[float, int]]]:
series_index = 0
series = self._parse_xpath_node(xpath=LubimyCzytac.SERIES, take_first=True)
if series:
if "tom " in series:
series_name, series_info = series.split(" (tom ", 1)
series_info = series_info.replace(" ", "").replace(")", "")
# Check if book is not a bundle, i.e. chapter 1-3
if "-" in series_info:
series_info = series_info.split("-", 1)[0]
if series_info.replace(".", "").isdigit() is True:
series_index = get_int_or_float(series_info)
return series_name, series_index
return None, None
def _parse_tags(self) -> List[str]:
tags = self._parse_xpath_node(xpath=LubimyCzytac.TAGS, take_first=False)
if tags:
return [
strip_accents(w.replace(", itd.", " itd."))
for w in tags
if isinstance(w, str)
]
return None
def _parse_from_summary(self, attribute_name: str) -> Optional[str]:
value = None
summary_text = self._parse_xpath_node(xpath=LubimyCzytac.SUMMARY)
if summary_text:
data = json.loads(summary_text)
value = data.get(attribute_name)
return value.strip() if value is not None else value
def _parse_rating(self) -> Optional[str]:
rating = self._parse_xpath_node(xpath=LubimyCzytac.RATING)
return round(float(rating.replace(",", ".")) / 2) if rating else rating
def _parse_date(self, xpath="first_publish") -> Optional[datetime.datetime]:
options = {
"first_publish": LubimyCzytac.FIRST_PUBLISH_DATE,
"first_publish_pl": LubimyCzytac.FIRST_PUBLISH_DATE_PL,
}
date = self._parse_xpath_node(xpath=options.get(xpath))
return parser.parse(date) if date else None
def _parse_isbn(self) -> Optional[str]:
return self._parse_xpath_node(xpath=LubimyCzytac.ISBN)
def _parse_description(self) -> str:
description = ""
description_node = self._parse_xpath_node(
xpath=LubimyCzytac.DESCRIPTION, strip_element=False
)
if description_node is not None:
for source in self.root.xpath('//p[@class="source"]'):
source.getparent().remove(source)
description = tostring(description_node, method="html")
description = sanitize_comments_html(description)
else:
description_node = self._parse_xpath_node(xpath=LubimyCzytac.META_TITLE)
if description_node is not None:
description = description_node
description = sanitize_comments_html(description)
description = self._add_extra_info_to_description(description=description)
return description
def _add_extra_info_to_description(self, description: str) -> str:
pages = self._parse_from_summary(attribute_name="numberOfPages")
if pages:
description += LubimyCzytacParser.PAGES_TEMPLATE.format(pages)
first_publish_date = self._parse_date()
if first_publish_date:
description += LubimyCzytacParser.PUBLISH_DATE_TEMPLATE.format(
first_publish_date.strftime("%d.%m.%Y")
)
first_publish_date_pl = self._parse_date(xpath="first_publish_pl")
if first_publish_date_pl:
description += LubimyCzytacParser.PUBLISH_DATE_PL_TEMPLATE.format(
first_publish_date_pl.strftime("%d.%m.%Y")
)
translator = self._parse_xpath_node(xpath=LubimyCzytac.TRANSLATOR)
if translator:
description += LubimyCzytacParser.TRANSLATOR_TEMPLATE.format(translator)
return description
+83
View File
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import itertools
from typing import Dict, List, Optional
from urllib.parse import quote, unquote
try:
from fake_useragent.errors import FakeUserAgentError
except (ImportError):
FakeUserAgentError = BaseException
try:
from scholarly import scholarly
except FakeUserAgentError:
raise ImportError("No module named 'scholarly'")
from cps import logger
from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata
log = logger.create()
class scholar(Metadata):
__name__ = "Google Scholar"
__id__ = "googlescholar"
META_URL = "https://scholar.google.com/"
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
val = list()
if self.active:
title_tokens = list(self.get_title_tokens(query, strip_joiners=False))
if title_tokens:
tokens = [quote(t.encode("utf-8")) for t in title_tokens]
query = " ".join(tokens)
try:
scholarly.set_timeout(20)
scholarly.set_retries(2)
scholar_gen = itertools.islice(scholarly.search_pubs(query), 10)
except Exception as e:
log.warning(e)
return list()
for result in scholar_gen:
match = self._parse_search_result(
result=result, generic_cover="", locale=locale
)
val.append(match)
return val
def _parse_search_result(
self, result: Dict, generic_cover: str, locale: str
) -> MetaRecord:
match = MetaRecord(
id=result.get("pub_url", result.get("eprint_url", "")),
title=result["bib"].get("title"),
authors=result["bib"].get("author", []),
url=result.get("pub_url", result.get("eprint_url", "")),
source=MetaSourceInfo(
id=self.__id__, description=self.__name__, link=scholar.META_URL
),
)
match.cover = result.get("image", {}).get("original_url", generic_cover)
match.description = unquote(result["bib"].get("abstract", ""))
match.publisher = result["bib"].get("venue", "")
match.publishedDate = result["bib"].get("pub_year") + "-01-01"
match.identifiers = {"scholar": match.id}
return match
+121 -132
View File
@@ -16,152 +16,141 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
from __future__ import division, print_function, unicode_literals
from flask import session
try:
from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user
from 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 = False # prevent storing values with this resultcode
backend_resultcode = True # 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
pass
try:
class OAuthBackend(SQLAlchemyBackend):
"""
Stores and retrieves OAuth tokens using a relational database through
the `SQLAlchemy`_ ORM.
.. _SQLAlchemy: https://www.sqlalchemy.org/
"""
def __init__(self, model, session, provider_id,
user=None, user_id=None, user_required=None, anon_user=None,
cache=None):
self.provider_id = provider_id
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
class OAuthBackend(SQLAlchemyBackend):
"""
Stores and retrieves OAuth tokens using a relational database through
the `SQLAlchemy`_ ORM.
def get(self, blueprint, user=None, user_id=None):
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
return session[self.provider_id + '_oauth_token']
# check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key)
if token:
return token
# if not cached, make database queries
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
use_provider_user_id = False
if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '':
query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id'])
use_provider_user_id = True
if self.user_required and not u and not uid and not use_provider_user_id:
# raise ValueError("Cannot get OAuth token without an associated user")
return None
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
try:
token = query.one().token
except NoResultFound:
token = None
# cache the result
self.cache.set(cache_key, token)
_SQLAlchemy: https://www.sqlalchemy.org/
"""
def __init__(self, model, session, provider_id,
user=None, user_id=None, user_required=None, anon_user=None,
cache=None):
self.provider_id = provider_id
super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache)
def get(self, blueprint, user=None, user_id=None):
if self.provider_id + '_oauth_token' in session and session[self.provider_id + '_oauth_token'] != '':
return session[self.provider_id + '_oauth_token']
# check cache
cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id)
token = self.cache.get(cache_key)
if token:
return token
def set(self, blueprint, token, user=None, user_id=None):
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
# if not cached, make database queries
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user")
use_provider_user_id = False
if self.provider_id + '_oauth_user_id' in session and session[self.provider_id + '_oauth_user_id'] != '':
query = query.filter_by(provider_user_id=session[self.provider_id + '_oauth_user_id'])
use_provider_user_id = True
# if there was an existing model, delete it
existing_query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
# check for user ID
has_user_id = hasattr(self.model, "user_id")
if has_user_id and uid:
existing_query = existing_query.filter_by(user_id=uid)
# check for user (relationship property)
has_user = hasattr(self.model, "user")
if has_user and u:
existing_query = existing_query.filter_by(user=u)
# queue up delete query -- won't be run until commit()
existing_query.delete()
# create a new model for this token
kwargs = {
"provider": self.provider_id,
"token": token,
}
if has_user_id and uid:
kwargs["user_id"] = uid
if has_user and u:
kwargs["user"] = u
self.session.add(self.model(**kwargs))
# commit to delete and add simultaneously
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id
))
if self.user_required and not u and not uid and not use_provider_user_id:
# raise ValueError("Cannot get OAuth token without an associated user")
return None
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
try:
token = query.one().token
except NoResultFound:
token = None
def delete(self, blueprint, user=None, user_id=None):
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
# cache the result
self.cache.set(cache_key, token)
if self.user_required and not u and not uid:
raise ValueError("Cannot delete OAuth token without an associated user")
return token
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
query.delete()
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id,
))
def set(self, blueprint, token, user=None, user_id=None):
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
except Exception:
pass
if self.user_required and not u and not uid:
raise ValueError("Cannot set OAuth token without an associated user")
# if there was an existing model, delete it
existing_query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
# check for user ID
has_user_id = hasattr(self.model, "user_id")
if has_user_id and uid:
existing_query = existing_query.filter_by(user_id=uid)
# check for user (relationship property)
has_user = hasattr(self.model, "user")
if has_user and u:
existing_query = existing_query.filter_by(user=u)
# queue up delete query -- won't be run until commit()
existing_query.delete()
# create a new model for this token
kwargs = {
"provider": self.provider_id,
"token": token,
}
if has_user_id and uid:
kwargs["user_id"] = uid
if has_user and u:
kwargs["user"] = u
self.session.add(self.model(**kwargs))
# commit to delete and add simultaneously
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id
))
def delete(self, blueprint, user=None, user_id=None):
query = (
self.session.query(self.model)
.filter_by(provider=self.provider_id)
)
uid = first([user_id, self.user_id, blueprint.config.get("user_id")])
u = first(_get_real_user(ref, self.anon_user)
for ref in (user, self.user, blueprint.config.get("user")))
if self.user_required and not u and not uid:
raise ValueError("Cannot delete OAuth token without an associated user")
# check for user ID
if hasattr(self.model, "user_id") and uid:
query = query.filter_by(user_id=uid)
# check for user (relationship property)
elif hasattr(self.model, "user") and u:
query = query.filter_by(user=u)
# if we have the property, but not value, filter by None
elif hasattr(self.model, "user_id"):
query = query.filter_by(user_id=None)
# run query
query.delete()
self.session.commit()
# invalidate cache
self.cache.delete(self.make_cache_key(
blueprint=blueprint, user=user, user_id=user_id,
))
+190 -158
View File
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
from __future__ import division, print_function, unicode_literals
import json
from functools import wraps
@@ -30,15 +29,21 @@ from flask_babel import gettext as _
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.contrib.google import make_google_blueprint, google
from flask_login import login_user, current_user, login_required
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
from .cw_login import login_user, current_user
from sqlalchemy.orm.exc import NoResultFound
from .usermanagement import user_login_required
from . import constants, logger, config, app, ub
from .oauth import OAuthBackend, backend_resultcode
try:
from .oauth import OAuthBackend, backend_resultcode
except NameError:
pass
oauth_check = {}
oauthblueprints = []
oauth = Blueprint('oauth', __name__)
log = logger.create()
@@ -70,7 +75,7 @@ def register_user_with_oauth(user=None):
if len(all_oauth.keys()) == 0:
return
if user is None:
flash(_(u"Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
flash(_("Register with %(provider)s", provider=", ".join(list(all_oauth.values()))), category="success")
else:
for oauth_key in all_oauth.keys():
# Find this OAuth token in the database, or create it
@@ -84,7 +89,7 @@ def register_user_with_oauth(user=None):
except NoResultFound:
# no found, return error
return
ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key))
ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key))
def logout_oauth_user():
@@ -93,8 +98,124 @@ def logout_oauth_user():
session.pop(str(oauth_key) + '_oauth_user_id')
if ub.oauth_support:
oauthblueprints = []
def oauth_update_token(provider_id, token, provider_user_id):
session[provider_id + "_oauth_user_id"] = provider_user_id
session[provider_id + "_oauth_token"] = token
# Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.one()
# update token
oauth_entry.token = token
except NoResultFound:
oauth_entry = ub.OAuth(
provider=provider_id,
provider_user_id=provider_user_id,
token=token,
)
ub.session.add(oauth_entry)
ub.session_commit()
# Disable Flask-Dance's default behavior for saving the OAuth token
# Value differrs depending on flask-dance version
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.first()
# already bind with user, just login
if oauth_entry.user:
# If a user is already logged in and it's a different account, reject the link
# to prevent account takeover via shared OAuth identities
if current_user and current_user.is_authenticated and oauth_entry.user_id != current_user.id:
flash(_("This %(oauth)s account is already linked to a different user",
oauth=provider_name), category="error")
log.warning("User %s tried to link OAuth account already bound to user %s",
current_user.id, oauth_entry.user_id)
return redirect(url_for('web.profile'))
login_user(oauth_entry.user)
log.debug("You are now logged in as: '%s'", oauth_entry.user.name)
flash(_("Success! You are now logged in as: %(nickname)s", nickname=oauth_entry.user.name),
category="success")
return redirect(url_for('web.index'))
else:
# bind to current user
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.add(oauth_entry)
ub.session.commit()
flash(_("Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
log.info("Link to {} Succeeded".format(provider_name))
return redirect(url_for('web.profile'))
except Exception as ex:
log.error_or_exception(ex)
ub.session.rollback()
else:
flash(_("Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg:
# return redirect(url_for('web.register'))
# else:
# flash(_("Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url))
except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url))
def get_oauth_status():
status = []
query = ub.session.query(ub.OAuth).filter_by(
user_id=current_user.id,
)
try:
oauths = query.all()
for oauth_entry in oauths:
status.append(int(oauth_entry.provider))
return status
except NoResultFound:
return None
def unlink_oauth(provider):
if request.host_url + 'me' != request.referrer:
pass
query = ub.session.query(ub.OAuth).filter_by(
provider=provider,
user_id=current_user.id,
)
try:
oauth_entry = query.one()
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.delete(oauth_entry)
ub.session.commit()
logout_oauth_user()
flash(_("Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
except Exception as ex:
log.error_or_exception(ex)
ub.session.rollback()
flash(_("Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_("Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
def generate_oauth_blueprints():
if not ub.session.query(ub.OAuthProvider).count():
for provider in ("github", "google"):
oauthProvider = ub.OAuthProvider()
@@ -138,17 +259,23 @@ if ub.oauth_support:
app.register_blueprint(blueprint, url_prefix="/login")
if element['active']:
register_oauth_blueprint(element['id'], element['provider_name'])
return oauthblueprints
if ub.oauth_support:
oauthblueprints = generate_oauth_blueprints()
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
def github_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with GitHub."), category="error")
flash(_("Failed to log in with GitHub."), category="error")
log.error("Failed to log in with GitHub")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
flash(_(u"Failed to fetch user info from GitHub."), category="error")
flash(_("Failed to fetch user info from GitHub."), category="error")
log.error("Failed to fetch user info from GitHub")
return False
github_info = resp.json()
@@ -159,12 +286,14 @@ if ub.oauth_support:
@oauth_authorized.connect_via(oauthblueprints[1]['blueprint'])
def google_logged_in(blueprint, token):
if not token:
flash(_(u"Failed to log in with Google."), category="error")
flash(_("Failed to log in with Google."), category="error")
log.error("Failed to log in with Google")
return False
resp = blueprint.session.get("/oauth2/v2/userinfo")
if not resp.ok:
flash(_(u"Failed to fetch user info from Google."), category="error")
flash(_("Failed to fetch user info from Google."), category="error")
log.error("Failed to fetch user info from Google")
return False
google_info = resp.json()
@@ -172,119 +301,13 @@ if ub.oauth_support:
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
def oauth_update_token(provider_id, token, provider_user_id):
session[provider_id + "_oauth_user_id"] = provider_user_id
session[provider_id + "_oauth_token"] = token
# Find this OAuth token in the database, or create it
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.one()
# update token
oauth_entry.token = token
except NoResultFound:
oauth_entry = ub.OAuth(
provider=provider_id,
provider_user_id=provider_user_id,
token=token,
)
ub.session.add(oauth_entry)
ub.session_commit()
# Disable Flask-Dance's default behavior for saving the OAuth token
# Value differrs depending on flask-dance version
return backend_resultcode
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
query = ub.session.query(ub.OAuth).filter_by(
provider=provider_id,
provider_user_id=provider_user_id,
)
try:
oauth_entry = query.first()
# already bind with user, just login
if oauth_entry.user:
login_user(oauth_entry.user)
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname),
category="success")
return redirect(url_for('web.index'))
else:
# bind to current user
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.add(oauth_entry)
ub.session.commit()
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
return redirect(url_for('web.profile'))
except Exception as e:
log.debug_or_exception(e)
ub.session.rollback()
else:
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
log.info('Login failed, No User Linked With OAuth Account')
return redirect(url_for('web.login'))
# return redirect(url_for('web.login'))
# if config.config_public_reg:
# return redirect(url_for('web.register'))
# else:
# flash(_(u"Public registration is not enabled"), category="error")
# return redirect(url_for(redirect_url))
except (NoResultFound, AttributeError):
return redirect(url_for(redirect_url))
def get_oauth_status():
status = []
query = ub.session.query(ub.OAuth).filter_by(
user_id=current_user.id,
)
try:
oauths = query.all()
for oauth_entry in oauths:
status.append(int(oauth_entry.provider))
return status
except NoResultFound:
return None
def unlink_oauth(provider):
if request.host_url + 'me' != request.referrer:
pass
query = ub.session.query(ub.OAuth).filter_by(
provider=provider,
user_id=current_user.id,
)
try:
oauth_entry = query.one()
if current_user and current_user.is_authenticated:
oauth_entry.user = current_user
try:
ub.session.delete(oauth_entry)
ub.session.commit()
logout_oauth_user()
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
except Exception as e:
log.debug_or_exception(e)
ub.session.rollback()
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
except NoResultFound:
log.warning("oauth %s for user %d not found", provider, current_user.id)
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
return redirect(url_for('web.profile'))
# notify on OAuth provider error
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
def github_error(blueprint, error, error_description=None, error_uri=None):
msg = (
u"OAuth error from {name}! "
u"error={error} description={description} uri={uri}"
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
@@ -293,44 +316,11 @@ if ub.oauth_support:
) # ToDo: Translate
flash(msg, category="error")
@oauth.route('/link/github')
@oauth_required
def github_login():
if not github.authorized:
return redirect(url_for('github.login'))
account_info = github.get('/user')
if account_info.ok:
account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_(u"GitHub Oauth error, please retry later."), category="error")
return redirect(url_for('web.login'))
@oauth.route('/unlink/github', methods=["GET"])
@login_required
def github_login_unlink():
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/link/google')
@oauth_required
def google_login():
if not google.authorized:
return redirect(url_for("google.login"))
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_(u"Google Oauth error, please retry later."), category="error")
return redirect(url_for('web.login'))
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
def google_error(blueprint, error, error_description=None, error_uri=None):
msg = (
u"OAuth error from {name}! "
u"error={error} description={description} uri={uri}"
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
@@ -340,7 +330,49 @@ if ub.oauth_support:
flash(msg, category="error")
@oauth.route('/unlink/google', methods=["GET"])
@login_required
def google_login_unlink():
return unlink_oauth(oauthblueprints[1]['id'])
@oauth.route('/link/github')
@oauth_required
def github_login():
if not github.authorized:
return redirect(url_for('github.login'))
try:
account_info = github.get('/user')
if account_info.ok:
account_info_json = account_info.json()
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
flash(_("GitHub Oauth error, please retry later."), category="error")
log.error("GitHub Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_("GitHub Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
@oauth.route('/unlink/github', methods=["GET"])
@user_login_required
def github_login_unlink():
return unlink_oauth(oauthblueprints[0]['id'])
@oauth.route('/link/google')
@oauth_required
def google_login():
if not google.authorized:
return redirect(url_for("google.login"))
try:
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
account_info_json = resp.json()
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
flash(_("Google Oauth error, please retry later."), category="error")
log.error("Google Oauth error, please retry later")
except (InvalidGrantError, TokenExpiredError) as e:
flash(_("Google Oauth error: {}").format(e), category="error")
log.error(e)
return redirect(url_for('web.login'))
@oauth.route('/unlink/google', methods=["GET"])
@user_login_required
def google_login_unlink():
return unlink_oauth(oauthblueprints[1]['id'])
+278 -180
View File
@@ -20,57 +20,29 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import datetime
from functools import wraps
from urllib.parse import unquote_plus
from flask import Blueprint, request, render_template, Response, g, make_response, abort
from flask_login import current_user
from sqlalchemy.sql.expression import func, text, or_, and_
from werkzeug.security import check_password_hash
from flask import Blueprint, request, render_template, make_response, abort, g, jsonify
from flask_babel import get_locale
from flask_babel import gettext as _
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
from sqlalchemy.sql.expression import func, text, or_, and_, true
from sqlalchemy.exc import InvalidRequestError, OperationalError
from . import logger, config, db, calibre_db, ub, isoLanguages, constants
from .usermanagement import requires_basic_auth_if_no_ano, auth
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
from .web import render_read_books
from .usermanagement import load_user_from_request
from flask_babel import gettext as _
from babel import Locale as LC
from babel.core import UnknownLocaleError
opds = Blueprint('opds', __name__)
log = logger.create()
def requires_basic_auth_if_no_ano(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if config.config_anonbrowse != 1:
if not auth or auth.type != 'basic' or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
if config.config_login_type == constants.LOGIN_LDAP and services.ldap and config.config_anonbrowse != 1:
return services.ldap.basic_auth_required(f)
return decorated
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
@@ -84,97 +56,142 @@ def feed_osd():
return render_xml_template('osd.xml', lang='en-EN')
@opds.route("/opds/search", defaults={'query': ""})
@opds.route("/opds/search/<query>")
# @opds.route("/opds/search", defaults={'query': ""})
@opds.route("/opds/search/<path:query>")
@requires_basic_auth_if_no_ano
def feed_cc_search(query):
return feed_search(query.strip())
# Handle strange query from Libera Reader with + instead of spaces
plus_query = unquote_plus(request.environ['RAW_URI'].split('/opds/search/')[1]).strip()
return feed_search(plus_query)
@opds.route("/opds/search", methods=["GET"])
@requires_basic_auth_if_no_ano
def feed_normal_search():
return feed_search(request.args.get("query").strip())
return feed_search(request.args.get("query", "").strip())
@opds.route("/opds/books")
@requires_basic_auth_if_no_ano
def feed_booksindex():
return render_element_index(db.Books.sort, None, 'opds.feed_letter_books')
@opds.route("/opds/books/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_books(book_id):
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id)
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
letter,
[db.Books.sort],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/new")
@requires_basic_auth_if_no_ano
def feed_new():
if not auth.current_user().check_visibility(constants.SIDEBAR_RECENT):
abort(404)
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, True, [db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
db.Books, True, [db.Books.timestamp.desc()],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/discover")
@requires_basic_auth_if_no_ano
def feed_discover():
entries = calibre_db.session.query(db.Books).filter(calibre_db.common_filters()).order_by(func.random())\
.limit(config.config_books_per_page)
if not auth.current_user().check_visibility(constants.SIDEBAR_RANDOM):
abort(404)
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/rated")
@requires_basic_auth_if_no_ano
def feed_best_rated():
if not auth.current_user().check_visibility(constants.SIDEBAR_BEST_RATED):
abort(404)
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
[db.Books.timestamp.desc()])
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
[db.Books.timestamp.desc()],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/hot")
@requires_basic_auth_if_no_ano
def feed_hot():
if not auth.current_user().check_visibility(constants.SIDEBAR_HOT):
abort(404)
off = request.args.get("offset") or 0
all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by(
func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
downloadBook = calibre_db.get_book(book.Downloads.book_id)
if downloadBook:
entries.append(
calibre_db.get_filtered_book(book.Downloads.book_id)
)
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
download_book = query.filter(calibre_db.common_filters()).filter(
book.Downloads.book_id == db.Books.id).first()
if download_book:
entries.append(download_book)
else:
ub.delete_download(book.Downloads.book_id)
numBooks = entries.__len__()
num_books = entries.__len__()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1),
config.config_books_per_page, numBooks)
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
config.config_books_per_page, num_books)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/author")
@requires_basic_auth_if_no_ano
def feed_authorindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_AUTHOR):
abort(404)
return render_element_index(db.Authors.sort, db.books_authors_link, 'opds.feed_letter_author')
@opds.route("/opds/author/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_author(book_id):
if not auth.current_user().check_visibility(constants.SIDEBAR_AUTHOR):
abort(404)
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
.filter(calibre_db.common_filters())\
.filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_authors_link.author'))\
.order_by(db.Authors.sort).limit(config.config_books_per_page)\
.offset(off)
.order_by(db.Authors.sort)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Authors).all()))
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
entries.count())
entries = entries.limit(config.config_books_per_page).offset(off).all()
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination, cc=cc)
@opds.route("/opds/author/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_author(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
return render_xml_dataset(db.Authors, book_id)
@opds.route("/opds/publisher")
@requires_basic_auth_if_no_ano
def feed_publisherindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_PUBLISHER):
abort(404)
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Publishers)\
.join(db.books_publishers_link)\
@@ -184,62 +201,76 @@ def feed_publisherindex():
.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(calibre_db.session.query(db.Publishers).all()))
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination, cc=cc)
@opds.route("/opds/publisher/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_publisher(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
return render_xml_dataset(db.Publishers, book_id)
@opds.route("/opds/category")
@requires_basic_auth_if_no_ano
def feed_categoryindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_CATEGORY):
abort(404)
return render_element_index(db.Tags.name, db.books_tags_link, 'opds.feed_letter_category')
@opds.route("/opds/category/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_category(book_id):
if not auth.current_user().check_visibility(constants.SIDEBAR_CATEGORY):
abort(404)
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
entries = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
.filter(calibre_db.common_filters())\
.filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name)\
.offset(off)\
.limit(config.config_books_per_page)
.order_by(db.Tags.name)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Tags).all()))
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
entries.count())
entries = entries.offset(off).limit(config.config_books_per_page).all()
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination, cc=cc)
@opds.route("/opds/category/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_category(book_id):
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
return render_xml_dataset(db.Tags, book_id)
@opds.route("/opds/series")
@requires_basic_auth_if_no_ano
def feed_seriesindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_SERIES):
abort(404)
return render_element_index(db.Series.sort, db.books_series_link, 'opds.feed_letter_series')
@opds.route("/opds/series/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_series(book_id):
if not auth.current_user().check_visibility(constants.SIDEBAR_SERIES):
abort(404)
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
entries = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\
.join(db.Books)\
.filter(calibre_db.common_filters())\
.filter(calibre_db.common_filters()).filter(letter)\
.group_by(text('books_series_link.series'))\
.order_by(db.Series.sort)\
.offset(off).all()
.order_by(db.Series.sort)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(calibre_db.session.query(db.Series).all()))
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
entries.count())
entries = entries.offset(off).limit(config.config_books_per_page).all()
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination, cc=cc)
@opds.route("/opds/series/<int:book_id>")
@@ -249,16 +280,20 @@ def feed_series(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
[db.Books.series_index],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/ratings")
@requires_basic_auth_if_no_ano
def feed_ratingindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_RATING):
abort(404)
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \
(db.Ratings.rating / 2).label('name')) \
.join(db.books_ratings_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
@@ -269,24 +304,22 @@ def feed_ratingindex():
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)
element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination, cc=cc)
@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 = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
return render_xml_dataset(db.Ratings, book_id)
@opds.route("/opds/formats")
@requires_basic_auth_if_no_ano
def feed_formatindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_FORMAT):
abort(404)
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Data).join(db.Books)\
.filter(calibre_db.common_filters()) \
@@ -294,11 +327,11 @@ def feed_formatindex():
.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:
element.append(FeedObject(entry.format, entry.format))
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination, cc=cc)
@opds.route("/opds/formats/<book_id>")
@@ -308,31 +341,29 @@ def feed_format(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
[db.Books.timestamp.desc()],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/language")
@opds.route("/opds/language/")
@requires_basic_auth_if_no_ano
def feed_languagesindex():
if not auth.current_user().check_visibility(constants.SIDEBAR_LANGUAGE):
abort(404)
off = request.args.get("offset") or 0
if current_user.filter_language() == u"all":
if auth.current_user().filter_language() == "all":
languages = calibre_db.speaking_language()
else:
try:
cur_l = LC.parse(current_user.filter_language())
except UnknownLocaleError:
cur_l = None
languages = calibre_db.session.query(db.Languages).filter(
db.Languages.lang_code == current_user.filter_language()).all()
if cur_l:
languages[0].name = cur_l.get_language_name(get_locale())
else:
languages[0].name = _(isoLanguages.get(part3=languages[0].lang_code).name)
db.Languages.lang_code == auth.current_user().filter_language()).all()
languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(languages))
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination, cc=cc)
@opds.route("/opds/language/<int:book_id>")
@@ -342,59 +373,74 @@ def feed_languages(book_id):
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
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)
[db.Books.timestamp.desc()],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
@opds.route("/opds/shelfindex")
@requires_basic_auth_if_no_ano
def feed_shelfindex():
if not (auth.current_user().is_authenticated or g.allow_anonymous):
abort(404)
off = request.args.get("offset") or 0
shelf = g.shelves_access
shelf = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == auth.current_user().id)).order_by(ub.Shelf.name).all()
number = len(shelf)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
number)
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination, cc=cc)
@opds.route("/opds/shelf/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_shelf(book_id):
if not (auth.current_user().is_authenticated or g.allow_anonymous):
abort(404)
off = request.args.get("offset") or 0
if current_user.is_anonymous:
if auth.current_user().is_anonymous:
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),
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(auth.current_user().id),
ub.Shelf.id == book_id),
and_(ub.Shelf.is_public == 1,
ub.Shelf.id == book_id))).first()
result = list()
pagination = list()
# user is allowed to access shelf
if shelf:
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == book_id).order_by(
ub.BookShelf.order.asc()).all()
for book in books_in_shelf:
cur_book = calibre_db.get_book(book.book_id)
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)
result, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1),
config.config_books_per_page,
db.Books,
ub.BookShelf.shelf == shelf.id,
[ub.BookShelf.order.asc()],
True, config.config_read_column,
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
# delete shelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf) \
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
.filter(db.Books.id == None).all()
for entry in wrong_entries:
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
try:
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete()
ub.session.commit()
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc)
@opds.route("/opds/download/<book_id>/<book_format>/")
@requires_basic_auth_if_no_ano
def opds_download_link(book_id, book_format):
# I gave up with this: With enabled ldap login, the user doesn't get logged in, therefore it's always guest
# workaround, loading the user from the request and checking it's download rights here
# in case of anonymous browsing user is None
user = load_user_from_request(request) or current_user
if not user.role_download():
return abort(403)
if "Kobo" in request.headers.get('User-Agent'):
client = "kobo"
else:
client = ""
if not auth.current_user().role_download():
return abort(401)
client = "kobo" if "Kobo" in request.headers.get('User-Agent') else ""
return get_download_link(book_id, book_format.lower(), client)
@@ -412,41 +458,15 @@ def get_metadata_calibre_companion(uuid, library):
return ""
def feed_search(term):
if term:
entries, __, ___ = calibre_db.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:
try:
username = username.encode('windows-1252')
except UnicodeEncodeError:
username = username.encode('utf-8')
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
username.decode('utf-8').lower()).first()
return bool(user and check_password_hash(str(user.password), password))
def authenticate():
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def render_xml_template(*args, **kwargs):
# ToDo: return time in current timezone similar to %z
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/stats")
@requires_basic_auth_if_no_ano
def get_database_stats():
stat = dict()
stat['books'] = calibre_db.session.query(db.Books).count()
stat['authors'] = calibre_db.session.query(db.Authors).count()
stat['categories'] = calibre_db.session.query(db.Tags).count()
stat['series'] = calibre_db.session.query(db.Series).count()
return make_response(jsonify(stat))
@opds.route("/opds/thumb_240_240/<book_id>")
@@ -461,14 +481,92 @@ def feed_get_cover(book_id):
@opds.route("/opds/readbooks")
@requires_basic_auth_if_no_ano
def feed_read_books():
if not (auth.current_user().check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not auth.current_user().is_anonymous):
return abort(403)
off = request.args.get("offset") or 0
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)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc)
@opds.route("/opds/unreadbooks")
@requires_basic_auth_if_no_ano
def feed_unread_books():
if not (auth.current_user().check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not auth.current_user().is_anonymous):
return abort(403)
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc)
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
def feed_search(term):
if term:
entries, __, ___ = calibre_db.get_search_results(term, config=config)
entries_count = len(entries) if len(entries) > 0 else 1
pagination = Pagination(1, entries_count, entries_count)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination, cc=cc)
else:
return render_xml_template('feed.xml', searchterm="")
def render_xml_template(*args, **kwargs):
# 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, constants=constants.sidebar_settings, *args, **kwargs)
response = make_response(xml)
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
return response
def render_xml_dataset(data_table, book_id):
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books,
getattr(db.Books, data_table.__tablename__).any(data_table.id == book_id),
[db.Books.timestamp.desc()],
True, config.config_read_column)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc)
def render_element_index(database_column, linked_table, folder):
shift = 0
off = int(request.args.get("offset") or 0)
entries = calibre_db.session.query(func.upper(func.substr(database_column, 1, 1)).label('id'), None, None)
# query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
if linked_table is not None:
entries = entries.join(linked_table).join(db.Books)
entries = entries.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(database_column, 1, 1))).all()
elements = []
if off == 0 and entries:
elements.append({'id': "00", 'name': _("All")})
shift = 1
for entry in entries[
off + shift - 1:
int(off + int(config.config_books_per_page) - shift)]:
elements.append({'id': entry.id, 'name': entry.id})
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries) + 1)
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_xml_template('feed.xml',
letterelements=elements,
folder=folder,
pagination=pagination,
cc=cc)
+4 -5
View File
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from math import ceil
@@ -58,10 +57,10 @@ class Pagination(object):
def has_next(self):
return self.page < self.pages
# right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shwn
# left_edge: first left_edges count of all pages are shown as number -> 1,2 shwn
# left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shwn
# left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shwn
# right_edge: last right_edges count of all pages are shown as number, means, if 10 pages are paginated -> 9,10 shown
# left_edge: first left_edges count of all pages are shown as number -> 1,2 shown
# left_current: left_current count below current page are shown as number, means if current page 5 -> 3,4 shown
# left_current: right_current count above current page are shown as number, means if current page 5 -> 6,7 shown
def iter_pages(self, left_edge=2, left_current=2,
right_current=4, right_edge=2):
last = 0
+12 -17
View File
@@ -25,15 +25,11 @@
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# http://flask.pocoo.org/snippets/62/
# https://web.archive.org/web/20120517003641/http://flask.pocoo.org/snippets/62/
from __future__ import division, print_function, unicode_literals
try:
from urllib.parse import urlparse, urljoin
except ImportError:
from urlparse import urlparse, urljoin
from urllib.parse import urlparse, urljoin
from flask import request, url_for, redirect
from flask import request, url_for, current_app
def is_safe_url(target):
@@ -42,16 +38,15 @@ def is_safe_url(target):
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
def get_redirect_target():
for target in request.values.get('next'), request.referrer:
if not target:
continue
if is_safe_url(target):
return target
def remove_prefix(text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return ""
def redirect_back(endpoint, **values):
target = request.form['next']
if not target or not is_safe_url(target):
def get_redirect_location(next, endpoint, **values):
target = next or url_for(endpoint, **values)
adapter = current_app.url_map.bind(urlparse(request.host_url).netloc)
if not len(adapter.allowed_methods(remove_prefix(target, request.environ.get('HTTP_X_SCRIPT_NAME',"")))):
target = url_for(endpoint, **values)
return redirect(target)
return target
+17 -19
View File
@@ -22,19 +22,17 @@
import json
from datetime import datetime
from functools import wraps
from flask import Blueprint, request, make_response, abort, url_for, flash, redirect
from flask_login import login_required, current_user, login_user
from .cw_login import login_user, current_user
from flask_babel import gettext as _
from sqlalchemy.sql.expression import true
from . import config, logger, ub
from .render_template import render_title_template
from .usermanagement import user_login_required
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
remotelogin = Blueprint('remotelogin', __name__)
log = logger.create()
@@ -61,21 +59,21 @@ def remote_login():
ub.session.add(auth_token)
ub.session_commit()
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
log.debug("Remot Login request with token: %s", auth_token.auth_token)
return render_title_template('remote_login.html', title=_("Login"), token=auth_token.auth_token,
verify_url=verify_url, page="remotelogin")
@remotelogin.route('/verify/<token>')
@remote_login_required
@login_required
@user_login_required
def verify_token(token):
auth_token = ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.auth_token == token).first()
# Token not found
if auth_token is None:
flash(_(u"Token not found"), category="error")
log.error(u"Remote Login token not found")
flash(_("Token not found"), category="error")
log.error("Remote Login token not found")
return redirect(url_for('web.index'))
# Token expired
@@ -83,8 +81,8 @@ def verify_token(token):
ub.session.delete(auth_token)
ub.session_commit()
flash(_(u"Token has expired"), category="error")
log.error(u"Remote Login token expired")
flash(_("Token has expired"), category="error")
log.error("Remote Login token expired")
return redirect(url_for('web.index'))
# Update token with user information
@@ -92,8 +90,8 @@ def verify_token(token):
auth_token.verified = True
ub.session_commit()
flash(_(u"Success! Please return to your device"), category="success")
log.debug(u"Remote Login token for userid %s verified", auth_token.user_id)
flash(_("Success! Please return to your device"), category="success")
log.debug("Remote Login token for userid %s verified", auth_token.user_id)
return redirect(url_for('web.index'))
@@ -108,7 +106,7 @@ def token_verified():
# Token not found
if auth_token is None:
data['status'] = 'error'
data['message'] = _(u"Token not found")
data['message'] = _("Token not found")
# Token expired
elif datetime.now() > auth_token.expiration:
@@ -116,7 +114,7 @@ def token_verified():
ub.session_commit()
data['status'] = 'error'
data['message'] = _(u"Token has expired")
data['message'] = _("Token has expired")
elif not auth_token.verified:
data['status'] = 'not_verified'
@@ -126,11 +124,11 @@ def token_verified():
login_user(user)
ub.session.delete(auth_token)
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname))
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
data['status'] = 'success'
log.debug(u"Remote Login for userid %s succeded", user.id)
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
log.debug("Remote Login for userid %s succeeded", user.id)
flash(_("Success! You are now logged in as: %(nickname)s", nickname=user.name), category="success")
response = make_response(json.dumps(data, ensure_ascii=False))
response.headers["Content-Type"] = "application/json; charset=utf-8"
+55 -53
View File
@@ -16,13 +16,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import render_template
from flask import render_template, g, abort, request
from flask_babel import gettext as _
from flask import g
from werkzeug.local import LocalProxy
from flask_login import current_user
from .cw_login import current_user
from sqlalchemy.sql.expression import or_
from . import config, constants, ub, logger, db, calibre_db
from . import config, constants, logger, ub
from .ub import User
@@ -30,87 +30,89 @@ log = logger.create()
def get_sidebar_config(kwargs=None):
kwargs = kwargs or []
simple = bool([e for e in ['kindle', 'tolino', "kobo", "bookeen"]
if (e in request.headers.get('User-Agent', "").lower())])
if 'content' in kwargs:
content = kwargs['content']
content = isinstance(content, (User, LocalProxy)) and not content.role_anonymous()
else:
content = 'conf' in kwargs
sidebar = list()
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new",
sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new",
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
"show_text": _('Show recent books'), "config_show":False})
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
"show_text": _('Show Hot Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
if current_user.role_admin():
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'), "no_param":True,
"config_show": content})
else:
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not current_user.is_anonymous),
"page": "download", "show_text": _('Show Downloaded Books'),
"config_show": content})
sidebar.append(
{"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated",
"visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated",
"show_text": _('Show Top Rated Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous),
"page": "read", "show_text": _('Show read and unread'), "config_show": content})
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous),
"page": "read", "show_text": _('Show Read and Unread'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread",
"visibility": constants.SIDEBAR_READ_AND_UNREAD, 'public': (not current_user.is_anonymous), "page": "unread",
"show_text": _('Show unread'), "config_show": False})
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
"show_text": _('Show random books'), "config_show": True})
"show_text": _('Show Random Books'), "config_show": True})
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
"show_text": _('Show category selection'), "config_show": True})
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category", "no_param":True,
"show_text": _('Show Category Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-bookmark", "text": _('Series'), "link": 'web.series_list', "id": "serie",
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series",
"show_text": _('Show series selection'), "config_show": True})
"visibility": constants.SIDEBAR_SERIES, 'public': True, "page": "series", "no_param":True,
"show_text": _('Show Series Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-user", "text": _('Authors'), "link": 'web.author_list', "id": "author",
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author",
"show_text": _('Show author selection'), "config_show": True})
"visibility": constants.SIDEBAR_AUTHOR, 'public': True, "page": "author", "no_param":True,
"show_text": _('Show Author Section'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-text-size", "text": _('Publishers'), "link": 'web.publisher_list', "id": "publisher",
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher",
"show_text": _('Show publisher selection'), "config_show":True})
"visibility": constants.SIDEBAR_PUBLISHER, 'public': True, "page": "publisher", "no_param":True,
"show_text": _('Show Publisher Section'), "config_show":True})
sidebar.append({"glyph": "glyphicon-flag", "text": _('Languages'), "link": 'web.language_overview', "id": "lang",
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (g.user.filter_language() == 'all'),
"page": "language",
"show_text": _('Show language selection'), "config_show": True})
"visibility": constants.SIDEBAR_LANGUAGE, 'public': (current_user.filter_language() == 'all'),
"page": "language", "no_param":True,
"show_text": _('Show Language Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-star-empty", "text": _('Ratings'), "link": 'web.ratings_list', "id": "rate",
"visibility": constants.SIDEBAR_RATING, 'public': True,
"page": "rating", "show_text": _('Show ratings selection'), "config_show": True})
"visibility": constants.SIDEBAR_RATING, 'public': True, "no_param":True,
"page": "rating", "show_text": _('Show Ratings Section'), "config_show": True})
sidebar.append({"glyph": "glyphicon-file", "text": _('File formats'), "link": 'web.formats_list', "id": "format",
"visibility": constants.SIDEBAR_FORMAT, 'public': True,
"page": "format", "show_text": _('Show file formats selection'), "config_show": True})
"visibility": constants.SIDEBAR_FORMAT, 'public': True, "no_param":True,
"page": "format", "show_text": _('Show File Formats Section'), "config_show": True})
sidebar.append(
{"glyph": "glyphicon-trash", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not g.user.is_anonymous), "page": "archived",
"show_text": _('Show archived books'), "config_show": content})
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not g.user.is_anonymous), "page": "list",
"show_text": _('Show Books List'), "config_show": content})
{"glyph": "glyphicon-folder-open", "text": _('Archived Books'), "link": 'web.books_list', "id": "archived",
"visibility": constants.SIDEBAR_ARCHIVED, 'public': (not current_user.is_anonymous), "page": "archived",
"show_text": _('Show Archived Books'), "config_show": content})
if not simple:
sidebar.append(
{"glyph": "glyphicon-th-list", "text": _('Books List'), "link": 'web.books_table', "id": "list",
"visibility": constants.SIDEBAR_LIST, 'public': (not current_user.is_anonymous),
"show_text": _('Show Books List'), "config_show": content, "no_param":True})
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
return sidebar
return sidebar, simple
def get_readbooks_ids():
if not config.config_read_column:
readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\
.filter(ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED).all()
return frozenset([x.book_id for x in readBooks])
else:
try:
readBooks = calibre_db.session.query(db.cc_classes[config.config_read_column])\
.filter(db.cc_classes[config.config_read_column].value == True).all()
return frozenset([x.book for x in readBooks])
except (KeyError, AttributeError):
log.error("Custom Column No.%d is not existing in calibre database", config.config_read_column)
return []
# Returns the template for rendering and includes the instance name
def render_title_template(*args, **kwargs):
sidebar = get_sidebar_config(kwargs)
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar,
accept=constants.EXTENSIONS_UPLOAD, read_book_ids=get_readbooks_ids(),
*args, **kwargs)
sidebar, simple = get_sidebar_config(kwargs)
try:
return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple,
accept=config.config_upload_formats.split(','),
*args, **kwargs)
except PermissionError:
log.error("No permission to access {} file.".format(args[0]))
abort(403)
+4 -4
View File
@@ -36,16 +36,14 @@
#
# Inspired by http://flask.pocoo.org/snippets/35/
from __future__ import division, print_function, unicode_literals
class ReverseProxied(object):
"""Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
different from what is used locally.
Code courtesy of: http://flask.pocoo.org/snippets/35/
Code courtesy of: https://flask.pocoo.org/snippets/35/
In nginx:
location /myprefix {
@@ -63,11 +61,13 @@ class ReverseProxied(object):
def __call__(self, environ, start_response):
self.proxied = False
self.script_name = "/"
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', '')
self.script_name = script_name
if path_info and path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 mmonkey
#
# 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 datetime
from . import config, constants
from .services.background_scheduler import BackgroundScheduler, CronTrigger, use_APScheduler
from .tasks.database import TaskReconnectDatabase
from .tasks.clean import TaskClean
from .tasks.thumbnail import TaskGenerateCoverThumbnails, TaskGenerateSeriesThumbnails, TaskClearCoverThumbnailCache
from .services.worker import WorkerThread
from .tasks.metadata_backup import TaskBackupMetadata
def get_scheduled_tasks(reconnect=True):
tasks = list()
# Reconnect Calibre database (metadata.db) based on config.schedule_reconnect
if reconnect:
tasks.append([lambda: TaskReconnectDatabase(), 'reconnect', False])
# Delete temp folder
tasks.append([lambda: TaskClean(), 'delete temp', True])
# Generate metadata.opf file for each changed book
if config.schedule_metadata_backup:
tasks.append([lambda: TaskBackupMetadata("en"), 'backup metadata', False])
# Generate all missing book cover thumbnails
if config.schedule_generate_book_covers:
tasks.append([lambda: TaskClearCoverThumbnailCache(0), 'delete superfluous book covers', True])
tasks.append([lambda: TaskGenerateCoverThumbnails(), 'generate book covers', False])
# Generate all missing series thumbnails
if config.schedule_generate_series_covers:
tasks.append([lambda: TaskGenerateSeriesThumbnails(), 'generate book covers', False])
return tasks
def end_scheduled_tasks():
worker = WorkerThread.get_instance()
for __, __, __, task, __ in worker.tasks:
if task.scheduled and task.is_cancellable:
worker.end_task(task.id)
def register_scheduled_tasks(reconnect=True):
scheduler = BackgroundScheduler()
if scheduler:
# Remove all existing jobs
scheduler.remove_all_jobs()
start = config.schedule_start_time
duration = config.schedule_duration
# Register scheduled tasks
timezone_info = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
scheduler.schedule_tasks(tasks=get_scheduled_tasks(reconnect), trigger=CronTrigger(hour=start,
timezone=timezone_info))
end_time = calclulate_end_time(start, duration)
scheduler.schedule(func=end_scheduled_tasks, trigger=CronTrigger(hour=end_time.hour, minute=end_time.minute,
timezone=timezone_info),
name="end scheduled task")
# Kick-off tasks, if they should currently be running
if should_task_be_running(start, duration):
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(reconnect))
def register_startup_tasks():
scheduler = BackgroundScheduler()
if scheduler:
start = config.schedule_start_time
duration = config.schedule_duration
# Run scheduled tasks immediately for development and testing
# Ignore tasks that should currently be running, as these will be added when registering scheduled tasks
if constants.APP_MODE in ['development', 'test'] and not should_task_be_running(start, duration):
scheduler.schedule_tasks_immediately(tasks=get_scheduled_tasks(False))
else:
scheduler.schedule_tasks_immediately(tasks=[[lambda: TaskClean(), 'delete temp', True]])
def should_task_be_running(start, duration):
now = datetime.datetime.now()
start_time = datetime.datetime.now().replace(hour=start, minute=0, second=0, microsecond=0)
end_time = start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
return start_time < now < end_time
def calclulate_end_time(start, duration):
start_time = datetime.datetime.now().replace(hour=start, minute=0)
return start_time + datetime.timedelta(hours=duration // 60, minutes=duration % 60)
+444
View File
@@ -0,0 +1,444 @@
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2022 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 json
from datetime import datetime
from flask import Blueprint, request, redirect, url_for, flash
from flask import session as flask_session
from .cw_login import current_user
from flask_babel import format_date
from flask_babel import gettext as _
from sqlalchemy.sql.expression import func, not_, and_, or_, text, true
from sqlalchemy.sql.functions import coalesce
from . import logger, db, calibre_db, config, ub
from .string_helper import strip_whitespaces
from .usermanagement import login_required_if_no_ano
from .render_template import render_title_template
from .pagination import Pagination
search = Blueprint('search', __name__)
log = logger.create()
@search.route("/search", methods=["GET"])
@login_required_if_no_ano
def simple_search():
term = request.args.get("query")
if term:
return redirect(url_for('web.books_list', data="search", sort_param='stored', query=term.strip()))
else:
return render_title_template('search.html',
searchterm="",
result_count=0,
title=_("Search"),
page="search")
@search.route("/advsearch", methods=['POST'])
@login_required_if_no_ano
def advanced_search():
values = dict(request.form)
params = ['include_tag', 'exclude_tag', 'include_serie', 'exclude_serie', 'include_shelf', 'exclude_shelf',
'include_language', 'exclude_language', 'include_extension', 'exclude_extension']
for param in params:
values[param] = list(request.form.getlist(param))
flask_session['query'] = json.dumps(values)
return redirect(url_for('web.books_list', data="advsearch", sort_param='stored', query=""))
@search.route("/advsearch", methods=['GET'])
@login_required_if_no_ano
def advanced_search_form():
# Build custom columns names
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
return render_prepare_search_form(cc)
def adv_search_custom_columns(cc, term, q):
for c in cc:
if c.datatype == "datetime":
custom_start = term.get('custom_column_' + str(c.id) + '_start')
custom_end = term.get('custom_column_' + str(c.id) + '_end')
if custom_start:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
func.datetime(db.cc_classes[c.id].value) >= func.datetime(custom_start)))
if custom_end:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
func.datetime(db.cc_classes[c.id].value) <= func.datetime(custom_end)))
elif c.datatype in ["int", "float"]:
custom_low = term.get('custom_column_' + str(c.id) + '_low')
custom_high = term.get('custom_column_' + str(c.id) + '_high')
if custom_low:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value >= custom_low))
if custom_high:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value <= custom_high))
else:
custom_query = term.get('custom_column_' + str(c.id))
if c.datatype == 'bool':
if custom_query != "Any":
if custom_query == "":
q = q.filter(~getattr(db.Books, 'custom_column_' + str(c.id)).
any(db.cc_classes[c.id].value >= 0))
else:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value == bool(custom_query == "True")))
elif custom_query != '' and custom_query is not None:
if c.datatype == 'rating':
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
db.cc_classes[c.id].value == int(float(custom_query) * 2)))
else:
q = q.filter(getattr(db.Books, 'custom_column_' + str(c.id)).any(
func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%")))
return q
def adv_search_language(q, include_languages_inputs, exclude_languages_inputs):
if current_user.filter_language() != "all":
q = q.filter(db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()))
else:
for language in include_languages_inputs:
q = q.filter(db.Books.languages.any(db.Languages.id == language))
for language in exclude_languages_inputs:
q = q.filter(not_(db.Books.series.any(db.Languages.id == language)))
return q
def adv_search_ratings(q, rating_high, rating_low):
if rating_high:
rating_high = int(rating_high) * 2
q = q.filter(db.Books.ratings.any(db.Ratings.rating <= rating_high))
if rating_low:
rating_low = int(rating_low) * 2
q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low))
return q
def adv_search_read_status(read_status):
if not config.config_read_column:
if read_status == "True":
db_filter = and_(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.read_status == ub.ReadBook.STATUS_FINISHED)
else:
db_filter = coalesce(ub.ReadBook.read_status, 0) != ub.ReadBook.STATUS_FINISHED
else:
try:
if read_status == "":
db_filter = coalesce(db.cc_classes[config.config_read_column].value, 2) == 2
else:
db_filter = db.cc_classes[config.config_read_column].value == bool(read_status == "True")
except (KeyError, AttributeError, IndexError):
log.error("Custom Column No.{} does not exist in calibre database".format(config.config_read_column))
flash(_("Custom Column No.%(column)d does not exist in calibre database",
column=config.config_read_column),
category="error")
return true()
return db_filter
def adv_search_extension(q, include_extension_inputs, exclude_extension_inputs):
for extension in include_extension_inputs:
q = q.filter(db.Books.data.any(db.Data.format == extension))
for extension in exclude_extension_inputs:
q = q.filter(not_(db.Books.data.any(db.Data.format == extension)))
return q
def adv_search_tag(q, include_tag_inputs, exclude_tag_inputs):
for tag in include_tag_inputs:
q = q.filter(db.Books.tags.any(db.Tags.id == tag))
for tag in exclude_tag_inputs:
q = q.filter(not_(db.Books.tags.any(db.Tags.id == tag)))
return q
def adv_search_serie(q, include_series_inputs, exclude_series_inputs):
for serie in include_series_inputs:
q = q.filter(db.Books.series.any(db.Series.id == serie))
for serie in exclude_series_inputs:
q = q.filter(not_(db.Books.series.any(db.Series.id == serie)))
return q
def adv_search_shelf(q, include_shelf_inputs, exclude_shelf_inputs):
if len(include_shelf_inputs):
in_shelf = (
calibre_db.session.query(ub.BookShelf.id)
.filter(ub.BookShelf.book_id == db.Books.id)
.filter(ub.BookShelf.shelf.in_(include_shelf_inputs))
.exists()
)
q = q.filter(in_shelf)
if len(exclude_shelf_inputs):
ex_shelf = (
calibre_db.session.query(ub.BookShelf.id)
.filter(ub.BookShelf.book_id == db.Books.id)
.filter(ub.BookShelf.shelf.in_(exclude_shelf_inputs))
.exists()
)
q = q.filter(~ex_shelf)
return q
def extend_search_term(searchterm,
author_name,
book_title,
publisher,
pub_start,
pub_end,
tags,
rating_high,
rating_low,
read_status,
):
searchterm.extend((author_name.replace('|', ','), book_title, publisher))
if pub_start:
try:
searchterm.extend([_("Published after ") +
format_date(datetime.strptime(pub_start, "%Y-%m-%d"),
format='medium')])
except ValueError:
pub_start = ""
if pub_end:
try:
searchterm.extend([_("Published before ") +
format_date(datetime.strptime(pub_end, "%Y-%m-%d"),
format='medium')])
except ValueError:
pub_end = ""
elements = {'tag': db.Tags, 'serie':db.Series, 'shelf':ub.Shelf}
for key, db_element in elements.items():
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['include_' + key])).all()
searchterm.extend(tag.name for tag in tag_names)
tag_names = calibre_db.session.query(db_element).filter(db_element.id.in_(tags['exclude_' + key])).all()
searchterm.extend(tag.name for tag in tag_names)
language_names = calibre_db.session.query(db.Languages). \
filter(db.Languages.id.in_(tags['include_language'])).all()
if language_names:
language_names = calibre_db.speaking_language(language_names)
searchterm.extend(language.name for language in language_names)
language_names = calibre_db.session.query(db.Languages). \
filter(db.Languages.id.in_(tags['exclude_language'])).all()
if language_names:
language_names = calibre_db.speaking_language(language_names)
searchterm.extend(language.name for language in language_names)
if rating_high:
searchterm.extend([_("Rating <= %(rating)s", rating=rating_high)])
if rating_low:
searchterm.extend([_("Rating >= %(rating)s", rating=rating_low)])
if read_status != "Any":
searchterm.extend([_("Read Status = '%(status)s'", status=read_status)])
searchterm.extend(ext for ext in tags['include_extension'])
searchterm.extend(ext for ext in tags['exclude_extension'])
# handle custom columns
searchterm = " + ".join(filter(None, searchterm))
return searchterm, pub_start, pub_end
def render_adv_search_results(term, offset=None, order=None, limit=None):
sort = order[0] if order else [db.Books.sort]
pagination = None
cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True)
calibre_db.create_functions()
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
q = query.outerjoin(db.books_series_link, db.Books.id == db.books_series_link.c.book)\
.outerjoin(db.Series)\
.filter(calibre_db.common_filters(True))
# parse multi selects to a complete dict
tags = dict()
elements = ['tag', 'serie', 'shelf', 'language', 'extension']
for element in elements:
tags['include_' + element] = term.get('include_' + element)
tags['exclude_' + element] = term.get('exclude_' + element)
author_name = term.get("authors")
book_title = term.get("title")
publisher = term.get("publisher")
pub_start = term.get("publishstart")
pub_end = term.get("publishend")
rating_low = term.get("ratinghigh")
rating_high = term.get("ratinglow")
description = term.get("comments")
read_status = term.get("read_status")
if author_name:
author_name = strip_whitespaces(author_name).lower().replace(',', '|')
if book_title:
book_title = strip_whitespaces(book_title).lower()
if publisher:
publisher = strip_whitespaces(publisher).lower()
search_term = []
cc_present = False
for c in cc:
if c.datatype == "datetime":
column_start = term.get('custom_column_' + str(c.id) + '_start')
column_end = term.get('custom_column_' + str(c.id) + '_end')
if column_start:
search_term.extend(["{} >= {}".format(c.name,
format_date(datetime.strptime(column_start, "%Y-%m-%d").date(),
format='medium')
)])
cc_present = True
if column_end:
search_term.extend(["{} <= {}".format(c.name,
format_date(datetime.strptime(column_end, "%Y-%m-%d").date(),
format='medium')
)])
cc_present = True
if c.datatype in ["int", "float"]:
column_low = term.get('custom_column_' + str(c.id) + '_low')
column_high = term.get('custom_column_' + str(c.id) + '_high')
if column_low:
search_term.extend(["{} >= {}".format(c.name, column_low)])
cc_present = True
if column_high:
search_term.extend(["{} <= {}".format(c.name,column_high)])
cc_present = True
elif c.datatype == "bool":
if term.get('custom_column_' + str(c.id)) != "Any":
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
cc_present = True
elif term.get('custom_column_' + str(c.id)):
search_term.extend([("{}: {}".format(c.name, term.get('custom_column_' + str(c.id))))])
cc_present = True
if any(tags.values()) or author_name or book_title or publisher or pub_start or pub_end or rating_low \
or rating_high or description or cc_present or read_status != "Any":
search_term, pub_start, pub_end = extend_search_term(search_term,
author_name,
book_title,
publisher,
pub_start,
pub_end,
tags,
rating_high,
rating_low,
read_status)
if author_name:
q = q.filter(db.Books.authors.any(func.lower(db.Authors.name).ilike("%" + author_name + "%")))
if book_title:
q = q.filter(func.lower(db.Books.title).ilike("%" + book_title + "%"))
if pub_start:
q = q.filter(func.datetime(db.Books.pubdate) > func.datetime(pub_start))
if pub_end:
q = q.filter(func.datetime(db.Books.pubdate) < func.datetime(pub_end))
if read_status != "Any":
q = q.filter(adv_search_read_status(read_status))
if publisher:
q = q.filter(db.Books.publishers.any(func.lower(db.Publishers.name).ilike("%" + publisher + "%")))
q = adv_search_tag(q, tags['include_tag'], tags['exclude_tag'])
q = adv_search_serie(q, tags['include_serie'], tags['exclude_serie'])
q = adv_search_shelf(q, tags['include_shelf'], tags['exclude_shelf'])
q = adv_search_extension(q, tags['include_extension'], tags['exclude_extension'])
q = adv_search_language(q, tags['include_language'], tags['exclude_language'])
q = adv_search_ratings(q, rating_high, rating_low)
if description:
pass
q = q.filter(db.Books.comments.any(func.lower(db.Comments.text).ilike("%" + description + "%")))
# search custom columns
try:
pass
q = adv_search_custom_columns(cc, term, q)
except AttributeError as ex:
log.debug_or_exception(ex)
flash(_("Error on search for custom columns, please restart Calibre-Web"), category="error")
q = q.order_by(*sort).all()
flask_session['query'] = json.dumps(term)
ub.store_combo_ids(q)
result_count = len(q)
if offset is not None and limit is not None:
offset = int(offset)
limit_all = offset + int(limit)
pagination = Pagination((offset / (int(limit)) + 1), limit, result_count)
else:
offset = 0
limit_all = result_count
entries = calibre_db.order_authors(q[offset:limit_all], list_return=True, combined=True)
return render_title_template('search.html',
adv_searchterm=search_term,
pagination=pagination,
entries=entries,
result_count=result_count,
title=_("Advanced Search"), page="advsearch",
order=order[1])
def render_prepare_search_form(cc):
# prepare data for search-form
tags = calibre_db.session.query(db.Tags)\
.join(db.books_tags_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(text('books_tags_link.tag'))\
.order_by(db.Tags.name).all()
series = calibre_db.session.query(db.Series)\
.join(db.books_series_link)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(text('books_series_link.series'))\
.order_by(db.Series.name)\
.filter(calibre_db.common_filters()).all()
shelves = ub.session.query(ub.Shelf)\
.filter(or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == int(current_user.id)))\
.order_by(ub.Shelf.name).all()
extensions = calibre_db.session.query(db.Data)\
.join(db.Books)\
.filter(calibre_db.common_filters()) \
.group_by(db.Data.format)\
.order_by(db.Data.format).all()
if current_user.filter_language() == "all":
languages = calibre_db.speaking_language()
else:
languages = None
return render_title_template('search_form.html', tags=tags, languages=languages, extensions=extensions,
series=series,shelves=shelves, title=_("Advanced Search"), cc=cc, page="advsearch")
def render_search_results(term, offset=None, order=None, limit=None):
if term:
join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series
entries, result_count, pagination = calibre_db.get_search_results(term,
config,
offset,
order,
limit,
*join)
else:
entries = list()
order = [None, None]
pagination = result_count = None
return render_title_template('search.html',
searchterm=term,
pagination=pagination,
query=term,
adv_searchterm=term,
entries=entries,
result_count=result_count,
title=_("Search"),
page="search",
order=order[1])
+139
View File
@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import concurrent.futures
import importlib
import inspect
import json
import os
import sys
from flask import Blueprint, request, url_for, make_response, jsonify
from .cw_login import current_user
from flask_babel import get_locale
from sqlalchemy.exc import InvalidRequestError, OperationalError
from sqlalchemy.orm.attributes import flag_modified
from cps.services.Metadata import Metadata
from . import constants, logger, ub, web_server
from .usermanagement import user_login_required
meta = Blueprint("metadata", __name__)
log = logger.create()
try:
from dataclasses import asdict
except ImportError:
log.info('*** "dataclasses" is needed for calibre-web to run. Please install it using pip: "pip install dataclasses" ***')
print('*** "dataclasses" is needed for calibre-web to run. Please install it using pip: "pip install dataclasses" ***')
web_server.stop(True)
sys.exit(6)
new_list = list()
meta_dir = os.path.join(constants.BASE_DIR, "cps", "metadata_provider")
modules = os.listdir(os.path.join(constants.BASE_DIR, "cps", "metadata_provider"))
for f in modules:
if os.path.isfile(os.path.join(meta_dir, f)) and not f.endswith("__init__.py"):
a = os.path.basename(f)[:-3]
try:
importlib.import_module("cps.metadata_provider." + a)
new_list.append(a)
except (IndentationError, SyntaxError) as e:
log.error("Syntax error for metadata source: {} - {}".format(a, e))
except ImportError as e:
log.debug("Import error for metadata source: {} - {}".format(a, e))
def list_classes(provider_list):
classes = list()
for element in provider_list:
for name, obj in inspect.getmembers(
sys.modules["cps.metadata_provider." + element]
):
if (
inspect.isclass(obj)
and name != "Metadata"
and issubclass(obj, Metadata)
):
classes.append(obj())
return classes
cl = list_classes(new_list)
@meta.route("/metadata/provider")
@user_login_required
def metadata_provider():
active = current_user.view_settings.get("metadata", {})
provider = list()
for c in cl:
ac = active.get(c.__id__, True)
provider.append(
{"name": c.__name__, "active": ac, "initial": ac, "id": c.__id__}
)
return make_response(jsonify(provider))
@meta.route("/metadata/provider", methods=["POST"])
@meta.route("/metadata/provider/<prov_name>", methods=["POST"])
@user_login_required
def metadata_change_active_provider(prov_name):
new_state = request.get_json()
active = current_user.view_settings.get("metadata", {})
active[new_state["id"]] = new_state["value"]
current_user.view_settings["metadata"] = active
try:
try:
flag_modified(current_user, "view_settings")
except AttributeError:
pass
ub.session.commit()
except (InvalidRequestError, OperationalError):
log.error("Invalid request received: {}".format(request))
return "Invalid request", 400
if "initial" in new_state and prov_name:
data = []
provider = next((c for c in cl if c.__id__ == prov_name), None)
if provider is not None:
data = provider.search(new_state.get("query", ""))
return make_response(jsonify([asdict(x) for x in data]))
return ""
@meta.route("/metadata/search", methods=["POST"])
@user_login_required
def metadata_search():
query = request.form.to_dict().get("query")
data = list()
active = current_user.view_settings.get("metadata", {})
locale = get_locale()
if query:
static_cover = url_for("static", filename="generic_cover.jpg")
# ret = cl[0].search(query, static_cover, locale)
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
meta = {
executor.submit(c.search, query, static_cover, locale): c
for c in cl
if active.get(c.__id__, True)
}
for future in concurrent.futures.as_completed(meta):
data.extend([asdict(x) for x in (future.result() or []) if x])
return make_response(jsonify(data))
+163 -41
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import sys
import os
import errno
@@ -25,21 +24,24 @@ import socket
try:
from gevent.pywsgi import WSGIServer
from .gevent_wsgi import MyWSGIHandler
from gevent.pool import Pool
from gevent.socket import socket as GeventSocket
from gevent import __version__ as _version
from greenlet import GreenletExit
import ssl
VERSION = 'Gevent ' + _version
_GEVENT = True
except ImportError:
from tornado.wsgi import WSGIContainer
from .tornado_wsgi import MyWSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado import netutil
from tornado import version as _version
VERSION = 'Tornado ' + _version
_GEVENT = False
from . import logger
from . import logger, constants
log = logger.create()
@@ -94,7 +96,13 @@ class WebServer(object):
log.warning('Cert path: %s', certfile_path)
log.warning('Key path: %s', keyfile_path)
def _make_gevent_unix_socket(self, socket_file):
@staticmethod
def _make_gevent_socket_activated():
# Reuse an already open socket on fd=SD_LISTEN_FDS_START
SD_LISTEN_FDS_START = 3
return GeventSocket(fileno=SD_LISTEN_FDS_START)
def _prepare_unix_socket(self, socket_file):
# the socket file must not exist prior to bind()
if os.path.exists(socket_file):
# avoid nuking regular files and symbolic links (could be a mistype or security issue)
@@ -102,49 +110,122 @@ class WebServer(object):
raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), socket_file)
os.remove(socket_file)
unix_sock = WSGIServer.get_listener(socket_file, family=socket.AF_UNIX)
self.unix_socket_file = socket_file
# ensure current user and group have r/w permissions, no permissions for other users
# this way the socket can be shared in a semi-secure manner
# between the user running calibre-web and the user running the fronting webserver
os.chmod(socket_file, 0o660)
return unix_sock
def _make_gevent_socket(self):
def _make_gevent_listener(self):
if os.name != 'nt':
socket_activated = os.environ.get("LISTEN_FDS")
if socket_activated:
sock = self._make_gevent_socket_activated()
sock_info = sock.getsockname()
return sock, "systemd-socket:" + _readable_listen_address(sock_info[0], sock_info[1])
unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET")
if unix_socket_file:
return self._make_gevent_unix_socket(unix_socket_file), "unix:" + unix_socket_file
self._prepare_unix_socket(unix_socket_file)
unix_sock = WSGIServer.get_listener(unix_socket_file, family=socket.AF_UNIX)
# ensure current user and group have r/w permissions, no permissions for other users
# this way the socket can be shared in a semi-secure manner
# between the user running calibre-web and the user running the fronting webserver
os.chmod(unix_socket_file, 0o660)
return unix_sock, "unix:" + unix_socket_file
if self.listen_address:
return (self.listen_address, self.listen_port), None
return ((self.listen_address, self.listen_port),
_readable_listen_address(self.listen_address, self.listen_port))
if os.name == 'nt':
self.listen_address = '0.0.0.0'
return (self.listen_address, self.listen_port), None
return ((self.listen_address, self.listen_port),
_readable_listen_address(self.listen_address, self.listen_port))
address = ('::', self.listen_port)
try:
address = ('::', self.listen_port)
sock = WSGIServer.get_listener(address, family=socket.AF_INET6)
except socket.error as ex:
log.error('%s', ex)
log.warning('Unable to listen on "", trying on IPv4 only...')
log.warning('Unable to listen on {}, trying on IPv4 only...'.format(address))
address = ('', self.listen_port)
sock = WSGIServer.get_listener(address, family=socket.AF_INET)
return sock, _readable_listen_address(*address)
@staticmethod
def _get_args_for_reloading():
"""Determine how the script was executed, and return the args needed
to execute it again in a new process.
Code from https://github.com/pyload/pyload. Author GammaC0de, voulter
"""
rv = [sys.executable]
py_script = sys.argv[0]
args = sys.argv[1:]
# Need to look at main module to determine how it was executed.
__main__ = sys.modules["__main__"]
# The value of __package__ indicates how Python was called. It may
# not exist if a setuptools script is installed as an egg. It may be
# set incorrectly for entry points created with pip on Windows.
if getattr(__main__, "__package__", "") in ["", None] or (
os.name == "nt"
and __main__.__package__ == ""
and not os.path.exists(py_script)
and os.path.exists("{}.exe".format(py_script))
):
# Executed a file, like "python app.py".
py_script = os.path.abspath(py_script)
if os.name == "nt":
# Windows entry points have ".exe" extension and should be
# called directly.
if not os.path.exists(py_script) and os.path.exists("{}.exe".format(py_script)):
py_script += ".exe"
if (
os.path.splitext(sys.executable)[1] == ".exe"
and os.path.splitext(py_script)[1] == ".exe"
):
rv.pop(0)
rv.append(py_script)
else:
# Executed a module, like "python -m module".
if sys.argv[0] == "-m":
args = sys.argv
else:
if os.path.isfile(py_script):
# Rewritten by Python from "-m script" to "/path/to/script.py".
py_module = __main__.__package__
name = os.path.splitext(os.path.basename(py_script))[0]
if name != "__main__":
py_module += ".{}".format(name)
else:
# Incorrectly rewritten by pydevd debugger from "-m script" to "script".
py_module = py_script
rv.extend(("-m", py_module.lstrip(".")))
rv.extend(args)
if os.name == 'nt':
rv = ['"{}"'.format(a) for a in rv]
return rv
def _start_gevent(self):
ssl_args = self.ssl_args or {}
try:
sock, output = self._make_gevent_socket()
if output is None:
output = _readable_listen_address(self.listen_address, self.listen_port)
sock, output = self._make_gevent_listener()
log.info('Starting Gevent server on %s', output)
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, spawn=Pool(), **ssl_args)
try:
# Also print to stdout so interactive terminals show a clear success message
if constants.APP_MODE not in ['development', 'test']:
print(f"Calibre-Web: server started on {output}")
except Exception:
print(f"Calibre-Web: error {output}")
pass
self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler,
error_log=log,
spawn=Pool(), **ssl_args)
if ssl_args:
wrap_socket = self.wsgiserver.wrap_socket
def my_wrap_socket(*args, **kwargs):
@@ -165,17 +246,49 @@ class WebServer(object):
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))
try:
# Max Buffersize set to 200MB
http_server = HTTPServer(MyWSGIContainer(self.app),
max_buffer_size=209700000,
ssl_options=self.ssl_args)
# Max Buffersize set to 200MB )
http_server = HTTPServer(WSGIContainer(self.app),
max_buffer_size=209700000,
ssl_options=self.ssl_args)
http_server.listen(self.listen_port, self.listen_address)
self.wsgiserver = IOLoop.current()
self.wsgiserver.start()
# wait for stop signal
self.wsgiserver.close(True)
unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET")
if os.environ.get("LISTEN_FDS") and os.name != 'nt':
SD_LISTEN_FDS_START = 3
sock = socket.socket(fileno=SD_LISTEN_FDS_START)
http_server.add_socket(sock)
sock.setblocking(0)
socket_name =sock.getsockname()
output = "systemd-socket:" + _readable_listen_address(socket_name[0], socket_name[1])
elif unix_socket_file and os.name != 'nt':
self._prepare_unix_socket(unix_socket_file)
output = "unix:" + unix_socket_file
unix_socket = netutil.bind_unix_socket(self.unix_socket_file)
http_server.add_socket(unix_socket)
# ensure current user and group have r/w permissions, no permissions for other users
# this way the socket can be shared in a semi-secure manner
# between the user running calibre-web and the user running the fronting webserver
os.chmod(self.unix_socket_file, 0o660)
else:
output = _readable_listen_address(self.listen_address, self.listen_port)
http_server.listen(self.listen_port, self.listen_address)
log.info('Starting Tornado server on %s', output)
# Also print to stdout so interactive terminals show a clear success message
try:
if constants.APP_MODE not in ['development', 'test']:
print(f"Calibre-Web: server started on {output}")
except Exception:
print(f"Calibre-Web: error {output}")
pass
self.wsgiserver = IOLoop.current()
self.wsgiserver.start()
# wait for stop signal
self.wsgiserver.close(True)
finally:
if self.unix_socket_file:
os.remove(self.unix_socket_file)
self.unix_socket_file = None
def start(self):
try:
@@ -192,19 +305,23 @@ class WebServer(object):
finally:
self.wsgiserver = None
# prevent irritating log of pending tasks message from asyncio
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
if not self.restart:
log.info("Performing shutdown of Calibre-Web")
# prevent irritiating log of pending tasks message from asyncio
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
return True
log.info("Performing restart of Calibre-Web")
arguments = list(sys.argv)
arguments.insert(0, sys.executable)
if os.name == 'nt':
arguments = ["\"%s\"" % a for a in arguments]
os.execv(sys.executable, arguments)
return True
args = self._get_args_for_reloading()
os.execv(args[0].lstrip('"').rstrip('"'), args)
@staticmethod
def shutdown_scheduler():
from .services.background_scheduler import BackgroundScheduler
scheduler = BackgroundScheduler()
if scheduler:
scheduler.scheduler.shutdown()
def _killServer(self, __, ___):
self.stop()
@@ -214,9 +331,14 @@ class WebServer(object):
updater_thread.stop()
log.info("webserver stop (restart=%s)", restart)
self.shutdown_scheduler()
self.restart = restart
if self.wsgiserver:
if _GEVENT:
self.wsgiserver.close()
else:
self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop)
if restart:
self.wsgiserver.call_later(1.0, self.wsgiserver.stop)
else:
self.wsgiserver.asyncio_loop.call_soon_threadsafe(self.wsgiserver.stop)
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import abc
import dataclasses
import os
import re
from typing import Dict, Generator, List, Optional, Union
from cps import constants
@dataclasses.dataclass
class MetaSourceInfo:
id: str
description: str
link: str
@dataclasses.dataclass
class MetaRecord:
id: Union[str, int]
title: str
authors: List[str]
url: str
source: MetaSourceInfo
cover: str = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg')
description: Optional[str] = ""
series: Optional[str] = None
series_index: Optional[Union[int, float]] = 0
identifiers: Dict[str, Union[str, int]] = dataclasses.field(default_factory=dict)
publisher: Optional[str] = None
publishedDate: Optional[str] = None
rating: Optional[int] = 0
languages: Optional[List[str]] = dataclasses.field(default_factory=list)
tags: Optional[List[str]] = dataclasses.field(default_factory=list)
class Metadata:
__name__ = "Generic"
__id__ = "generic"
def __init__(self):
self.active = True
def set_status(self, state):
self.active = state
@abc.abstractmethod
def search(
self, query: str, generic_cover: str = "", locale: str = "en"
) -> Optional[List[MetaRecord]]:
pass
@staticmethod
def get_title_tokens(
title: str, strip_joiners: bool = True
) -> Generator[str, None, None]:
"""
Taken from calibre source code
It's a simplified (cut out what is unnecessary) version of
https://github.com/kovidgoyal/calibre/blob/99d85b97918625d172227c8ffb7e0c71794966c0/
src/calibre/ebooks/metadata/sources/base.py#L363-L367
(src/calibre/ebooks/metadata/sources/base.py - lines 363-398)
"""
title_patterns = [
(re.compile(pat, re.IGNORECASE), repl)
for pat, repl in [
# Remove things like: (2010) (Omnibus) etc.
(
r"(?i)[({\[](\d{4}|omnibus|anthology|hardcover|"
r"audiobook|audio\scd|paperback|turtleback|"
r"mass\s*market|edition|ed\.)[\])}]",
"",
),
# Remove any strings that contain the substring edition inside
# parentheses
(r"(?i)[({\[].*?(edition|ed.).*?[\]})]", ""),
# Remove commas used a separators in numbers
(r"(\d+),(\d+)", r"\1\2"),
# Remove hyphens only if they have whitespace before them
(r"(\s-)", " "),
# Replace other special chars with a space
(r"""[:,;!@$%^&*(){}.`~"\s\[\]/]《》「」“”""", " "),
]
]
for pat, repl in title_patterns:
title = pat.sub(repl, title)
tokens = title.split()
for token in tokens:
token = token.strip().strip('"').strip("'")
if token and (
not strip_joiners or token.lower() not in ("a", "and", "the", "&")
):
yield token
+20 -24
View File
@@ -19,12 +19,8 @@
import sys
from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__
from jsonschema import validate, exceptions
from datetime import datetime
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import json
from .. import logger
@@ -34,13 +30,10 @@ log = logger.create()
def b64encode_json(json_data):
if sys.version_info < (3, 0):
return b64encode(json.dumps(json_data))
else:
return b64encode(json.dumps(json_data).encode())
return b64encode(json.dumps(json_data).encode()).decode("utf-8")
# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2.
# Python3 has a timestamp() method we could be calling, however it's not available in python2.
def to_epoch_timestamp(datetime_object):
return (datetime_object - datetime(1970, 1, 1)).total_seconds()
@@ -54,7 +47,7 @@ def get_datetime_from_json(json_object, field_name):
class SyncToken:
""" The SyncToken is used to persist state accross requests.
""" The SyncToken is used to persist state across 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.
@@ -64,7 +57,7 @@ class SyncToken:
books_last_modified: Datetime representing the last modified book that the device knows about.
"""
SYNC_TOKEN_HEADER = "x-kobo-synctoken"
SYNC_TOKEN_HEADER = "x-kobo-synctoken" # nosec
VERSION = "1-1-0"
LAST_MODIFIED_ADDED_VERSION = "1-1-0"
MIN_VERSION = "1-0-0"
@@ -84,8 +77,8 @@ class SyncToken:
"books_last_created": {"type": "string"},
"archive_last_modified": {"type": "string"},
"reading_state_last_modified": {"type": "string"},
"tags_last_modified": {"type": "string"},
"books_last_id": {"type": "integer", "optional": True}
"tags_last_modified": {"type": "string"}
# "books_last_id": {"type": "integer", "optional": True}
},
}
@@ -96,21 +89,21 @@ class SyncToken:
books_last_modified=datetime.min,
archive_last_modified=datetime.min,
reading_state_last_modified=datetime.min,
tags_last_modified=datetime.min,
books_last_id=-1
):
tags_last_modified=datetime.min
# books_last_id=-1
): # nosec
self.raw_kobo_store_token = raw_kobo_store_token
self.books_last_created = books_last_created
self.books_last_modified = books_last_modified
self.archive_last_modified = archive_last_modified
self.reading_state_last_modified = reading_state_last_modified
self.tags_last_modified = tags_last_modified
self.books_last_id = books_last_id
# self.books_last_id = books_last_id
@staticmethod
def from_headers(headers):
sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "")
if sync_token_header == "":
if sync_token_header == "": # nosec
return SyncToken()
# On the first sync from a Kobo device, we may receive the SyncToken
@@ -140,12 +133,9 @@ class SyncToken:
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
books_last_id = data_json["books_last_id"]
except TypeError:
log.error("SyncToken timestamps don't parse to a datetime.")
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
except KeyError:
books_last_id = -1
return SyncToken(
raw_kobo_store_token=raw_kobo_store_token,
@@ -154,7 +144,6 @@ class SyncToken:
archive_last_modified=archive_last_modified,
reading_state_last_modified=reading_state_last_modified,
tags_last_modified=tags_last_modified,
books_last_id=books_last_id
)
def set_kobo_store_header(self, store_headers):
@@ -178,7 +167,14 @@ class SyncToken:
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified),
"books_last_id":self.books_last_id
},
}
return b64encode_json(token)
def __str__(self):
return "{},{},{},{},{},{}".format(self.books_last_created,
self.books_last_modified,
self.archive_last_modified,
self.reading_state_last_modified,
self.tags_last_modified,
self.raw_kobo_store_token)
+8 -5
View File
@@ -16,15 +16,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from .. import logger
log = logger.create()
try: from . import goodreads_support
try:
from . import goodreads_support
except ImportError as err:
log.debug("Cannot import goodreads, showing authors-metadata will not work: %s", err)
goodreads_support = None
@@ -45,3 +42,9 @@ except ImportError as err:
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
kobo = None
SyncToken = None
try:
from . import gmail
except ImportError as err:
log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err)
gmail = None
+85
View File
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 mmonkey
#
# 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 atexit
from .. import logger
from .worker import WorkerThread
try:
from apscheduler.schedulers.background import BackgroundScheduler as BScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
use_APScheduler = True
except (ImportError, RuntimeError) as e:
use_APScheduler = False
log = logger.create()
log.info('APScheduler not found. Unable to schedule tasks.')
class BackgroundScheduler:
_instance = None
def __new__(cls):
if not use_APScheduler:
return False
if cls._instance is None:
cls._instance = super(BackgroundScheduler, cls).__new__(cls)
cls.log = logger.create()
logger.logging.getLogger('tzlocal').setLevel(logger.logging.WARNING)
cls.scheduler = BScheduler()
cls.scheduler.start()
return cls._instance
def schedule(self, func, trigger, name=None):
if use_APScheduler:
return self.scheduler.add_job(func=func, trigger=trigger, name=name)
# Expects a lambda expression for the task
def schedule_task(self, task, user=None, name=None, hidden=False, trigger=None):
if use_APScheduler:
def scheduled_task():
worker_task = task()
worker_task.scheduled = True
WorkerThread.add(user, worker_task, hidden=hidden)
return self.schedule(func=scheduled_task, trigger=trigger, name=name)
# Expects a list of lambda expressions for the tasks
def schedule_tasks(self, tasks, user=None, trigger=None):
if use_APScheduler:
for task in tasks:
self.schedule_task(task[0], user=user, trigger=trigger, name=task[1], hidden=task[2])
# Expects a lambda expression for the task
def schedule_task_immediately(self, task, user=None, name=None, hidden=False):
if use_APScheduler:
def immediate_task():
WorkerThread.add(user, task(), hidden)
return self.schedule(func=immediate_task, trigger=DateTrigger(), name=name)
# Expects a list of lambda expressions for the tasks
def schedule_tasks_immediately(self, tasks, user=None):
if use_APScheduler:
for task in tasks:
self.schedule_task_immediately(task[0], user, name="immediately " + task[1], hidden=task[2])
# Remove all jobs
def remove_all_jobs(self):
self.scheduler.remove_all_jobs()
+101
View File
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2021 OzzieIsaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from datetime import datetime
import base64
from flask_babel import gettext as _
from ..constants import CONFIG_DIR
from .. import logger
log = logger.create()
SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email']
def setup_gmail(token):
# If there are no (valid) credentials available, let the user log in.
creds = None
user_info = None
if "token" in token:
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
token_uri=token['token_uri'],
client_id=token['client_id'],
client_secret=token['client_secret'],
scopes=token['scopes'],
)
creds.expiry = datetime.fromisoformat(token['expiry'])
if not creds or not creds.valid:
# don't forget to dump one more time after the refresh
# also, some file-locking routines wouldn't be needless
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
cred_file = os.path.join(CONFIG_DIR, 'gmail.json')
if not os.path.exists(cred_file):
raise Exception(_("Found no valid gmail.json file with OAuth information"))
flow = InstalledAppFlow.from_client_secrets_file(
os.path.join(CONFIG_DIR, 'gmail.json'), SCOPES)
creds = flow.run_local_server(port=0)
user_info = get_user_info(creds)
return {
'token': creds.token,
'refresh_token': creds.refresh_token,
'token_uri': creds.token_uri,
'client_id': creds.client_id,
'client_secret': creds.client_secret,
'scopes': creds.scopes,
'expiry': creds.expiry.isoformat(),
'email': user_info
}
return {}
def get_user_info(credentials):
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
user_info = user_info_service.userinfo().get().execute()
return user_info.get('email', "")
def send_messsage(token, msg):
log.debug("Start sending e-mail via Gmail")
creds = Credentials(
token=token['token'],
refresh_token=token['refresh_token'],
token_uri=token['token_uri'],
client_id=token['client_id'],
client_secret=token['client_secret'],
scopes=token['scopes'],
)
creds.expiry = datetime.fromisoformat(token['expiry'])
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
service = build('gmail', 'v1', credentials=creds)
message_as_bytes = msg.as_bytes() # the message should be converted from string to bytes.
message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding)
raw = message_as_base64.decode() # convert to something JSON serializable
body = {'raw': raw}
(service.users().messages().send(userId='me', body=body).execute())
log.debug("E-mail send successfully via Gmail")
+83 -12
View File
@@ -16,19 +16,88 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import time
from functools import reduce
import requests
from goodreads.client import GoodreadsClient
from goodreads.request import GoodreadsRequest
from lxml import etree
try:
from goodreads.client import GoodreadsClient
import Levenshtein
except ImportError:
from betterreads.client import GoodreadsClient
try: import Levenshtein
except ImportError: Levenshtein = False
Levenshtein = False
from .. import logger
from ..clean_html import clean_string
def etree_to_dict(t):
"""
Convert lxml ElementTree to a nested dict (similar to xmltodict).
"""
d = {t.tag: {} if t.attrib else None}
children = list(t)
if children:
dd = {}
for dc in map(etree_to_dict, children):
for k, v in dc.items():
if k in dd:
if not isinstance(dd[k], list):
dd[k] = [dd[k]]
dd[k].append(v)
else:
dd[k] = v
d = {t.tag: dd}
if t.attrib:
d[t.tag].update(('@' + k, v) for k, v in t.attrib.items())
text = (t.text or '').strip()
if text:
if children or t.attrib:
d[t.tag]['#text'] = text
else:
d[t.tag] = text
return d
class my_GoodreadsClient(GoodreadsClient):
def request(self, *args, **kwargs):
"""Create a GoodreadsRequest object and make that request"""
req = my_GoodreadsRequest(self, *args, **kwargs)
return req.request()
class GoodreadsRequestException(Exception):
def __init__(self, error_msg, url):
self.error_msg = error_msg
self.url = url
def __str__(self):
return self.url, ':', self.error_msg
class my_GoodreadsRequest(GoodreadsRequest):
def request(self):
resp = requests.get(self.host+self.path, params=self.params,
headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) "
"Gecko/20100101 Firefox/125.0"})
if resp.status_code != 200:
raise GoodreadsRequestException(resp.reason, self.path)
if self.req_format == 'xml':
root = etree.fromstring(resp.content, parser=etree.XMLParser(resolve_entities=False, no_network=True))
data_dict = etree_to_dict(root)
return data_dict['GoodreadsResponse']
else:
raise Exception("Invalid format")
log = logger.create()
@@ -39,20 +108,20 @@ _CACHE_TIMEOUT = 23 * 60 * 60 # 23 hours (in seconds)
_AUTHORS_CACHE = {}
def connect(key=None, secret=None, enabled=True):
def connect(key=None, enabled=True):
global _client
if not enabled or not key or not secret:
if not enabled or not key:
_client = None
return
if _client:
# make sure the configuration has not changed since last we used the client
if _client.client_key != key or _client.client_secret != secret:
if _client.client_key != key:
_client = None
if not _client:
_client = GoodreadsClient(key, secret)
_client = my_GoodreadsClient(key, None)
def get_author_info(author_name):
@@ -77,12 +146,13 @@ def get_author_info(author_name):
if author_info:
author_info._timestamp = now
author_info.safe_about = clean_string(author_info.about)
_AUTHORS_CACHE[author_name] = author_info
return author_info
def get_other_books(author_info, library_books=None):
# Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates
# Get all identifiers (ISBN, Goodreads, etc.) and filter author's books by that list so we show fewer duplicates
# Note: Not all images will be shown, even though they're available on Goodreads.com.
# See https://www.goodreads.com/topic/show/18213769-goodreads-book-images
@@ -92,7 +162,8 @@ def get_other_books(author_info, library_books=None):
identifiers = []
library_titles = []
if library_books:
identifiers = list(reduce(lambda acc, book: acc + [i.val for i in book.identifiers if i.val], library_books, []))
identifiers = list(
reduce(lambda acc, book: acc + [i.val for i in book.identifiers if i.val], library_books, []))
library_titles = [book.title for book in library_books]
for book in author_info.books:
+65 -11
View File
@@ -16,11 +16,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import base64
from flask_simpleldap import LDAP, LDAPException
from flask_simpleldap import ldap as pyLDAP
from flask import current_app
from .. import constants, logger
try:
@@ -29,7 +29,59 @@ except ImportError:
pass
log = logger.create()
_ldap = LDAP()
def _escape_ldap_filter(s):
"""Escape special characters for safe use in LDAP filter strings (RFC 4515)."""
s = s.replace('\\', '\\5c')
s = s.replace('*', '\\2a')
s = s.replace('(', '\\28')
s = s.replace(')', '\\29')
s = s.replace('\x00', '\\00')
return s
class LDAPLogger(object):
@staticmethod
def write(message):
try:
log.debug(message.strip("\n").replace("\n", ""))
except Exception:
log.debug("Logging Error")
class mySimpleLDap(LDAP):
@staticmethod
def init_app(app):
super(mySimpleLDap, mySimpleLDap).init_app(app)
app.config.setdefault('LDAP_LOGLEVEL', 0)
@property
def initialize(self):
"""Initialize a connection to the LDAP server.
:return: LDAP connection object.
"""
try:
log_level = 2 if current_app.config['LDAP_LOGLEVEL'] == logger.logging.DEBUG else 0
conn = pyLDAP.initialize('{0}://{1}:{2}'.format(
current_app.config['LDAP_SCHEMA'],
current_app.config['LDAP_HOST'],
current_app.config['LDAP_PORT']), trace_level=log_level, trace_file=LDAPLogger())
conn.set_option(pyLDAP.OPT_NETWORK_TIMEOUT,
current_app.config['LDAP_TIMEOUT'])
conn = self._set_custom_options(conn)
conn.protocol_version = pyLDAP.VERSION3
if current_app.config['LDAP_USE_TLS']:
conn.start_tls_s()
return conn
except pyLDAP.LDAPError as e:
raise LDAPException(self.error(e.args))
_ldap = mySimpleLDap()
def init_app(app, config):
@@ -45,15 +97,15 @@ def init_app(app, config):
app.config['LDAP_SCHEMA'] = 'ldap'
if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS:
if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE:
if config.config_ldap_serv_password is None:
config.config_ldap_serv_password = ''
app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password)
if config.config_ldap_serv_password_e is None:
config.config_ldap_serv_password_e = ''
app.config['LDAP_PASSWORD'] = config.config_ldap_serv_password_e
else:
app.config['LDAP_PASSWORD'] = base64.b64decode("")
app.config['LDAP_PASSWORD'] = ""
app.config['LDAP_USERNAME'] = config.config_ldap_serv_username
else:
app.config['LDAP_USERNAME'] = ""
app.config['LDAP_PASSWORD'] = base64.b64decode("")
app.config['LDAP_PASSWORD'] = ""
if bool(config.config_ldap_cert_path):
app.config['LDAP_CUSTOM_OPTIONS'].update({
pyLDAP.OPT_X_TLS_REQUIRE_CERT: pyLDAP.OPT_X_TLS_DEMAND,
@@ -71,7 +123,7 @@ def init_app(app, config):
app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap)
app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter
app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field
app.config['LDAP_LOGLEVEL'] = config.config_log_level
try:
_ldap.init_app(app)
except ValueError:
@@ -85,7 +137,7 @@ def init_app(app, config):
log.error(e)
def get_object_details(user=None,query_filter=None):
def get_object_details(user=None, query_filter=None):
return _ldap.get_object_details(user, query_filter=query_filter)
@@ -106,9 +158,11 @@ def bind_user(username, password):
:returns: True if login succeeded, False if login failed, None if server unavailable.
'''
# Escape LDAP special characters to prevent LDAP injection in search filters
safe_username = _escape_ldap_filter(username)
try:
if _ldap.get_object_details(username):
result = _ldap.bind_user(username, password)
if _ldap.get_object_details(safe_username):
result = _ldap.bind_user(safe_username, password)
log.debug("LDAP login '%s': %r", username, result)
return result is not None, None
return None, None # User not found
+73 -21
View File
@@ -1,5 +1,21 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2020 pwr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
import threading
import abc
import uuid
@@ -21,11 +37,13 @@ STAT_WAITING = 0
STAT_FAIL = 1
STAT_STARTED = 2
STAT_FINISH_SUCCESS = 3
STAT_ENDED = 4
STAT_CANCELLED = 5
# Only retain this many tasks in dequeued list
TASK_CLEANUP_TRIGGER = 20
QueuedTask = namedtuple('QueuedTask', 'num, user, added, task')
QueuedTask = namedtuple('QueuedTask', 'num, user, added, task, hidden')
def _get_main_thread():
@@ -35,7 +53,6 @@ def _get_main_thread():
raise Exception("main thread not found?!")
class ImprovedQueue(queue.Queue):
def to_list(self):
"""
@@ -45,12 +62,13 @@ class ImprovedQueue(queue.Queue):
with self.mutex:
return list(self.queue)
#Class for all worker tasks in the background
# Class for all worker tasks in the background
class WorkerThread(threading.Thread):
_instance = None
@classmethod
def getInstance(cls):
def get_instance(cls):
if cls._instance is None:
cls._instance = WorkerThread()
return cls._instance
@@ -66,14 +84,17 @@ class WorkerThread(threading.Thread):
self.start()
@classmethod
def add(cls, user, task):
ins = cls.getInstance()
def add(cls, user, task, hidden=False):
ins = cls.get_instance()
ins.num += 1
username = user if user is not None else 'System'
log.debug("Add Task for user: {} - {}".format(username, task))
ins.queue.put(QueuedTask(
num=ins.num,
user=user,
user=username,
added=datetime.now(),
task=task,
hidden=hidden
))
@property
@@ -94,10 +115,10 @@ class WorkerThread(threading.Thread):
if delta > TASK_CLEANUP_TRIGGER:
ret = alive
else:
# otherwise, lop off the oldest dead tasks until we hit the target trigger
ret = sorted(dead, key=lambda x: x.task.end_time)[-TASK_CLEANUP_TRIGGER:] + alive
# otherwise, loop off the oldest dead tasks until we hit the target trigger
ret = sorted(dead, key=lambda y: y.task.end_time)[-TASK_CLEANUP_TRIGGER:] + alive
self.dequeued = sorted(ret, key=lambda x: x.num)
self.dequeued = sorted(ret, key=lambda y: y.num)
# Main thread loop starting the different tasks
def run(self):
@@ -124,11 +145,21 @@ class WorkerThread(threading.Thread):
# sometimes tasks (like Upload) don't actually have work to do and are created as already finished
if item.task.stat is STAT_WAITING:
# CalibreTask.start() should wrap all exceptions in it's own error handling
# CalibreTask.start() should wrap all exceptions in its own error handling
item.task.start(self)
# remove self_cleanup tasks and hidden "System Tasks" from list
if item.task.self_cleanup or item.hidden:
self.dequeued.remove(item)
self.queue.task_done()
def end_task(self, task_id):
ins = self.get_instance()
for __, __, __, task, __ in ins.tasks:
if str(task.id) == str(task_id) and task.is_cancellable:
task.stat = STAT_CANCELLED if task.stat == STAT_WAITING else STAT_ENDED
class CalibreTask:
__metaclass__ = abc.ABCMeta
@@ -141,10 +172,12 @@ class CalibreTask:
self.end_time = None
self.message = message
self.id = uuid.uuid4()
self.self_cleanup = False
self._scheduled = False
@abc.abstractmethod
def run(self, worker_thread):
"""Provides the caller some human-readable name for this class"""
"""The main entry-point for this task"""
raise NotImplementedError
@abc.abstractmethod
@@ -152,6 +185,11 @@ class CalibreTask:
"""Provides the caller some human-readable name for this class"""
raise NotImplementedError
@abc.abstractmethod
def is_cancellable(self):
"""Does this task gracefully handle being cancelled (STAT_ENDED, STAT_CANCELLED)?"""
raise NotImplementedError
def start(self, *args):
self.start_time = datetime.now()
self.stat = STAT_STARTED
@@ -159,9 +197,9 @@ class CalibreTask:
# catch any unhandled exceptions in a task and automatically fail it
try:
self.run(*args)
except Exception as e:
self._handleError(str(e))
log.debug_or_exception(e)
except Exception as ex:
self._handleError(str(ex))
log.exception(ex)
self.end_time = datetime.now()
@@ -202,12 +240,23 @@ class CalibreTask:
We have a separate dictating this because there may be certain tasks that want to override this
"""
# By default, we're good to clean a task if it's "Done"
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL, STAT_ENDED, STAT_CANCELLED)
@progress.setter
def progress(self, x):
# todo: throw error if outside of [0,1]
self._progress = x
@property
def self_cleanup(self):
return self._self_cleanup
@self_cleanup.setter
def self_cleanup(self, is_self_cleanup):
self._self_cleanup = is_self_cleanup
@property
def scheduled(self):
return self._scheduled
@scheduled.setter
def scheduled(self, is_scheduled):
self._scheduled = is_scheduled
def _handleError(self, error_message):
self.stat = STAT_FAIL
@@ -217,3 +266,6 @@ class CalibreTask:
def _handleSuccess(self):
self.stat = STAT_FINISH_SUCCESS
self.progress = 1
def __str__(self):
return self.name
+307 -200
View File
@@ -20,69 +20,48 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import division, print_function, unicode_literals
from datetime import datetime
import sys
from datetime import datetime, timezone
from flask import Blueprint, request, flash, redirect, url_for
from flask import Blueprint, flash, redirect, request, url_for, abort
from flask_babel import gettext as _
from flask_login import login_required, current_user
from .cw_login import current_user
from sqlalchemy.exc import InvalidRequestError, OperationalError
from sqlalchemy.sql.expression import func, true
from sqlalchemy.exc import OperationalError, InvalidRequestError
from . import logger, ub, calibre_db, db
from . import calibre_db, config, db, logger, ub
from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano
from .usermanagement import login_required_if_no_ano, user_login_required
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
shelf = Blueprint('shelf', __name__)
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
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
@user_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 xhr:
flash(_(u"Invalid shelf specified"), category="error")
flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
if not check_shelf_edit_permissions(shelf):
if not xhr:
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
category="error")
flash(_("Sorry you are not allowed to add a book to that shelf"), category="error")
return redirect(url_for('web.index'))
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
return "Sorry you are not allowed to add a book to the that shelf", 403
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
ub.BookShelf.book_id == book_id).first()
if book_in_shelf:
log.error("Book %s is already part of %s", book_id, shelf)
if not xhr:
flash(_(u"Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
flash(_("Book is already part of the shelf: %(shelfname)s", shelfname=shelf.name), category="error")
return redirect(url_for('web.index'))
return "Book is already part of the shelf: %s" % shelf.name, 400
@@ -92,20 +71,30 @@ def add_to_shelf(shelf_id, book_id):
else:
maxOrder = maxOrder[0]
if not calibre_db.session.query(db.Books).filter(db.Books.id == book_id).one_or_none():
log.error("Invalid Book Id: %s. Could not be added to shelf %s", book_id, shelf.name)
if not xhr:
flash(_("%(book_id)s is a invalid Book Id. Could not be added to Shelf", book_id=book_id),
category="error")
return redirect(url_for('web.index'))
return "%s is a invalid Book Id. Could not be added to Shelf" % book_id, 400
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
shelf.last_modified = datetime.utcnow()
shelf.last_modified = datetime.now(timezone.utc)
try:
ub.session.merge(shelf)
ub.session.commit()
except (OperationalError, InvalidRequestError):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
if not xhr:
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
log.debug("Book has been added to shelf: {}".format(shelf.name))
flash(_("Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
@@ -113,26 +102,71 @@ def add_to_shelf(shelf_id, book_id):
return "", 204
@shelf.route("/shelf/massadd/<int:shelf_id>")
@login_required
def search_to_shelf(shelf_id):
@shelf.route("/shelf/massremove/<int:shelf_id>", methods=["POST"])
@user_login_required
def search_from_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None:
log.error("Invalid shelf specified: %s", shelf_id)
flash(_(u"Invalid shelf specified"), category="error")
log.error("Invalid shelf specified: {}".format(shelf_id))
flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
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")
log.warning("You are not allowed to remove a book from the shelf".format(shelf.name))
flash(_("You are not allowed to remove a book from the shelf"), category="error")
return redirect(url_for('web.index'))
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
books_from_shelf = list()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
if books_in_shelf:
book_ids = [book_id.book_id for book_id in books_in_shelf]
for searchid in ub.searched_ids[current_user.id]:
if searchid in book_ids:
books_from_shelf.append(searchid)
else:
log.error("No Books are part of {}".format(shelf.name))
flash(_("No Books are part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
# maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
for book in books_from_shelf:
ub.session.delete(ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).filter(
ub.BookShelf.book_id == book).first())
shelf.last_modified = datetime.now(timezone.utc)
try:
ub.session.commit()
flash(_("Books have been removed from shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
else:
log.error("Could not remove books from shelf: {}".format(shelf.name))
flash(_("Could not remove books from shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index'))
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
@user_login_required
def search_to_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf is None:
log.error("Invalid shelf specified: {}".format(shelf_id))
flash(_("Invalid shelf specified"), category="error")
return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
flash(_("You are not allowed to add a book to the shelf"), category="error")
return redirect(url_for('web.index'))
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
books_for_shelf = list()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
if books_in_shelf:
book_ids = list()
for book_id in books_in_shelf:
book_ids.append(book_id.book_id)
book_ids = [book_id.book_id for book_id in books_in_shelf]
for searchid in ub.searched_ids[current_user.id]:
if searchid not in book_ids:
books_for_shelf.append(searchid)
@@ -140,8 +174,8 @@ def search_to_shelf(shelf_id):
books_for_shelf = ub.searched_ids[current_user.id]
if not books_for_shelf:
log.error("Books are already part of %s", shelf.name)
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
log.error("Books are already part of {}".format(shelf.name))
flash(_("Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
return redirect(url_for('web.index'))
maxOrder = ub.session.query(func.max(ub.BookShelf.order)).filter(ub.BookShelf.shelf == shelf_id).first()[0] or 0
@@ -149,26 +183,28 @@ def search_to_shelf(shelf_id):
for book in books_for_shelf:
maxOrder += 1
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
shelf.last_modified = datetime.utcnow()
shelf.last_modified = datetime.now(timezone.utc)
try:
ub.session.merge(shelf)
ub.session.commit()
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError):
flash(_("Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
else:
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
log.error("Could not add books to shelf: {}".format(shelf.name))
flash(_("Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
return redirect(url_for('web.index'))
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
@login_required
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
@user_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)
log.error("Invalid shelf specified: {}".format(shelf_id))
if not xhr:
return redirect(url_for('web.index'))
return "Invalid shelf specified", 400
@@ -193,17 +229,18 @@ def remove_from_shelf(shelf_id, book_id):
try:
ub.session.delete(book_shelf)
shelf.last_modified = datetime.utcnow()
shelf.last_modified = datetime.now(timezone.utc)
ub.session.commit()
except (OperationalError, InvalidRequestError):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
return redirect(url_for('web.index'))
if not xhr:
flash(_(u"Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
flash(_("Book has been removed from shelf: %(sname)s", sname=shelf.name), category="success")
if "HTTP_REFERER" in request.environ:
return redirect(request.environ["HTTP_REFERER"])
else:
@@ -211,107 +248,43 @@ def remove_from_shelf(shelf_id, book_id):
return "", 204
else:
if not xhr:
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
flash(_("Sorry you are not allowed to remove a book from this shelf"),
category="error")
return redirect(url_for('web.index'))
return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403
return "Sorry you are not allowed to remove a book from this shelf", 403
@shelf.route("/shelf/create", methods=["GET", "POST"])
@login_required
@user_login_required
def create_shelf():
shelf = ub.Shelf()
return create_edit_shelf(shelf, title=_(u"Create a Shelf"), page="shelfcreate")
return create_edit_shelf(shelf, page_title=_("Create a Shelf"), page="shelfcreate")
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
@login_required
@user_login_required
def edit_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
return create_edit_shelf(shelf, title=_(u"Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
if not check_shelf_edit_permissions(shelf):
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
return redirect(url_for('web.index'))
return create_edit_shelf(shelf, page_title=_("Edit a shelf"), page="shelfedit", shelf_id=shelf_id)
# if shelf ID is set, we are editing a shelf
def create_edit_shelf(shelf, title, page, shelf_id=False):
if request.method == "POST":
to_save = request.form.to_dict()
if "is_public" in to_save:
shelf.is_public = 1
else:
shelf.is_public = 0
if check_shelf_is_unique(shelf, to_save, shelf_id):
shelf.name = to_save["title"]
# shelf.last_modified = datetime.utcnow()
if not shelf_id:
shelf.user_id = int(current_user.id)
ub.session.add(shelf)
shelf_action = "created"
flash_text = _(u"Shelf %(title)s created", title=to_save["title"])
else:
shelf_action = "changed"
flash_text = _(u"Shelf %(title)s changed", title=to_save["title"])
try:
ub.session.commit()
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action))
flash(flash_text, category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.debug_or_exception(e)
flash(_(u"Settings DB is not Writeable"), category="error")
except Exception as e:
ub.session.rollback()
log.debug_or_exception(e)
flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)
def check_shelf_is_unique(shelf, to_save, shelf_id=False):
if shelf_id:
ident = ub.Shelf.id != shelf_id
else:
ident = true()
if shelf.is_public == 1:
is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == to_save["title"]) & (ub.Shelf.is_public == 1)) \
.filter(ident) \
.first() is None
if not is_shelf_name_unique:
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
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(ident) \
.first() is None
if not is_shelf_name_unique:
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
category="error")
return is_shelf_name_unique
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("successfully deleted Shelf {}".format(cur_shelf.name))
@shelf.route("/shelf/delete/<int:shelf_id>")
@login_required
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
@user_login_required
def delete_shelf(shelf_id):
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
try:
delete_shelf_helper(cur_shelf)
except InvalidRequestError:
if not delete_shelf_helper(cur_shelf):
flash(_("Error deleting Shelf"), category="error")
else:
flash(_("Shelf successfully deleted"), category="success")
except InvalidRequestError as e:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return redirect(url_for('web.index'))
@@ -321,7 +294,7 @@ def show_simpleshelf(shelf_id):
return render_show_shelf(2, shelf_id, 1, None)
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "order", 'page': 1})
@shelf.route("/shelf/<int:shelf_id>", defaults={"sort_param": "stored", 'page': 1})
@shelf.route("/shelf/<int:shelf_id>/<sort_param>", defaults={'page': 1})
@shelf.route("/shelf/<int:shelf_id>/<sort_param>/<int:page>")
@login_required_if_no_ano
@@ -330,37 +303,158 @@ def show_shelf(shelf_id, sort_param, page):
@shelf.route("/shelf/order/<int:shelf_id>", methods=["GET", "POST"])
@login_required
@user_login_required
def order_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
if shelf and check_shelf_view_permissions(shelf):
if request.method == "POST":
if not check_shelf_edit_permissions(shelf):
flash(_("Sorry you are not allowed to edit this shelf"), category="error")
return redirect(url_for('web.index'))
to_save = request.form.to_dict()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
ub.BookShelf.order.asc()).all()
counter = 0
for book in books_in_shelf:
setattr(book, 'order', to_save[str(book.book_id)])
counter += 1
# if order different from before -> shelf.last_modified = datetime.now(timezone.utc)
try:
ub.session.commit()
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
result = list()
if shelf:
result = calibre_db.session.query(db.Books) \
.join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id, isouter=True) \
.add_columns(calibre_db.common_filters().label("visible")) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
return render_title_template('shelf_order.html', entries=result,
title=_("Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder")
else:
abort(404)
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 {} not allowed to edit shelf: {}".format(current_user.id, cur_shelf.name))
return False
if cur_shelf.is_public and not current_user.role_edit_shelfs():
log.info("User {} not allowed to edit public shelves".format(current_user.id))
return False
return True
def check_shelf_view_permissions(cur_shelf):
try:
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: {}".format(cur_shelf.name))
return False
except Exception as e:
log.error(e)
return True
# if shelf ID is set, we are editing a shelf
def create_edit_shelf(shelf, page_title, page, shelf_id=False):
sync_only_selected_shelves = current_user.kobo_only_shelves_sync
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
if request.method == "POST":
to_save = request.form.to_dict()
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by(
ub.BookShelf.order.asc()).all()
counter = 0
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()
try:
ub.session.commit()
except (OperationalError, InvalidRequestError):
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
flash(_("Sorry you are not allowed to create a public shelf"), category="error")
return redirect(url_for('web.index'))
is_public = 1 if to_save.get("is_public") == "on" else 0
if config.config_kobo_sync:
shelf.kobo_sync = True if to_save.get("kobo_sync") else False
if shelf.kobo_sync:
ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id).filter(
ub.ShelfArchive.uuid == shelf.uuid).delete()
ub.session_commit()
shelf_title = to_save.get("title", "")
if check_shelf_is_unique(shelf_title, is_public, shelf_id):
shelf.name = shelf_title
shelf.is_public = is_public
if not shelf_id:
shelf.user_id = int(current_user.id)
ub.session.add(shelf)
shelf_action = "created"
flash_text = _("Shelf %(title)s created", title=shelf_title)
else:
shelf_action = "changed"
flash_text = _("Shelf %(title)s changed", title=shelf_title)
try:
ub.session.commit()
log.info("Shelf {} {}".format(shelf_title, shelf_action))
flash(flash_text, category="success")
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
except (OperationalError, InvalidRequestError) as ex:
ub.session.rollback()
log.error_or_exception(ex)
log.error_or_exception("Settings Database error: {}".format(ex))
flash(_("Oops! Database Error: %(error)s.", error=ex.orig), category="error")
except Exception as ex:
ub.session.rollback()
log.error_or_exception(ex)
flash(_("There was an error"), category="error")
return render_title_template('shelf_edit.html',
shelf=shelf,
title=page_title,
page=page,
kobo_sync_enabled=config.config_kobo_sync,
sync_only_selected_shelves=sync_only_selected_shelves)
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
result = list()
if shelf and check_shelf_view_permissions(shelf):
result = calibre_db.session.query(db.Books)\
.join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id , isouter=True) \
.add_columns(calibre_db.common_filters().label("visible")) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(ub.BookShelf.order.asc()).all()
return render_title_template('shelf_order.html', entries=result,
title=_(u"Change order of Shelf: '%(name)s'", name=shelf.name),
shelf=shelf, page="shelforder")
def check_shelf_is_unique(title, is_public, shelf_id=False):
if shelf_id:
ident = ub.Shelf.id != shelf_id
else:
ident = true()
if is_public == 1:
is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
.filter(ident) \
.first() is None
if not is_shelf_name_unique:
log.error("A public shelf with the name '{}' already exists.".format(title))
flash(_("A public shelf with the name '%(title)s' already exists.", title=title),
category="error")
else:
is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 0) &
(ub.Shelf.user_id == int(current_user.id))) \
.filter(ident) \
.first() is None
if not is_shelf_name_unique:
log.error("A private shelf with the name '{}' already exists.".format(title))
flash(_("A private shelf with the name '%(title)s' already exists.", title=title),
category="error")
return is_shelf_name_unique
def delete_shelf_helper(cur_shelf):
if not cur_shelf or not check_shelf_edit_permissions(cur_shelf):
return False
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("successfully deleted Shelf {}".format(cur_shelf.name))
return True
def change_shelf_order(shelf_id, order):
result = calibre_db.session.query(db.Books).join(ub.BookShelf,ub.BookShelf.book_id == db.Books.id)\
result = calibre_db.session.query(db.Books).outerjoin(db.books_series_link,
db.Books.id == db.books_series_link.c.book)\
.outerjoin(db.Series).join(ub.BookShelf, ub.BookShelf.book_id == db.Books.id) \
.filter(ub.BookShelf.shelf == shelf_id).order_by(*order).all()
for index, entry in enumerate(result):
book = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
@@ -371,28 +465,37 @@ def change_shelf_order(shelf_id, order):
def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
status = current_user.get_view_property("shelf", 'man')
# check user is allowed to access shelf
if shelf and check_shelf_view_permissions(shelf):
if shelf_type == 1:
# order = [ub.BookShelf.order.asc()]
if sort_param == 'pubnew':
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
if sort_param == 'pubold':
change_shelf_order(shelf_id, [db.Books.pubdate])
if sort_param == 'abc':
change_shelf_order(shelf_id, [db.Books.sort])
if sort_param == 'zyx':
change_shelf_order(shelf_id, [db.Books.sort.desc()])
if sort_param == 'new':
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
if sort_param == 'old':
change_shelf_order(shelf_id, [db.Books.timestamp])
if sort_param == 'authaz':
change_shelf_order(shelf_id, [db.Books.author_sort.asc()])
if sort_param == 'authza':
change_shelf_order(shelf_id, [db.Books.author_sort.desc()])
if status != 'on':
if sort_param == 'stored':
sort_param = current_user.get_view_property("shelf", 'stored')
else:
current_user.set_view_property("shelf", 'stored', sort_param)
if sort_param == 'pubnew':
change_shelf_order(shelf_id, [db.Books.pubdate.desc()])
if sort_param == 'pubold':
change_shelf_order(shelf_id, [db.Books.pubdate])
if sort_param == 'shelfnew':
change_shelf_order(shelf_id, [ub.BookShelf.date_added.desc()])
if sort_param == 'shelfold':
change_shelf_order(shelf_id, [ub.BookShelf.date_added])
if sort_param == 'abc':
change_shelf_order(shelf_id, [db.Books.sort])
if sort_param == 'zyx':
change_shelf_order(shelf_id, [db.Books.sort.desc()])
if sort_param == 'new':
change_shelf_order(shelf_id, [db.Books.timestamp.desc()])
if sort_param == 'old':
change_shelf_order(shelf_id, [db.Books.timestamp])
if sort_param == 'authaz':
change_shelf_order(shelf_id, [db.Books.author_sort.asc(), db.Series.name, db.Books.series_index])
if sort_param == 'authza':
change_shelf_order(shelf_id, [db.Books.author_sort.desc(),
db.Series.name.desc(),
db.Books.series_index.desc()])
page = "shelf.html"
pagesize = 0
else:
@@ -400,29 +503,33 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
page = 'shelfdown.html'
result, __, pagination = calibre_db.fill_indexpage(page_no, pagesize,
db.Books,
ub.BookShelf.shelf == shelf_id,
[ub.BookShelf.order.asc()],
ub.BookShelf,ub.BookShelf.book_id == db.Books.id)
# delete chelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf)\
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True)\
db.Books,
ub.BookShelf.shelf == shelf_id,
[ub.BookShelf.order.asc()],
True, config.config_read_column,
ub.BookShelf, ub.BookShelf.book_id == db.Books.id)
# delete shelf entries where book is not existent anymore, can happen if book is deleted outside calibre-web
wrong_entries = calibre_db.session.query(ub.BookShelf) \
.join(db.Books, ub.BookShelf.book_id == db.Books.id, isouter=True) \
.filter(db.Books.id == None).all()
for entry in wrong_entries:
log.info('Not existing book {} in {} deleted'.format(entry.book_id, shelf))
try:
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == entry.book_id).delete()
ub.session.commit()
except (OperationalError, InvalidRequestError):
except (OperationalError, InvalidRequestError) as e:
ub.session.rollback()
flash(_(u"Settings DB is not Writeable"), category="error")
log.error_or_exception("Settings Database error: {}".format(e))
flash(_("Oops! Database Error: %(error)s.", error=e.orig), category="error")
return render_title_template(page,
entries=result,
pagination=pagination,
title=_(u"Shelf: '%(name)s'", name=shelf.name),
title=_("Shelf: '%(name)s'", name=shelf.name),
shelf=shelf,
page="shelf")
page="shelf",
status=status,
order=sort_param)
else:
flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error")
flash(_("Error opening shelf. Shelf does not exist or is not accessible"), category="error")
return redirect(url_for("web.index"))
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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