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
gdrive_credentials
#kindlegen
vendor/kindlegen
vendor

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

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

@ -55,10 +55,38 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
.block-label {display: block;}
.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-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,
#shelf-action-errors {
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();
});
@ -6,34 +8,51 @@ $("#have_read_cb").on("change", function() {
$(this).closest("form").submit();
});
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
(function() {
var templates = {
add: _.template(
$("#template-shelf-add").html()
),
remove: _.template(
$("#template-shelf-remove").html()
)
};
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
e.preventDefault();
$.get(this.href)
.done(() => {
const $this = $(this);
.done(function() {
var $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>`);
$("#remove-from-shelves").append(
templates.remove({
add: this.href,
remove: $this.data("remove-href"),
content: this.textContent
})
);
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>`);
$("#add-to-shelves").append(
templates.add({
add: $this.data("add-href"),
remove: this.href,
content: this.textContent
})
);
break;
}
this.parentNode.removeChild(this);
})
.fail((xhr) => {
const $msg = $("<span/>", { "class": "text-danger"}).text(xhr.responseText);
}.bind(this))
.fail(function(xhr) {
var $msg = $("<span/>", { "class": "text-danger"}).text(xhr.responseText);
$("#shelf-action-status").html($msg);
setTimeout(() => {
setTimeout(function() {
$msg.remove();
}, 10000);
});
});
});
})();

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

@ -4,11 +4,11 @@
* 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)
*/
/* global i18nMsg, tinymce */
/* global _, i18nMsg, tinymce */
var dbResults = [];
var ggResults = [];
$(document).ready(function () {
$(function () {
var msg = i18nMsg;
var douban = "https://api.douban.com";
var dbSearch = "/v2/book/search";
@ -22,113 +22,138 @@ $(document).ready(function () {
var ggDone = false;
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 () {
var book;
var i;
var bookHtml;
showFlag++;
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 (!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;
}
}
if (ggDone && ggResults.length > 0) {
for (i = 0; i < ggResults.length; i++) {
book = ggResults[i];
var bookCover;
if (book.volumeInfo.imageLinks) {
bookCover = book.volumeInfo.imageLinks.thumbnail;
} else {
bookCover = "/static/generic_cover.jpg";
}
bookHtml = "<li class=\"media\">" +
"<img class=\"pull-left img-responsive\" data-toggle=\"modal\" data-target=\"#metaModal\" src=\"" +
bookCover + "\" alt=\"Cover\" style=\"width:100px;height:150px\" onclick='getMeta(\"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>";
$("#metaModal #book-list").append(bookHtml);
ggResults.forEach(function(result) {
var book = {
id: result.id,
title: result.volumeInfo.title,
authors: result.volumeInfo.authors || [],
description: result.volumeInfo.description || "",
publisher: result.volumeInfo.publisher || "",
publishedDate: result.volumeInfo.publishedDate || "",
tags: result.volumeInfo.categories || [],
rating: result.volumeInfo.averageRating || 0,
cover: result.volumeInfo.imageLinks ?
result.volumeInfo.imageLinks.thumbnail :
"/static/generic_cover.jpg",
url: "https://books.google.com/books?id=" + result.id,
source: {
id: "google",
description: "Google Books",
url: "https://books.google.com/"
}
};
var $book = $(templates.bookResult(book));
$book.find("img").on("click", function () {
populateForm(book);
});
$("#book-list").append($book);
});
ggDone = false;
}
if (dbDone && dbResults.length > 0) {
for (i = 0; i < dbResults.length; i++) {
book = dbResults[i];
bookHtml = "<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='getMeta(\"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>";
$("#metaModal #book-list").append(bookHtml);
dbResults.forEach(function(result) {
var book = {
id: result.id,
title: result.title,
authors: result.author || [],
description: result.summary,
publisher: result.publisher || "",
publishedDate: result.pubdate || "",
tags: result.tags.map(function(tag) {
return tag.title;
}),
rating: result.rating.average || 0,
cover: result.image,
url: "https://book.douban.com/subject/" + result.id,
source: {
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;
}
}
function ggSearchBook (title) {
title = title.replaceAll(/\s+/, "+");
var url = google + ggSearch + "?q=" + title;
$.ajax({
url,
url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"),
type: "GET",
dataType: "jsonp",
jsonp: "callback",
success (data) {
success: function success(data) {
ggResults = data.items;
},
complete () {
complete: function complete() {
ggDone = true;
showResult();
$("#show-google").trigger("change");
}
});
}
function dbSearchBook (title) {
var url = douban + dbSearch + "?q=" + title + "&fields=all&count=10";
$.ajax({
url,
url: douban + dbSearch + "?q=" + title + "&fields=all&count=10",
type: "GET",
dataType: "jsonp",
jsonp: "callback",
success (data) {
success: function success(data) {
dbResults = data.books;
},
error () {
$("#metaModal #meta-info").html("<p class=\"text-danger\">"+ msg.search_error+"!</p>");
error: function error() {
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>");
},
complete () {
complete: function complete() {
dbDone = true;
showResult();
$("#show-douban").trigger("change");
}
});
}
function doSearch (keyword) {
showFlag = 0;
$("#metaModal #meta-info").text(msg.loading);
$("#meta-info").text(msg.loading);
// var keyword = $("#keyword").val();
if (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();
if (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
// e.g.
// <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 showOrHide = $this.prop("checked");
$("[data-related=\""+name+"\"]").each(function () {
$("[data-related=\"" + name + "\"]").each(function () {
$(this).toggle(showOrHide);
});
});
$(function() {
var updateTimerID;
var updateText;
// Allow ajax prefilters to be added/removed dynamically
// eslint-disable-next-line new-cap
const preFilters = $.Callbacks();
var preFilters = $.Callbacks();
$.ajaxPrefilter(preFilters.fire);
function restartTimer() {
@ -31,28 +29,28 @@ $(function() {
function updateTimer() {
$.ajax({
dataType: "json",
url: window.location.pathname+"/../../get_updater_status",
success(data) {
url: window.location.pathname + "/../../get_updater_status",
success: function success(data) {
// console.log(data.status);
$("#UpdateprogressDialog #Updatecontent").html(updateText[data.status]);
if (data.status >6){
$("#Updatecontent").html(updateText[data.status]);
if (data.status > 6) {
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#UpdateprogressDialog #updateFinished").removeClass("hidden");
$("#updateFinished").removeClass("hidden");
$("#check_for_update").removeClass("hidden");
$("#perform_update").addClass("hidden");
}
},
error() {
error: function error() {
// console.log('Done');
clearInterval(updateTimerID);
$("#spinner2").hide();
$("#UpdateprogressDialog #Updatecontent").html(updateText[7]);
$("#UpdateprogressDialog #updateFinished").removeClass("hidden");
$("#Updatecontent").html(updateText[7]);
$("#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)
itemSelector : ".load-more .book",
animate : true,
extraScrollPx: 300,
extraScrollPx: 300
// selector for all items you'll retrieve
}, function(data){
}, function(data) {
$(".load-more .row").isotope( "appended", $(data), null );
});
$("#sendbtn").click(function(){
$("#sendbtn").click(function() {
var $this = $(this);
$this.text("Please wait...");
$this.addClass("disabled");
@ -84,36 +82,39 @@ $(function() {
$("#restart").click(function() {
$.ajax({
dataType: "json",
url: window.location.pathname+"/../../shutdown",
url: window.location.pathname + "/../../shutdown",
data: {"parameter":0},
success(data) {
success: function success() {
$("#spinner").show();
displaytext=data.text;
setTimeout(restartTimer, 3000);}
setTimeout(restartTimer, 3000);
}
});
});
$("#shutdown").click(function() {
$.ajax({
dataType: "json",
url: window.location.pathname+"/../../shutdown",
url: window.location.pathname + "/../../shutdown",
data: {"parameter":1},
success(data) {
return alert(data.text);}
success: function success(data) {
return alert(data.text);
}
});
});
$("#check_for_update").click(function() {
var buttonText = $("#check_for_update").html();
$("#check_for_update").html("...");
var $this = $(this);
var buttonText = $this.html();
$this.html("...");
$.ajax({
dataType: "json",
url: window.location.pathname+"/../../get_update_status",
success(data) {
$("#check_for_update").html(buttonText);
url: window.location.pathname + "/../../get_update_status",
success: function success(data) {
$this.html(buttonText);
if (data.status === true) {
$("#check_for_update").addClass("hidden");
$("#perform_update").removeClass("hidden");
$("#update_info").removeClass("hidden");
$("#update_info").find("span").html(data.commit);
$("#update_info")
.removeClass("hidden")
.find("span").html(data.commit);
}
}
});
@ -121,7 +122,7 @@ $(function() {
$("#restart_database").click(function() {
$.ajax({
dataType: "json",
url: window.location.pathname+"/../../shutdown",
url: window.location.pathname + "/../../shutdown",
data: {"parameter":2}
});
});
@ -131,12 +132,13 @@ $(function() {
type: "POST",
dataType: "json",
data: { start: "True"},
url: window.location.pathname+"/../../get_updater_status",
success(data) {
updateText=data.text;
$("#UpdateprogressDialog #Updatecontent").html(updateText[data.status]);
url: window.location.pathname + "/../../get_updater_status",
success: function success(data) {
updateText = data.text;
$("#Updatecontent").html(updateText[data.status]);
// console.log(data.status);
updateTimerID=setInterval(updateTimer, 2000);}
updateTimerID = setInterval(updateTimer, 2000);
}
});
});
@ -144,10 +146,10 @@ $(function() {
$("#bookDetailsModal")
.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
const useCache = (options) => {
var useCache = function(options) {
options.async = true;
options.cache = true;
};
@ -162,7 +164,7 @@ $(function() {
$(this).find(".modal-body").html("...");
});
$(window).resize(function(event) {
$(window).resize(function() {
$(".discover .row").isotope("reLayout");
});
});

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

@ -11,16 +11,17 @@
{%if author.about is not none %}
<p>{{author.about|safe}}</p>
{% endif %}
</section>
<a href="{{author.link}}" class="author-link" target="_blank">
<img src="{{ url_for('static', filename='img/goodreads.svg') }}" alt="Goodreads">
</a>
- {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a>
</section>
<div class="clearfix"></div>
{% endif %}
<div class="discover load-more">
{% if author is not none %}
<h3>{{_("In Library")}}</h3>
{% endif %}
<div class="row">
{% if entries[0] %}
{% for entry in entries %}
@ -62,4 +63,48 @@
{% endif %}
</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 %}

@ -139,7 +139,7 @@
<div class="modal-header bg-danger text-center">
<span>{{_('Are really you sure?')}}</span>
</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>{{_('and from hard disk')}}</span>
</div>
@ -154,23 +154,36 @@
{% endif %}
<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-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="metaModalLabel">{{_('Get metadata')}}</h4>
<form class="form-inline">
<div class="form-group">
<form class="padded-bottom" id="meta-search">
<div class="input-group">
<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>
<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>{{_('Click the cover to load metadata to the form')}}</div>
</div>
<div class="modal-body">
<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 class="modal-body" id="meta-info">
<div id="meta-info">
{{_("Loading...")}}
</div>
<ul id="book-list" class="media-list"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div>
@ -180,6 +193,31 @@
{% endblock %}
{% 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>
var i18nMsg = {
'loading': {{_('Loading...')|safe|tojson}},

@ -72,6 +72,13 @@
<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">
</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">
<label for="config_log_level">{{_('Log Level')}}</label>
<select name="config_log_level" id="config_log_level" class="form-control">

@ -259,5 +259,17 @@
{% endblock %}
{% 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>
{% endblock %}

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

@ -41,7 +41,10 @@
</select>
</div>
<div class="col-sm-6">
<div class="form-group">
<input type="checkbox" name="show_mature_content" id="show_mature_content" {% if content.mature_content %}checked{% endif %}>
<label for="show_mature_content">{{_('Show mature content')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="show_random" id="show_random" {% if content.show_random_books() %}checked{% endif %}>
<label for="show_random">{{_('Show random books')}}</label>
@ -78,7 +81,7 @@
<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 class="col-sm-6">
{% if g.user and g.user.role_admin() and not profile %}
{% 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")
sidebar_view = Column(Integer, default=1)
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
@ -166,13 +167,14 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.loadSettings()
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()
self.nickname = data.nickname
self.role = data.role
self.sidebar_view = data.sidebar_view
self.default_language = data.default_language
self.locale = data.locale
self.mature_content = data.mature_content
self.anon_browse = settings.config_anonbrowse
def role_admin(self):
@ -266,6 +268,7 @@ class Settings(Base):
config_use_goodreads = Column(Boolean)
config_goodreads_api_key = Column(String)
config_goodreads_api_secret = Column(String)
config_mature_content_tags = Column(String) # type: str
def __repr__(self):
pass
@ -297,7 +300,7 @@ class Config:
self.loadSettings()
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_port = data.config_port
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_goodreads_api_key = data.config_goodreads_api_key
self.config_goodreads_api_secret = data.config_goodreads_api_secret
self.config_mature_content_tags = data.config_mature_content_tags
@property
def get_main_dir(self):
@ -371,6 +375,8 @@ class Config:
return bool((self.config_default_role is not None) and
(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):
ret_value=""
@ -470,6 +476,11 @@ def migrate_Database():
'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES, 'side_category': SIDEBAR_CATEGORY,
'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR, 'detail_random': DETAIL_RANDOM})
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:
create_anonymous_user()
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_goodreads_api_key` 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():
# Remove expired remote login tokens

@ -8,11 +8,16 @@ except ImportError:
gdrive_support = False
try:
from goodreads import client as gr_client
from goodreads.client import GoodreadsClient
goodreads_support = True
except ImportError:
goodreads_support = False
try:
from functools import reduce
except ImportError:
pass # We're not using Python 3
import mimetypes
import logging
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,
abort, Markup, stream_with_context)
from flask import __version__ as flaskVersion
import cache_buster
import ub
from ub import config
import helper
@ -200,6 +206,7 @@ mimetypes.add_type('image/vnd.djvu', '.djvu')
app = (Flask(__name__))
app.wsgi_app = ReverseProxied(app.wsgi_app)
cache_buster.init_cache_busting(app)
gevent_server = None
@ -499,21 +506,30 @@ def edit_required(f):
return inner
# Fill indexpage with all requested data from database
def fill_indexpage(page, database, db_filter, order):
# Language and content filters
def common_filters():
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
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():
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:
random = false
off = int(int(config.config_books_per_page) * (page - 1))
pagination = Pagination(page, config.config_books_per_page,
len(db.session.query(database).filter(db_filter).filter(lang_filter).all()))
entries = db.session.query(database).filter(db_filter).filter(lang_filter).order_by(order).offset(off).limit(
config.config_books_per_page)
len(db.session.query(database)
.filter(db_filter).filter(common_filters()).all()))
entries = db.session.query(database).filter(common_filters())\
.order_by(order).offset(off).limit(config.config_books_per_page)
return entries, random, pagination
@ -634,16 +650,13 @@ def feed_normal_search():
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:
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.authors.any(db.Authors.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
pagination = Pagination(1, entriescount, entriescount)
xml = render_title_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
@ -671,11 +684,8 @@ def feed_new():
@app.route("/opds/discover")
@requires_basic_auth_if_no_ano
def feed_discover():
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(lang_filter).order_by(func.random()).limit(config.config_books_per_page)
entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\
.limit(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)
response = make_response(xml)
@ -703,10 +713,6 @@ def feed_hot():
off = request.args.get("offset")
if not off:
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(
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)
@ -715,7 +721,9 @@ def feed_hot():
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(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:
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
ub.session.commit()
@ -733,11 +741,7 @@ def feed_authorindex():
off = request.args.get("offset")
if not off:
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
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(lang_filter)\
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\
.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,
len(db.session.query(db.Authors).all()))
@ -767,12 +771,8 @@ def feed_categoryindex():
off = request.args.get("offset")
if not off:
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
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)
entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\
.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,
len(db.session.query(db.Tags).all()))
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")
if not off:
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
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()
entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\
.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,
len(db.session.query(db.Series).all()))
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>')
@login_required_if_no_ano
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():
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:
random = false
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()
if downloadBook:
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:
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
ub.session.commit()
@ -1120,13 +1115,9 @@ def discover(page):
@app.route("/author")
@login_required_if_no_ano
def author_list():
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.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()
entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count'))\
.join(db.books_authors_link).join(db.Books).filter(common_filters())\
.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"))
@ -1136,31 +1127,34 @@ def author_list():
def author(book_id, page):
entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id),
db.Books.timestamp.desc())
if entries:
if entries is None:
flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
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 = gr_client.GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
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)
else:
flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
return redirect(url_for("index"))
title=name, author=author_info, other_books=other_books)
@app.route("/series")
@login_required_if_no_ano
def series_list():
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.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()
entries = db.session.query(db.Series, func.count('books_series_link.book').label('count'))\
.join(db.books_series_link).join(db.Books).filter(common_filters())\
.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"))
@ -1227,13 +1221,9 @@ def language(name, page):
@app.route("/category")
@login_required_if_no_ano
def category_list():
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.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()
entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count'))\
.join(db.books_tags_link).join(db.Books).filter(common_filters())\
.group_by('books_tags_link.tag').all()
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>")
@login_required_if_no_ano
def show_book(book_id):
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.Books.id == book_id).filter(lang_filter).first()
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
if entries:
for index in range(0, len(entries.languages)):
try:
@ -1547,15 +1533,12 @@ def update():
def search():
term = request.args.get("query").strip()
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 + "%")),
db.Books.series.any(db.Series.name.like("%" + term + "%")),
db.Books.authors.any(db.Authors.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)
else:
return render_title_template('search.html', searchterm="")
@ -2273,8 +2256,9 @@ def profile():
content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
if "show_detail_random" in to_save:
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:
ub.session.commit()
except IntegrityError:
@ -2319,7 +2303,7 @@ def configuration_helper(origin):
success = False
if request.method == "POST":
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 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:
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
if "admin_role" in to_save:
@ -2470,6 +2457,7 @@ def new_user():
content.nickname = to_save["nickname"]
content.email = to_save["email"]
content.default_language = to_save["default_language"]
content.mature_content = "show_mature_content" in to_save
if "locale" in to_save:
content.locale = to_save["locale"]
content.sidebar_view = 0
@ -2557,7 +2545,7 @@ def edit_mailsettings():
@login_required
@admin_required
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()
languages = db.session.query(db.Languages).all()
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():
content.sidebar_view -= ub.DETAIL_RANDOM
content.mature_content = "show_mature_content" in to_save
if "default_language" in to_save:
content.default_language = to_save["default_language"]
if "locale" in to_save and to_save["locale"]:
@ -2691,11 +2681,8 @@ def edit_book(book_id):
# create the function for sorting...
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()
if current_user.filter_language() != "all":
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
else:
lang_filter = True
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(lang_filter).first()
book = db.session.query(db.Books)\
.filter(db.Books.id == book_id).filter(common_filters()).first()
author_names = []
# Book not found
@ -2738,18 +2725,7 @@ def edit_book(book_id):
edited_books_id.add(book.id)
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":
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()
if to_save["cover_url"] and save_cover(to_save["cover_url"], book.path):
book.has_cover = 1
if book.series_index != to_save["series_index"]:
@ -2901,6 +2877,25 @@ def edit_book(book_id):
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"])
@login_required_if_no_ano
@upload_required
@ -2928,12 +2923,14 @@ def upload():
title = meta.title
author = meta.author
title_dir = helper.get_valid_filename(title, False)
author_dir = helper.get_valid_filename(author, False)
tags = meta.tags
series = meta.series
series_index = meta.series_id
title_dir = helper.get_valid_filename(title)
author_dir = helper.get_valid_filename(author)
data_name = 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):
try:
@ -2967,6 +2964,14 @@ def upload():
db_author = db.Authors(author, helper.get_sorted_author(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
input_language = meta.languages
db_language = None
@ -2980,9 +2985,11 @@ def upload():
db.session.add(db_language)
# combine path and normalize path from windows systems
path = os.path.join(author_dir, title_dir).replace('\\', '/')
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), 1,
datetime.datetime.now(), path, has_cover, db_author, [], db_language)
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1),
series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language)
db_book.authors.append(db_author)
if db_series:
db_book.series.append(db_series)
if db_language is not None:
db_book.languages.append(db_language)
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, data_name)
@ -2995,6 +3002,11 @@ def upload():
if upload_comment != "":
db.session.add(db.Comments(upload_comment, db_book.id))
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
db_book.languages[0].language_name = _(meta.languages)
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
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)
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
@ -70,7 +70,7 @@ Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the sen
## 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

Loading…
Cancel
Save