From 499a66dfb0ff47e4f5a0a3116db640d2ce7ce490 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 30 Jun 2019 09:02:59 +0200 Subject: [PATCH] Additional glyphicons for music on search and author page Fix duplicate user and email (now case insensitive) Output of calibre on stderr is now logged (full traceback in debug-log, otherwise, only errormessage) Natural sorting for comic reader Fix for long running tasks --- cps/admin.py | 33 ++++++++++--- cps/helper.py | 33 ++++++++++++- cps/logger.py | 2 +- cps/opds.py | 5 +- cps/static/js/archive/archive.js | 80 +++++++++++++++++++++++++------- cps/static/js/archive/unrar.js | 8 +--- cps/static/js/archive/untar.js | 3 +- cps/static/js/archive/unzip.js | 55 +--------------------- cps/subproc_wrapper.py | 4 +- cps/templates/author.html | 5 ++ cps/templates/search.html | 7 ++- cps/worker.py | 31 +++++-------- 12 files changed, 153 insertions(+), 113 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 4984e2f0..38b4ad95 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -603,13 +603,23 @@ def new_user(): return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, registered_oauth=oauth_check, title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) - content.nickname = to_save["nickname"] - if config.config_public_reg and not check_valid_domain(to_save["email"]): - flash(_(u"E-mail is not from valid domain"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - registered_oauth=oauth_check, title=_(u"Add new user")) + existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\ + .first() + existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower())\ + .first() + if not existing_user and not existing_email: + content.nickname = to_save["nickname"] + if config.config_public_reg and not check_valid_domain(to_save["email"]): + flash(_(u"E-mail is not from valid domain"), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + registered_oauth=oauth_check, title=_(u"Add new user")) + else: + content.email = to_save["email"] else: - content.email = to_save["email"] + flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") + return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, + languages=languages, title=_(u"Add new user"), page="newuser", + registered_oauth=oauth_check) try: ub.session.add(content) ub.session.commit() @@ -753,7 +763,16 @@ def edit_user(user_id): if "locale" in to_save and to_save["locale"]: content.locale = to_save["locale"] if to_save["email"] and to_save["email"] != content.email: - content.email = to_save["email"] + existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ + .first() + if not existing_email: + content.email = to_save["email"] + else: + flash(_(u"Found an existing account for this e-mail address."), category="error") + return render_title_template("user_edit.html", translations=translations, languages=languages, + new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: content.kindle_mail = to_save["kindle_mail"] try: diff --git a/cps/helper.py b/cps/helper.py index eb5374bc..5520b6af 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -30,13 +30,14 @@ import requests import shutil import time import unicodedata -from datetime import datetime +from datetime import datetime, timedelta from functools import reduce from tempfile import gettempdir from babel import Locale as LC from babel.core import UnknownLocaleError -from babel.dates import format_datetime +from babel.dates import format_datetime, format_timedelta +from babel.units import format_unit from flask import send_from_directory, make_response, redirect, abort from flask_babel import gettext as _ from flask_login import current_user @@ -589,8 +590,34 @@ def json_serial(obj): if isinstance(obj, (datetime)): return obj.isoformat() + if isinstance(obj, (timedelta)): + return { + '__type__': 'timedelta', + 'days': obj.days, + 'seconds': obj.seconds, + 'microseconds': obj.microseconds, + } + # return obj.isoformat() raise TypeError ("Type %s not serializable" % type(obj)) + +# helper function for displaying the runtime of tasks +def format_runtime(runtime): + retVal = "" + if runtime.days: + retVal = format_unit(runtime.days, 'duration-day', length="long", locale=web.get_locale()) + ', ' + mins, seconds = divmod(runtime.seconds, 60) + hours, minutes = divmod(mins, 60) + # ToDo: locale.number_symbols._data['timeSeparator'] -> localize time separator ? + if hours: + retVal += '{:d}:{:02d}:{:02d}s'.format(hours, minutes, seconds) + elif minutes: + retVal += '{:2d}:{:02d}s'.format(minutes, seconds) + else: + retVal += '{:2d}s'.format(seconds) + return retVal + + # helper function to apply localize status information in tasklist entries def render_task_status(tasklist): renderedtasklist=list() @@ -603,6 +630,8 @@ def render_task_status(tasklist): if 'starttime' not in task: task['starttime'] = "" + task['runtime'] = format_runtime(task['formRuntime']) + # localize the task status if isinstance( task['stat'], int ): if task['stat'] == STAT_WAITING: diff --git a/cps/logger.py b/cps/logger.py index 6baefa14..6b13f50d 100644 --- a/cps/logger.py +++ b/cps/logger.py @@ -107,7 +107,7 @@ def setup(log_file, log_level=None): return r.debug("logging to %s level %s", log_file, r.level) - if 1 == 1: # log_file == LOG_TO_STDERR: + if log_file == LOG_TO_STDERR: file_handler = StreamHandler() file_handler.baseFilename = LOG_TO_STDERR else: diff --git a/cps/opds.py b/cps/opds.py index ee3e86fc..48e2b968 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -46,14 +46,13 @@ ldap_support = ldap1.ldap_supported() def requires_basic_auth_if_no_ano(f): @wraps(f) def decorated(*args, **kwargs): - if config.config_login_type == 1 and ldap_support: - return ldap1.ldap.basic_auth_required(*args, **kwargs) auth = request.authorization if config.config_anonbrowse != 1: if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) - + if config.config_login_type == 1 and ldap_support: + return ldap1.ldap.basic_auth_required(f) return decorated diff --git a/cps/static/js/archive/archive.js b/cps/static/js/archive/archive.js index b7c38cb0..cfc7bd40 100644 --- a/cps/static/js/archive/archive.js +++ b/cps/static/js/archive/archive.js @@ -1,3 +1,67 @@ +/* alphanum.js (C) Brian Huisman + * Based on the Alphanum Algorithm by David Koelle + * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com + * + * Distributed under same license as original + * + * Released under the MIT License - https://opensource.org/licenses/MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + /* ******************************************************************** + * Alphanum sort() function version - case insensitive + * - Slower, but easier to modify for arrays of objects which contain + * string properties + * + */ +function alphanumCase(a, b) { + function chunkify(t) { + var tz = new Array(); + var x = 0, y = -1, n = 0, i, j; + + while (i = (j = t.charAt(x++)).charCodeAt(0)) { + var m = (i == 46 || (i >=48 && i <= 57)); + if (m !== n) { + tz[++y] = ""; + n = m; + } + tz[y] += j; + } + return tz; + } + + var aa = chunkify(a.filename.toLowerCase()); + var bb = chunkify(b.filename.toLowerCase()); + + for (x = 0; aa[x] && bb[x]; x++) { + if (aa[x] !== bb[x]) { + var c = Number(aa[x]), d = Number(bb[x]); + if (c == aa[x] && d == bb[x]) { + return c - d; + } else return (aa[x] > bb[x]) ? 1 : -1; + } + } + return aa.length - bb.length; +} +// =========================================================================== + + /** * archive.js * @@ -13,22 +77,6 @@ var bitjs = bitjs || {}; bitjs.archive = bitjs.archive || {}; -function naturalCompare(a, b) { - var ax = [], bx = []; - - a.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) }); - b.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) }); - - while(ax.length && bx.length) { - var an = ax.shift(); - var bn = bx.shift(); - var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]); - if(nn) return nn; - } - - return ax.length - bx.length; -} - (function() { // =========================================================================== diff --git a/cps/static/js/archive/unrar.js b/cps/static/js/archive/unrar.js index 4c97b4dc..fadb791e 100644 --- a/cps/static/js/archive/unrar.js +++ b/cps/static/js/archive/unrar.js @@ -1332,13 +1332,7 @@ var unrar = function(arrayBuffer) { totalFilesInArchive = localFiles.length; // now we have all information but things are unpacked - // TODO: unpack - localFiles.sort(naturalCompare); - /*localFiles = localFiles.sort(function(a, b) { - var aname = a.filename.toLowerCase(); - var bname = b.filename.toLowerCase(); - return aname > bname ? 1 : -1; - });*/ + localFiles.sort(alphanumCase); info(localFiles.map(function(a) { return a.filename; diff --git a/cps/static/js/archive/untar.js b/cps/static/js/archive/untar.js index e3a2c079..cc1499ef 100644 --- a/cps/static/js/archive/untar.js +++ b/cps/static/js/archive/untar.js @@ -136,7 +136,8 @@ var untar = function(arrayBuffer) { allLocalFiles.push(localFile); postProgress(); } - allLocalFiles.sort(naturalCompare); + // got all local files, now sort them + allLocalFiles.sort(alphanumCase); allLocalFiles.forEach(function(oneLocalFile) { // While we don't encounter an empty block, keep making TarLocalFiles. diff --git a/cps/static/js/archive/unzip.js b/cps/static/js/archive/unzip.js index 46e6ec11..886f4b80 100644 --- a/cps/static/js/archive/unzip.js +++ b/cps/static/js/archive/unzip.js @@ -162,12 +162,7 @@ var unzip = function(arrayBuffer) { totalFilesInArchive = localFiles.length; // got all local files, now sort them - localFiles.sort(naturalCompare); - /*localFiles.sort(function(a, b) { - var aname = a.filename.toLowerCase(); - var bname = b.filename.toLowerCase(); - return aname > bname ? 1 : -1; - });*/ + localFiles.sort(alphanumCase); // archive extra data record if (bstream.peekNumber(4) === zArchiveExtraDataSignature) { @@ -663,51 +658,3 @@ function inflate(compressedData, numDecompressedBytes) { onmessage = function(event) { unzip(event.data.file, true); }; - -/* -function naturalCompare(a, b) { - var ax = [], bx = []; - - a.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) }); - b.filename.toLowerCase().replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) }); - - while(ax.length && bx.length) { - var an = ax.shift(); - var bn = bx.shift(); - var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]); - if(nn) return nn; - } - - return ax.length - bx.length; -}*/ - - -/*var re = /([a-z]+)(\d+)(.+)/i; -function naturalCompare(a, b) { - var ma = a.match(re), - mb = b.match(re), - a_str = ma[1], - b_str = mb[1], - a_num = parseInt(ma[2],10), - b_num = parseInt(mb[2],10), - a_rem = ma[3], - b_rem = mb[3]; - return a_str > b_str ? 1 : a_str < b_str ? -1 : a_num > b_num ? 1 : a_num < b_num ? -1 : a_rem > b_rem; -}*/ - -/*function naturalCompare(a, b) { - var ax = [], bx = []; - - a.replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) }); - b.replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) }); - - while(ax.length && bx.length) { - var an = ax.shift(); - var bn = bx.shift(); - var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]); - if(nn) return nn; - } - - return ax.length - bx.length; -}*/ - diff --git a/cps/subproc_wrapper.py b/cps/subproc_wrapper.py index 0371fd7e..8dceca65 100644 --- a/cps/subproc_wrapper.py +++ b/cps/subproc_wrapper.py @@ -23,7 +23,7 @@ import os import subprocess -def process_open(command, quotes=(), env=None, sout=subprocess.PIPE): +def process_open(command, quotes=(), env=None, sout=subprocess.PIPE, serr=subprocess.PIPE): # Linux py2.7 encode as list without quotes no empty element for parameters # linux py3.x no encode and as list without quotes no empty element for parameters # windows py2.7 encode as string with quotes empty element for parameters is okay @@ -42,4 +42,4 @@ def process_open(command, quotes=(), env=None, sout=subprocess.PIPE): else: exc_command = [x for x in command] - return subprocess.Popen(exc_command, shell=False, stdout=sout, universal_newlines=True, env=env) + return subprocess.Popen(exc_command, shell=False, stdout=sout, stderr=serr, universal_newlines=True, env=env) diff --git a/cps/templates/author.html b/cps/templates/author.html index 14883ef7..27a16fbc 100644 --- a/cps/templates/author.html +++ b/cps/templates/author.html @@ -64,6 +64,11 @@ {{author.name.replace('|',',')|shortentitle(30)}} {% endif %} {% endfor %} + {% for format in entry.data %} + {% if format.format|lower == 'mp3' %} + + {% endif %} + {% endfor %}

