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']