Merge branch 'master' into develop

# Conflicts:
#	cps/db.py
#	cps/ub.py
#	cps/web.py
#	readme.md
#	requirements.txt
This commit is contained in:
Jack Darlington
2017-03-07 22:03:10 +00:00
1745 changed files with 422 additions and 315997 deletions
+5 -5
View File
@@ -14,28 +14,28 @@ try:
from wand.image import Image
from wand import version as ImageVersion
use_generic_pdf_cover = False
except ImportError, e:
except ImportError as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
use_generic_pdf_cover = True
try:
from PyPDF2 import PdfFileReader
from PyPDF2 import __version__ as PyPdfVersion
use_pdf_meta = True
except ImportError, e:
except ImportError as e:
logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e)
use_pdf_meta = False
try:
import epub
use_epub_meta = True
except ImportError, e:
except ImportError as e:
logger.warning('cannot import epub, extracting epub metadata will not work: %s', e)
use_epub_meta = False
try:
import fb2
use_fb2_meta = True
except ImportError, e:
except ImportError as e:
logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False
@@ -48,7 +48,7 @@ def process(tmp_file_path, original_file_name, original_file_extension):
return epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension)
if ".FB2" == original_file_extension.upper() and use_fb2_meta is True:
return fb2.get_fb2_info(tmp_file_path, original_file_extension)
except Exception, e:
except Exception as e:
logger.warning('cannot parse metadata, using default: %s', e)
return default_meta(tmp_file_path, original_file_name, original_file_extension)
+21 -27
View File
@@ -11,10 +11,8 @@ from ub import config
import ub
session = None
cc_exceptions = None
cc_classes = {}
cc_ids = []
books_custom_column_links = {}
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
cc_classes = None
engine = None
@@ -283,24 +281,19 @@ class Custom_Columns(Base):
def setup_db():
global session
global cc_exceptions
global cc_classes
global cc_ids
global books_custom_column_links
global engine
global session
global cc_classes
if config.config_calibre_dir is None or config.config_calibre_dir == u'':
return False
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
if not os.path.exists(dbpath):
return False
engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False)
#engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False, isolation_level="SERIALIZABLE")
engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE")
try:
conn = engine.connect()
except:
except Exception as e:
content = ub.session.query(ub.Settings).first()
content.config_calibre_dir = None
content.db_configured = False
@@ -313,19 +306,21 @@ def setup_db():
config.loadSettings()
conn.connection.create_function('title_sort', 1, title_sort)
cc = conn.execute("SELECT id, datatype FROM custom_columns")
if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
for row in cc:
if row.datatype not in cc_exceptions:
if row.id not in books_custom_column_links:
cc_ids = []
books_custom_column_links = {}
cc_classes = {}
for row in cc:
if row.datatype not in cc_exceptions:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' + str(row.id) + '.id'),
primary_key=True)
)
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' + str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
if row.datatype == 'bool':
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
@@ -338,8 +333,7 @@ def setup_db():
'value': Column(String)}
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
for id in cc_ids:
if not hasattr(Books, 'custom_column_' + str(id[0])):
for id in cc_ids:
if id[1] == 'bool':
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
primaryjoin=(
+7 -5
View File
@@ -4,8 +4,10 @@
from lxml import etree
import os
import uploader
import StringIO
try:
from io import StringIO
except ImportError as e:
import StringIO
def get_fb2_info(tmp_file_path, original_file_extension):
@@ -37,16 +39,16 @@ def get_fb2_info(tmp_file_path, original_file_extension):
first_name = u''
return first_name + ' ' + middle_name + ' ' + last_name
author = unicode(", ".join(map(get_author, authors)))
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 = unicode(title[0])
title = str(title[0])
else:
title = u''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description):
description = unicode(description[0])
description = str(description[0])
else:
description = u''
+20 -10
View File
@@ -13,11 +13,18 @@ import os
import traceback
import re
import unicodedata
from StringIO import StringIO
try:
from StringIO import StringIO
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
except ImportError as e:
from io import StringIO
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import encoders
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.generator import Generator
from email.utils import formatdate
from email.utils import make_msgid
@@ -34,7 +41,7 @@ import web
try:
import unidecode
use_unidecode=True
except:
except Exception as e:
use_unidecode=False
# Global variables
@@ -149,7 +156,7 @@ def send_raw_email(kindle_mail, msg):
smtplib.stderr = org_stderr
except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException), e:
except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException) as e:
app.logger.error(traceback.print_exc())
return _("Failed to send mail: %s" % str(e))
@@ -240,8 +247,11 @@ def get_valid_filename(value, replace_whitespace=True):
value=value.replace(u'§',u'SS')
value=value.replace(u'ß',u'ss')
value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile('[^\w\s-]', re.UNICODE)
value = unicode(re_slugify.sub('', value).strip())
re_slugify = re.compile('[\W\s-]', re.UNICODE)
if type(value) is str: #Python3 str, Python2 unicode
value = re_slugify.sub('', value).strip()
else:
value = unicode(re_slugify.sub('', value).strip())
if replace_whitespace:
#*+:\"/<>? werden durch _ ersetzt
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U)
@@ -405,7 +415,7 @@ class Updater(threading.Thread):
try:
os.chown(dst_file, permission.st_uid, permission.st_uid)
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
except:
except Exception as e:
e = sys.exc_info()
logging.getLogger('cps.web').debug('Fail '+str(dst_file)+' error: '+str(e))
return
@@ -447,7 +457,7 @@ class Updater(threading.Thread):
logging.getLogger('cps.web').debug("Delete file " + item_path)
log_from_thread("Delete file " + item_path)
os.remove(item_path)
except:
except Exception as e:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
+180
View File
@@ -0,0 +1,180 @@
/*
* Get Metadata from Douban Books api and Google Books api
* Created by idalin<dalin.lin@gmail.com>
* Google Books api document: https://developers.google.com/books/docs/v1/using
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
*/
$(document).ready(function () {
var msg = i18n_msg;
var douban = 'https://api.douban.com';
var db_search = '/v2/book/search';
var db_get_info = '/v2/book/';
var db_get_info_by_isbn = '/v2/book/isbn/ ';
var db_done = false;
var google = 'https://www.googleapis.com/';
var gg_search = '/books/v1/volumes';
var gg_get_info = '/books/v1/volumes/';
var gg_done = false;
var db_results = [];
var gg_results = [];
var show_flag = 0;
String.prototype.replaceAll = function (s1, s2) {  
return this.replace(new RegExp(s1, "gm"), s2);  
}
gg_search_book = function (title) {
title = title.replaceAll(/\s+/, '+');
var url = google + gg_search + '?q=' + title;
$.ajax({
url: url,
type: "GET",
dataType: "jsonp",
jsonp: 'callback',
success: function (data) {
gg_results = data.items;
},
complete: function () {
gg_done = true;
show_result();
}
});
}
get_meta = function (source, id) {
var meta;
if (source == 'google') {;
meta = gg_results[id];
$('#description').val(meta.volumeInfo.description);
$('#bookAuthor').val(meta.volumeInfo.authors.join(' & '));
$('#book_title').val(meta.volumeInfo.title);
if (meta.volumeInfo.categories) {
var tags = meta.volumeInfo.categories.join(',');
$('#tags').val(tags);
}
if (meta.volumeInfo.averageRating) {
$('#rating').val(Math.round(meta.volumeInfo.averageRating));
}
return;
}
if (source == 'douban') {
meta = db_results[id];
$('#description').val(meta.summary);
$('#bookAuthor').val(meta.author.join(' & '));
$('#book_title').val(meta.title);
var tags = '';
for (var i = 0; i < meta.tags.length; i++) {
tags = tags + meta.tags[i].title + ',';
}
$('#tags').val(tags);
$('#rating').val(Math.round(meta.rating.average / 2));
return;
}
}
do_search = function (keyword) {
show_flag = 0;
$('#meta-info').text(msg.loading);
var keyword = $('#keyword').val();
if (keyword) {
db_search_book(keyword);
gg_search_book(keyword);
}
}
db_search_book = function (title) {
var url = douban + db_search + '?q=' + title + '&fields=all&count=10';
$.ajax({
url: url,
type: "GET",
dataType: "jsonp",
jsonp: 'callback',
success: function (data) {
db_results = data.books;
},
error: function () {
$('#meta-info').html('<p class="text-danger">'+ msg.search_error+'!</p>');
},
complete: function () {
db_done = true;
show_result();
}
});
}
show_result = function () {
show_flag++;
if (show_flag == 1) {
$('#meta-info').html('<ul id="book-list" class="media-list"></ul>');
}
if (gg_done && db_done) {
if (!gg_results && !db_results) {
$('#meta-info').html('<p class="text-danger">'+ msg.no_result +'</p>');
return;
}
}
if (gg_done && gg_results.length > 0) {
for (var i = 0; i < gg_results.length; i++) {
var book = gg_results[i];
var book_cover;
if (book.volumeInfo.imageLinks) {
book_cover = book.volumeInfo.imageLinks.thumbnail;
} else {
book_cover = '/static/generic_cover.jpg';
}
var book_html = '<li class="media">' +
'<img class="pull-left img-responsive" data-toggle="modal" data-target="#metaModal" src="' +
book_cover + '" alt="Cover" style="width:100px;height:150px" onclick=\'javascript:get_meta("google",' +
i + ')\'>' +
'<div class="media-body">' +
'<h4 class="media-heading"><a href="https://books.google.com/books?id=' +
book.id + '" target="_blank">' + book.volumeInfo.title + '</a></h4>' +
'<p>'+ msg.author +':' + book.volumeInfo.authors + '</p>' +
'<p>'+ msg.publisher + ':' + book.volumeInfo.publisher + '</p>' +
'<p>'+ msg.description + ':' + book.volumeInfo.description + '</p>' +
'<p>'+ msg.source + ':<a href="https://books.google.com" target="_blank">Google Books</a></p>' +
'</div>' +
'</li>';
$("#book-list").append(book_html);
}
gg_done = false;
}
if (db_done && db_results.length > 0) {
for (var i = 0; i < db_results.length; i++) {
var book = db_results[i];
var book_html = '<li class="media">' +
'<img class="pull-left img-responsive" data-toggle="modal" data-target="#metaModal" src="' +
book.image + '" alt="Cover" style="width:100px;height: 150px" onclick=\'javascript:get_meta("douban",' +
i + ')\'>' +
'<div class="media-body">' +
'<h4 class="media-heading"><a href="https://book.douban.com/subject/' +
book.id + '" target="_blank">' + book.title + '</a></h4>' +
'<p>' + msg.author + ':' + book.author + '</p>' +
'<p>' + msg.publisher + ':' + book.publisher + '</p>' +
'<p>' + msg.description + ':' + book.summary + '</p>' +
'<p>' + msg.source + ':<a href="https://book.douban.com" target="_blank">Douban Books</a></p>' +
'</div>' +
'</li>';
$("#book-list").append(book_html);
}
db_done = false;
}
}
$('#do-search').click(function () {
var keyword = $('#keyword').val();
if (keyword) {
do_search(keyword);
}
});
$('#get_meta').click(function () {
var book_title = $('#book_title').val();
if (book_title) {
$('#keyword').val(book_title);
do_search(book_title);
}
});
});
+1
View File
@@ -0,0 +1 @@
!function(a){"use strict";function b(a){return"[data-value"+(a?"="+a:"")+"]"}function c(a,b,c){var d=c.activeIcon,e=c.inactiveIcon;a.removeClass(b?e:d).addClass(b?d:e)}function d(b,c){var d=a.extend({},i,b.data(),c);return d.inline=""===d.inline||d.inline,d.readonly=""===d.readonly||d.readonly,d.clearable===!1?d.clearableLabel="":d.clearableLabel=d.clearable,d.clearable=""===d.clearable||d.clearable,d}function e(b,c){if(c.inline)var d=a('<span class="rating-input"></span>');else var d=a('<div class="rating-input"></div>');d.addClass(b.attr("class")),d.removeClass("rating");for(var e=c.min;e<=c.max;e++)d.append('<i class="'+c.iconLib+'" data-value="'+e+'"></i>');return c.clearable&&!c.readonly&&d.append("&nbsp;").append('<a class="'+f+'"><i class="'+c.iconLib+" "+c.clearableIcon+'"/>'+c.clearableLabel+"</a>"),d}var f="rating-clear",g="."+f,h="hidden",i={min:1,max:5,"empty-value":0,iconLib:"glyphicon",activeIcon:"glyphicon-star",inactiveIcon:"glyphicon-star-empty",clearable:!1,clearableIcon:"glyphicon-remove",clearableRemain:!1,inline:!1,readonly:!1},j=function(a,b){var c=this.$input=a;this.options=d(c,b);var f=this.$el=e(c,this.options);c.addClass(h).before(f),c.attr("type","hidden"),this.highlight(c.val())};j.VERSION="0.4.0",j.DEFAULTS=i,j.prototype={clear:function(){this.setValue(this.options["empty-value"])},setValue:function(a){this.highlight(a),this.updateInput(a)},highlight:function(a,d){var e=this.options,f=this.$el;if(a>=this.options.min&&a<=this.options.max){var i=f.find(b(a));c(i.prevAll("i").andSelf(),!0,e),c(i.nextAll("i"),!1,e)}else c(f.find(b()),!1,e);d||(this.options.clearableRemain?f.find(g).removeClass(h):a&&a!=this.options["empty-value"]?f.find(g).removeClass(h):f.find(g).addClass(h))},updateInput:function(a){var b=this.$input;b.val()!=a&&b.val(a).change()}};var k=a.fn.rating=function(c){return this.filter("input[type=number]").each(function(){var d=a(this),e="object"==typeof c&&c||{},f=new j(d,e);f.options.readonly||f.$el.on("mouseenter",b(),function(){f.highlight(a(this).data("value"),!0)}).on("mouseleave",b(),function(){f.highlight(d.val(),!0)}).on("click",b(),function(){f.setValue(a(this).data("value"))}).on("click",g,function(){f.clear()})})};k.Constructor=j,a(function(){a("input.rating[type=number]").each(function(){a(this).rating()})})}(jQuery);
+7
View File
@@ -65,6 +65,13 @@ $(function() {
}
});
});
$("#restart_database").click(function() {
$.ajax({
dataType: 'json',
url: window.location.pathname+"/../../shutdown",
data: {"parameter":2}
});
});
$("#perform_update").click(function() {
$('#spinner2').show();
$.ajax({
+1
View File
@@ -80,6 +80,7 @@
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
<p></p>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-web')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-web')}}</div>
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
+41 -1
View File
@@ -39,7 +39,7 @@
</div>
<div class="form-group">
<label for="rating">{{_('Rating')}}</label>
<input type="number" min="0" max="5" step="1" class="form-control" name="rating" id="rating" value="{% if book.ratings %}{{book.ratings[0].rating / 2}}{% endif %}">
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
</div>
<div class="form-group">
<label for="cover_url">{{_('Cover URL (jpg)')}}</label>
@@ -104,16 +104,56 @@
<input name="detail_view" type="checkbox" checked> {{_('view book after edit')}}
</label>
</div>
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Get Metadata')}}</a>
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
<a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
</form>
</div>
{% endif %}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="metaModalLabel">{{_('Get metadata')}}</h4>
<form class="form-inline">
<div class="form-group">
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
<input type="text" class="form-control" id="keyword" placeholder="{{_(" Search keyword ")}}">
</div>
<button type="button" class="btn btn-default" id="do-search">{{_("Go!")}}</button>
<span>{{_('Click the cover to load metadata to the form')}}</span>
</form>
</div>
<div class="modal-body" id="meta-info">
{{_("Loading...")}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
var i18n_msg = {
'loading': {{_('Loading...')|safe|tojson}},
'search_error': {{_('Search error!')|safe|tojson}},
'no_result': {{_('No Result! Please try anonther keyword.')|safe|tojson}},
'author': {{_('Author')|safe|tojson}},
'publisher': {{_('Publisher')|safe|tojson}},
'description': {{_('Description')|safe|tojson}},
'source': {{_('Source')|safe|tojson}},
};
</script>
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
<<<<<<< HEAD
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
=======
<script src="{{ url_for('static', filename='js/get_meta.js') }}"></script>
{% endblock %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">
+4 -1
View File
@@ -62,7 +62,10 @@
<label for="config_random_books">{{_('No. of random books to show')}}</label>
<input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off">
</div>
<div class="form-group">
<label for="config_columns_to_ignore">{{_('Regular expression for ignoring columns')}}</label>
<input type="text" class="form-control" name="config_columns_to_ignore" id="config_columns_to_ignore" value="{% if content.config_columns_to_ignore != None %}{{ content.config_columns_to_ignore }}{% endif %}" autocomplete="off">
</div>
<div class="form-group">
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if content.config_title_regex != None %}{{ content.config_title_regex }}{% endif %}" autocomplete="off">
+2
View File
@@ -35,6 +35,7 @@
<uri>https://github.com/janeczku/calibre-web</uri>
</author>
{% if entries[0] %}
{% for entry in entries %}
<entry>
<title>{{entry.title}}</title>
@@ -60,6 +61,7 @@
{% endfor %}
</entry>
{% endfor %}
{% endif %}
{% for entry in listelements %}
<entry>
<title>{{entry.name}}</title>
+2
View File
@@ -40,6 +40,7 @@
<div class="discover load-more">
<h2>{{title}}</h2>
<div class="row">
{% if entries[0] %}
{% for entry in entries %}
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
@@ -76,6 +77,7 @@
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
Binary file not shown.
@@ -325,7 +325,7 @@ msgstr "发送测试邮件时发生错误: %(res)s"
#: cps/web.py:1816
msgid "E-Mail settings updated"
msgstr ""
msgstr "E-Mail 设置已更新"
#: cps/web.py:1817
msgid "Edit mail settings"
@@ -357,11 +357,11 @@ msgstr "编辑元数据"
#: cps/web.py:2162
#, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr ""
msgstr "不能上传后缀为 \"%s\" 的文件到此服务器"
#: cps/web.py:2168
msgid "File to be uploaded must have an extension"
msgstr ""
msgstr "要上传的文件必须有一个后缀"
#: cps/web.py:2185
#, python-format
+12 -3
View File
@@ -11,6 +11,7 @@ import logging
from werkzeug.security import generate_password_hash
from flask_babel import gettext as _
import json
#from builtins import str
dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep), "app.db")
engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False)
@@ -92,7 +93,7 @@ class UserBase:
return False
def get_id(self):
return unicode(self.id)
return str(self.id)
def filter_language(self):
return self.default_language
@@ -269,6 +270,7 @@ class Settings(Base):
config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0)
config_default_role = Column(SmallInteger, default=0)
config_columns_to_ignore = Column(String)
config_use_google_drive = Column(Boolean)
config_google_drive_client_id = Column(String)
config_google_drive_client_secret = Column(String)
@@ -301,6 +303,7 @@ class Config:
self.config_anonbrowse = data.config_anonbrowse
self.config_public_reg = data.config_public_reg
self.config_default_role = data.config_default_role
self.config_columns_to_ignore = data.config_columns_to_ignore
self.config_use_google_drive = data.config_use_google_drive
self.config_google_drive_client_id = data.config_google_drive_client_id
self.config_google_drive_client_secret = data.config_google_drive_client_secret
@@ -406,6 +409,12 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_calibre_url_base` INTEGER DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
try:
session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
except exc.OperationalError:
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_columns_to_ignore` String DEFAULT ''")
session.commit()
try:
session.query(exists().where(Settings.config_default_role)).scalar()
session.commit()
@@ -484,7 +493,7 @@ def create_anonymous_user():
session.add(user)
try:
session.commit()
except:
except Exception as e:
session.rollback()
pass
@@ -503,7 +512,7 @@ def create_admin_user():
session.add(user)
try:
session.commit()
except:
except Exception as e:
session.rollback()
pass
+82 -34
View File
@@ -21,9 +21,8 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy import __version__ as sqlalchemyVersion
from math import ceil
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_login import __version__ as flask_loginVersion
from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed
from flask_login import __version__ as flask_principalVersion
from flask_principal import __version__ as flask_principalVersion
from flask_babel import Babel
from flask_babel import gettext as _
import requests
@@ -59,6 +58,18 @@ import threading
from tornado import version as tornadoVersion
try:
from urllib.parse import quote
from imp import reload
from past.builtins import xrange
except ImportError as e:
from urllib import quote
try:
from flask_login import __version__ as flask_loginVersion
except ImportError, e:
from flask_login.__about__ import __version__ as flask_loginVersion
import time
current_milli_time = lambda: int(round(time.time() * 1000))
@@ -67,7 +78,7 @@ try:
from wand.image import Image
use_generic_pdf_cover = False
except ImportError, e:
except ImportError as e:
use_generic_pdf_cover = True
from cgi import escape
@@ -209,6 +220,15 @@ lm.anonymous_user = ub.Anonymous
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'
db.setup_db()
if config.config_log_level == logging.DEBUG :
logging.getLogger("sqlalchemy.engine").addHandler(file_handler)
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
logging.getLogger("sqlalchemy.pool").addHandler(file_handler)
logging.getLogger("sqlalchemy.pool").setLevel(config.config_log_level)
logging.getLogger("sqlalchemy.orm").addHandler(file_handler)
logging.getLogger("sqlalchemy.orm").setLevel(config.config_log_level)
def is_gdrive_ready():
return os.path.exists('settings.yaml') and os.path.exists('gdrive_credentials')
@@ -361,7 +381,7 @@ def shortentitle_filter(s):
def mimetype_filter(val):
try:
s = mimetypes.types_map['.' + val]
except:
except Exception as e:
s = 'application/octet-stream'
return s
@@ -643,7 +663,13 @@ def feed_hot():
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
entries.append(db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first())
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
if downloadBook:
entries.append(
db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first())
else:
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
ub.session.commit()
numBooks = entries.__len__()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, numBooks)
xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
@@ -881,7 +907,7 @@ def get_updater_status():
elif request.method == "GET":
try:
status['status']=helper.updater_thread.get_update_status()
except:
except Exception as e:
status['status'] = 7
return json.dumps(status)
@@ -896,7 +922,7 @@ def get_languages_json():
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
entries = [s for s in languages if query in s.name.lower()]
json_dumps = json.dumps([dict(name=r.name) for r in entries])
@@ -966,9 +992,13 @@ def hot_books(page):
hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list()
for book in hot_books:
entry=db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first()
if entry:
entries.append(entry)
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
if downloadBook:
entries.append(
db.session.query(db.Books).filter(filter).filter(db.Books.id == book.Downloads.book_id).first())
else:
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
ub.session.commit()
numBooks = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, numBooks)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
@@ -1058,13 +1088,13 @@ def language_overview():
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
else:
try:
langfound = 1
cur_l = LC.parse(current_user.filter_language())
except:
except Exception as e:
langfound = 0
languages = db.session.query(db.Languages).filter(
db.Languages.lang_code == current_user.filter_language()).all()
@@ -1088,7 +1118,7 @@ def language(name, page):
try:
cur_l = LC.parse(name)
name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
name = _(isoLanguages.get(part3=name).name)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
title=_(u"Language: %(name)s", name=name))
@@ -1149,10 +1179,19 @@ def show_book(id):
try:
entries.languages[index].language_name = LC.parse(entries.languages[index].lang_code).get_language_name(
get_locale())
except:
except Exception as e:
entries.languages[index].language_name = _(
isoLanguages.get(part3=entries.languages[index].lang_code).name)
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
if config.config_columns_to_ignore:
cc=[]
for col in tmpcc:
r= re.compile(config.config_columns_to_ignore)
if r.match(col.label):
cc.append(col)
else:
cc=tmpcc
book_in_shelfs = []
shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == id).all()
for entry in shelfs:
@@ -1199,6 +1238,8 @@ def stats():
stdin=subprocess.PIPE)
p.wait()
for lines in p.stdout.readlines():
if type(lines) is bytes:
lines = lines.decode('utf-8')
if re.search('Amazon kindlegen\(', lines):
versions['KindlegenVersion'] = lines
versions['PythonVersion'] = sys.version
@@ -1334,6 +1375,11 @@ def shutdown():
showtext['text'] = _(u'Performing shutdown of server, please close window')
return json.dumps(showtext)
else:
if task == 2:
db.session.close()
db.engine.dispose()
db.setup_db()
return json.dumps({})
abort(404)
@app.route("/update")
@@ -1396,7 +1442,7 @@ def advanced_search():
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
searchterm.extend(language.name for language in language_names)
searchterm = " + ".join(filter(None, searchterm))
@@ -1428,7 +1474,7 @@ def advanced_search():
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
else:
languages = None
@@ -1553,22 +1599,22 @@ def read_book(book_id, format):
zfile.close()
return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"))
elif format.lower() == "pdf":
all_name = str(book_id) + "/" + urllib.quote(book.data[0].name) + ".pdf"
tmp_file = os.path.join(book_dir, urllib.quote(book.data[0].name)) + ".pdf"
all_name = str(book_id) + "/" + book.data[0].name + ".pdf"
tmp_file = os.path.join(book_dir, book.data[0].name) + ".pdf"
if not os.path.exists(tmp_file):
pdf_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".pdf"
copyfile(pdf_file, tmp_file)
return render_title_template('readpdf.html', pdffile=all_name, title=_(u"Read a Book"))
elif format.lower() == "txt":
all_name = str(book_id) + "/" + urllib.quote(book.data[0].name) + ".txt"
tmp_file = os.path.join(book_dir, urllib.quote(book.data[0].name)) + ".txt"
all_name = str(book_id) + "/" + book.data[0].name + ".txt"
tmp_file = os.path.join(book_dir, book.data[0].name) + ".txt"
if not os.path.exists(all_name):
txt_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".txt"
copyfile(txt_file, tmp_file)
return render_title_template('readtxt.html', txtfile=all_name, title=_(u"Read a Book"))
elif format.lower() == "cbr":
all_name = str(book_id) + "/" + urllib.quote(book.data[0].name) + ".cbr"
tmp_file = os.path.join(book_dir, urllib.quote(book.data[0].name)) + ".cbr"
all_name = str(book_id) + "/" + book.data[0].name + ".cbr"
tmp_file = os.path.join(book_dir, book.data[0].name) + ".cbr"
if not os.path.exists(all_name):
cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".cbr"
copyfile(cbr_file, tmp_file)
@@ -1635,7 +1681,7 @@ def register():
try:
ub.session.add(content)
ub.session.commit()
except:
except Exception as e:
ub.session.rollback()
flash(_(u"An unknown error occured. Please try again later."), category="error")
return render_title_template('register.html', title=_(u"register"))
@@ -1760,7 +1806,7 @@ def create_shelf():
ub.session.add(shelf)
ub.session.commit()
flash(_(u"Shelf %(title)s created", title=to_save["title"]), category="success")
except:
except Exception as e:
flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"create a shelf"))
else:
@@ -1788,7 +1834,7 @@ def edit_shelf(shelf_id):
try:
ub.session.commit()
flash(_(u"Shelf %(title)s changed", title=to_save["title"]), category="success")
except:
except Exception as e:
flash(_(u"There was an error"), category="error")
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"))
else:
@@ -1876,7 +1922,7 @@ def profile():
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
translations = babel.list_translations() + [LC('en')]
for book in content.downloads:
@@ -2007,6 +2053,8 @@ def configuration_helper(origin):
reboot_required = True
if "config_calibre_web_title" in to_save:
content.config_calibre_web_title = to_save["config_calibre_web_title"]
if "config_columns_to_ignore" in to_save:
content.config_columns_to_ignore = to_save["config_columns_to_ignore"]
if "config_title_regex" in to_save:
if content.config_title_regex != to_save["config_title_regex"]:
content.config_title_regex = to_save["config_title_regex"]
@@ -2086,7 +2134,7 @@ def new_user():
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
translations = [LC('en')] + babel.list_translations()
if request.method == "POST":
@@ -2186,7 +2234,7 @@ def edit_user(user_id):
try:
cur_l = LC.parse(lang.lang_code)
lang.name = cur_l.get_language_name(get_locale())
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
translations = babel.list_translations() + [LC('en')]
for book in content.downloads:
@@ -2313,7 +2361,7 @@ def edit_book(book_id):
try:
book.languages[index].language_name = LC.parse(book.languages[index].lang_code).get_language_name(
get_locale())
except:
except Exception as e:
book.languages[index].language_name = _(isoLanguages.get(part3=book.languages[index].lang_code).name)
for author in book.authors:
author_names.append(author.name)
@@ -2363,7 +2411,7 @@ def edit_book(book_id):
for lang in languages:
try:
lang.name = LC.parse(lang.lang_code).get_language_name(get_locale()).lower()
except:
except Exception as e:
lang.name = _(isoLanguages.get(part3=lang.lang_code).name).lower()
for inp_lang in input_languages:
if inp_lang == lang.name:
@@ -2554,12 +2602,12 @@ def upload():
return redirect(url_for('index'))
try:
copyfile(meta.file_path, saved_filename)
except OSError, e:
except OSError as e:
flash(_(u"Failed to store file %s (Permission denied)." % saved_filename), category="error")
return redirect(url_for('index'))
try:
os.unlink(meta.file_path)
except OSError, e:
except OSError as e:
flash(_(u"Failed to delete file %s (Permission denied)." % meta.file_path), category="warning")
file_size = os.path.getsize(saved_filename)
@@ -2591,7 +2639,7 @@ def upload():
db.session.add(db_language)
# combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\','/')
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 01, 01), 1,
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), 1,
datetime.datetime.now(), path, has_cover, db_author, [], db_language)
db_book.authors.append(db_author)
if db_language is not None: