Merge pull request #12 from janeczku/master

merge from janeczku/master
pull/610/head
Ethan Lin 7 years ago committed by GitHub
commit 4c030700fb

@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
[*.{js,py}]
indent_size = 4

@ -0,0 +1,96 @@
{
"env": {
"browser": true,
"jquery": true
},
"globals": {
"alert": true
},
"rules": {
"arrow-parens": 2,
"block-scoped-var": 1,
"brace-style": 2,
"camelcase": 1,
"comma-spacing": 2,
"curly": [2, "multi-line", "consistent"],
"eqeqeq": 2,
"indent": [
2,
4,
{
"SwitchCase": 1
}
],
"keyword-spacing": 2,
"linebreak-style": 2,
"new-cap": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-caller": 2,
"no-class-assign": 2,
"no-cond-assign": 2,
"no-const-assign": 2,
"no-console": 2,
"no-debugger": 2,
"no-delete-var": 2,
"no-empty": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-boolean-cast": 2,
"no-extra-semi": 2,
"no-fallthrough": [
2,
{
"commentPattern": "break[\\s\\w]*omitted"
}
],
"no-implied-eval": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-loop-func": 2,
"no-mixed-operators": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multi-str": 2,
"no-new": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-script-url": 2,
"no-sparse-arrays": 2,
"no-undef": 2,
"no-undefined": 2,
"no-unreachable": 2,
"no-unsafe-negation": 2,
"no-unused-vars": 2,
"no-use-before-define": [
2,
{
"classes": false,
"functions": false
}
],
"quotes": [
2,
"double"
],
"require-yield": 2,
"semi": [
2,
"always"
],
"space-before-blocks": 2,
"space-infix-ops": 2,
"space-unary-ops": 2,
"use-isnan": 2,
"valid-typeof": 2,
"wrap-iife": [
2,
"any"
],
"yield-star-spacing": 2
}
}

3
.gitignore vendored

@ -27,5 +27,4 @@ tags
settings.yaml settings.yaml
gdrive_credentials gdrive_credentials
#kindlegen vendor
vendor/kindlegen

@ -0,0 +1,60 @@
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
# Uses query strings so CSS font files are found without having to resort to absolute URLs
import hashlib
import os
def init_cache_busting(app):
"""
Configure `app` to so that `url_for` adds a unique query string to URLs generated
for the `'static'` endpoint.
This allows setting long cache expiration values on static resources
because whenever the resource changes, so does its URL.
"""
static_folder = os.path.join(app.static_folder, '') # path to the static file folder, with trailing slash
hash_table = {} # map of file hashes
app.logger.debug('Computing cache-busting values...')
# compute file hashes
for dirpath, dirnames, filenames in os.walk(static_folder):
for filename in filenames:
# compute version component
rooted_filename = os.path.join(dirpath, filename)
with open(rooted_filename, 'r') as f:
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
# save version to tables
file_path = rooted_filename.replace(static_folder, "")
file_path = file_path.replace("\\", "/") # Convert Windows path to web path
hash_table[file_path] = file_hash
app.logger.debug('Finished computing cache-busting values')
def bust_filename(filename):
return hash_table.get(filename, "")
def unbust_filename(filename):
return filename.split("?", 1)[0]
@app.url_defaults
def reverse_to_cache_busted_url(endpoint, values):
"""
Make `url_for` produce busted filenames when using the 'static' endpoint.
"""
if endpoint == "static":
file_hash = bust_filename(values["filename"])
if file_hash:
values["q"] = file_hash
def debusting_static_view(filename):
"""
Serve a request for a static file having a busted name.
"""
return original_static_view(filename=unbust_filename(filename))
# Replace the default static file view with our debusting view.
original_static_view = app.view_functions["static"]
app.view_functions["static"] = debusting_static_view

@ -43,13 +43,16 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
epub_metadata = {} epub_metadata = {}
for s in ['title', 'description', 'creator', 'language']: for s in ['title', 'description', 'creator', 'language', 'subject']:
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns) tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
if len(tmp) > 0: if len(tmp) > 0:
epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0] epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0]
else: else:
epub_metadata[s] = "Unknown" epub_metadata[s] = "Unknown"
if epub_metadata['subject'] == "Unknown":
epub_metadata['subject'] = ''
if epub_metadata['description'] == "Unknown": if epub_metadata['description'] == "Unknown":
description = tree.xpath("//*[local-name() = 'description']/text()") description = tree.xpath("//*[local-name() = 'description']/text()")
if len(description) > 0: if len(description) > 0:
@ -68,6 +71,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
else: else:
epub_metadata['language'] = "" epub_metadata['language'] = ""
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
if len(series) > 0:
epub_metadata['series'] = series[0]
else:
epub_metadata['series'] = ''
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
if len(series_id) > 0:
epub_metadata['series_id'] = series_id[0]
else:
epub_metadata['series_id'] = '1'
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:
@ -101,7 +116,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'), author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
cover=coverfile, cover=coverfile,
description=epub_metadata['description'], description=epub_metadata['description'],
tags="", tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
series="", series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
series_id="", series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
languages=epub_metadata['language']) languages=epub_metadata['language'])

@ -278,7 +278,7 @@ def get_valid_filename(value, replace_whitespace=True):
else: else:
value = unicode(re_slugify.sub('', value).strip()) value = unicode(re_slugify.sub('', value).strip())
if replace_whitespace: if replace_whitespace:
#*+:\"/<>? werden durch _ ersetzt #*+:\"/<>? are replaced by _
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U) value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U)
value = value[:128] value = value[:128]

@ -55,10 +55,38 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
.block-label {display: block;} .block-label {display: block;}
.fake-input {position: absolute; pointer-events: none; top: 0;} .fake-input {position: absolute; pointer-events: none; top: 0;}
input.pill { position: absolute; opacity: 0; }
input.pill + label {
border: 2px solid #45b29d;
border-radius: 15px;
color: #45b29d;
cursor: pointer;
display: inline-block;
padding: 3px 15px;
user-select: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input.pill:checked + label {
background-color: #45b29d;
border-color: #fff;
color: #fff;
}
input.pill:not(:checked) + label .glyphicon {
display: none;
}
.author-bio img {margin: 0 1em 1em 0;} .author-bio img {margin: 0 1em 1em 0;}
.author-link img {display: inline-block;max-width: 100px;} .author-link {display: inline-block; margin-top: 10px; width: 100px;}
.author-link img {display: block; height: 100%;}
#remove-from-shelves .btn, #remove-from-shelves .btn,
#shelf-action-errors { #shelf-action-errors {
margin-left: 5px; margin-left: 5px;
} }
.tags_click, .serie_click, .language_click {margin-right: 5px;}
#meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; }
.padded-bottom { margin-bottom: 15px; }

@ -1,4 +1,6 @@
$( document ).ready(function() { /* global _ */
$(function() {
$("#have_read_form").ajaxForm(); $("#have_read_form").ajaxForm();
}); });
@ -6,34 +8,51 @@ $("#have_read_cb").on("change", function() {
$(this).closest("form").submit(); $(this).closest("form").submit();
}); });
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) { (function() {
e.preventDefault(); var templates = {
add: _.template(
$("#template-shelf-add").html()
),
remove: _.template(
$("#template-shelf-remove").html()
)
};
$.get(this.href) $("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
.done(() => { e.preventDefault();
const $this = $(this);
switch ($this.data("shelf-action")) {
case "add":
$("#remove-from-shelves").append(`<a href="${$this.data("remove-href")}"
data-add-href="${this.href}"
class="btn btn-sm btn-default" data-shelf-action="remove"
><span class="glyphicon glyphicon-remove"></span> ${this.textContent}</a>`);
break;
case "remove":
$("#add-to-shelves").append(`<li><a href="${$this.data("add-href")}"
data-remove-href="${this.href}"
data-shelf-action="add"
>${this.textContent}</a></li>`);
break;
}
this.parentNode.removeChild(this);
})
.fail((xhr) => {
const $msg = $("<span/>", { "class": "text-danger"}).text(xhr.responseText);
$("#shelf-action-status").html($msg);
setTimeout(() => { $.get(this.href)
$msg.remove(); .done(function() {
}, 10000); var $this = $(this);
}); switch ($this.data("shelf-action")) {
}); case "add":
$("#remove-from-shelves").append(
templates.remove({
add: this.href,
remove: $this.data("remove-href"),
content: this.textContent
})
);
break;
case "remove":
$("#add-to-shelves").append(
templates.add({
add: $this.data("add-href"),
remove: this.href,
content: this.textContent
})
);
break;
}
this.parentNode.removeChild(this);
}.bind(this))
.fail(function(xhr) {
var $msg = $("<span/>", { "class": "text-danger"}).text(xhr.responseText);
$("#shelf-action-status").html($msg);
setTimeout(function() {
$msg.remove();
}, 10000);
});
});
})();

