Merge branch 'master' into travis

# Conflicts:
#	cps/epub.py
#	cps/web.py
#	readme.md
This commit is contained in:
林檎 2017-03-08 23:50:14 +08:00
commit dcc0958c39
19 changed files with 381 additions and 1574 deletions

View File

@ -11,10 +11,8 @@ from ub import config
import ub import ub
session = None session = None
cc_exceptions = None cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
cc_classes = None cc_classes = None
cc_ids = None
books_custom_column_links = None
engine = None engine = None
@ -283,12 +281,9 @@ class Custom_Columns(Base):
def setup_db(): def setup_db():
global session
global cc_exceptions
global cc_classes
global cc_ids
global books_custom_column_links
global engine global engine
global session
global cc_classes
if config.config_calibre_dir is None or config.config_calibre_dir == u'': if config.config_calibre_dir is None or config.config_calibre_dir == u'':
return False return False
@ -298,7 +293,6 @@ def setup_db():
engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE") engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE")
try: try:
conn = engine.connect() conn = engine.connect()
except Exception as e: except Exception as e:
content = ub.session.query(ub.Settings).first() content = ub.session.query(ub.Settings).first()
content.config_calibre_dir = None content.config_calibre_dir = None
@ -312,43 +306,43 @@ def setup_db():
config.loadSettings() config.loadSettings()
conn.connection.create_function('title_sort', 1, title_sort) 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_ids = [] cc_ids = []
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series'] books_custom_column_links = {}
books_custom_column_links = {} cc_classes = {}
cc_classes = {} for row in cc:
for row in cc: if row.datatype not in cc_exceptions:
if row.datatype not in cc_exceptions: books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata, Column('book', Integer, ForeignKey('books.id'),
Column('book', Integer, ForeignKey('books.id'), primary_key=True),
primary_key=True), Column('value', Integer,
Column('value', Integer, ForeignKey('custom_column_' + str(row.id) + '.id'),
ForeignKey('custom_column_' + str(row.id) + '.id'), primary_key=True)
primary_key=True) )
) cc_ids.append([row.id, row.datatype])
cc_ids.append([row.id, row.datatype]) if row.datatype == 'bool':
if row.datatype == 'bool': ccdict = {'__tablename__': 'custom_column_' + str(row.id),
ccdict = {'__tablename__': 'custom_column_' + str(row.id), 'id': Column(Integer, primary_key=True),
'id': Column(Integer, primary_key=True), 'book': Column(Integer, ForeignKey('books.id')),
'book': Column(Integer, ForeignKey('books.id')), 'value': Column(Boolean)}
'value': Column(Boolean)} else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'value': Column(String)}
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
for id in cc_ids:
if id[1] == 'bool':
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
primaryjoin=(
Books.id == cc_classes[id[0]].book),
backref='books'))
else: else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id), setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
'id': Column(Integer, primary_key=True), secondary=books_custom_column_links[id[0]],
'value': Column(String)} backref='books'))
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
for id in cc_ids:
if id[1] == 'bool':
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
primaryjoin=(
Books.id == cc_classes[id[0]].book),
backref='books'))
else:
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
secondary=books_custom_column_links[id[0]],
backref='books'))
# Base.metadata.create_all(engine) # Base.metadata.create_all(engine)
Session = sessionmaker() Session = sessionmaker()

View File

@ -41,11 +41,14 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
epub_metadata = {} epub_metadata = {}
<<<<<<< HEAD
try:#maybe description isn't present try:#maybe description isn't present
comments = tree.xpath("//*[local-name() = 'description']/text()")[0] comments = tree.xpath("//*[local-name() = 'description']/text()")[0]
epub_metadata['comments'] = comments epub_metadata['comments'] = comments
except IndexError as e: except IndexError as e:
epub_metadata['comments'] = "" epub_metadata['comments'] = ""
=======
>>>>>>> master
for s in ['title', 'description', 'creator', 'language']: for s in ['title', 'description', 'creator', 'language']:
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns) tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
@ -71,7 +74,11 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
epub_metadata['language'] = isoLanguages.get(part3=lang).name epub_metadata['language'] = isoLanguages.get(part3=lang).name
else: else:
epub_metadata['language'] = "" epub_metadata['language'] = ""
<<<<<<< HEAD
=======
>>>>>>> master
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
coverfile = None coverfile = None
if len(coversection) > 0: if len(coversection) > 0:

View File

