Merge branch 'master' into travis
# Conflicts: # cps/epub.py # cps/web.py # readme.md
This commit is contained in:
commit
dcc0958c39
14
cps/db.py
14
cps/db.py
|
@ -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,10 +306,10 @@ 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)
|
||||||
|
|
||||||
|
if not cc_classes:
|
||||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
180
cps/static/js/get_meta.js
Normal 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
cps/static/js/libs/bootstrap-rating-input.min.js
vendored
Normal file
1
cps/static/js/libs/bootstrap-rating-input.min.js
vendored
Normal 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(" ").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);
|
|
@ -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({
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">×</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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Binary file not shown.
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
68
cps/web.py
68
cps/web.py
|
@ -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"]
|
||||||
|
|
10
readme.md
10
readme.md
|
@ -8,6 +8,7 @@ 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,10 +30,11 @@ 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
|
||||||
|
|
|
@ -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
0
vendor/.gitempty
vendored
Normal file
1472
vendor/validate.py
vendored
1472
vendor/validate.py
vendored
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user