@ -3,6 +3,7 @@
*/ */
/* global Bloodhound, language, Modernizr, tinymce */ /* global Bloodhound, language, Modernizr, tinymce */
if ($("#description").length) {
tinymce.init({ tinymce.init({
selector: "#description", selector: "#description",
branding: false, branding: false,
@ -10,6 +11,7 @@ tinymce.init({
language language
}); });
if (!Modernizr.inputtypes.date) { if (!Modernizr.inputtypes.date) {
$("#pubdate").datepicker({ $("#pubdate").datepicker({
format: "yyyy-mm-dd", format: "yyyy-mm-dd",
@ -26,13 +28,13 @@ if (!Modernizr.inputtypes.date) {
.removeClass("hidden"); .removeClass("hidden");
}).trigger("change"); }).trigger("change");
} }
}
/* /*
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
and returns the completions it gets from the bloodhound engine prefixed. and returns the completions it gets from the bloodhound engine prefixed.
*/ */
function prefixedSource(prefix, query, cb, bhAdapter) { function prefixedSource(prefix, query, cb, bhAdapter) {
bhAdapter(query, function(retArray){ bhAdapter(query, function(retArray) {
var matches = []; var matches = [];
for (var i = 0; i < retArray.length; i++) { for (var i = 0; i < retArray.length; i++) {
var obj = {name : prefix + retArray[i].name}; var obj = {name : prefix + retArray[i].name};
@ -41,7 +43,7 @@ function prefixedSource(prefix, query, cb, bhAdapter) {
cb(matches); cb(matches);
}); });
} }
function getPath(){ function getPath() {
var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path
jsFileLocation = jsFileLocation.replace("/static/js/edit_books.js", ""); // the js folder path jsFileLocation = jsFileLocation.replace("/static/js/edit_books.js", ""); // the js folder path
return jsFileLocation; return jsFileLocation;
@ -49,7 +51,7 @@ function getPath(){
var authors = new Bloodhound({ var authors = new Bloodhound({
name: "authors", name: "authors",
datumTokenizer(datum) { datumTokenizer: function datumTokenizer(datum) {
return [datum.name]; return [datum.name];
}, },
queryTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace,
@ -60,15 +62,15 @@ var authors = new Bloodhound({
var series = new Bloodhound({ var series = new Bloodhound({
name: "series", name: "series",
datumTokenizer(datum) { datumTokenizer: function datumTokenizer(datum) {
return [datum.name]; return [datum.name];
}, },
queryTokenizer(query) { queryTokenizer: function queryTokenizer(query) {
return [query]; return [query];
}, },
remote: { remote: {
url: getPath()+"/get_series_json?q=", url: getPath()+"/get_series_json?q=",
replace(url, query) { replace: function replace(url, query) {
return url+encodeURIComponent(query); return url+encodeURIComponent(query);
} }
} }
@ -77,10 +79,10 @@ var series = new Bloodhound({
var tags = new Bloodhound({ var tags = new Bloodhound({
name: "tags", name: "tags",
datumTokenizer(datum) { datumTokenizer: function datumTokenizer(datum) {
return [datum.name]; return [datum.name];
}, },
queryTokenizer(query) { queryTokenizer: function queryTokenizer(query) {
var tokens = query.split(","); var tokens = query.split(",");
tokens = [tokens[tokens.length-1].trim()]; tokens = [tokens[tokens.length-1].trim()];
return tokens; return tokens;
@ -92,15 +94,15 @@ var tags = new Bloodhound({
var languages = new Bloodhound({ var languages = new Bloodhound({
name: "languages", name: "languages",
datumTokenizer(datum) { datumTokenizer: function datumTokenizer(datum) {
return [datum.name]; return [datum.name];
}, },
queryTokenizer(query) { queryTokenizer: function queryTokenizer(query) {
return [query]; return [query];
}, },
remote: { remote: {
url: getPath()+"/get_languages_json?q=", url: getPath()+"/get_languages_json?q=",
replace(url, query) { replace: function replace(url, query) {
return url+encodeURIComponent(query); return url+encodeURIComponent(query);
} }
} }
@ -115,9 +117,9 @@ function sourceSplit(query, cb, split, source) {
tokens.splice(tokens.length-1, 1); // remove last element tokens.splice(tokens.length-1, 1); // remove last element
var prefix = ""; var prefix = "";
var newSplit; var newSplit;
if (split === "&"){ if (split === "&") {
newSplit = " " + split + " "; newSplit = " " + split + " ";
}else{ } else {
newSplit = split + " "; newSplit = split + " ";
} }
for (var i = 0; i < tokens.length; i++) { for (var i = 0; i < tokens.length; i++) {
@ -127,76 +129,78 @@ function sourceSplit(query, cb, split, source) {
} }
var promiseAuthors = authors.initialize(); var promiseAuthors = authors.initialize();
promiseAuthors.done(function(){ promiseAuthors.done(function() {
$("#bookAuthor").typeahead( $("#bookAuthor").typeahead(
{ {
highlight: true, minLength: 1, highlight: true, minLength: 1,
hint: true hint: true
}, { }, {
name: "authors", name: "authors",
displayKey: "name", displayKey: "name",
source(query, cb){ source: function source(query, cb) {
return sourceSplit(query, cb, "&", authors); //sourceSplit //("&") return sourceSplit(query, cb, "&", authors); //sourceSplit //("&")
} }
}); }
);
}); });
var promiseSeries = series.initialize(); var promiseSeries = series.initialize();
promiseSeries.done(function(){ promiseSeries.done(function() {
$("#series").typeahead( $("#series").typeahead(
{ {
highlight: true, minLength: 0, highlight: true, minLength: 0,
hint: true hint: true
}, { }, {
name: "series", name: "series",
displayKey: "name", displayKey: "name",
source: series.ttAdapter() source: series.ttAdapter()
} }
); );
}); });
var promiseTags = tags.initialize(); var promiseTags = tags.initialize();
promiseTags.done(function(){ promiseTags.done(function() {
$("#tags").typeahead( $("#tags").typeahead(
{ {
highlight: true, minLength: 0, highlight: true, minLength: 0,
hint: true hint: true
}, { }, {
name: "tags", name: "tags",
displayKey: "name", displayKey: "name",
source(query, cb){ source: function source(query, cb) {
return sourceSplit(query, cb, ",", tags); return sourceSplit(query, cb, ",", tags);
} }
}); }
}); );
});
var promiseLanguages = languages.initialize(); var promiseLanguages = languages.initialize();
promiseLanguages.done(function(){ promiseLanguages.done(function() {
$("#languages").typeahead( $("#languages").typeahead(
{ {
highlight: true, minLength: 0, highlight: true, minLength: 0,
hint: true hint: true
}, { }, {
name: "languages", name: "languages",
displayKey: "name", displayKey: "name",
source(query, cb){ source: function source(query, cb) {
return sourceSplit(query, cb, ",", languages); //(",") return sourceSplit(query, cb, ",", languages); //(",")
} }
}); }
}); );
});
$("#search").on("change input.typeahead:selected", function(){ $("#search").on("change input.typeahead:selected", function() {
var form = $("form").serialize(); var form = $("form").serialize();
$.getJSON( getPath()+"/get_matching_tags", form, function( data ) { $.getJSON( getPath()+"/get_matching_tags", form, function( data ) {
$(".tags_click").each(function() { $(".tags_click").each(function() {
if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) { if ($.inArray(parseInt($(this).children("input").first().val(), 10), data.tags) === -1 ) {
if (!($(this).hasClass("active"))) { if (!($(this).hasClass("active"))) {
$(this).addClass("disabled"); $(this).addClass("disabled");
} }
} } else {
else { $(this).removeClass("disabled");
$(this).removeClass("disabled"); }
} });
});
}); });
}); });

@ -4,11 +4,11 @@
* Google Books api document: https://developers.google.com/books/docs/v1/using * 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) * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
*/ */
/* global i18nMsg, tinymce */ /* global _, i18nMsg, tinymce */
var dbResults = []; var dbResults = [];
var ggResults = []; var ggResults = [];
$(document).ready(function () { $(function () {
var msg = i18nMsg; var msg = i18nMsg;
var douban = "https://api.douban.com"; var douban = "https://api.douban.com";
var dbSearch = "/v2/book/search"; var dbSearch = "/v2/book/search";
@ -22,113 +22,138 @@ $(document).ready(function () {
var ggDone = false; var ggDone = false;
var showFlag = 0; var showFlag = 0;
String.prototype.replaceAll = function (s1, s2) {
return this.replace(new RegExp(s1, "gm"), s2); var templates = {
bookResult: _.template(
$("#template-book-result").html()
)
}; };
function populateForm (book) {
tinymce.get("description").setContent(book.description);
$("#bookAuthor").val(book.authors);
$("#book_title").val(book.title);
$("#tags").val(book.tags.join(","));
$("#rating").data("rating").setValue(Math.round(book.rating));
$(".cover img").attr("src", book.cover);
$("#cover_url").val(book.cover);
}
function showResult () { function showResult () {
var book;
var i;
var bookHtml;
showFlag++; showFlag++;
if (showFlag === 1) { if (showFlag === 1) {
$("#metaModal #meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>"); $("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
} }
if (ggDone && dbDone) { if (ggDone && dbDone) {
if (!ggResults && !dbResults) { if (!ggResults && !dbResults) {
$("#metaModal #meta-info").html("<p class=\"text-danger\">"+ msg.no_result +"</p>"); $("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
return; return;
} }
} }
if (ggDone && ggResults.length > 0) { if (ggDone && ggResults.length > 0) {
for (i = 0; i < ggResults.length; i++) { ggResults.forEach(function(result) {
book = ggResults[i]; var book = {
var bookCover; id: result.id,
if (book.volumeInfo.imageLinks) { title: result.volumeInfo.title,
bookCover = book.volumeInfo.imageLinks.thumbnail; authors: result.volumeInfo.authors || [],
} else { description: result.volumeInfo.description || "",
bookCover = "/static/generic_cover.jpg"; publisher: result.volumeInfo.publisher || "",
} publishedDate: result.volumeInfo.publishedDate || "",
bookHtml = "<li class=\"media\">" + tags: result.volumeInfo.categories || [],
"<img class=\"pull-left img-responsive\" data-toggle=\"modal\" data-target=\"#metaModal\" src=\"" + rating: result.volumeInfo.averageRating || 0,
bookCover + "\" alt=\"Cover\" style=\"width:100px;height:150px\" onclick='getMeta(\"google\"," + cover: result.volumeInfo.imageLinks ?
i + ")'>" + result.volumeInfo.imageLinks.thumbnail :
"<div class=\"media-body\">" + "/static/generic_cover.jpg",
"<h4 class=\"media-heading\"><a href=\"https://books.google.com/books?id=" + url: "https://books.google.com/books?id=" + result.id,
book.id + "\" target=\"_blank\">" + book.volumeInfo.title + "</a></h4>" + source: {
"<p>"+ msg.author +"" + book.volumeInfo.authors + "</p>" + id: "google",
"<p>"+ msg.publisher + "" + book.volumeInfo.publisher + "</p>" + description: "Google Books",
"<p>"+ msg.description + ":" + book.volumeInfo.description + "</p>" + url: "https://books.google.com/"
"<p>"+ msg.source + ":<a href=\"https://books.google.com\" target=\"_blank\">Google Books</a></p>" + }
"</div>" + };
"</li>";
$("#metaModal #book-list").append(bookHtml); var $book = $(templates.bookResult(book));
} $book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
ggDone = false; ggDone = false;
} }
if (dbDone && dbResults.length > 0) { if (dbDone && dbResults.length > 0) {
for (i = 0; i < dbResults.length; i++) { dbResults.forEach(function(result) {
book = dbResults[i]; var book = {
bookHtml = "<li class=\"media\">" + id: result.id,
"<img class=\"pull-left img-responsive\" data-toggle=\"modal\" data-target=\"#metaModal\" src=\"" + title: result.title,
book.image + "\" alt=\"Cover\" style=\"width:100px;height: 150px\" onclick='getMeta(\"douban\"," + authors: result.author || [],
i + ")'>" + description: result.summary,
"<div class=\"media-body\">" + publisher: result.publisher || "",
"<h4 class=\"media-heading\"><a href=\"https://book.douban.com/subject/" + publishedDate: result.pubdate || "",
book.id + "\" target=\"_blank\">" + book.title + "</a></h4>" + tags: result.tags.map(function(tag) {
"<p>" + msg.author + "" + book.author + "</p>" + return tag.title;
"<p>" + msg.publisher + "" + book.publisher + "</p>" + }),
"<p>" + msg.description + ":" + book.summary + "</p>" + rating: result.rating.average || 0,
"<p>" + msg.source + ":<a href=\"https://book.douban.com\" target=\"_blank\">Douban Books</a></p>" + cover: result.image,
"</div>" + url: "https://book.douban.com/subject/" + result.id,
"</li>"; source: {
$("#metaModal #book-list").append(bookHtml); id: "douban",
} description: "Douban Books",
url: "https://book.douban.com/"
}
};
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
dbDone = false; dbDone = false;
} }
} }
function ggSearchBook (title) { function ggSearchBook (title) {
title = title.replaceAll(/\s+/, "+");
var url = google + ggSearch + "?q=" + title;
$.ajax({ $.ajax({
url, url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"),
type: "GET", type: "GET",
dataType: "jsonp", dataType: "jsonp",
jsonp: "callback", jsonp: "callback",
success (data) { success: function success(data) {
ggResults = data.items; ggResults = data.items;
}, },
complete () { complete: function complete() {
ggDone = true; ggDone = true;
showResult(); showResult();
$("#show-google").trigger("change");
} }
}); });
} }
function dbSearchBook (title) { function dbSearchBook (title) {
var url = douban + dbSearch + "?q=" + title + "&fields=all&count=10";
$.ajax({ $.ajax({
url, url: douban + dbSearch + "?q=" + title + "&fields=all&count=10",
type: "GET", type: "GET",
dataType: "jsonp", dataType: "jsonp",
jsonp: "callback", jsonp: "callback",
success (data) { success: function success(data) {
dbResults = data.books; dbResults = data.books;
}, },
error () { error: function error() {
$("#metaModal #meta-info").html("<p class=\"text-danger\">"+ msg.search_error+"!</p>"); $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>");
}, },
complete () { complete: function complete() {
dbDone = true; dbDone = true;
showResult(); showResult();
$("#show-douban").trigger("change");
} }
}); });
} }
function doSearch (keyword) { function doSearch (keyword) {
showFlag = 0; showFlag = 0;
$("#metaModal #meta-info").text(msg.loading); $("#meta-info").text(msg.loading);
// var keyword = $("#keyword").val(); // var keyword = $("#keyword").val();
if (keyword) { if (keyword) {
dbSearchBook(keyword); dbSearchBook(keyword);
@ -136,7 +161,8 @@ $(document).ready(function () {
} }
} }
$("#do-search").click(function () { $("#meta-search").on("submit", function (e) {
e.preventDefault();
var keyword = $("#keyword").val(); var keyword = $("#keyword").val();
if (keyword) { if (keyword) {
doSearch(keyword); doSearch(keyword);
@ -152,35 +178,3 @@ $(document).ready(function () {
}); });
}); });
function getMeta (source, id) {
var meta;
var tags;
if (source === "google") {
meta = ggResults[id];
tinymce.get("description").setContent(meta.volumeInfo.description);
$("#bookAuthor").val(meta.volumeInfo.authors.join(" & "));
$("#book_title").val(meta.volumeInfo.title);
if (meta.volumeInfo.categories) {
tags = meta.volumeInfo.categories.join(",");
$("#tags").val(tags);
}
if (meta.volumeInfo.averageRating) {
$("#rating").val(Math.round(meta.volumeInfo.averageRating));
}
return;
}
if (source === "douban") {
meta = dbResults[id];
tinymce.get("description").setContent(meta.summary);
$("#bookAuthor").val(meta.author.join(" & "));
$("#book_title").val(meta.title);
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;
}
}

@ -1 +1,2 @@
!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); /** @link https://github.com/javiertoledo/bootstrap-rating-input */
!function(a){"use strict";function n(a){return"[data-value"+(a?"="+a:"")+"]"}function e(a,n,e){var i=e.activeIcon,t=e.inactiveIcon;a.removeClass(n?t:i).addClass(n?i:t)}function i(n,e){var i=a.extend({},s,n.data(),e);return i.inline=""===i.inline||i.inline,i.readonly=""===i.readonly||i.readonly,!1===i.clearable?i.clearableLabel="":i.clearableLabel=i.clearable,i.clearable=""===i.clearable||i.clearable,i}function t(n,e){if(e.inline)i=a('<span class="rating-input"></span>');else var i=a('<div class="rating-input"></div>');i.addClass(n.attr("class")),i.removeClass("rating");for(var t=e.min;t<=e.max;t++)i.append('<i class="'+e.iconLib+'" data-value="'+t+'"></i>');return e.clearable&&!e.readonly&&i.append("&nbsp;").append('<a class="'+l+'"><i class="'+e.iconLib+" "+e.clearableIcon+'"/>'+e.clearableLabel+"</a>"),i}var l="rating-clear",o="."+l,s={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},r=function(a,n){var e=this.$input=a;this.options=i(e,n);var l=this.$el=t(e,this.options);e.addClass("hidden").before(l),e.attr("type","hidden"),this.highlight(e.val())};r.VERSION="0.4.0",r.DEFAULTS=s,r.prototype={clear:function(){this.setValue(this.options["empty-value"])},setValue:function(a){this.highlight(a),this.updateInput(a)},highlight:function(a,i){var t=this.options,l=this.$el;if(a>=this.options.min&&a<=this.options.max){var s=l.find(n(a));e(s.prevAll("i").addBack(),!0,t),e(s.nextAll("i"),!1,t)}else e(l.find(n()),!1,t);i||(this.options.clearableRemain?l.find(o).removeClass("hidden"):a&&a!=this.options["empty-value"]?l.find(o).removeClass("hidden"):l.find(o).addClass("hidden"))},updateInput:function(a){var n=this.$input;n.val()!=a&&n.val(a).change()}},(a.fn.rating=function(e){return this.filter("input[type=number]").each(function(){var i=a(this),t=new r(i,"object"==typeof e&&e||{});t.options.readonly||(t.$el.on("mouseenter",n(),function(){t.highlight(a(this).data("value"),!0)}).on("mouseleave",n(),function(){t.highlight(i.val(),!0)}).on("click",n(),function(){t.setValue(a(this).data("value"))}).on("click",o,function(){t.clear()}),i.data("rating",t))})}).Constructor=r,a(function(){a("input.rating[type=number]").each(function(){a(this).rating()})})}(jQuery);

@ -1,7 +1,3 @@
var displaytext;
var updateTimerID;
var updateText;
// Generic control/related handler to show/hide fields based on a checkbox' value // Generic control/related handler to show/hide fields based on a checkbox' value
// e.g. // e.g.
// <input type="checkbox" data-control="stuff-to-show"> // <input type="checkbox" data-control="stuff-to-show">
@ -11,16 +7,18 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () {
var name = $this.data("control"); var name = $this.data("control");
var showOrHide = $this.prop("checked"); var showOrHide = $this.prop("checked");
$("[data-related=\""+name+"\"]").each(function () { $("[data-related=\"" + name + "\"]").each(function () {
$(this).toggle(showOrHide); $(this).toggle(showOrHide);
}); });
}); });
$(function() { $(function() {
var updateTimerID;
var updateText;
// Allow ajax prefilters to be added/removed dynamically // Allow ajax prefilters to be added/removed dynamically
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const preFilters = $.Callbacks(); var preFilters = $.Callbacks();
$.ajaxPrefilter(preFilters.fire); $.ajaxPrefilter(preFilters.fire);
function restartTimer() { function restartTimer() {
@ -30,29 +28,29 @@ $(function() {
function updateTimer() { function updateTimer() {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname+"/../../get_updater_status", url: window.location.pathname + "/../../get_updater_status",
success(data) { success: function success(data) {
// console.log(data.status); // console.log(data.status);
$("#UpdateprogressDialog #Updatecontent").html(updateText[data.status]); $("#Updatecontent").html(updateText[data.status]);
if (data.status >6){ if (data.status > 6) {
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#updateFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden");
}
},
error: function error() {
// console.log('Done');
clearInterval(updateTimerID); clearInterval(updateTimerID);
$("#spinner2").hide(); $("#spinner2").hide();
$("#UpdateprogressDialog #updateFinished").removeClass("hidden"); $("#Updatecontent").html(updateText[7]);
$("#updateFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden"); $("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden"); $("#perform_update").addClass("hidden");
}
},
error() {
// console.log('Done');
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#UpdateprogressDialog #Updatecontent").html(updateText[7]);
$("#UpdateprogressDialog #updateFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden");
}, },
timeout:2000 timeout: 2000
}); });
} }
@ -70,13 +68,13 @@ $(function() {
// selector for the NEXT link (to page 2) // selector for the NEXT link (to page 2)
itemSelector : ".load-more .book", itemSelector : ".load-more .book",
animate : true, animate : true,
extraScrollPx: 300, extraScrollPx: 300
// selector for all items you'll retrieve // selector for all items you'll retrieve
}, function(data){ }, function(data) {
$(".load-more .row").isotope( "appended", $(data), null ); $(".load-more .row").isotope( "appended", $(data), null );
}); });
$("#sendbtn").click(function(){ $("#sendbtn").click(function() {
var $this = $(this); var $this = $(this);
$this.text("Please wait..."); $this.text("Please wait...");
$this.addClass("disabled"); $this.addClass("disabled");
@ -84,36 +82,39 @@ $(function() {
$("#restart").click(function() { $("#restart").click(function() {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname+"/../../shutdown", url: window.location.pathname + "/../../shutdown",
data: {"parameter":0}, data: {"parameter":0},
success(data) { success: function success() {
$("#spinner").show(); $("#spinner").show();
displaytext=data.text; setTimeout(restartTimer, 3000);
setTimeout(restartTimer, 3000);} }
}); });
}); });
$("#shutdown").click(function() { $("#shutdown").click(function() {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname+"/../../shutdown", url: window.location.pathname + "/../../shutdown",
data: {"parameter":1}, data: {"parameter":1},
success(data) { success: function success(data) {
return alert(data.text);} return alert(data.text);
}
}); });
}); });
$("#check_for_update").click(function() { $("#check_for_update").click(function() {
var buttonText = $("#check_for_update").html(); var $this = $(this);
$("#check_for_update").html("..."); var buttonText = $this.html();
$this.html("...");
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname+"/../../get_update_status", url: window.location.pathname + "/../../get_update_status",
success(data) { success: function success(data) {
$("#check_for_update").html(buttonText); $this.html(buttonText);
if (data.status === true) { if (data.status === true) {
$("#check_for_update").addClass("hidden"); $("#check_for_update").addClass("hidden");
$("#perform_update").removeClass("hidden"); $("#perform_update").removeClass("hidden");
$("#update_info").removeClass("hidden"); $("#update_info")
$("#update_info").find("span").html(data.commit); .removeClass("hidden")
.find("span").html(data.commit);
} }
} }
}); });
@ -121,22 +122,23 @@ $(function() {
$("#restart_database").click(function() { $("#restart_database").click(function() {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname+"/../../shutdown", url: window.location.pathname + "/../../shutdown",
data: {"parameter":2} data: {"parameter":2}
}); });
}); });
$("#perform_update").click(function() { $("#perform_update").click(function() {
$("#spinner2").show(); $("#spinner2").show();
$.ajax({ $.ajax({
type: "POST", type: "POST",
dataType: "json", dataType: "json",
data: { start: "True"}, data: { start: "True"},
url: window.location.pathname+"/../../get_updater_status", url: window.location.pathname + "/../../get_updater_status",
success(data) { success: function success(data) {
updateText=data.text; updateText = data.text;
$("#UpdateprogressDialog #Updatecontent").html(updateText[data.status]); $("#Updatecontent").html(updateText[data.status]);
// console.log(data.status); // console.log(data.status);
updateTimerID=setInterval(updateTimer, 2000);} updateTimerID = setInterval(updateTimer, 2000);
}
}); });
}); });
@ -144,10 +146,10 @@ $(function() {
$("#bookDetailsModal") $("#bookDetailsModal")
.on("show.bs.modal", function(e) { .on("show.bs.modal", function(e) {
const $modalBody = $(this).find(".modal-body"); var $modalBody = $(this).find(".modal-body");
// Prevent static assets from loading multiple times // Prevent static assets from loading multiple times
const useCache = (options) => { var useCache = function(options) {
options.async = true; options.async = true;
options.cache = true; options.cache = true;
}; };
@ -162,7 +164,7 @@ $(function() {
$(this).find(".modal-body").html("..."); $(this).find(".modal-body").html("...");
}); });
$(window).resize(function(event) { $(window).resize(function() {
$(".discover .row").isotope("reLayout"); $(".discover .row").isotope("reLayout");
}); });
}); });

@ -1,31 +1,31 @@
/* global Sortable,sortTrue */ /* global Sortable,sortTrue */
var sortable = Sortable.create(sortTrue, { Sortable.create(sortTrue, {
group: "sorting", group: "sorting",
sort: true sort: true
}); });
function sendData(path){ // eslint-disable-next-line no-unused-vars
function sendData(path) {
var elements; var elements;
var counter; var counter;
var maxElements; var maxElements;
var tmp=[]; var tmp = [];
elements=Sortable.utils.find(sortTrue,"div"); elements = Sortable.utils.find(sortTrue, "div");
maxElements=elements.length; maxElements = elements.length;
var form = document.createElement("form"); var form = document.createElement("form");
form.setAttribute("method", "post"); form.setAttribute("method", "post");
form.setAttribute("action", path); form.setAttribute("action", path);
for (counter = 0;counter < maxElements;counter++) {
for(counter=0;counter<maxElements;counter++){ tmp[counter] = elements[counter].getAttribute("id");
tmp[counter]=elements[counter].getAttribute("id"); var hiddenField = document.createElement("input");
var hiddenField = document.createElement("input"); hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("type", "hidden"); hiddenField.setAttribute("name", elements[counter].getAttribute("id"));
hiddenField.setAttribute("name", elements[counter].getAttribute("id")); hiddenField.setAttribute("value", String(counter + 1));
hiddenField.setAttribute("value", counter+1); form.appendChild(hiddenField);
form.appendChild(hiddenField);
} }
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();

@ -11,16 +11,17 @@
{%if author.about is not none %} {%if author.about is not none %}
<p>{{author.about|safe}}</p> <p>{{author.about|safe}}</p>
{% endif %} {% endif %}
</section>
<a href="{{author.link}}" class="author-link" target="_blank"> - {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a>
<img src="{{ url_for('static', filename='img/goodreads.svg') }}" alt="Goodreads"> </section>
</a>
<div class="clearfix"></div> <div class="clearfix"></div>
{% endif %} {% endif %}
<div class="discover load-more"> <div class="discover load-more">
{% if author is not none %}
<h3>{{_("In Library")}}</h3>
{% endif %}
<div class="row"> <div class="row">
{% if entries[0] %} {% if entries[0] %}
{% for entry in entries %} {% for entry in entries %}
@ -62,4 +63,48 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if other_books is not none %}
<div class="discover">
<h3>{{_("More by")}} {{ author.name|safe }}</h3>
<div class="row">
{% for entry in other_books %}
<div class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
<img src="{{ entry.image_url }}" />
</a>
</div>
<div class="meta">
<p class="title">{{entry.title|shortentitle}}</p>
<p class="author">
{% for author in entry.authors %}
<a href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">
{{author.name}}
</a>
{% if not loop.last %}
&amp;
{% endif %}
{% endfor %}
</p>
<div class="rating">
{% for number in range((entry.average_rating)|float|round|int(2)) %}
<span class="glyphicon glyphicon-star good"></span>
{% if loop.last and loop.index < 5 %}
{% for numer in range(5 - loop.index) %}
<span class="glyphicon glyphicon-star"></span>
{% endfor %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">
<img src="{{ url_for('static', filename='img/goodreads.svg') }}" alt="Goodreads">
</a>
</div>
{% endif %}
{% endblock %} {% endblock %}

@ -139,7 +139,7 @@
<div class="modal-header bg-danger text-center"> <div class="modal-header bg-danger text-center">
<span>{{_('Are really you sure?')}}</span> <span>{{_('Are really you sure?')}}</span>
</div> </div>
<div class="modal-body text-center" id="meta-info"> <div class="modal-body text-center">
<span>{{_('Book will be deleted from Calibre database')}}</span> <span>{{_('Book will be deleted from Calibre database')}}</span>
<span>{{_('and from hard disk')}}</span> <span>{{_('and from hard disk')}}</span>
</div> </div>
@ -154,22 +154,35 @@
{% endif %} {% endif %}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel"> <div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <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> <h4 class="modal-title" id="metaModalLabel">{{_('Get metadata')}}</h4>
<form class="form-inline"> <form class="padded-bottom" id="meta-search">
<div class="form-group"> <div class="input-group">
<label class="sr-only" for="keyword">{{_('Keyword')}}</label> <label class="sr-only" for="keyword">{{_('Keyword')}}</label>
<input type="text" class="form-control" id="keyword" placeholder="{{_(" Search keyword ")}}"> <input type="text" class="form-control" id="keyword" name="keyword" placeholder="{{_(" Search keyword ")}}">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary" id="do-search">{{_("Go!")}}</button>
</span>
</div> </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> </form>
<div>{{_('Click the cover to load metadata to the form')}}</div>
</div> </div>
<div class="modal-body" id="meta-info"> <div class="modal-body">
{{_("Loading...")}} <div class="text-center padded-bottom">
<input type="checkbox" id="show-douban" class="pill" data-control="douban" checked>
<label for="show-douban">Douban <span class="glyphicon glyphicon-ok"></span></label>
<input type="checkbox" id="show-google" class="pill" data-control="google" checked>
<label for="show-google">Google <span class="glyphicon glyphicon-ok"></span></label>
</div>
<div id="meta-info">
{{_("Loading...")}}
</div>
<ul id="book-list" class="media-list"></ul>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
@ -180,6 +193,31 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script type="text/template" id="template-book-result">
<li class="media" data-related="<%= source.id %>">
<img class="pull-left img-responsive"
data-toggle="modal"
data-target="#metaModal"
src="<%= cover %>"
alt="Cover"
>
<div class="media-body">
<h4 class="media-heading">
<a href="<%= url %>" target="_blank" rel="noopener"><%= title %></a>
</h4>
<p>{{_('Author')}}<%= authors.join(" & ") %></p>
<% if (publisher) { %>
<p>{{_('Publisher')}}<%= publisher %></p>
<% } %>
<% if (description) { %>
<p>{{_('Description')}}: <%= description %></p>
<% } %>
<p>{{_('Source')}}:
<a href="<%= source.url %>" target="_blank" rel="noopener"><%= source.description %></a>
</p>
</div>
</li>
</script>
<script> <script>
var i18nMsg = { var i18nMsg = {
'loading': {{_('Loading...')|safe|tojson}}, 'loading': {{_('Loading...')|safe|tojson}},

@ -72,6 +72,13 @@
<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">
</div> </div>
<div class="form-group">
<label for="config_mature_content_tags">{{_('Tags for Mature Content')}}</label>
<input type="text" class="form-control" name="config_mature_content_tags" id="config_mature_content_tags"
value="{% if content.config_mature_content_tags != None%}{{ content.config_mature_content_tags }}{% endif %}"
autocomplete="off"
>
</div>
<div class="form-group"> <div class="form-group">
<label for="config_log_level">{{_('Log Level')}}</label> <label for="config_log_level">{{_('Log Level')}}</label>
<select name="config_log_level" id="config_log_level" class="form-control"> <select name="config_log_level" id="config_log_level" class="form-control">

@ -259,5 +259,17 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script type="text/template" id="template-shelf-add">
<li>
<a href="<%= add %>" data-remove-href="<%= remove %>" data-shelf-action="add">
<%= content %>
</a>
</li>
</script>
<script type="text/template" id="template-shelf-remove">
<a href="<%= remove %>" data-add-href="<%= add %>" class="btn btn-sm btn-default" data-shelf-action="remove">
<span class="glyphicon glyphicon-remove"></span> <%= content %>
</a>
</script>
<script src="{{ url_for('static', filename='js/details.js') }}"></script> <script src="{{ url_for('static', filename='js/details.js') }}"></script>
{% endblock %} {% endblock %}

@ -26,9 +26,7 @@
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}" href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
{% endif %} {% endif %}
<link rel="search" <link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
href="{{url_for('feed_osd')}}"
type="application/opensearchdescription+xml"/>
<title>{{instance}}</title> <title>{{instance}}</title>
<author> <author>
<name>{{instance}}</name> <name>{{instance}}</name>

@ -4,8 +4,7 @@
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/> <link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}" <link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/> type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="search" title="{{_('Search')}}" href="{{url_for('feed_osd')}}" <link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
type="application/opensearchdescription+xml"/>
<title>{{instance}}</title> <title>{{instance}}</title>
<author> <author>
<name>{{instance}}</name> <name>{{instance}}</name>

@ -40,45 +40,48 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_mature_content" id="show_mature_content" {% if content.mature_content %}checked{% endif %}>
<input type="checkbox" name="show_random" id="show_random" {% if content.show_random_books() %}checked{% endif %}> <label for="show_mature_content">{{_('Show mature content')}}</label>
<label for="show_random">{{_('Show random books')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_random" id="show_random" {% if content.show_random_books() %}checked{% endif %}>
<input type="checkbox" name="show_hot" id="show_hot" {% if content.show_hot_books() %}checked{% endif %}> <label for="show_random">{{_('Show random books')}}</label>
<label for="show_hot">{{_('Show hot books')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_hot" id="show_hot" {% if content.show_hot_books() %}checked{% endif %}>
<input type="checkbox" name="show_best_rated" id="show_best_rated" {% if content.show_best_rated_books() %}checked{% endif %}> <label for="show_hot">{{_('Show hot books')}}</label>
<label for="show_best_rated">{{_('Show best rated books')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_best_rated" id="show_best_rated" {% if content.show_best_rated_books() %}checked{% endif %}>
<input type="checkbox" name="show_language" id="show_language" {% if content.show_language() %}checked{% endif %}> <label for="show_best_rated">{{_('Show best rated books')}}</label>
<label for="show_language">{{_('Show language selection')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_language" id="show_language" {% if content.show_language() %}checked{% endif %}>
<input type="checkbox" name="show_series" id="show_series" {% if content.show_series() %}checked{% endif %}> <label for="show_language">{{_('Show language selection')}}</label>
<label for="show_series">{{_('Show series selection')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_series" id="show_series" {% if content.show_series() %}checked{% endif %}>
<input type="checkbox" name="show_category" id="show_category" {% if content.show_category() %}checked{% endif %}> <label for="show_series">{{_('Show series selection')}}</label>
<label for="show_category">{{_('Show category selection')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_category" id="show_category" {% if content.show_category() %}checked{% endif %}>
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}> <label for="show_category">{{_('Show category selection')}}</label>
<label for="show_author">{{_('Show author selection')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}> <label for="show_author">{{_('Show author selection')}}</label>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label> </div>
</div> <div class="form-group">
<div class="form-group"> <input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}> <label for="show_read_and_unread">{{_('Show read and unread')}}</label>
<label for="show_detail_random">{{_('Show random books in detail view')}}</label> </div>
<div class="form-group">
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="show_detail_random">{{_('Show random books in detail view')}}</label>
</div>
</div> </div>
</div>
<div class="col-sm-6"> <div class="col-sm-6">
{% if g.user and g.user.role_admin() and not profile %} {% if g.user and g.user.role_admin() and not profile %}
{% if not content.role_anonymous() %} {% if not content.role_anonymous() %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -157,6 +157,7 @@ class User(UserBase, Base):
locale = Column(String(2), default="en") locale = Column(String(2), default="en")
sidebar_view = Column(Integer, default=1) sidebar_view = Column(Integer, default=1)
default_language = Column(String(3), default="all") default_language = Column(String(3), default="all")
mature_content = Column(Boolean, default=True)
# Class for anonymous user is derived from User base and complets overrides methods and properties for the # Class for anonymous user is derived from User base and complets overrides methods and properties for the
@ -166,13 +167,14 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.loadSettings() self.loadSettings()
def loadSettings(self): def loadSettings(self):
data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() # type: User
settings = session.query(Settings).first() settings = session.query(Settings).first()
self.nickname = data.nickname self.nickname = data.nickname
self.role = data.role self.role = data.role
self.sidebar_view = data.sidebar_view self.sidebar_view = data.sidebar_view
self.default_language = data.default_language self.default_language = data.default_language
self.locale = data.locale self.locale = data.locale
self.mature_content = data.mature_content
self.anon_browse = settings.config_anonbrowse self.anon_browse = settings.config_anonbrowse
def role_admin(self): def role_admin(self):
@ -266,6 +268,7 @@ class Settings(Base):
config_use_goodreads = Column(Boolean) config_use_goodreads = Column(Boolean)
config_goodreads_api_key = Column(String) config_goodreads_api_key = Column(String)
config_goodreads_api_secret = Column(String) config_goodreads_api_secret = Column(String)
config_mature_content_tags = Column(String) # type: str
def __repr__(self): def __repr__(self):
pass pass
@ -297,7 +300,7 @@ class Config:
self.loadSettings() self.loadSettings()
def loadSettings(self): def loadSettings(self):
data = session.query(Settings).first() data = session.query(Settings).first() # type: Settings
self.config_calibre_dir = data.config_calibre_dir self.config_calibre_dir = data.config_calibre_dir
self.config_port = data.config_port self.config_port = data.config_port
self.config_calibre_web_title = data.config_calibre_web_title self.config_calibre_web_title = data.config_calibre_web_title
@ -326,6 +329,7 @@ class Config:
self.config_use_goodreads = data.config_use_goodreads self.config_use_goodreads = data.config_use_goodreads
self.config_goodreads_api_key = data.config_goodreads_api_key self.config_goodreads_api_key = data.config_goodreads_api_key
self.config_goodreads_api_secret = data.config_goodreads_api_secret self.config_goodreads_api_secret = data.config_goodreads_api_secret
self.config_mature_content_tags = data.config_mature_content_tags
@property @property
def get_main_dir(self): def get_main_dir(self):
@ -371,6 +375,8 @@ class Config:
return bool((self.config_default_role is not None) and return bool((self.config_default_role is not None) and
(self.config_default_role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS)) (self.config_default_role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS))
def mature_content_tags(self):
return list(map(unicode.lstrip, self.config_mature_content_tags.split(",")))
def get_Log_Level(self): def get_Log_Level(self):
ret_value="" ret_value=""
@ -470,6 +476,11 @@ def migrate_Database():
'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES, 'side_category': SIDEBAR_CATEGORY, 'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES, 'side_category': SIDEBAR_CATEGORY,
'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR, 'detail_random': DETAIL_RANDOM}) 'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR, 'detail_random': DETAIL_RANDOM})
session.commit() session.commit()
try:
session.query(exists().where(User.mature_content)).scalar()
except exc.OperationalError:
conn = engine.connect()
conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")
if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None: if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
create_anonymous_user() create_anonymous_user()
try: try:
@ -484,6 +495,11 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_use_goodreads` INTEGER DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_use_goodreads` INTEGER DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_key` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_key` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_secret` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_secret` String DEFAULT ''")
try:
session.query(exists().where(Settings.config_mature_content_tags)).scalar()
except exc.OperationalError:
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_mature_content_tags` String DEFAULT ''")
def clean_database(): def clean_database():
# Remove expired remote login tokens # Remove expired remote login tokens

@ -8,11 +8,16 @@ except ImportError:
gdrive_support = False gdrive_support = False
try: try:
from goodreads import client as gr_client from goodreads.client import GoodreadsClient
goodreads_support = True goodreads_support = True
except ImportError: except ImportError:
goodreads_support = False goodreads_support = False
try:
from functools import reduce
except ImportError:
pass # We're not using Python 3
import mimetypes import mimetypes
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
@ -21,6 +26,7 @@ from flask import (Flask, render_template, request, Response, redirect,
url_for, send_from_directory, make_response, g, flash, url_for, send_from_directory, make_response, g, flash,
abort, Markup, stream_with_context) abort, Markup, stream_with_context)
from flask import __version__ as flaskVersion from flask import __version__ as flaskVersion
import cache_buster
import ub import ub
from ub import config from ub import config
import helper import helper
@ -200,6 +206,7 @@ mimetypes.add_type('image/vnd.djvu', '.djvu')
app = (Flask(__name__)) app = (Flask(__name__))
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
cache_buster.init_cache_busting(app)
gevent_server = None gevent_server = None
@ -499,21 +506,30 @@ def edit_required(f):
return inner return inner
# Fill indexpage with all requested data from database # Language and content filters
def fill_indexpage(page, database, db_filter, order): def common_filters():
if current_user.filter_language() != "all": if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else: else:
lang_filter = True lang_filter = True
content_rating_filter = false() if current_user.mature_content else \
db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
return and_(lang_filter, ~content_rating_filter)
# Fill indexpage with all requested data from database
def fill_indexpage(page, database, db_filter, order):
if current_user.show_detail_random(): if current_user.show_detail_random():
random = db.session.query(db.Books).filter(lang_filter).order_by(func.random()).limit(config.config_random_books) random = db.session.query(db.Books).filter(common_filters())\
.order_by(func.random()).limit(config.config_random_books)
else: else:
random = false random = false
off = int(int(config.config_books_per_page) * (page - 1)) off = int(int(config.config_books_per_page) * (page - 1))
pagination = Pagination(page, config.config_books_per_page, pagination = Pagination(page, config.config_books_per_page,
len(db.session.query(database).filter(db_filter).filter(lang_filter).all())) len(db.session.query(database)
entries = db.session.query(database).filter(db_filter).filter(lang_filter).order_by(order).offset(off).limit( .filter(db_filter).filter(common_filters()).all()))
config.config_books_per_page) entries = db.session.query(database).filter(common_filters())\
.order_by(order).offset(off).limit(config.config_books_per_page)
return entries, random, pagination return entries, random, pagination
@ -634,16 +650,13 @@ def feed_normal_search():
def feed_search(term): def feed_search(term):
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
if term: if term:
entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")), entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")),
db.Books.series.any(db.Series.name.like("%" + term + "%")), db.Books.series.any(db.Series.name.like("%" + term + "%")),
db.Books.authors.any(db.Authors.name.like("%" + term + "%")), db.Books.authors.any(db.Authors.name.like("%" + term + "%")),
db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")), db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")),
db.Books.title.like("%" + term + "%"))).filter(lang_filter).all() db.Books.title.like("%" + term + "%")))\
.filter(common_filters()).all()
entriescount = len(entries) if len(entries) > 0 else 1 entriescount = len(entries) if len(entries) > 0 else 1
pagination = Pagination(1, entriescount, entriescount) pagination = Pagination(1, entriescount, entriescount)
xml = render_title_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) xml = render_title_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
@ -671,11 +684,8 @@ def feed_new():
@app.route("/opds/discover") @app.route("/opds/discover")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_discover(): def feed_discover():
if current_user.filter_language() != "all": entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .limit(config.config_books_per_page)
else:
lang_filter = True
entries = db.session.query(db.Books).filter(lang_filter).order_by(func.random()).limit(config.config_books_per_page)
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page)) pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
xml = render_title_template('feed.xml', entries=entries, pagination=pagination) xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
response = make_response(xml) response = make_response(xml)
@ -703,10 +713,6 @@ def feed_hot():
off = request.args.get("offset") off = request.args.get("offset")
if not off: if not off:
off = 0 off = 0
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by( all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by(
ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id) ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
hot_books = all_books.offset(off).limit(config.config_books_per_page) hot_books = all_books.offset(off).limit(config.config_books_per_page)
@ -715,7 +721,9 @@ def feed_hot():
downloadBook = db.session.query(db.Books).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: if downloadBook:
entries.append( entries.append(
db.session.query(db.Books).filter(lang_filter).filter(db.Books.id == book.Downloads.book_id).first()) db.session.query(db.Books).filter(common_filters())
.filter(db.Books.id == book.Downloads.book_id).first()
)
else: else:
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
ub.session.commit() ub.session.commit()
@ -733,11 +741,7 @@ def feed_authorindex():
off = request.args.get("offset") off = request.args.get("offset")
if not off: if not off:
off = 0 off = 0
if current_user.filter_language() != "all": entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(lang_filter)\
.group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off) .group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(db.session.query(db.Authors).all())) len(db.session.query(db.Authors).all()))
@ -767,12 +771,8 @@ def feed_categoryindex():
off = request.args.get("offset") off = request.args.get("offset")
if not off: if not off:
off = 0 off = 0
if current_user.filter_language() != "all": entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page)
else:
lang_filter = True
entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(lang_filter).\
group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page)
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(db.session.query(db.Tags).all())) len(db.session.query(db.Tags).all()))
xml = render_title_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination) xml = render_title_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination)
@ -801,12 +801,8 @@ def feed_seriesindex():
off = request.args.get("offset") off = request.args.get("offset")
if not off: if not off:
off = 0 off = 0
if current_user.filter_language() != "all": entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all()
else:
lang_filter = True
entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(lang_filter).\
group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(db.session.query(db.Series).all())) len(db.session.query(db.Series).all()))
xml = render_title_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination) xml = render_title_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination)
@ -1071,12 +1067,9 @@ def titles_descending(page):
@app.route('/hot/page/<int:page>') @app.route('/hot/page/<int:page>')
@login_required_if_no_ano @login_required_if_no_ano
def hot_books(page): def hot_books(page):
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
if current_user.show_detail_random(): if current_user.show_detail_random():
random = db.session.query(db.Books).filter(lang_filter).order_by(func.random()).limit(config.config_random_books) random = db.session.query(db.Books).filter(common_filters())\
.order_by(func.random()).limit(config.config_random_books)
else: else:
random = false random = false
off = int(int(config.config_books_per_page) * (page - 1)) off = int(int(config.config_books_per_page) * (page - 1))
@ -1088,7 +1081,9 @@ def hot_books(page):
downloadBook = db.session.query(db.Books).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: if downloadBook:
entries.append( entries.append(
db.session.query(db.Books).filter(lang_filter).filter(db.Books.id == book.Downloads.book_id).first()) db.session.query(db.Books).filter(common_filters())
.filter(db.Books.id == book.Downloads.book_id).first()
)
else: else:
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
ub.session.commit() ub.session.commit()
@ -1120,13 +1115,9 @@ def discover(page):
@app.route("/author") @app.route("/author")
@login_required_if_no_ano @login_required_if_no_ano
def author_list(): def author_list():
if current_user.filter_language() != "all": entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count'))\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .join(db.books_authors_link).join(db.Books).filter(common_filters())\
else: .group_by('books_authors_link.author').order_by(db.Authors.sort).all()
lang_filter = True
entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count')).join(
db.books_authors_link).join(db.Books).filter(
lang_filter).group_by('books_authors_link.author').order_by(db.Authors.sort).all()
return render_title_template('list.html', entries=entries, folder='author', title=_(u"Author list")) return render_title_template('list.html', entries=entries, folder='author', title=_(u"Author list"))
@ -1136,31 +1127,34 @@ def author_list():
def author(book_id, page): def author(book_id, page):
entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id), entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id),
db.Books.timestamp.desc()) db.Books.timestamp.desc())
if entries: if entries is None:
name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name
author_info = None
if goodreads_support and config.config_use_goodreads:
gc = gr_client.GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
author_info = gc.find_author(author_name=name)
return render_title_template('author.html', entries=entries, pagination=pagination,
title=name, author=author_info)
else:
flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
return redirect(url_for("index")) return redirect(url_for("index"))
name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name
author_info = None
other_books = None
if goodreads_support and config.config_use_goodreads:
gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
author_info = gc.find_author(author_name=name)
# Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates
# Note: Not all images will be shown, even though they're available on Goodreads.com.
# See https://www.goodreads.com/topic/show/18213769-goodreads-book-images
identifiers = reduce(lambda acc, book: acc + map(lambda identifier: identifier.val, book.identifiers), entries.all(), [])
other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, author_info.books)
return render_title_template('author.html', entries=entries, pagination=pagination,
title=name, author=author_info, other_books=other_books)
@app.route("/series") @app.route("/series")
@login_required_if_no_ano @login_required_if_no_ano
def series_list(): def series_list():
if current_user.filter_language() != "all": entries = db.session.query(db.Series, func.count('books_series_link.book').label('count'))\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .join(db.books_series_link).join(db.Books).filter(common_filters())\
else: .group_by('books_series_link.series').order_by(db.Series.sort).all()
lang_filter = True
entries = db.session.query(db.Series, func.count('books_series_link.book').label('count')).join(
db.books_series_link).join(db.Books).filter(
lang_filter).group_by('books_series_link.series').order_by(db.Series.sort).all()
return render_title_template('list.html', entries=entries, folder='series', title=_(u"Series list")) return render_title_template('list.html', entries=entries, folder='series', title=_(u"Series list"))
@ -1227,13 +1221,9 @@ def language(name, page):
@app.route("/category") @app.route("/category")
@login_required_if_no_ano @login_required_if_no_ano
def category_list(): def category_list():
if current_user.filter_language() != "all": entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count'))\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .join(db.books_tags_link).join(db.Books).filter(common_filters())\
else: .group_by('books_tags_link.tag').all()
lang_filter = True
entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count')).join(
db.books_tags_link).join(db.Books).filter(
lang_filter).group_by('books_tags_link.tag').all()
return render_title_template('list.html', entries=entries, folder='category', title=_(u"Category list")) return render_title_template('list.html', entries=entries, folder='category', title=_(u"Category list"))
@ -1270,11 +1260,7 @@ def toggle_read(book_id):
@app.route("/book/<int:book_id>") @app.route("/book/<int:book_id>")
@login_required_if_no_ano @login_required_if_no_ano
def show_book(book_id): def show_book(book_id):
if current_user.filter_language() != "all": entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(lang_filter).first()
if entries: if entries:
for index in range(0, len(entries.languages)): for index in range(0, len(entries.languages)):
try: try:
@ -1547,15 +1533,12 @@ def update():
def search(): def search():
term = request.args.get("query").strip() term = request.args.get("query").strip()
if term: if term:
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")), entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")),
db.Books.series.any(db.Series.name.like("%" + term + "%")), db.Books.series.any(db.Series.name.like("%" + term + "%")),
db.Books.authors.any(db.Authors.name.like("%" + term + "%")), db.Books.authors.any(db.Authors.name.like("%" + term + "%")),
db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")), db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")),
db.Books.title.like("%" + term + "%"))).filter(lang_filter).all() db.Books.title.like("%" + term + "%")))\
.filter(common_filters()).all()
return render_title_template('search.html', searchterm=term, entries=entries) return render_title_template('search.html', searchterm=term, entries=entries)
else: else:
return render_title_template('search.html', searchterm="") return render_title_template('search.html', searchterm="")
@ -2273,8 +2256,9 @@ def profile():
content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
if "show_detail_random" in to_save: if "show_detail_random" in to_save:
content.sidebar_view += ub.DETAIL_RANDOM content.sidebar_view += ub.DETAIL_RANDOM
if "default_language" in to_save:
content.default_language = to_save["default_language"] content.mature_content = "show_mature_content" in to_save
try: try:
ub.session.commit() ub.session.commit()
except IntegrityError: except IntegrityError:
@ -2319,7 +2303,7 @@ def configuration_helper(origin):
success = False success = False
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() to_save = request.form.to_dict()
content = ub.session.query(ub.Settings).first() content = ub.session.query(ub.Settings).first() # type: ub.Settings
if "config_calibre_dir" in to_save: if "config_calibre_dir" in to_save:
if content.config_calibre_dir != to_save["config_calibre_dir"]: if content.config_calibre_dir != to_save["config_calibre_dir"]:
content.config_calibre_dir = to_save["config_calibre_dir"] content.config_calibre_dir = to_save["config_calibre_dir"]
@ -2393,6 +2377,9 @@ def configuration_helper(origin):
if "config_goodreads_api_secret" in to_save: if "config_goodreads_api_secret" in to_save:
content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
# Mature Content configuration
if "config_mature_content_tags" in to_save:
content.config_mature_content_tags = to_save["config_mature_content_tags"].strip()
content.config_default_role = 0 content.config_default_role = 0
if "admin_role" in to_save: if "admin_role" in to_save:
@ -2470,6 +2457,7 @@ def new_user():
content.nickname = to_save["nickname"] content.nickname = to_save["nickname"]
content.email = to_save["email"] content.email = to_save["email"]
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
content.mature_content = "show_mature_content" in to_save
if "locale" in to_save: if "locale" in to_save:
content.locale = to_save["locale"] content.locale = to_save["locale"]
content.sidebar_view = 0 content.sidebar_view = 0
@ -2557,7 +2545,7 @@ def edit_mailsettings():
@login_required @login_required
@admin_required @admin_required
def edit_user(user_id): def edit_user(user_id):
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
downloads = list() downloads = list()
languages = db.session.query(db.Languages).all() languages = db.session.query(db.Languages).all()
for lang in languages: for lang in languages:
@ -2665,6 +2653,8 @@ def edit_user(user_id):
elif "show_detail_random" not in to_save and content.show_detail_random(): elif "show_detail_random" not in to_save and content.show_detail_random():
content.sidebar_view -= ub.DETAIL_RANDOM content.sidebar_view -= ub.DETAIL_RANDOM
content.mature_content = "show_mature_content" in to_save
if "default_language" in to_save: if "default_language" in to_save:
content.default_language = to_save["default_language"] content.default_language = to_save["default_language"]
if "locale" in to_save and to_save["locale"]: if "locale" in to_save and to_save["locale"]:
@ -2691,11 +2681,8 @@ def edit_book(book_id):
# create the function for sorting... # create the function for sorting...
db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
if current_user.filter_language() != "all": book = db.session.query(db.Books)\
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language()) .filter(db.Books.id == book_id).filter(common_filters()).first()
else:
lang_filter = True
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(lang_filter).first()
author_names = [] author_names = []
# Book not found # Book not found
@ -2738,18 +2725,7 @@ def edit_book(book_id):
edited_books_id.add(book.id) edited_books_id.add(book.id)
book.author_sort = helper.get_sorted_author(input_authors[0]) book.author_sort = helper.get_sorted_author(input_authors[0])
if to_save["cover_url"] and os.path.splitext(to_save["cover_url"])[1].lower() == ".jpg": if to_save["cover_url"] and save_cover(to_save["cover_url"], book.path):
img = requests.get(to_save["cover_url"])
if config.config_use_google_drive:
tmpDir = tempfile.gettempdir()
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
f.write(img.content)
f.close()
gdriveutils.uploadFileToEbooksFolder(Gdrive.Instance().drive, os.path.join(book.path, 'cover.jpg'), os.path.join(tmpDir, f.name))
else:
f = open(os.path.join(config.config_calibre_dir, book.path, "cover.jpg"), "wb")
f.write(img.content)
f.close()
book.has_cover = 1 book.has_cover = 1
if book.series_index != to_save["series_index"]: if book.series_index != to_save["series_index"]:
@ -2901,6 +2877,25 @@ def edit_book(book_id):
title=_(u"edit metadata")) title=_(u"edit metadata"))
def save_cover(url, book_path):
img = requests.get(url)
if img.headers.get('content-type') != 'image/jpeg':
return false
if config.config_use_google_drive:
tmpDir = tempfile.gettempdir()
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
f.write(img.content)
f.close()
gdriveutils.uploadFileToEbooksFolder(Gdrive.Instance().drive, os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name))
return true
f = open(os.path.join(config.config_calibre_dir, book_path, "cover.jpg"), "wb")
f.write(img.content)
f.close()
return true
@app.route("/upload", methods=["GET", "POST"]) @app.route("/upload", methods=["GET", "POST"])
@login_required_if_no_ano @login_required_if_no_ano
@upload_required @upload_required
@ -2928,12 +2923,14 @@ def upload():
title = meta.title title = meta.title
author = meta.author author = meta.author
tags = meta.tags
title_dir = helper.get_valid_filename(title, False) series = meta.series
author_dir = helper.get_valid_filename(author, False) series_index = meta.series_id
title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(author)
data_name = title_dir data_name = title_dir
filepath = config.config_calibre_dir + os.sep + author_dir + os.sep + title_dir filepath = config.config_calibre_dir + os.sep + author_dir + os.sep + title_dir
saved_filename = filepath + os.sep + data_name + meta.extension saved_filename = filepath + os.sep + data_name + meta.extension.lower()
if not os.path.exists(filepath): if not os.path.exists(filepath):
try: try:
@ -2967,6 +2964,14 @@ def upload():
db_author = db.Authors(author, helper.get_sorted_author(author), "") db_author = db.Authors(author, helper.get_sorted_author(author), "")
db.session.add(db_author) db.session.add(db_author)
db_series = None
is_series = db.session.query(db.Series).filter(db.Series.name == series).first()
if is_series:
db_series = is_series
elif series != '':
db_series = db.Series(series, "")
db.session.add(db_series)
# add language actually one value in list # add language actually one value in list
input_language = meta.languages input_language = meta.languages
db_language = None db_language = None
@ -2980,9 +2985,11 @@ def upload():
db.session.add(db_language) db.session.add(db_language)
# combine path and normalize path from windows systems # combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/') path = os.path.join(author_dir, title_dir).replace('\\', '/')
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), 1, db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1),
datetime.datetime.now(), path, has_cover, db_author, [], db_language) series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language)
db_book.authors.append(db_author) db_book.authors.append(db_author)
if db_series:
db_book.series.append(db_series)
if db_language is not None: if db_language is not None:
db_book.languages.append(db_language) db_book.languages.append(db_language)
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, data_name) db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, data_name)
@ -2995,6 +3002,11 @@ def upload():
if upload_comment != "": if upload_comment != "":
db.session.add(db.Comments(upload_comment, db_book.id)) db.session.add(db.Comments(upload_comment, db_book.id))
db.session.commit() db.session.commit()
input_tags = tags.split(',')
input_tags = map(lambda it: it.strip(), input_tags)
modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags')
if db_language is not None: # display Full name instead of iso639.part3 if db_language is not None: # display Full name instead of iso639.part3
db_book.languages[0].language_name = _(meta.languages) db_book.languages[0].language_name = _(meta.languages)
author_names = [] author_names = []

File diff suppressed because it is too large Load Diff

@ -30,7 +30,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
## Quick start ## Quick start
1. Install required dependencies by executing `pip install -r requirements.txt` 1. Install dependencies by running `pip install --target vendor -r requirements.txt`.
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window) 2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog 3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button 4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
@ -70,7 +70,7 @@ Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the sen
## Using Google Drive integration ## Using Google Drive integration
Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install -r optional-requirements.txt` Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install --target vendor -r optional-requirements.txt`
To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com

Loading…
Cancel
Save