{% if entry.ratings.__len__() > 0 %}
diff --git a/cps/templates/search.html b/cps/templates/search.html index 21aee4ae..dfa42799 100644 --- a/cps/templates/search.html +++ b/cps/templates/search.html @@ -61,7 +61,7 @@ {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %} {% if not loop.first %} & - {% endif %} + {% endif %} {{author.name.replace('|',',')|shortentitle(30)}} {% if loop.last %} (...) @@ -73,6 +73,11 @@ {{author.name.replace('|',',')|shortentitle(30)}} {% endif %} {% endfor %} + {% for format in entry.data %} + {% if format.format|lower == 'mp3' %} + + {% endif %} + {% endfor %}

{% if entry.ratings.__len__() > 0 %}
diff --git a/cps/worker.py b/cps/worker.py index 345d79bb..f074cf7b 100644 --- a/cps/worker.py +++ b/cps/worker.py @@ -25,7 +25,7 @@ import smtplib import socket import time import threading -from datetime import datetime +from datetime import datetime, timedelta try: from StringIO import StringIO @@ -226,8 +226,10 @@ class WorkerThread(threading.Thread): if self.UIqueue[self.current]['stat'] == STAT_STARTED: if self.queue[self.current]['taskType'] == TASK_EMAIL: self.UIqueue[self.current]['progress'] = self.get_send_status() - self.UIqueue[self.current]['runtime'] = self._formatRuntime( - datetime.now() - self.queue[self.current]['starttime']) + self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime'] + self.UIqueue[self.current]['rt'] = self.UIqueue[self.current]['formRuntime'].days*24*60 \ + + self.UIqueue[self.current]['formRuntime'].seconds \ + + self.UIqueue[self.current]['formRuntime'].microseconds return self.UIqueue def _convert_any_format(self): @@ -336,6 +338,11 @@ class WorkerThread(threading.Thread): # process returncode check = p.returncode + calibre_traceback = p.stderr.readlines() + for ele in calibre_traceback: + log.debug(ele.strip('\n')) + if not ele.startswith('Traceback') and not ele.startswith(' File'): + error_message = "Calibre failed with error: %s" % ele.strip('\n') # kindlegen returncodes # 0 = Info(prcgen):I1036: Mobi file built successfully @@ -491,28 +498,14 @@ class WorkerThread(threading.Thread): self._handleError(u'Error sending email: ' + e.strerror) return None - def _formatRuntime(self, runtime): - self.UIqueue[self.current]['rt'] = runtime.total_seconds() - val = re.split('\:|\.', str(runtime))[0:3] - erg = list() - for v in val: - if int(v) > 0: - erg.append(v) - retVal = (':'.join(erg)).lstrip('0') + ' s' - if retVal == ' s': - retVal = '0 s' - return retVal - def _handleError(self, error_message): log.error(error_message) self.UIqueue[self.current]['stat'] = STAT_FAIL self.UIqueue[self.current]['progress'] = "100 %" - self.UIqueue[self.current]['runtime'] = self._formatRuntime( - datetime.now() - self.queue[self.current]['starttime']) + self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime'] self.UIqueue[self.current]['message'] = error_message def _handleSuccess(self): self.UIqueue[self.current]['stat'] = STAT_FINISH_SUCCESS self.UIqueue[self.current]['progress'] = "100 %" - self.UIqueue[self.current]['runtime'] = self._formatRuntime( - datetime.now() - self.queue[self.current]['starttime']) + self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime']