diff --git a/cps/admin.py b/cps/admin.py index 93c1a3a9..045a9523 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -33,7 +33,7 @@ from functools import wraps from urllib.parse import urlparse from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response -from flask import Markup +from markupsafe import Markup from flask_login import login_required, current_user, logout_user from flask_babel import gettext as _ from flask_babel import get_locale, format_time, format_datetime, format_timedelta diff --git a/cps/db.py b/cps/db.py index f0295fe5..ceb692ec 100644 --- a/cps/db.py +++ b/cps/db.py @@ -663,7 +663,7 @@ class CalibreDB: cls.session_factory = scoped_session(sessionmaker(autocommit=False, autoflush=True, - bind=cls.engine)) + bind=cls.engine, future=True)) for inst in cls.instances: inst.init_session() diff --git a/cps/editbooks.py b/cps/editbooks.py index 5a15740c..40f62713 100755 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -25,16 +25,15 @@ from datetime import datetime import json from shutil import copyfile from uuid import uuid4 -from markupsafe import escape # dependency of flask +from markupsafe import escape, Markup # dependency of flask from functools import wraps -import re try: from lxml.html.clean import clean_html, Cleaner except ImportError: clean_html = None -from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response +from flask import Blueprint, request, flash, redirect, url_for, abort, Response from flask_babel import gettext as _ from flask_babel import lazy_gettext as N_ from flask_babel import get_locale diff --git a/cps/epub.py b/cps/epub.py index c22bad7b..50adba59 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -21,10 +21,11 @@ import zipfile from lxml import etree from . import isoLanguages, cover -from . import config +from . import config, logger from .helper import split_authors from .constants import BookMeta +log = logger.create() def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name): if cover_file is None: @@ -49,15 +50,20 @@ def get_epub_layout(book, book_data): } file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower())) - epubZip = zipfile.ZipFile(file_path) - 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) - p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] + try: + epubZip = zipfile.ZipFile(file_path) + 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) - layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) + tree = etree.fromstring(cf) + p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] + + layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) + except (etree.XMLSyntaxError, KeyError, IndexError) 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 diff --git a/cps/helper.py b/cps/helper.py index 92bcb2ad..0c526d01 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -732,28 +732,27 @@ def delete_book(book, calibrepath, book_format): return delete_book_file(book, calibrepath, book_format) -def get_cover_on_failure(use_generic_cover): - if use_generic_cover: - try: - return send_from_directory(_STATIC_DIR, "generic_cover.jpg") - except PermissionError: - log.error("No permission to access generic_cover.jpg file.") - abort(403) - abort(404) +def get_cover_on_failure(): + try: + return send_from_directory(_STATIC_DIR, "generic_cover.jpg") + except PermissionError: + log.error("No permission to access generic_cover.jpg file.") + abort(403) def get_book_cover(book_id, resolution=None): book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) - return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution) + return get_book_cover_internal(book, resolution=resolution) -# Called only by kobo sync -> cover not found should be answered with 404 and not with default cover def get_book_cover_with_uuid(book_uuid, resolution=None): book = calibre_db.get_book_by_uuid(book_uuid) - return get_book_cover_internal(book, use_generic_cover_on_failure=False, resolution=resolution) + if not book: + return # allows kobo.HandleCoverImageRequest to proxy request + return get_book_cover_internal(book, resolution=resolution) -def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None): +def get_book_cover_internal(book, resolution=None): if book and book.has_cover: # Send the book cover thumbnail if it exists in cache @@ -769,16 +768,16 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None) if config.config_use_google_drive: try: if not gd.is_gdrive_ready(): - return get_cover_on_failure(use_generic_cover_on_failure) + return get_cover_on_failure() path = gd.get_cover_via_gdrive(book.path) if path: return redirect(path) else: log.error('{}/cover.jpg not found on Google Drive'.format(book.path)) - return get_cover_on_failure(use_generic_cover_on_failure) + return get_cover_on_failure() except Exception as ex: log.error_or_exception(ex) - return get_cover_on_failure(use_generic_cover_on_failure) + return get_cover_on_failure() # Send the book cover from the Calibre directory else: @@ -786,9 +785,9 @@ def get_book_cover_internal(book, use_generic_cover_on_failure, resolution=None) if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): return send_from_directory(cover_file_path, "cover.jpg") else: - return get_cover_on_failure(use_generic_cover_on_failure) + return get_cover_on_failure() else: - return get_cover_on_failure(use_generic_cover_on_failure) + return get_cover_on_failure() def get_book_cover_thumbnail(book, resolution): @@ -811,7 +810,7 @@ def get_series_thumbnail_on_failure(series_id, resolution): .filter(db.Books.has_cover == 1) \ .first() - return get_book_cover_internal(book, use_generic_cover_on_failure=True, resolution=resolution) + return get_book_cover_internal(book, resolution=resolution) def get_series_cover_thumbnail(series_id, resolution=None): diff --git a/cps/kobo.py b/cps/kobo.py index a8cdf25c..1655fb5e 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -930,20 +930,26 @@ def get_current_bookmark_response(current_bookmark): @kobo.route("//////image.jpg") @requires_kobo_auth def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale): - book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=COVER_THUMBNAIL_SMALL) - if not book_cover: - if config.config_kobo_proxy: - log.debug("Cover for unknown book: %s proxied to kobo" % book_uuid) - return redirect(KOBO_IMAGEHOST_URL + - "/{book_uuid}/{width}/{height}/false/image.jpg".format(book_uuid=book_uuid, - width=width, - height=height), 307) - else: - log.debug("Cover for unknown book: %s requested" % book_uuid) - # additional proxy request make no sense, -> direct return - return make_response(jsonify({})) - log.debug("Cover request received for book %s" % book_uuid) - return book_cover + try: + resolution = None if int(height) > 1000 else COVER_THUMBNAIL_SMALL + except ValueError: + log.error("Requested height %s of book %s is invalid" % (book_uuid, height)) + resolution = COVER_THUMBNAIL_SMALL + book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=resolution) + if book_cover: + log.debug("Serving local cover image of book %s" % book_uuid) + return book_cover + + if not config.config_kobo_proxy: + log.debug("Returning 404 for cover image of unknown book %s" % book_uuid) + # additional proxy request make no sense, -> direct return + return abort(404) + + log.debug("Redirecting request for cover image of unknown book %s to Kobo" % book_uuid) + return redirect(KOBO_IMAGEHOST_URL + + "/{book_uuid}/{width}/{height}/false/image.jpg".format(book_uuid=book_uuid, + width=width, + height=height), 307) @kobo.route("") diff --git a/cps/metadata_provider/amazon.py b/cps/metadata_provider/amazon.py index a83747e6..30291a3f 100644 --- a/cps/metadata_provider/amazon.py +++ b/cps/metadata_provider/amazon.py @@ -98,7 +98,7 @@ class Amazon(Metadata): try: match.authors = [next( filter(lambda i: i != " " and i != "\n" and not i.startswith("{"), - x.findAll(text=True))).strip() + x.findAll(string=True))).strip() for x in soup2.findAll("span", attrs={"class": "author"})] except (AttributeError, TypeError, StopIteration): match.authors = "" diff --git a/cps/metadata_provider/lubimyczytac.py b/cps/metadata_provider/lubimyczytac.py index e4abe9db..4644cad9 100644 --- a/cps/metadata_provider/lubimyczytac.py +++ b/cps/metadata_provider/lubimyczytac.py @@ -102,7 +102,7 @@ class LubimyCzytac(Metadata): 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 = "//nav[@aria-label='breadcrumb']//a[contains(@href,'/ksiazki/k/')]/text()" + TAGS = "//nav[@aria-label='breadcrumbs']//a[contains(@href,'/ksiazki/k/')]/span/text()" RATING = "//meta[@property='books:rating:value']/@content" COVER = "//meta[@property='og:image']/@content" diff --git a/cps/static/css/caliBlur.css b/cps/static/css/caliBlur.css index dbbea88e..cf743761 100644 --- a/cps/static/css/caliBlur.css +++ b/cps/static/css/caliBlur.css @@ -7279,6 +7279,11 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div. float: right } + body.blur #main-nav + #scnd-nav .create-shelf, body.blur #main-nav + .col-sm-2 #scnd-nav .create-shelf { + float: none; + margin: 5px 0 10px -10px; + } + #main-nav + #scnd-nav .nav-head.hidden-xs { display: list-item !important; width: 225px diff --git a/cps/static/js/details.js b/cps/static/js/details.js index 24b98437..b8b8b21e 100644 --- a/cps/static/js/details.js +++ b/cps/static/js/details.js @@ -40,6 +40,7 @@ $(".sendbtn-form").click(function() { $.ajax({ method: 'post', url: $(this).data('href'), + data: {csrf_token: $("input[name='csrf_token']").val()}, success: function (data) { handleResponse(data) } diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 072abcc0..28b139e6 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -71,7 +71,8 @@ var settings = { fitMode: kthoom.Key.B, theme: "light", direction: 0, // 0 = Left to Right, 1 = Right to Left - scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar + nextPage: 0, // 0 = Reset to Top, 1 = Remember Position + scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar pageDisplay: 0 // 0 = Single Page, 1 = Long Strip }; @@ -131,7 +132,7 @@ var createURLFromArray = function(array, mimeType) { } if ((typeof URL !== "function" && typeof URL !== "object") || - typeof URL.createObjectURL !== "function") { + typeof URL.createObjectURL !== "function") { throw "Browser support for Object URLs is missing"; } @@ -206,7 +207,7 @@ function initProgressClick() { }); } -function loadFromArrayBuffer(ab) { +function loadFromArrayBuffer(ab, lastCompletion = 0) { const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); loadArchiveFormats(['rar', 'zip', 'tar'], function() { // Open the file as an archive @@ -246,7 +247,7 @@ function loadFromArrayBuffer(ab) { } else { $("#left").show(); } - updatePage(); + updatePage(lastCompletion); } } else { totalImages--; @@ -261,17 +262,6 @@ function loadFromArrayBuffer(ab) { } function scrollTocToActive() { - $(".page").text((currentImage + 1 ) + "/" + totalImages); - - // Mark the current page in the TOC - $("#tocView a[data-page]") - // Remove the currently active thumbnail - .removeClass("active") - // Find the new one - .filter("[data-page=" + (currentImage + 1) + "]") - // Set it to active - .addClass("active"); - // Scroll to the thumbnail in the TOC on page change $("#tocView").stop().animate({ scrollTop: $("#tocView a.active").position().top @@ -279,12 +269,31 @@ function scrollTocToActive() { } function updatePage() { + $(".page").text((currentImage + 1 ) + "/" + totalImages); + + // Mark the current page in the TOC + $("#tocView a[data-page]") + // Remove the currently active thumbnail + .removeClass("active") + // Find the new one + .filter("[data-page=" + (currentImage + 1) + "]") + // Set it to active + .addClass("active"); + scrollTocToActive(); - scrollCurrentImageIntoView(); updateProgress(); pageDisplayUpdate(); setTheme(); + if (imageFiles[currentImage]) { + setImage(imageFiles[currentImage].dataURI); + } else { + setImage("loading"); + } + + $("body").toggleClass("dark-theme", settings.theme === "dark"); + $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); + kthoom.setSettings(); kthoom.saveSettings(); } @@ -359,6 +368,7 @@ function setImage(url, _canvas) { img.onerror = function() { canvas.width = innerWidth - 100; canvas.height = 300; + updateScale(true); x.fillStyle = "black"; x.font = "50px sans-serif"; x.strokeStyle = "black"; @@ -412,6 +422,8 @@ function setImage(url, _canvas) { scrollTo(0, 0); x.drawImage(img, 0, 0); + updateScale(false); + canvas.style.display = ""; $("body").css("overflowY", ""); x.restore(); @@ -450,6 +462,9 @@ function showPrevPage() { currentImage++; } else { updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } updateDirectionButtons(); } @@ -461,6 +476,9 @@ function showNextPage() { currentImage--; } else { updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } updateDirectionButtons(); } @@ -477,7 +495,7 @@ function scrollCurrentImageIntoView() { } } -function updateScale() { +function updateScale(clear) { var canvasArray = $("#mainContent > canvas"); var maxheight = innerHeight - 50; @@ -486,7 +504,7 @@ function updateScale() { canvasArray.css("maxWidth", ""); canvasArray.css("maxHeight", ""); - if(settings.pageDisplay === 0) { + if(!clear) { canvasArray.addClass("hide"); pageDisplayUpdate(); } @@ -653,7 +671,7 @@ function init(filename) { request.responseType = "arraybuffer"; request.addEventListener("load", function() { if (request.status >= 200 && request.status < 300) { - loadFromArrayBuffer(request.response); + loadFromArrayBuffer(request.response, currentImage); } else { console.warn(request.statusText, request.responseText); } @@ -708,7 +726,7 @@ function init(filename) { } updatePage(); - updateScale(); + updateScale(false); }); // Close modal @@ -721,6 +739,9 @@ function init(filename) { $("#thumbnails").on("click", "a", function() { currentImage = $(this).data("page") - 1; updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } }); // Fullscreen mode diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 7a1b3376..30430663 100755 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -43,30 +43,30 @@ {% endif %} {% endif %} - {% endif %} - {% if current_user.kindle_mail and entry.email_share_list %} - - {% if entry.email_share_list.__len__() == 1 %} -
- -
- {% else %} -
- - -
+ {% if current_user.kindle_mail and entry.email_share_list %} + + {% if entry.email_share_list.__len__() == 1 %} +
+ +
+ {% else %} +
+ + +
+ {% endif %} {% endif %} {% endif %} {% if entry.reader_list and current_user.role_viewer() %} diff --git a/cps/templates/readcbr.html b/cps/templates/readcbr.html index 8b42cb79..7a54e850 100644 --- a/cps/templates/readcbr.html +++ b/cps/templates/readcbr.html @@ -1,5 +1,6 @@ + @@ -20,23 +21,6 @@ - -
- - - - - - - - - - +
{{_('Settings')}}
{{_('Theme')}}: -
+
+
+
+ + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - -
{{_('Settings')}}
{{_('Theme')}}: +
@@ -139,59 +123,118 @@ - -
{{_('Rotate')}}: -
- - - - -
-
{{_('Flip')}}: -
- - -
-
{{_('Direction')}}: -
+
+
{{_('Rotate')}}: +
+ + + + +
+
{{_('Flip')}}: +
+ + +
+
{{_('Direction')}}: +
{{_('Next Page')}}: +
+ + +
+
{{_('Scrollbar')}}:
-
-
+
+ + + + + +
-
- -
- +
+ + + + diff --git a/cps/uploader.py b/cps/uploader.py index 42b776aa..bf30094d 100644 --- a/cps/uploader.py +++ b/cps/uploader.py @@ -88,7 +88,7 @@ def process(tmp_file_path, original_file_name, original_file_extension, rar_exec log.warning('cannot parse metadata, using default: %s', ex) if not meta.title.strip(): - meta = original_file_name + meta = meta._replace(title=original_file_name) if not meta.author.strip() or meta.author.lower() == 'unknown': meta = meta._replace(author=_('Unknown')) return meta diff --git a/cps/web.py b/cps/web.py index 51ff32b3..9793f01a 100755 --- a/cps/web.py +++ b/cps/web.py @@ -1561,7 +1561,7 @@ def read_book(book_id, book_format): title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.') log.debug("Start comic reader for %d", book_id) return render_title_template('readcbr.html', comicfile=all_name, title=title, - extension=fileExt) + extension=fileExt, bookmark=bookmark) log.debug("Selected book is unavailable. File does not exist or is not accessible") flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"), category="error")