From 9c1b3f136f9df2d5d56cc7a7b7a23c54ddd24cc7 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 16 Mar 2019 15:48:09 +0100 Subject: [PATCH] Improved sorting for rated,random, hot books, read/unread book --- cps/admin.py | 5 +- cps/editbooks.py | 3 +- cps/helper.py | 112 ++++++ cps/opds.py | 4 +- cps/templates/discover.html | 1 - cps/templates/index.html | 14 +- cps/templates/layout.html | 14 +- cps/ub.py | 12 +- cps/web.py | 706 ++++++++++++++++-------------------- 9 files changed, 445 insertions(+), 426 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 9b7402c5..cc8a163e 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -25,8 +25,8 @@ import os from flask import Blueprint, flash, redirect, url_for from flask import abort, request, make_response from flask_login import login_required, current_user, logout_user -from web import admin_required, render_title_template, before_request, speaking_language, unconfigured, \ - login_required_if_no_ano, check_valid_domain +from web import admin_required, render_title_template, before_request, unconfigured, \ + login_required_if_no_ano from cps import db, ub, Server, get_locale, config, app, updater_thread, babel import json from datetime import datetime, timedelta @@ -37,6 +37,7 @@ from babel import Locale as LC from sqlalchemy.exc import IntegrityError from gdriveutils import is_gdrive_ready, gdrive_support, downloadFile, deleteDatabaseOnChange, listRootFolders import helper +from helper import speaking_language, check_valid_domain from werkzeug.security import generate_password_hash try: from imp import reload diff --git a/cps/editbooks.py b/cps/editbooks.py index 1da3aa77..a39e32a0 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -31,8 +31,9 @@ import json from flask_babel import gettext as _ from uuid import uuid4 import helper +from helper import order_authors, common_filters from flask_login import current_user -from web import login_required_if_no_ano, common_filters, order_authors, render_title_template, edit_required, \ +from web import login_required_if_no_ano, render_title_template, edit_required, \ upload_required, login_required, EXTENSIONS_UPLOAD import gdriveutils from shutil import move, copyfile diff --git a/cps/helper.py b/cps/helper.py index 1175f9f0..63f77950 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -32,9 +32,15 @@ from flask import send_from_directory, make_response, redirect, abort from flask_babel import gettext as _ from flask_login import current_user from babel.dates import format_datetime +from babel.core import UnknownLocaleError from datetime import datetime +from babel import Locale as LC import shutil import requests +from sqlalchemy.sql.expression import true, and_, false, text, func +from iso639 import languages as isoLanguages +from pagination import Pagination + try: import gdriveutils as gd except ImportError: @@ -558,3 +564,109 @@ def render_task_status(tasklist): renderedtasklist.append(task) return renderedtasklist + + +# Language and content filters for displaying in the UI +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) + + +# Creates for all stored languages a translated speaking name in the array for the UI +def speaking_language(languages=None): + if not languages: + languages = db.session.query(db.Languages).all() + for lang in languages: + try: + cur_l = LC.parse(lang.lang_code) + lang.name = cur_l.get_language_name(get_locale()) + except UnknownLocaleError: + lang.name = _(isoLanguages.get(part3=lang.lang_code).name) + return languages + +# checks if domain is in database (including wildcards) +# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; +# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ +def check_valid_domain(domain_text): + domain_text = domain_text.split('@', 1)[-1].lower() + sql = "SELECT * FROM registration WHERE :domain LIKE domain;" + result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all() + return len(result) + + +# Orders all Authors in the list according to authors sort +def order_authors(entry): + sort_authors = entry.author_sort.split('&') + authors_ordered = list() + error = False + for auth in sort_authors: + # ToDo: How to handle not found authorname + result = db.session.query(db.Authors).filter(db.Authors.sort == auth.lstrip().strip()).first() + if not result: + error = True + break + authors_ordered.append(result) + if not error: + entry.authors = authors_ordered + return entry + + +# Fill indexpage with all requested data from database +def fill_indexpage(page, database, db_filter, order, *join): + if current_user.show_detail_random(): + randm = db.session.query(db.Books).filter(common_filters())\ + .order_by(func.random()).limit(config.config_random_books) + else: + randm = 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(common_filters()).all())) + entries = db.session.query(database).join(*join, isouter=True).filter(db_filter).filter(common_filters()).\ + order_by(*order).offset(off).limit(config.config_books_per_page).all() + for book in entries: + book = order_authors(book) + return entries, randm, pagination + + +# read search results from calibre-database and return it (function is used for feed and simple search +def get_search_results(term): + q = list() + authorterms = re.split("[, ]+", term) + for authorterm in authorterms: + q.append(db.Books.authors.any(db.Authors.name.ilike("%" + authorterm + "%"))) + db.session.connection().connection.connection.create_function("lower", 1, db.lcase) + db.Books.authors.any(db.Authors.name.ilike("%" + term + "%")) + + return db.session.query(db.Books).filter(common_filters()).filter( + db.or_(db.Books.tags.any(db.Tags.name.ilike("%" + term + "%")), + db.Books.series.any(db.Series.name.ilike("%" + term + "%")), + db.Books.authors.any(and_(*q)), + db.Books.publishers.any(db.Publishers.name.ilike("%" + term + "%")), + db.Books.title.ilike("%" + term + "%"))).all() + + +def get_unique_other_books(library_books, author_books): + # 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), + library_books, []) + other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, + author_books) + + # Fuzzy match book titles + if feature_support['levenshtein']: + library_titles = reduce(lambda acc, book: acc + [book.title], library_books, []) + other_books = filter(lambda author_book: not filter( + lambda library_book: + # Remove items in parentheses before comparing + Levenshtein.ratio(re.sub(r"\(.*\)", "", author_book.title), library_book) > 0.7, + library_titles + ), other_books) + + return other_books diff --git a/cps/opds.py b/cps/opds.py index 73cd790d..873f7440 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -30,12 +30,12 @@ import datetime import ub from flask_login import current_user from functools import wraps -from web import login_required_if_no_ano, fill_indexpage, common_filters, get_search_results, render_read_books +from web import login_required_if_no_ano, common_filters, get_search_results, render_read_books, download_required from sqlalchemy.sql.expression import func, text import helper from werkzeug.security import check_password_hash from werkzeug.datastructures import Headers -from web import download_required +from helper import fill_indexpage import sys try: diff --git a/cps/templates/discover.html b/cps/templates/discover.html index 532d53a2..d23f32d7 100644 --- a/cps/templates/discover.html +++ b/cps/templates/discover.html @@ -3,7 +3,6 @@

