diff --git a/cps/db.py b/cps/db.py index f6ee790e..f25848c7 100755 --- a/cps/db.py +++ b/cps/db.py @@ -56,6 +56,10 @@ books_languages_link = Table('books_languages_link', Base.metadata, Column('lang_code', Integer, ForeignKey('languages.id'), primary_key=True) ) +books_publishers_link = Table('books_publishers_link', Base.metadata, + Column('book', Integer, ForeignKey('books.id'), primary_key=True), + Column('publisher', Integer, ForeignKey('publishers.id'), primary_key=True) + ) class Identifiers(Base): __tablename__ = 'identifiers' @@ -182,6 +186,21 @@ class Languages(Base): def __repr__(self): return u"".format(self.lang_code) +class Publishers(Base): + __tablename__ = 'publishers' + + id = Column(Integer, primary_key=True) + name = Column(String) + sort = Column(String) + + def __init__(self, name,sort): + self.name = name + self.sort = sort + + def __repr__(self): + return u"".format(self.name, self.sort) + + class Data(Base): __tablename__ = 'data' @@ -224,6 +243,7 @@ class Books(Base): series = relationship('Series', secondary=books_series_link, backref='books') ratings = relationship('Ratings', secondary=books_ratings_link, backref='books') languages = relationship('Languages', secondary=books_languages_link, backref='books') + publishers = relationship('Publishers', secondary=books_publishers_link, backref='books') identifiers = relationship('Identifiers', backref='books') def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover, @@ -274,7 +294,7 @@ def setup_db(): return False dbpath = os.path.join(config.config_calibre_dir, "metadata.db") - engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False) + engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False, isolation_level="SERIALIZABLE") try: conn = engine.connect() diff --git a/cps/epub.py b/cps/epub.py index e6945d75..f9d46362 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -7,13 +7,14 @@ import os import uploader -def extractCover(zip, coverFile, tmp_file_name): +def extractCover(zip, coverFile, coverpath, tmp_file_name): if coverFile is None: return None else: - cf = zip.read("OPS/" + coverFile) + zipCoverPath = os.path.join(coverpath , coverFile).replace('\\','/') + cf = zip.read(zipCoverPath) prefix = os.path.splitext(tmp_file_name)[0] - tmp_cover_name = prefix + "." + coverFile + tmp_cover_name = prefix + '.' + os.path.basename(zipCoverPath) image = open(tmp_cover_name, 'wb') image.write(cf) image.close() @@ -32,10 +33,11 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): txt = zip.read('META-INF/container.xml') tree = etree.fromstring(txt) cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0] - cf = zip.read(cfname) tree = etree.fromstring(cf) + coverpath=os.path.dirname(cfname) + p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] epub_metadata = {} @@ -46,11 +48,16 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): else: epub_metadata[s] = "Unknown" - coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover']/@href", namespaces=ns) + coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) if len(coversection) > 0: - coverfile = extractCover(zip, coversection[0], tmp_file_path) + coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path) else: - coverfile = None + coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover']/@href", namespaces=ns) + if len(coversection) > 0: + coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path) + else: + coverfile = None + if epub_metadata['title'] is None: title = original_file_name else: diff --git a/cps/helper.py b/cps/helper.py index 5e9c9210..54fa1946 100755 --- a/cps/helper.py +++ b/cps/helper.py @@ -19,6 +19,8 @@ from email.MIMEBase import MIMEBase from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.generator import Generator +from email.utils import formatdate +from email.utils import make_msgid from flask_babel import gettext as _ import subprocess import threading @@ -165,6 +167,8 @@ def send_mail(book_id, kindle_mail, calibrepath): # create MIME message msg = MIMEMultipart() msg['Subject'] = _(u'Send to Kindle') + msg['Message-Id'] = make_msgid('calibre-web') + msg['Date'] = formatdate(localtime=True) text = _(u'This email has been sent via calibre web.') msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 74e1a1d8..8b0b099f 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -36,7 +36,7 @@ $(function() { success: function(data) { $('#spinner').show(); displaytext=data.text; - window.setTimeout(restartTimer, 3000);} + setTimeout(restartTimer, 3000);} }); }); $("#shutdown").click(function() { @@ -50,7 +50,7 @@ $(function() { }); $("#check_for_update").click(function() { var button_text = $("#check_for_update").html(); - $("#check_for_update").html('Checking...'); + $("#check_for_update").html('...'); $.ajax({ dataType: 'json', url: window.location.pathname+"/../../get_update_status", @@ -110,7 +110,8 @@ function updateTimer() { $('#UpdateprogressDialog #updateFinished').removeClass('hidden'); $("#check_for_update").removeClass('hidden'); $("#perform_update").addClass('hidden'); - } + }, + timeout:2000 }); } diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 8775f079..9639e954 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -50,6 +50,7 @@ {% if entry.identifiers|length > 0 %}
+

{% for identifier in entry.identifiers %} {{identifier.formatType()}} @@ -66,10 +67,16 @@ {% for tag in entry.tags %} {{tag.name}} {%endfor%} -

{% endif %} + {% if entry.publishers|length > 0 %} +
+

+ {{_('Publisher')}}:{% for publisher in entry.publishers %} {{publisher.name}}{% if not loop.last %},{% endif %}{% endfor %} +

+
+ {% endif %} {% if entry.pubdate[:10] != '0101-01-01' %}

{{_('Publishing date')}}: {{entry.pubdate|formatdate}}

{% endif %} @@ -125,10 +132,10 @@ {% endif %} - {% if g.user.is_authenticated %} - {% if g.user.kindle_mail %} + {% if g.user.kindle_mail and g.user.is_authenticated %} {{_('Send to Kindle')}} {% endif %} + {% if (g.user.role_download() and g.user.is_anonymous()) or g.user.is_authenticated %}
{% endblock %} diff --git a/cps/templates/list.html b/cps/templates/list.html index d424d495..0a63b139 100644 --- a/cps/templates/list.html +++ b/cps/templates/list.html @@ -2,11 +2,17 @@ {% block body %}

{{title}}

+
{% for entry in entries %} + {% if loop.index0 == (loop.length/2)|int and loop.length > 20 %} +
+
+ {% endif %}
{{entry.count}}
{% endfor %}
+
{% endblock %} diff --git a/cps/templates/search_form.html b/cps/templates/search_form.html index 2e9f4d3d..e61195ec 100644 --- a/cps/templates/search_form.html +++ b/cps/templates/search_form.html @@ -10,63 +10,67 @@ -
+ + +
+ +
{% for tag in tags %} {% endfor %}
- +
{% for tag in tags %} {% endfor %}
- +
{% for serie in series %} {% endfor %}
- +
{% for serie in series %} {% endfor %}
{% if languages %} - +
{% for language in languages %} {% endfor %}
- +
{% for language in languages %} {% endfor %}
diff --git a/cps/templates/stats.html b/cps/templates/stats.html index 22826003..4b08afda 100644 --- a/cps/templates/stats.html +++ b/cps/templates/stats.html @@ -1,7 +1,27 @@ {% extends "layout.html" %} {% block body %} +

{{_('Calibre library statistics')}}

+ + + + + + + + + + + + + + + + + + + +
{{bookcounter}}{{_('Books in this Library')}}
{{authorcounter}}{{_('Authors in this Library')}}
{{categorycounter}}{{_('Categories in this Library')}}
{{seriecounter}}{{_('Series in this Library')}}

{{_('Linked libraries')}}

- @@ -24,30 +44,51 @@ - + - -
PyPDF2{{versions['PyPdfVersion']}}v{{versions['PyPdfVersion']}}
- -

{{_('Calibre library statistics')}}

- - - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + +
{{bookcounter}}{{_('Books in this Library')}}Babelv{{versions['babel']}}
{{authorcounter}}{{_('Authors in this Library')}}SqlAlchemyv{{versions['sqlalchemy']}}
{{categorycounter}}{{_('Categories in this Library')}}Flaskv{{versions['flask']}}
{{seriecounter}}{{_('Series in this Library')}}Flask Loginv{{versions['flasklogin']}}
Flask Principalv{{versions['flask_principal']}}
Tornado web serverv{{versions['tornado']}}
ISO639 Languagesv{{versions['iso639']}}
Requestsv{{versions['requests']}}
SQlitev{{versions['sqlite']}}
Pysqlitev{{versions['pysqlite']}}
+ {% endblock %} + + diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index b9cff3eb..2d4354ec 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -40,6 +40,8 @@ {% endfor %}
+
+
@@ -72,7 +74,8 @@
- +
+
{% if g.user and g.user.role_admin() and not profile %} {% if not content.role_anonymous() %}
@@ -106,13 +109,17 @@
{% endif %} +
+
{% if not profile %} {{_('Back')}} +
{% endif %} {% if downloads %} +

{{_('Recent Downloads')}}

{% for entry in downloads %}
@@ -121,6 +128,7 @@
{% endfor %} +
{% endif %} {% endblock %} diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo index d3a83c0d..5cb1b1c9 100644 Binary files a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo and b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ diff --git a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po index 5356f3b2..c68904f6 100644 --- a/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/cps/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -61,31 +61,31 @@ msgstr "游客" #: cps/web.py:734 msgid "Requesting update package" -msgstr "" +msgstr "正在请求更新包" #: cps/web.py:735 msgid "Downloading update package" -msgstr "" +msgstr "正在下载更新包" #: cps/web.py:736 msgid "Unzipping update package" -msgstr "" +msgstr "正在解压更新包" #: cps/web.py:737 msgid "Files are replaced" -msgstr "" +msgstr "文件已替换" #: cps/web.py:738 msgid "Database connections are closed" -msgstr "" +msgstr "数据库连接已关闭" #: cps/web.py:739 msgid "Server is stopped" -msgstr "" +msgstr "服务器已停止" #: cps/web.py:740 msgid "Update finished, please press okay and reload page" -msgstr "" +msgstr "更新完成,请按确定并刷新页面" #: cps/web.py:810 msgid "Latest Books" @@ -97,7 +97,7 @@ msgstr "热门书籍(最多下载)" #: cps/web.py:845 msgid "Best rated books" -msgstr "" +msgstr "最高评分书籍" #: cps/templates/index.xml:36 cps/web.py:854 msgid "Random Books" @@ -108,9 +108,9 @@ msgid "Author list" msgstr "作者列表" #: cps/web.py:878 -#, python-format +#, python-forma msgid "Author: %(name)s" -msgstr "" +msgstr "作者: %(name)s" #: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235 #: cps/web.py:2115 @@ -150,7 +150,7 @@ msgstr "统计" #: cps/web.py:1061 msgid "Server restarted, please reload page" -msgstr "" +msgstr "服务器已重启,请刷新页面" #: cps/web.py:1063 msgid "Performing shutdown of server, please close window" @@ -158,7 +158,7 @@ msgstr "正在关闭服务器,请关闭窗口" #: cps/web.py:1073 msgid "Update done" -msgstr "" +msgstr "更新完成" #: cps/web.py:1147 cps/web.py:1160 msgid "search" @@ -475,11 +475,11 @@ msgstr "管理" #: cps/templates/admin.html:80 msgid "Current commit timestamp" -msgstr "" +msgstr "当前提交时间戳" #: cps/templates/admin.html:81 msgid "Newest commit timestamp" -msgstr "" +msgstr "最新提交时间戳" #: cps/templates/admin.html:83 msgid "Restart Calibre-web" @@ -491,11 +491,11 @@ msgstr "停止 Calibre-web" #: cps/templates/admin.html:85 msgid "Check for update" -msgstr "" +msgstr "检查更新" #: cps/templates/admin.html:86 msgid "Perform Update" -msgstr "" +msgstr "执行更新" #: cps/templates/admin.html:96 msgid "Do you really want to restart Calibre-web?" @@ -519,7 +519,7 @@ msgstr "您确定要关闭 Calibre-web 吗?" #: cps/templates/admin.html:127 msgid "Updating, please do not reload page" -msgstr "" +msgstr "正在更新,请不要刷新页面" #: cps/templates/book_edit.html:16 cps/templates/search_form.html:6 msgid "Book Title" @@ -610,7 +610,7 @@ msgstr "启用注册" #: cps/templates/config_edit.html:52 msgid "Default Settings for new users" -msgstr "" +msgstr "新用户默认设置" #: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80 msgid "Admin user" @@ -651,7 +651,7 @@ msgstr "语言" #: cps/templates/detail.html:74 msgid "Publishing date" -msgstr "" +msgstr "出版日期" #: cps/templates/detail.html:106 msgid "Description:" @@ -723,11 +723,11 @@ msgstr "热门书籍" #: cps/templates/index.xml:19 msgid "Popular publications from this catalog based on Downloads." -msgstr "" +msgstr "基于下载数的热门书籍" #: cps/templates/index.xml:22 cps/templates/layout.html:129 msgid "Best rated Books" -msgstr "" +msgstr "最高评分书籍" #: cps/templates/index.xml:26 msgid "Popular publications from this catalog based on Rating." @@ -933,11 +933,11 @@ msgstr "个作者在此书库" #: cps/templates/stats.html:45 msgid "Categories in this Library" -msgstr "" +msgstr "个分类在此书库" #: cps/templates/stats.html:49 msgid "Series in this Library" -msgstr "" +msgstr "个丛书在此书库" #: cps/templates/user_edit.html:23 msgid "Kindle E-Mail" @@ -961,7 +961,7 @@ msgstr "显示热门书籍" #: cps/templates/user_edit.html:53 msgid "Show best rated books" -msgstr "" +msgstr "显示最高评分书籍" #: cps/templates/user_edit.html:57 msgid "Show language selection" diff --git a/cps/ub.py b/cps/ub.py index f5207f06..653454ce 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -7,7 +7,6 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * from flask_login import AnonymousUserMixin import os -import traceback import logging from werkzeug.security import generate_password_hash from flask_babel import gettext as _ diff --git a/cps/web.py b/cps/web.py index bfe42444..1777a77f 100755 --- a/cps/web.py +++ b/cps/web.py @@ -4,8 +4,9 @@ import mimetypes import logging from logging.handlers import RotatingFileHandler import textwrap -from flask import Flask, render_template, session, request, Response, redirect, url_for, send_from_directory, \ +from flask import Flask, render_template, request, Response, redirect, url_for, send_from_directory, \ make_response, g, flash, abort +from flask import __version__ as flaskVersion import ub from ub import config import helper @@ -14,9 +15,12 @@ import errno from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import false from sqlalchemy.exc import IntegrityError +from sqlalchemy import __version__ as sqlalchemyVersion from math import ceil from flask_login import LoginManager, login_user, logout_user, login_required, current_user +from flask_login import __version__ as flask_loginVersion from flask_principal import Principal, Identity, AnonymousIdentity, identity_changed +from flask_login import __version__ as flask_principalVersion from flask_babel import Babel from flask_babel import gettext as _ import requests @@ -24,6 +28,7 @@ import zipfile from werkzeug.security import generate_password_hash, check_password_hash from babel import Locale as LC from babel import negotiate_locale +from babel import __version__ as babelVersion from babel.dates import format_date from functools import wraps import base64 @@ -32,16 +37,16 @@ import json import urllib import datetime from iso639 import languages as isoLanguages +from iso639 import __version__ as iso639Version from uuid import uuid4 import os.path import sys import subprocess import re import db -import thread from shutil import move, copyfile from tornado.ioloop import IOLoop - +from tornado import version as tornadoVersion try: from wand.image import Image @@ -51,6 +56,7 @@ except ImportError, e: use_generic_pdf_cover = True from cgi import escape +ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'cbt', 'djvu', 'prc', 'doc', 'docx', 'fb2']) # Proxy Helper class class ReverseProxied(object): @@ -473,8 +479,10 @@ def feed_search(term): filter = True if term: entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")), - db.Books.authors.any(db.Authors.name.like("%" + term + "%")), - db.Books.title.like("%" + term + "%"))).filter(filter).all() + 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(filter).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) @@ -1039,6 +1047,17 @@ def stats(): if re.search('Amazon kindlegen\(', lines): versions['KindlegenVersion'] = lines versions['PythonVersion'] = sys.version + versions['babel'] = babelVersion + versions['sqlalchemy'] = sqlalchemyVersion + versions['flask'] = flaskVersion + versions['flasklogin'] = flask_loginVersion + versions['flask_principal'] = flask_principalVersion + versions['tornado'] = tornadoVersion + versions['iso639'] = iso639Version + versions['requests'] = requests.__version__ + versions['pysqlite'] = db.engine.dialect.dbapi.version + versions['sqlite'] = db.engine.dialect.dbapi.sqlite_version + return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=versions, categorycounter=categorys, seriecounter=series, title=_(u"Statistics")) @@ -1087,9 +1106,10 @@ def search(): else: 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.title.like("%" + term + "%"))).filter(filter).all() + 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(filter).all() return render_title_template('search.html', searchterm=term, entries=entries) else: return render_title_template('search.html', searchterm="") @@ -1109,12 +1129,14 @@ def advanced_search(): author_name = request.args.get("author_name") book_title = request.args.get("book_title") + publisher = request.args.get("publisher") if author_name: author_name = author_name.strip() if book_title: book_title = book_title.strip() + if publisher: publisher = publisher.strip() if include_tag_inputs or exclude_tag_inputs or include_series_inputs or exclude_series_inputs or \ - include_languages_inputs or exclude_languages_inputs or author_name or book_title: + include_languages_inputs or exclude_languages_inputs or author_name or book_title or publisher: searchterm = [] - searchterm.extend((author_name, book_title)) + searchterm.extend((author_name, book_title, publisher)) tag_names = db.session.query(db.Tags).filter(db.Tags.id.in_(include_tag_inputs)).all() searchterm.extend(tag.name for tag in tag_names) # searchterm = " + ".join(filter(None, searchterm)) @@ -1130,7 +1152,8 @@ def advanced_search(): searchterm.extend(language.name for language in language_names) searchterm = " + ".join(filter(None, searchterm)) q = q.filter(db.Books.authors.any(db.Authors.name.like("%" + author_name + "%")), - db.Books.title.like("%" + book_title + "%")) + db.Books.title.like("%" + book_title + "%"), + db.Books.publishers.any(db.Publishers.name.like("%" + publisher + "%"))) for tag in include_tag_inputs: q = q.filter(db.Books.tags.any(db.Tags.id == tag)) for tag in exclude_tag_inputs: @@ -1180,7 +1203,7 @@ def feed_get_cover(book_id): @app.route("/read//") -@login_required +@login_required_if_no_ano def read_book(book_id, format): book = db.session.query(db.Books).filter(db.Books.id == book_id).first() if book: @@ -1789,6 +1812,8 @@ def edit_mailsettings(): category="success") else: flash(_(u"There was an error sending the Test E-Mail: %(res)s", res=result), category="error") + else: + flash(_(u"E-Mail settings updated"), category="success") return render_title_template("email_edit.html", content=content, title=_(u"Edit mail settings")) @@ -2130,6 +2155,18 @@ def upload(): db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) if request.method == 'POST' and 'btn-upload' in request.files: file = request.files['btn-upload'] + if '.' in file.filename: + file_ext = file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in ALLOWED_EXTENSIONS: + flash( + _('File extension "%s" is not allowed to be uploaded to this server' % + file_ext), + category="error" + ) + return redirect(url_for('index')) + else: + flash(_('File to be uploaded must have an extension'), category="error") + return redirect(url_for('index')) meta = uploader.upload(file) title = meta.title