@ -14,15 +14,16 @@ import traceback
import re import re
import unicodedata import unicodedata
try: try:
from io import StringIO
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
except ImportError as e:
from StringIO import StringIO from StringIO import StringIO
from email.MIMEBase import MIMEBase from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText 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 import encoders
from email.generator import Generator from email.generator import Generator
from email.utils import formatdate from email.utils import formatdate

180
cps/static/js/get_meta.js Normal file
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);
}
});
});

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);

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() { $("#perform_update").click(function() {
$('#spinner2').show(); $('#spinner2').show();
$.ajax({ $.ajax({

View File

@ -80,6 +80,7 @@
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div> <div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div> <div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
<p></p> <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="#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" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-web')}}</div>
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div> <div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>

View File

@ -39,7 +39,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="rating">{{_('Rating')}}</label> <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>
<div class="form-group"> <div class="form-group">
<label for="cover_url">{{_('Cover URL (jpg)')}}</label> <label for="cover_url">{{_('Cover URL (jpg)')}}</label>
@ -104,16 +104,56 @@
<input name="detail_view" type="checkbox" checked> {{_('view book after edit')}} <input name="detail_view" type="checkbox" checked> {{_('view book after edit')}}
</label> </label>
</div> </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> <button type="submit" class="btn btn-default">{{_('Submit')}}</button>
<a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
</form> </form>
</div> </div>
{% endif %} {% 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 %} {% endblock %}
{% block js %} {% 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/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/edit_books.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 %} {% endblock %}
{% block header %} {% block header %}
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">

View File

@ -23,7 +23,10 @@
<label for="config_random_books">{{_('No. of random books to show')}}</label> <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"> <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>
<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"> <div class="form-group">
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label> <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"> <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">

View File

@ -35,6 +35,7 @@
<uri>https://github.com/janeczku/calibre-web</uri> <uri>https://github.com/janeczku/calibre-web</uri>
</author> </author>
{% if entries[0] %}
{% for entry in entries %} {% for entry in entries %}
<entry> <entry>
<title>{{entry.title}}</title> <title>{{entry.title}}</title>
@ -60,6 +61,7 @@
{% endfor %} {% endfor %}
</entry> </entry>
{% endfor %} {% endfor %}
{% endif %}
{% for entry in listelements %} {% for entry in listelements %}
<entry> <entry>
<title>{{entry.name}}</title> <title>{{entry.name}}</title>

View File

@ -40,6 +40,7 @@
<div class="discover load-more"> <div class="discover load-more">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<div class="row"> <div class="row">
{% if entries[0] %}
{% for entry in entries %} {% for entry in entries %}
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
@ -76,6 +77,7 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -325,7 +325,7 @@ msgstr "发送测试邮件时发生错误: %(res)s"
#: cps/web.py:1816 #: cps/web.py:1816
msgid "E-Mail settings updated" msgid "E-Mail settings updated"
msgstr "" msgstr "E-Mail 设置已更新"
#: cps/web.py:1817 #: cps/web.py:1817
msgid "Edit mail settings" msgid "Edit mail settings"
@ -357,11 +357,11 @@ msgstr "编辑元数据"
#: cps/web.py:2162 #: cps/web.py:2162
#, python-format #, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server" msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr "" msgstr "不能上传后缀为 \"%s\" 的文件到此服务器"
#: cps/web.py:2168 #: cps/web.py:2168
msgid "File to be uploaded must have an extension" msgid "File to be uploaded must have an extension"
msgstr "" msgstr "要上传的文件必须有一个后缀"
#: cps/web.py:2185 #: cps/web.py:2185
#, python-format #, python-format

View File

@ -254,6 +254,7 @@ class Settings(Base):
config_anonbrowse = Column(SmallInteger, default=0) config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0) config_public_reg = Column(SmallInteger, default=0)
config_default_role = Column(SmallInteger, default=0) config_default_role = Column(SmallInteger, default=0)
config_columns_to_ignore = Column(String)
def __repr__(self): def __repr__(self):
pass pass
@ -280,6 +281,7 @@ class Config:
self.config_anonbrowse = data.config_anonbrowse self.config_anonbrowse = data.config_anonbrowse
self.config_public_reg = data.config_public_reg self.config_public_reg = data.config_public_reg
self.config_default_role = data.config_default_role self.config_default_role = data.config_default_role
self.config_columns_to_ignore = data.config_columns_to_ignore
if self.config_calibre_dir is not None: if self.config_calibre_dir is not None:
self.db_configured = True self.db_configured = True
else: else:
@ -361,6 +363,12 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0")
session.commit() session.commit()
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: try:
session.query(exists().where(Settings.config_default_role)).scalar() session.query(exists().where(Settings.config_default_role)).scalar()
session.commit() session.commit()

View File

@ -18,7 +18,6 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy import __version__ as sqlalchemyVersion from sqlalchemy import __version__ as sqlalchemyVersion
from math import ceil from math import ceil
from flask_login import LoginManager, login_user, logout_user, login_required, current_user from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_login.__about__ import __version__ as flask_loginVersion
from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed
from flask_principal import __version__ as flask_principalVersion from flask_principal import __version__ as flask_principalVersion
from flask_babel import Babel from flask_babel import Babel
@ -47,7 +46,6 @@ import db
from shutil import move, copyfile from shutil import move, copyfile
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado import version as tornadoVersion from tornado import version as tornadoVersion
#from builtins import str
try: try:
from urllib.parse import quote from urllib.parse import quote
@ -56,6 +54,11 @@ try:
except ImportError as e: except ImportError as e:
from urllib import quote 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
try: try:
from wand.image import Image from wand.image import Image
@ -144,6 +147,15 @@ lm.anonymous_user = ub.Anonymous
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'
db.setup_db() 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)
@babel.localeselector @babel.localeselector
def get_locale(): def get_locale():
# if a user is logged in, use the locale from the user settings # if a user is logged in, use the locale from the user settings
@ -248,8 +260,6 @@ class Pagination(object):
def iter_pages(self, left_edge=2, left_current=2, def iter_pages(self, left_edge=2, left_current=2,
right_current=5, right_edge=2): right_current=5, right_edge=2):
last = 0 last = 0
if sys.version_info.major >= 3:
xrange = range
for num in xrange(1, self.pages + 1): # ToDo: can be simplified for num in xrange(1, self.pages + 1): # ToDo: can be simplified
if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) \ if num <= left_edge or (num > self.page - left_current - 1 and num < self.page + right_current) \
or num > self.pages - right_edge: or num > self.pages - right_edge:
@ -560,7 +570,13 @@ def feed_hot():
hot_books = all_books.offset(off).limit(config.config_books_per_page) hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list() entries = list()
for book in hot_books: 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__() numBooks = entries.__len__()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, numBooks) 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) xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
@ -849,7 +865,13 @@ def hot_books(page):
hot_books = all_books.offset(off).limit(config.config_books_per_page) hot_books = all_books.offset(off).limit(config.config_books_per_page)
entries = list() entries = list()
for book in hot_books: 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__() numBooks = entries.__len__()
pagination = Pagination(page, config.config_books_per_page, numBooks) pagination = Pagination(page, config.config_books_per_page, numBooks)
return render_title_template('index.html', random=random, entries=entries, pagination=pagination, return render_title_template('index.html', random=random, entries=entries, pagination=pagination,
@ -1016,7 +1038,16 @@ def show_book(id):
except Exception as e: except Exception as e:
entries.languages[index].language_name = _( entries.languages[index].language_name = _(
isoLanguages.get(part3=entries.languages[index].lang_code).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 = [] book_in_shelfs = []
shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == id).all() shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == id).all()
for entry in shelfs: for entry in shelfs:
@ -1097,6 +1128,11 @@ def shutdown():
showtext['text'] = _(u'Performing shutdown of server, please close window') showtext['text'] = _(u'Performing shutdown of server, please close window')
return json.dumps(showtext) return json.dumps(showtext)
else: else:
if task == 2:
db.session.close()
db.engine.dispose()
db.setup_db()
return json.dumps({})
abort(404) abort(404)
@app.route("/update") @app.route("/update")
@ -1248,22 +1284,22 @@ def read_book(book_id, format):
zfile.close() zfile.close()
return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book")) return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"))
elif format.lower() == "pdf": elif format.lower() == "pdf":
all_name = str(book_id) + "/" + quote(book.data[0].name) + ".pdf" all_name = str(book_id) + "/" + book.data[0].name + ".pdf"
tmp_file = os.path.join(book_dir, quote(book.data[0].name)) + ".pdf" tmp_file = os.path.join(book_dir, book.data[0].name) + ".pdf"
if not os.path.exists(tmp_file): if not os.path.exists(tmp_file):
pdf_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".pdf" pdf_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".pdf"
copyfile(pdf_file, tmp_file) copyfile(pdf_file, tmp_file)
return render_title_template('readpdf.html', pdffile=all_name, title=_(u"Read a Book")) return render_title_template('readpdf.html', pdffile=all_name, title=_(u"Read a Book"))
elif format.lower() == "txt": elif format.lower() == "txt":
all_name = str(book_id) + "/" + quote(book.data[0].name) + ".txt" all_name = str(book_id) + "/" + book.data[0].name + ".txt"
tmp_file = os.path.join(book_dir, quote(book.data[0].name)) + ".txt" tmp_file = os.path.join(book_dir, book.data[0].name) + ".txt"
if not os.path.exists(all_name): if not os.path.exists(all_name):
txt_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".txt" txt_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".txt"
copyfile(txt_file, tmp_file) copyfile(txt_file, tmp_file)
return render_title_template('readtxt.html', txtfile=all_name, title=_(u"Read a Book")) return render_title_template('readtxt.html', txtfile=all_name, title=_(u"Read a Book"))
elif format.lower() == "cbr": elif format.lower() == "cbr":
all_name = str(book_id) + "/" + quote(book.data[0].name) + ".cbr" all_name = str(book_id) + "/" + book.data[0].name + ".cbr"
tmp_file = os.path.join(book_dir, quote(book.data[0].name)) + ".cbr" tmp_file = os.path.join(book_dir, book.data[0].name) + ".cbr"
if not os.path.exists(all_name): if not os.path.exists(all_name):
cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".cbr" cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + ".cbr"
copyfile(cbr_file, tmp_file) copyfile(cbr_file, tmp_file)
@ -1295,7 +1331,11 @@ def get_download_link(book_id, format):
response.headers["Content-Type"] = mimetypes.types_map['.' + format] response.headers["Content-Type"] = mimetypes.types_map['.' + format]
except Exception as e: except Exception as e:
pass pass
<<<<<<< HEAD
response.headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (urllib.quote(file_name.encode('utf-8')), format) response.headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (urllib.quote(file_name.encode('utf-8')), format)
=======
response.headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf-8')), format)
>>>>>>> master
return response return response
else: else:
abort(404) abort(404)
@ -1666,6 +1706,8 @@ def configuration_helper(origin):
reboot_required = True reboot_required = True
if "config_calibre_web_title" in to_save: if "config_calibre_web_title" in to_save:
content.config_calibre_web_title = to_save["config_calibre_web_title"] 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 "config_title_regex" in to_save:
if content.config_title_regex != to_save["config_title_regex"]: if content.config_title_regex != to_save["config_title_regex"]:
content.config_title_regex = to_save["config_title_regex"] content.config_title_regex = to_save["config_title_regex"]

View File

@ -7,7 +7,8 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png) ![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png)
##Features ## Features
- Bootstrap 3 HTML5 interface - Bootstrap 3 HTML5 interface
- full graphical setup - full graphical setup
- User management - User management
@ -29,13 +30,14 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
## Quick start ## Quick start
1. Execute the command: `python cps.py` 1. Install required dependencies by executing `pip install -r requirements.txt`
2. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog 2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
3. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button 3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Go to Login page 4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
5. Go to Login page
**Default admin login:** **Default admin login:**
*Username:* admin *Username:* admin
*Password:* admin123 *Password:* admin123
## Runtime Configuration Options ## Runtime Configuration Options
@ -57,10 +59,10 @@ Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick librar
## Requirements ## Requirements
Python 2.7+ Python 2.7+
Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature:
[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder. Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature:
[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder.
## Docker image ## Docker image
@ -132,4 +134,4 @@ Replace the user and ExecStart with your user and foldernames.
`sudo systemctl enable cps.service` `sudo systemctl enable cps.service`
enables the service. enables the service.

View File

@ -1,25 +1,14 @@
future Babel>=1.3
#sqlalchemy Flask>=0.11
Flask-Babel==0.11.1
Flask-Login>=0.3.2
Flask-Principal>=0.3.2
iso-639>=0.4.5
PyPDF2==1.26.0
pytz>=2016.10
requests>=2.11.1
SQLAlchemy>=0.8.4
tornado>=4.1
Wand>=0.4.4
#future
PyPDF2
babel
blinker
click
flask
flask_babel
flask_login
flask_principal
iso-639
itsdangerous
jinja2
markupsafe
pytz
requests
singledispatch
six
sqlalchemy
tornado
#https://pypi.python.org/packages/02/f8/97105237d0ba693b6f0bdcd94da0504e9a4433988c4393d8d3049094be7a/validate-1.0.1.tar.gz
#validate
wand
werkzeug

0
vendor/.gitempty vendored Normal file
View File

1472
vendor/validate.py vendored

File diff suppressed because it is too large Load Diff