{{title}}

- {% for entry in entries %}
diff --git a/cps/templates/index.html b/cps/templates/index.html index 6a475e22..478379ae 100755 --- a/cps/templates/index.html +++ b/cps/templates/index.html @@ -58,14 +58,14 @@ web. {% for entry in random %}

{{_(title)}}

diff --git a/cps/templates/layout.html b/cps/templates/layout.html index 93c8ab23..d6b207da 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -121,22 +121,10 @@
    {% if g.user.check_visibility(1024) %} - {%endif%} {% for element in sidebar %} {% if g.user.check_visibility(element['visibility']) and element['public'] %} - + {% endif %} {% endfor %} diff --git a/cps/ub.py b/cps/ub.py index 2e5f33d5..df57de3d 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -107,21 +107,21 @@ def get_sidebar_config(kwargs=[]): sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new", "visibility": SIDEBAR_RECENT, 'public': True, "page": "root", "show_text": _('Show recent books'), "config_show":True}) - sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.hot_books', "id": "hot", + sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", "visibility": SIDEBAR_HOT, 'public': True, "page": "hot", "show_text": _('Show hot books'), "config_show":True}) sidebar.append( - {"glyph": "glyphicon-star", "text": _('Best rated Books'), "link": 'web.best_rated_books', "id": "rated", + {"glyph": "glyphicon-star", "text": _('Best rated Books'), "link": 'web.books_list', "id": "rated", "visibility": SIDEBAR_BEST_RATED, 'public': True, "page": "rated", "show_text": _('Show best rated books'), "config_show":True}) - sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.read_books', "id": "read", + sidebar.append({"glyph": "glyphicon-eye-open", "text": _('Read Books'), "link": 'web.books_list', "id": "read", "visibility": SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "read", "show_text": _('Show read and unread'), "config_show": content}) sidebar.append( - {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.unread_books', "id": "unread", - "visibility": SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "read", + {"glyph": "glyphicon-eye-close", "text": _('Unread Books'), "link": 'web.books_list', "id": "unread", + "visibility": SIDEBAR_READ_AND_UNREAD, 'public': (not g.user.is_anonymous), "page": "unread", "show_text": _('Show unread'), "config_show":False}) - sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.discover', "id": "rand", + sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand", "visibility": SIDEBAR_RANDOM, 'public': True, "page": "discover", "show_text": _('Show random books'), "config_show":True}) sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat", diff --git a/cps/web.py b/cps/web.py index 07495bd5..a4d328d2 100644 --- a/cps/web.py +++ b/cps/web.py @@ -22,10 +22,11 @@ # along with this program. If not, see . from cps import mimetypes, global_WorkerThread, searched_ids -from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, \ - url_for +from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for from werkzeug.exceptions import default_exceptions import helper +from helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \ + order_authors import os from sqlalchemy.exc import IntegrityError from flask_login import login_user, logout_user, login_required, current_user @@ -36,9 +37,7 @@ from babel import Locale as LC from babel.dates import format_date from babel.core import UnknownLocaleError import base64 -# from sqlalchemy.sql import * -from sqlalchemy import String as SQLString -from sqlalchemy.sql.expression import text, func, cast, true, and_, false +from sqlalchemy.sql.expression import text, func, true, and_, false, not_ import json import datetime from iso639 import languages as isoLanguages @@ -135,6 +134,7 @@ for ex in default_exceptions: web = Blueprint('web', __name__) +# ################################### Login logic and rights management ############################################### @lm.user_loader def load_user(user_id): @@ -239,89 +239,7 @@ def edit_required(f): return inner - -# Language and content filters for displaying in the UI -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) - - -# Creates for all stored languages a translated speaking name in the array for the UI -def speaking_language(languages=None): - if not languages: - languages = db.session.query(db.Languages).all() - for lang in languages: - try: - cur_l = LC.parse(lang.lang_code) - lang.name = cur_l.get_language_name(get_locale()) - except UnknownLocaleError: - lang.name = _(isoLanguages.get(part3=lang.lang_code).name) - return languages - -# checks if domain is in database (including wildcards) -# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name; -# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/ -def check_valid_domain(domain_text): - domain_text = domain_text.split('@', 1)[-1].lower() - sql = "SELECT * FROM registration WHERE :domain LIKE domain;" - result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all() - return len(result) - - -# Orders all Authors in the list according to authors sort -def order_authors(entry): - sort_authors = entry.author_sort.split('&') - authors_ordered = list() - error = False - for auth in sort_authors: - # ToDo: How to handle not found authorname - result = db.session.query(db.Authors).filter(db.Authors.sort == auth.lstrip().strip()).first() - if not result: - error = True - break - authors_ordered.append(result) - if not error: - entry.authors = authors_ordered - return entry - - -# Fill indexpage with all requested data from database -def fill_indexpage(page, database, db_filter, order, *join): - if current_user.show_detail_random(): - randm = db.session.query(db.Books).filter(common_filters())\ - .order_by(func.random()).limit(config.config_random_books) - else: - randm = 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(common_filters()).all())) - entries = db.session.query(database).join(*join, isouter=True).filter(db_filter).filter(common_filters()).\ - order_by(*order).offset(off).limit(config.config_books_per_page).all() - for book in entries: - book = order_authors(book) - return entries, randm, pagination - - -# read search results from calibre-database and return it (function is used for feed and simple search -def get_search_results(term): - q = list() - authorterms = re.split("[, ]+", term) - for authorterm in authorterms: - q.append(db.Books.authors.any(db.Authors.name.ilike("%" + authorterm + "%"))) - db.session.connection().connection.connection.create_function("lower", 1, db.lcase) - db.Books.authors.any(db.Authors.name.ilike("%" + term + "%")) - - return db.session.query(db.Books).filter(common_filters()).filter( - db.or_(db.Books.tags.any(db.Tags.name.ilike("%" + term + "%")), - db.Books.series.any(db.Series.name.ilike("%" + term + "%")), - db.Books.authors.any(and_(*q)), - db.Books.publishers.any(db.Publishers.name.ilike("%" + term + "%")), - db.Books.title.ilike("%" + term + "%"))).all() +# ################################### Helper functions ################################################################ # Returns the template for rendering and includes the instance name @@ -343,6 +261,9 @@ def before_request(): return redirect(url_for('admin.basic_configuration')) + +# ################################### data provider functions ######################################################### + @web.route("/ajax/emailstat") @login_required def get_email_status_json(): @@ -354,8 +275,59 @@ def get_email_status_json(): return response +@web.route("/ajax/bookmark//", methods=['POST']) +@login_required +def bookmark(book_id, book_format): + bookmark_key = request.form["bookmark"] + ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), + ub.Bookmark.book_id == book_id, + ub.Bookmark.format == book_format)).delete() + if not bookmark_key: + ub.session.commit() + return "", 204 + + lbookmark = ub.Bookmark(user_id=current_user.id, + book_id=book_id, + format=book_format, + bookmark_key=bookmark_key) + ub.session.merge(lbookmark) + ub.session.commit() + return "", 201 +@web.route("/ajax/toggleread/", methods=['POST']) +@login_required +def toggle_read(book_id): + if not config.config_read_column: + book = ub.session.query(ub.ReadBook).filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), + ub.ReadBook.book_id == book_id)).first() + if book: + book.is_read = not book.is_read + else: + readBook = ub.ReadBook() + readBook.user_id = int(current_user.id) + readBook.book_id = book_id + readBook.is_read = True + book = readBook + ub.session.merge(book) + ub.session.commit() + else: + try: + db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) + book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() + read_status = getattr(book, 'custom_column_' + str(config.config_read_column)) + if len(read_status): + read_status[0].value = not read_status[0].value + db.session.commit() + else: + cc_class = db.cc_classes[config.config_read_column] + new_cc = cc_class(value=1, book=book_id) + db.session.add(new_cc) + db.session.commit() + except KeyError: + app.logger.error( + u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) + return "" ''' @web.route("/ajax/getcomic///") @@ -408,6 +380,7 @@ def get_comic_book(book_id, book_format, page): return "", 204 ''' +# ################################### Typeahead ################################################################## @web.route("/get_authors_json", methods=['GET', 'POST']) @login_required_if_no_ano @@ -491,6 +464,8 @@ def get_matching_tags(): return json_dumps +# ################################### View Books list ################################################################## + @web.route("/", defaults={'page': 1}) @web.route('/page/') @login_required_if_no_ano @@ -500,71 +475,82 @@ def index(page): title=_(u"Recently Added Books"), page="root") -@web.route('/books/newest', defaults={'page': 1}) -@web.route('/books/newest/page/') -@login_required_if_no_ano -def newest_books(page): - entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.pubdate.desc()]) - return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Newest Books"), page="newest") - - -@web.route('/books/oldest', defaults={'page': 1}) -@web.route('/books/oldest/page/') +@web.route('//', defaults={'page': 1}) +@web.route('///') @login_required_if_no_ano -def oldest_books(page): - entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.pubdate]) - return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Oldest Books"), page="oldest") - - -@web.route('/books/a-z', defaults={'page': 1}) -@web.route('/books/a-z/page/') -@login_required_if_no_ano -def titles_ascending(page): - entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.sort]) - return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Books (A-Z)"), page="a-z") - - -@web.route('/books/z-a', defaults={'page': 1}) -@web.route('/books/z-a/page/') -@login_required_if_no_ano -def titles_descending(page): - entries, random, pagination = fill_indexpage(page, db.Books, True, [db.Books.sort.desc()]) - return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Books (Z-A)"), page="z-a") +def books_list(data,sort, page): + order = [db.Books.timestamp.desc()] + if sort == 'pubnew': + order = [db.Books.pubdate.desc()] + if sort == 'pubold': + order = [db.Books.pubdate] + if sort == 'abc': + [db.Books.sort] + if sort == 'zyx': + [db.Books.sort.desc()] + if sort == 'new': + order = [db.Books.timestamp.desc()] + if sort == 'old': + order = [db.Books.timestamp] + + if data == "rated": + if current_user.check_visibility(ub.SIDEBAR_BEST_RATED): + entries, random, pagination = fill_indexpage(page, db.Books, db.Books.ratings.any(db.Ratings.rating > 9), + order) + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, + title=_(u"Best rated books"), page="rated") + else: + abort(404) + elif data == "discover": + if current_user.check_visibility(ub.SIDEBAR_RANDOM): + entries, __, pagination = fill_indexpage(page, db.Books, True, [func.randomblob(2)]) + pagination = Pagination(1, config.config_books_per_page, config.config_books_per_page) + return render_title_template('discover.html', entries=entries, pagination=pagination, + title=_(u"Random Books"), page="discover") + else: + abort(404) + elif data == "unread": + return render_read_books(page, False, order=order) + elif data == "read": + return render_read_books(page, True, order=order) + elif data == "hot": + if current_user.check_visibility(ub.SIDEBAR_HOT): + if current_user.show_detail_random(): + 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)) + 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) + entries = list() + for book in hot_books: + downloadBook = db.session.query(db.Books).filter(common_filters()).filter( + db.Books.id == book.Downloads.book_id).first() + if downloadBook: + entries.append(downloadBook) + else: + ub.delete_download(book.Downloads.book_id) + # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() + # ub.session.commit() + numBooks = entries.__len__() + pagination = Pagination(page, config.config_books_per_page, numBooks) + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, + title=_(u"Hot Books (most downloaded)"), page="hot") + else: + abort(404) + else: + entries, random, pagination = fill_indexpage(page, db.Books, True, order) + return render_title_template('index.html', random=random, entries=entries, pagination=pagination, + title=_(u"Books"), page="newest") +''' @web.route("/hot", defaults={'page': 1}) @web.route('/hot/page/') @login_required_if_no_ano def hot_books(page): - if current_user.check_visibility(ub.SIDEBAR_HOT): - if current_user.show_detail_random(): - 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)) - 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) - entries = list() - for book in hot_books: - downloadBook = db.session.query(db.Books).filter(common_filters()).filter(db.Books.id == book.Downloads.book_id).first() - if downloadBook: - entries.append(downloadBook) - else: - ub.delete_download(book.Downloads.book_id) - # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() - # ub.session.commit() - numBooks = entries.__len__() - pagination = Pagination(page, config.config_books_per_page, numBooks) - return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(u"Hot Books (most downloaded)"), page="hot") - else: - abort(404) @web.route("/rated", defaults={'page': 1}) @@ -590,7 +576,7 @@ def discover(page): return render_title_template('discover.html', entries=entries, pagination=pagination, title=_(u"Random Books"), page="discover") else: - abort(404) + abort(404)''' @web.route("/author") @@ -629,7 +615,7 @@ def author(book_id, page): try: gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret) author_info = gc.find_author(author_name=name) - other_books = get_unique_other_books(entries.all(), author_info.books) + other_books = helper.get_unique_other_books(entries.all(), author_info.books) except Exception: # Skip goodreads, if site is down/inaccessible app.logger.error('Goodreads website is down/inaccessible') @@ -670,28 +656,6 @@ def publisher(book_id, page): abort(404) -def get_unique_other_books(library_books, author_books): - # 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), - library_books, []) - other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, - author_books) - - # Fuzzy match book titles - if feature_support['levenshtein']: - library_titles = reduce(lambda acc, book: acc + [book.title], library_books, []) - other_books = filter(lambda author_book: not filter( - lambda library_book: - # Remove items in parentheses before comparing - Levenshtein.ratio(re.sub(r"\(.*\)", "", author_book.title), library_book) > 0.7, - library_titles - ), other_books) - - return other_books - - @web.route("/series") @login_required_if_no_ano def series_list(): @@ -854,136 +818,17 @@ def category(book_id, page): abort(404) -@web.route("/ajax/toggleread/", methods=['POST']) -@login_required -def toggle_read(book_id): - if not config.config_read_column: - book = ub.session.query(ub.ReadBook).filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), - ub.ReadBook.book_id == book_id)).first() - if book: - book.is_read = not book.is_read - else: - readBook = ub.ReadBook() - readBook.user_id = int(current_user.id) - readBook.book_id = book_id - readBook.is_read = True - book = readBook - ub.session.merge(book) - ub.session.commit() - else: - try: - db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() - read_status = getattr(book, 'custom_column_' + str(config.config_read_column)) - if len(read_status): - read_status[0].value = not read_status[0].value - db.session.commit() - else: - cc_class = db.cc_classes[config.config_read_column] - new_cc = cc_class(value=1, book=book_id) - db.session.add(new_cc) - db.session.commit() - except KeyError: - app.logger.error( - u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) - return "" - - -@web.route("/book/") -@login_required_if_no_ano -def show_book(book_id): - 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: - entries.languages[index].language_name = LC.parse(entries.languages[index].lang_code).get_language_name( - get_locale()) - except UnknownLocaleError: - entries.languages[index].language_name = _( - isoLanguages.get(part3=entries.languages[index].lang_code).name) - tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() - - if config.config_columns_to_ignore: - cc = [] - for col in tmpcc: - r = re.compile(config.config_columns_to_ignore) - if r.match(col.label): - cc.append(col) - else: - cc = tmpcc - book_in_shelfs = [] - shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all() - for entry in shelfs: - book_in_shelfs.append(entry.shelf) - - if not current_user.is_anonymous: - if not config.config_read_column: - matching_have_read_book = ub.session.query(ub.ReadBook).\ - filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).all() - have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].is_read - else: - try: - matching_have_read_book = getattr(entries, 'custom_column_'+str(config.config_read_column)) - have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].value - except KeyError: - app.logger.error( - u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) - have_read = None - - else: - have_read = None - - entries.tags = sort(entries.tags, key=lambda tag: tag.name) - - entries = order_authors(entries) - - kindle_list = helper.check_send_to_kindle(entries) - reader_list = helper.check_read_formats(entries) - - audioentries = [] - for media_format in entries.data: - if media_format.format.lower() in EXTENSIONS_AUDIO: - audioentries.append(media_format.format.lower()) - - return render_title_template('detail.html', entry=entries, audioentries=audioentries, cc=cc, - is_xhr=request.is_xhr, title=entries.title, books_shelfs=book_in_shelfs, - have_read=have_read, kindle_list=kindle_list, reader_list=reader_list, page="book") - else: - flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") - return redirect(url_for("web.index")) - - -@web.route("/ajax/bookmark//", methods=['POST']) -@login_required -def bookmark(book_id, book_format): - bookmark_key = request.form["bookmark"] - ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), - ub.Bookmark.book_id == book_id, - ub.Bookmark.format == book_format)).delete() - if not bookmark_key: - ub.session.commit() - return "", 204 - - lbookmark = ub.Bookmark(user_id=current_user.id, - book_id=book_id, - format=book_format, - bookmark_key=bookmark_key) - ub.session.merge(lbookmark) - ub.session.commit() - return "", 201 - - @web.route("/tasks") @login_required def get_tasks_status(): # if current user admin, show all email, otherwise only own emails tasks = global_WorkerThread.get_taskstatus() - # UIanswer = copy.deepcopy(answer) answer = helper.render_task_status(tasks) - # foreach row format row return render_title_template('tasks.html', entries=answer, title=_(u"Tasks"), page="tasks") +# ################################### Search functions ################################################################ + @web.route("/search", methods=["GET"]) @login_required_if_no_ano def search(): @@ -1148,34 +993,7 @@ def advanced_search(): series=series, title=_(u"search"), cc=cc, page="advsearch") -@web.route("/cover/") -@login_required_if_no_ano -def get_cover(book_id): - book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - return helper.get_book_cover(book.path) - - -@web.route("/show//") -@login_required_if_no_ano -def serve_book(book_id, book_format): - book_format = book_format.split(".")[0] - book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper())\ - .first() - app.logger.info('Serving book: %s', data.name) - if config.config_use_google_drive: - headers = Headers() - try: - headers["Content-Type"] = mimetypes.types_map['.' + book_format] - except KeyError: - headers["Content-Type"] = "application/octet-stream" - df = gdriveutils.getFileFromEbooksFolder(book.path, data.name + "." + book_format) - return gdriveutils.do_gdrive_download(df, headers) - else: - return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format) - - -@web.route("/unreadbooks/", defaults={'page': 1}) +'''@web.route("/unreadbooks/", defaults={'page': 1}) @web.route("/unreadbooks/'") @login_required_if_no_ano def unread_books(page): @@ -1186,10 +1004,10 @@ def unread_books(page): @web.route("/readbooks/'") @login_required_if_no_ano def read_books(page): - return render_read_books(page, True) + return render_read_books(page, True)''' -def render_read_books(page, are_read, as_xml=False): +def render_read_books(page, are_read, as_xml=False, order=[]): if not config.config_read_column: readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\ .filter(ub.ReadBook.is_read is True).all() @@ -1208,7 +1026,7 @@ def render_read_books(page, are_read, as_xml=False): else: db_filter = ~db.Books.id.in_(readBookIds) - entries, random, pagination = fill_indexpage(page, db.Books, db_filter, [db.Books.timestamp.desc()]) + entries, random, pagination = fill_indexpage(page, db.Books, db_filter, order) if as_xml: xml = render_title_template('feed.xml', entries=entries, pagination=pagination) @@ -1218,68 +1036,42 @@ def render_read_books(page, are_read, as_xml=False): else: if are_read: name = _(u'Read Books') + ' (' + str(len(readBookIds)) + ')' + pagename = "read" else: total_books = db.session.query(func.count(db.Books.id)).scalar() name = _(u'Unread Books') + ' (' + str(total_books - len(readBookIds)) + ')' + pagename = "unread" return render_title_template('index.html', random=random, entries=entries, pagination=pagination, - title=_(name, name=name), page="read") + title=name, page=pagename) -@web.route("/read//") +# ################################### Download/Send ################################################################## + +@web.route("/cover/") @login_required_if_no_ano -def read_book(book_id, book_format): +def get_cover(book_id): book = db.session.query(db.Books).filter(db.Books.id == book_id).first() - if not book: - flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") - return redirect(url_for("web.index")) + return helper.get_book_cover(book.path) - # check if book was downloaded before - bookmark = None - if current_user.is_authenticated: - bookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), - ub.Bookmark.book_id == book_id, - ub.Bookmark.format == book_format.upper())).first() - if book_format.lower() == "epub": - return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=bookmark) - elif book_format.lower() == "pdf": - return render_title_template('readpdf.html', pdffile=book_id, title=_(u"Read a Book")) - elif book_format.lower() == "txt": - return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book")) - elif book_format.lower() == "mp3": - entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() - return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), - title=_(u"Read a Book"), entry=entries, bookmark=bookmark) - elif book_format.lower() == "m4b": - entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() - return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), - title=_(u"Read a Book"), entry=entries, bookmark=bookmark) - elif book_format.lower() == "m4a": - entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() - return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), - title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + +@web.route("/show//") +@login_required_if_no_ano +def serve_book(book_id, book_format): + book_format = book_format.split(".")[0] + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + data = db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format.upper())\ + .first() + app.logger.info('Serving book: %s', data.name) + if config.config_use_google_drive: + headers = Headers() + try: + headers["Content-Type"] = mimetypes.types_map['.' + book_format] + except KeyError: + headers["Content-Type"] = "application/octet-stream" + df = gdriveutils.getFileFromEbooksFolder(book.path, data.name + "." + book_format) + return gdriveutils.do_gdrive_download(df, headers) else: - book_dir = os.path.join(config.get_main_dir, "cps", "static", str(book_id)) - if not os.path.exists(book_dir): - os.mkdir(book_dir) - for fileext in ["cbr", "cbt", "cbz"]: - if book_format.lower() == fileext: - all_name = str(book_id) # + "/" + book.data[0].name + "." + fileext - # tmp_file = os.path.join(book_dir, book.data[0].name) + "." + fileext - # if not os.path.exists(all_name): - # cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + "." + fileext - # copyfile(cbr_file, tmp_file) - return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), - extension=fileext) - '''if feature_support['rar']: - extensionList = ["cbr","cbt","cbz"] - else: - extensionList = ["cbt","cbz"] - for fileext in extensionList: - if book_format.lower() == fileext: - return render_title_template('readcbr.html', comicfile=book_id, - extension=fileext, title=_(u"Read a Book"), book=book) - flash(_(u"Error opening eBook. File does not exist or file is not accessible."), category="error") - return redirect(url_for("web.index"))''' + return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format) @web.route("/download//") @@ -1317,6 +1109,29 @@ def get_download_link_ext(book_id, book_format, anyname): return get_download_link(book_id, book_format) +@web.route('/send///') +@login_required +@download_required +def send_to_kindle(book_id, book_format, convert): + settings = ub.get_mail_settings() + if settings.get("mail_server", "mail.example.com") == "mail.example.com": + flash(_(u"Please configure the SMTP mail settings first..."), category="error") + elif current_user.kindle_mail: + result = helper.send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir, + current_user.nickname) + if result is None: + flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail), + category="success") + ub.update_download(book_id, int(current_user.id)) + else: + flash(_(u"There was an error sending this book: %(res)s", res=result), category="error") + else: + flash(_(u"Please configure your kindle e-mail address first..."), category="error") + return redirect(request.environ["HTTP_REFERER"]) + + +# ################################### Login Logout ################################################################## + @web.route('/register', methods=['GET', 'POST']) def register(): if not config.config_public_reg: @@ -1502,26 +1317,7 @@ def token_verified(): return response -@web.route('/send///') -@login_required -@download_required -def send_to_kindle(book_id, book_format, convert): - settings = ub.get_mail_settings() - if settings.get("mail_server", "mail.example.com") == "mail.example.com": - flash(_(u"Please configure the SMTP mail settings first..."), category="error") - elif current_user.kindle_mail: - result = helper.send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir, - current_user.nickname) - if result is None: - flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail), - category="success") - ub.update_download(book_id, int(current_user.id)) - else: - flash(_(u"There was an error sending this book: %(res)s", res=result), category="error") - else: - flash(_(u"Please configure your kindle e-mail address first..."), category="error") - return redirect(request.environ["HTTP_REFERER"]) - +# ################################### Users own configuration ######################################################### @web.route("/me", methods=["GET", "POST"]) @login_required @@ -1589,3 +1385,125 @@ def profile(): name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) + +# ###################################Show single book ################################################################## + +@web.route("/read//") +@login_required_if_no_ano +def read_book(book_id, book_format): + book = db.session.query(db.Books).filter(db.Books.id == book_id).first() + if not book: + flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") + return redirect(url_for("web.index")) + + # check if book was downloaded before + bookmark = None + if current_user.is_authenticated: + bookmark = ub.session.query(ub.Bookmark).filter(ub.and_(ub.Bookmark.user_id == int(current_user.id), + ub.Bookmark.book_id == book_id, + ub.Bookmark.format == book_format.upper())).first() + if book_format.lower() == "epub": + return render_title_template('read.html', bookid=book_id, title=_(u"Read a Book"), bookmark=bookmark) + elif book_format.lower() == "pdf": + return render_title_template('readpdf.html', pdffile=book_id, title=_(u"Read a Book")) + elif book_format.lower() == "txt": + return render_title_template('readtxt.html', txtfile=book_id, title=_(u"Read a Book")) + elif book_format.lower() == "mp3": + entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() + return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), + title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + elif book_format.lower() == "m4b": + entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() + return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), + title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + elif book_format.lower() == "m4a": + entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() + return render_title_template('listenmp3.html', mp3file=book_id, audioformat=book_format.lower(), + title=_(u"Read a Book"), entry=entries, bookmark=bookmark) + else: + book_dir = os.path.join(config.get_main_dir, "cps", "static", str(book_id)) + if not os.path.exists(book_dir): + os.mkdir(book_dir) + for fileext in ["cbr", "cbt", "cbz"]: + if book_format.lower() == fileext: + all_name = str(book_id) # + "/" + book.data[0].name + "." + fileext + # tmp_file = os.path.join(book_dir, book.data[0].name) + "." + fileext + # if not os.path.exists(all_name): + # cbr_file = os.path.join(config.config_calibre_dir, book.path, book.data[0].name) + "." + fileext + # copyfile(cbr_file, tmp_file) + return render_title_template('readcbr.html', comicfile=all_name, title=_(u"Read a Book"), + extension=fileext) + '''if feature_support['rar']: + extensionList = ["cbr","cbt","cbz"] + else: + extensionList = ["cbt","cbz"] + for fileext in extensionList: + if book_format.lower() == fileext: + return render_title_template('readcbr.html', comicfile=book_id, + extension=fileext, title=_(u"Read a Book"), book=book) + flash(_(u"Error opening eBook. File does not exist or file is not accessible."), category="error") + return redirect(url_for("web.index"))''' + + +@web.route("/book/") +@login_required_if_no_ano +def show_book(book_id): + 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: + entries.languages[index].language_name = LC.parse(entries.languages[index].lang_code).get_language_name( + get_locale()) + except UnknownLocaleError: + entries.languages[index].language_name = _( + isoLanguages.get(part3=entries.languages[index].lang_code).name) + tmpcc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + + if config.config_columns_to_ignore: + cc = [] + for col in tmpcc: + r = re.compile(config.config_columns_to_ignore) + if r.match(col.label): + cc.append(col) + else: + cc = tmpcc + book_in_shelfs = [] + shelfs = ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).all() + for entry in shelfs: + book_in_shelfs.append(entry.shelf) + + if not current_user.is_anonymous: + if not config.config_read_column: + matching_have_read_book = ub.session.query(ub.ReadBook).\ + filter(ub.and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).all() + have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].is_read + else: + try: + matching_have_read_book = getattr(entries, 'custom_column_'+str(config.config_read_column)) + have_read = len(matching_have_read_book) > 0 and matching_have_read_book[0].value + except KeyError: + app.logger.error( + u"Custom Column No.%d is not exisiting in calibre database" % config.config_read_column) + have_read = None + + else: + have_read = None + + entries.tags = sort(entries.tags, key=lambda tag: tag.name) + + entries = order_authors(entries) + + kindle_list = helper.check_send_to_kindle(entries) + reader_list = helper.check_read_formats(entries) + + audioentries = [] + for media_format in entries.data: + if media_format.format.lower() in EXTENSIONS_AUDIO: + audioentries.append(media_format.format.lower()) + + return render_title_template('detail.html', entry=entries, audioentries=audioentries, cc=cc, + is_xhr=request.is_xhr, title=entries.title, books_shelfs=book_in_shelfs, + have_read=have_read, kindle_list=kindle_list, reader_list=reader_list, page="book") + else: + flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") + return redirect(url_for("web.index"))