Compare commits

..
90 Commits
Author SHA1 Message Date
Ozzieisaacs d106ada9ed Changed version
Update translation
2019-05-30 09:07:52 +02:00
Ozzieisaacs ed91048a63 Fix for magic link with python 3 2019-05-26 11:31:09 +02:00
Ozzieisaacs e6ff2f1d90 Fix file not found loading-icon.gif 2019-05-16 19:42:59 +02:00
Ozzieisaacs 1030e195a5 Fix #916 2019-05-11 13:03:38 +02:00
Ozzieisaacs 479b4b7d82 Update translations, integrated update of Italian translation 2019-05-08 20:24:10 +02:00
Ozzieisaacs a42ebdc096 Fix for #897 2019-05-07 19:54:13 +02:00
Ozzieisaacs 49ba221e85 Merge remote-tracking branch 'direction' 2019-05-07 18:54:27 +02:00
Ozzieisaacs cb0403a924 Merge remote-tracking branch 'adv_search/master' 2019-05-06 20:48:46 +02:00
Marvin Marx a2c7741e21 Fix "Internal Server Error" on advanced search
Custom boolean columns return that error if calibre does not have a
custom_column_1 in the DB, as this is queried in the removed line.
However the value is completely unused anyway -> removing.
2019-05-05 17:58:45 +02:00
Ozzieisaacs 55bb8d4590 Fix #900 2019-05-01 17:47:54 +02:00
Ozzieisaacs b80bfa5260 Improvement for #897 2019-04-29 19:01:30 +02:00
Ozzieisaacs f941908f73 Workaround for #889 2019-04-23 21:32:48 +02:00
subdiox c2bfb29726 Add reading direction settings to readcbr page 2019-04-21 18:20:15 +09:00
subdiox 204de4aef6 Fix an issue that fullscreen doens't work on some browsers 2019-04-21 18:02:02 +09:00
subdiox 8b6d165d64 Fix Japanese translation 2019-04-21 17:53:40 +09:00
Ozzieisaacs 6a7b8c7870 Update Version info
Fix #816
Clarified licences for isolanguages.py and reverseproxy.py
2019-04-21 08:48:51 +02:00
Ozzieisaacs cc5c2c0f65 Update Version info 2019-04-20 12:30:33 +02:00
Ozzieisaacs c748716ad2 Fix for opds feed of unread/read books 2019-04-19 12:07:46 +02:00
Ozzieisaacs 544405f5aa Update search for cyrillic letters #877 2019-04-19 06:54:12 +02:00
Ozzieisaacs 67736fe187 Catch errors on loading pickle file
Fix for cover images without Pillow
2019-04-18 21:17:59 +02:00
Ozzieisaacs 89516fc2d6 Make PIL optional #885 2019-04-17 20:45:08 +02:00
Ozzieisaacs e1e79a73e8 Fix #884 2019-04-17 20:14:24 +02:00
Ozzieisaacs 0b8624b542 Deleted load metadata from douban, as API seems to be shut down (#858) 2019-04-17 19:55:32 +02:00
Ozzieisaacs 91ebe33879 Merge remote-tracking branch 'image_formats/dev' 2019-04-15 20:57:25 +02:00
Ozzieisaacs 55bdd13fb1 Fix #877 2019-04-14 17:54:18 +02:00
Ozzieisaacs 4552208c02 Fix #848 2019-04-14 16:37:57 +02:00
Ozzieisaacs 436c2671d1 Fix delete domain entries
Fix error handling smtp.authentication error
2019-04-14 14:08:49 +02:00
Ozzieisaacs a669eacde9 Merge remote-tracking branch 'proxy/patch-1' 2019-04-14 11:19:36 +02:00
Ozzieisaacs 5188c62bea Merge remote-tracking branch 'translation/loc-hu' 2019-04-13 16:45:05 +02:00
Ozzieisaacs e754ce313c Merge remote-tracking branch 'pycountry/pycountry' 2019-04-13 16:34:04 +02:00
Ozzieisaacs dc933f3d66 Merge remote-tracking branch 'pdf.js/epubjs'
Updated pdf.js
2019-04-13 15:02:01 +02:00
Barnabás Nagy afc79416b2 HU Translation updates
Translation of the new untranslated strings
2019-04-06 23:38:59 +02:00
Barnabás Nagy 93720c9fdf Merge pull request #1 from janeczku/master
Synch the master
2019-04-06 23:06:00 +02:00
Josh Max cdfb6bf3e3 Use HTTP_X_FORWARDED_HOST instead of HTTP_X_FORWARDED_SERVER
Use HTTP_X_FORWARDED_HOST since HTTP_X_FORWARDED_SERVER holds the hostname of the proxy server on HAProxy/Traefik.
2019-04-03 01:25:29 -07:00
Daniel Pavel 858c80bb20 'except' style fix 2019-03-29 17:06:26 +02:00
Daniel Pavel f7e640fd98 support pycountry if iso-639 is not available 2019-03-29 07:00:10 +02:00
Daniel Pavel 439ce19b5d Updated epub.js to v0.3.66 2019-03-28 07:23:45 +02:00
Ozzieisaacs 4b301a7961 Additional fix for #845 (in case of error) 2019-03-26 18:19:39 +01:00
Yijun Zhao 6c493df2f5 support issue: #843 2019-03-26 14:34:51 +08:00
Ozzieisaacs 8923e71220 Remove debug logging 2019-03-24 15:18:01 +01:00
Ozzieisaacs 843a93fe28 Updater fix for slow servers 2019-03-24 15:15:36 +01:00
OzzieIsaacs fa36072699 Fix uk translation-file 2019-03-24 08:18:41 +01:00
OzzieIsaacs 8ab9365688 Prevent error 500 for cover of not exisiting book 2019-03-23 20:29:53 +01:00
Chad Bergeron 169ace777a Fixed infinite scroll for read and unread displays (#855) 2019-03-21 19:18:44 +01:00
Elifcan Cakmak ec2103a3d0 fixed a bug that affects allowing password change for users (#854) 2019-03-20 13:06:11 +01:00
Ozzieisaacs a0a15b4598 Fix max authors shown 2019-03-10 19:26:39 +01:00
Ozzieisaacs 76d40411dd Translation updated 2019-03-10 08:04:31 +01:00
Ozzieisaacs 8612dce97c Merge remote-tracking branch 'author/shorten-authors' 2019-03-09 20:14:16 +01:00
Ozzieisaacs 0b0e62fdf2 Merge remote-tracking branch 'dialog/master'
Fix for #833
2019-03-09 20:02:17 +01:00
Ozzieisaacs 6f80a63ca6 Fix for #827 2019-03-09 07:05:43 +01:00
Andrew Sullivan 3dfc2f2580 fixing issue #833 2019-03-08 20:44:45 -05:00
Ozzieisaacs 5b0a0e2139 Fix #835 2019-03-08 16:32:56 +01:00
Ozzie Isaacs 0744e84ba9 Fix for sqlalchemy version problem
In version 1.3.0 the follwoing error is thrown: Textual SQL expression 'xxx.xxx' should be explicitly declared as text
2019-03-07 20:50:58 +01:00
Ozzieisaacs 6f0b3bbda0 Fix for #812 2019-03-05 21:28:55 +01:00
Ozzieisaacs 466af21548 Code Cosmetics 2019-03-05 16:31:09 +01:00
Ozzieisaacs 6269a83c3d Fix mismerge user logon with python3 in opds feed 2019-03-03 20:51:19 +01:00
Ozzieisaacs 11b770b3fb Fix for #830
Fix user with umlauts
2019-03-03 08:41:50 +01:00
Ozzieisaacs fa276fd0d3 Add scrollbar to metadata load screen
Small UI improvements in metadata load screen
2019-03-01 21:00:09 +01:00
Ozzieisaacs 361a1243d7 Improvements #828 2019-02-27 19:30:13 +01:00
Virgil Grigoras bd7b0175a1 Fix remaining shown by codacy 2019-02-26 17:24:13 +01:00
Virgil Grigoras f5cf62f209 Fix issues shown by codacy 2019-02-26 11:17:06 +01:00
Virgil Grigoras 15aa49e163 shorten authors in author / discover / index / search and shelf 2019-02-25 19:27:59 +01:00
Virgil Grigoras 3272b4ca1e configuration field for max_authors 2019-02-25 19:26:43 +01:00
Virgil Grigoras d3ed0317bb styling and javascript for hiding authors 2019-02-25 19:25:08 +01:00
Virgil Grigoras e5bc1ae020 Add new database field for storing max_authors 2019-02-25 19:21:49 +01:00
Ozzieisaacs fad62a0909 translation update 2019-02-24 19:23:51 +01:00
Ozzieisaacs f6fbe2e135 Fix #766
Update pdf upload
Translation of uploadprogress dialog
2019-02-24 19:23:51 +01:00
Ozzieisaacs 17b7333992 Merge remote-tracking branch 'caliblur/master' 2019-02-20 20:00:08 +01:00
leram84 ae279c5123 fixed description read-more button 2019-02-20 12:57:38 -05:00
Ozzieisaacs ae7c4908f9 Fix #767 2019-02-20 18:04:55 +01:00
Ozzieisaacs ea1a882353 Merge remote-tracking branch 'origin/master' 2019-02-20 18:04:40 +01:00
hexeth 9a0e8fefb7 re-add loader code 2019-02-19 13:56:49 -08:00
Matt c07cb23ef2 Merge branch 'master' of git://github.com/janeczku/calibre-web 2019-02-19 13:47:53 -08:00
hexeth 5a863541de Fix object object 2019-02-19 13:44:40 -08:00
Ozzie Isaacs 4c80db4324 Revert bootstrap update 3.4.0 -> 3.3.7 2019-02-19 19:32:00 +01:00
Ozzieisaacs 2505f0c1e4 Merge remote-tracking branch 'filename/patch-1'
Fixes for Google Drive
2019-02-18 20:52:58 +01:00
Fabian Wüthrich 7fb9bf8889 Truncate single quote in upload file name 2019-02-18 13:35:30 +01:00
Ozzieisaacs 237502ae6b Output of errormessage for invalid CALIBRE_PORT value 2019-02-16 10:18:32 +01:00
Ozzieisaacs 56826f67fc progressbar during upload thanks to jim3ma 2019-02-16 10:05:51 +01:00
Ozzieisaacs 0fa45f5800 Merge remote-tracking branch 'github/master'
Improvement for #814
2019-02-16 08:03:42 +01:00
Ozzie Isaacs 3593f904dc Fixed spelling error readme 2019-02-13 19:55:56 +01:00
lorek123 7f3e98b477 Merge branch 'master' of github.com:lorek123/calibre-web 2019-02-10 18:30:22 +01:00
lorek123 83a06fa6c9 Merge branch 'master' of github.com:lorek123/calibre-web 2019-02-10 18:18:13 +01:00
lorek123 d508b909d1 update js and css libs, especially to fix xss in Jquery and Bootstrap 2019-02-10 18:17:33 +01:00
Lorek 6842a8ac91 update js and css libs, especially to fix xss in Jquery and Bootstrap 2019-02-10 18:04:15 +01:00
Ozzie Isaacs 47b5a5652b Update converter section 2019-02-10 07:54:57 +01:00
Ozzieisaacs d58d792f56 Catch Imagemagick policy error 2019-02-09 12:58:43 +01:00
Ozzieisaacs 6fc4b318e8 cover folders beautified 2019-02-03 20:29:16 +01:00
Ozzieisaacs 6661be91ff Update readme 2019-02-03 17:29:27 +01:00
Ozzieisaacs ea98323c06 Fixes Google Drive
Improved user guidance for Google Drive
Improved Errorhandling for Google Drive
Update french translation
Added ukranian translation
Improvements on updater: calibre-web.log1, calibre-web.log2, gdrive_credentials, settings.yaml, gdrive.db, .git are no longer deleted upon update
2019-02-03 16:38:27 +01:00
315 changed files with 120692 additions and 73288 deletions
+69 -7
View File
@@ -35,6 +35,7 @@ logger = logging.getLogger("book_formats")
try: try:
from wand.image import Image from wand.image import Image
from wand import version as ImageVersion from wand import version as ImageVersion
from wand.exceptions import PolicyError
use_generic_pdf_cover = False use_generic_pdf_cover = False
except (ImportError, RuntimeError) as e: except (ImportError, RuntimeError) as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
@@ -61,6 +62,13 @@ except ImportError as e:
logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False use_fb2_meta = False
try:
from PIL import Image
from PIL import __version__ as PILversion
use_PIL = True
except ImportError:
use_PIL = False
def process(tmp_file_path, original_file_name, original_file_extension): def process(tmp_file_path, original_file_name, original_file_extension):
meta = None meta = None
@@ -100,7 +108,7 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
def pdf_meta(tmp_file_path, original_file_name, original_file_extension): def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
if use_pdf_meta: if use_pdf_meta:
pdf = PdfFileReader(open(tmp_file_path, 'rb')) pdf = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
doc_info = pdf.getDocumentInfo() doc_info = pdf.getDocumentInfo()
else: else:
doc_info = None doc_info = None
@@ -130,12 +138,58 @@ def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover: if use_generic_pdf_cover:
return None return None
else: else:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" if use_PIL:
with Image(filename=tmp_file_path + "[0]", resolution=150) as img: try:
img.compression_quality = 88 input1 = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
img.save(filename=os.path.join(tmp_dir, cover_file_name)) page0 = input1.getPage(0)
return cover_file_name xObject = page0['/Resources']['/XObject'].getObject()
for obj in xObject:
if xObject[obj]['/Subtype'] == '/Image':
size = (xObject[obj]['/Width'], xObject[obj]['/Height'])
data = xObject[obj]._data # xObject[obj].getData()
if xObject[obj]['/ColorSpace'] == '/DeviceRGB':
mode = "RGB"
else:
mode = "P"
if '/Filter' in xObject[obj]:
if xObject[obj]['/Filter'] == '/FlateDecode':
img = Image.frombytes(mode, size, data)
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png"
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
# img.save(obj[1:] + ".png")
elif xObject[obj]['/Filter'] == '/DCTDecode':
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
img = open(cover_file_name, "wb")
img.write(data)
img.close()
return cover_file_name
elif xObject[obj]['/Filter'] == '/JPXDecode':
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jp2"
img = open(cover_file_name, "wb")
img.write(data)
img.close()
return cover_file_name
else:
img = Image.frombytes(mode, size, data)
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png"
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
except Exception as ex:
print(ex)
try:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
img.compression_quality = 88
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
except PolicyError as ex:
logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex)
return None
except Exception as ex:
logger.warning('Cannot extract cover image, using default: %s', ex)
return None
def get_versions(): def get_versions():
if not use_generic_pdf_cover: if not use_generic_pdf_cover:
@@ -152,4 +206,12 @@ def get_versions():
XVersion = 'v'+'.'.join(map(str, lxmlversion)) XVersion = 'v'+'.'.join(map(str, lxmlversion))
else: else:
XVersion = _(u'not installed') XVersion = _(u'not installed')
return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion} if use_PIL:
PILVersion = 'v' + PILversion
else:
PILVersion = _(u'not installed')
return {'Image Magick': IVersion,
'PyPdf': PVersion,
'lxml':XVersion,
'Wand': WVersion,
'Pillow': PILVersion}
+22 -9
View File
@@ -24,21 +24,34 @@ import uploader
def extractCover(tmp_file_name, original_file_extension): def extractCover(tmp_file_name, original_file_extension):
cover_data = None
if original_file_extension.upper() == '.CBZ': if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name) cf = zipfile.ZipFile(tmp_file_name)
compressed_name = cf.namelist()[0] for name in cf.namelist():
cover_data = cf.read(compressed_name) ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension == '.jpg':
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT': elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name) cf = tarfile.TarFile(tmp_file_name)
compressed_name = cf.getnames()[0] for name in cf.getnames():
cover_data = cf.extractfile(compressed_name).read() ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension == '.jpg':
cover_data = cf.extractfile(name).read()
break
prefix = os.path.dirname(tmp_file_name) prefix = os.path.dirname(tmp_file_name)
if cover_data:
tmp_cover_name = prefix + '/cover' + os.path.splitext(compressed_name)[1] tmp_cover_name = prefix + '/cover' + extension
image = open(tmp_cover_name, 'wb') image = open(tmp_cover_name, 'wb')
image.write(cover_data) image.write(cover_data)
image.close() image.close()
else:
tmp_cover_name = None
return tmp_cover_name return tmp_cover_name
+8 -3
View File
@@ -27,6 +27,7 @@ import ast
from ub import config from ub import config
import ub import ub
import sys import sys
import unidecode
session = None session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
@@ -46,7 +47,7 @@ def title_sort(title):
def lcase(s): def lcase(s):
return s.lower() return unidecode.unidecode(s.lower())
def ucase(s): def ucase(s):
@@ -112,6 +113,8 @@ class Identifiers(Base):
return u"Google Books" return u"Google Books"
elif self.type == "kobo": elif self.type == "kobo":
return u"Kobo" return u"Kobo"
if self.type == "lubimyczytac":
return u"Lubimyczytac"
else: else:
return self.type return self.type
@@ -130,6 +133,8 @@ class Identifiers(Base):
return u"https://books.google.com/books?id={0}".format(self.val) return u"https://books.google.com/books?id={0}".format(self.val)
elif self.type == "kobo": elif self.type == "kobo":
return u"https://www.kobo.com/ebook/{0}".format(self.val) return u"https://www.kobo.com/ebook/{0}".format(self.val)
elif self.type == "lubimyczytac":
return u" http://lubimyczytac.pl/ksiazka/{0}".format(self.val)
elif self.type == "url": elif self.type == "url":
return u"{0}".format(self.val) return u"{0}".format(self.val)
else: else:
@@ -355,8 +360,8 @@ def setup_db():
ub.session.commit() ub.session.commit()
config.loadSettings() config.loadSettings()
conn.connection.create_function('title_sort', 1, title_sort) conn.connection.create_function('title_sort', 1, title_sort)
conn.connection.create_function('lower', 1, lcase) # conn.connection.create_function('lower', 1, lcase)
conn.connection.create_function('upper', 1, ucase) # conn.connection.create_function('upper', 1, ucase)
if not cc_classes: if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns") cc = conn.execute("SELECT id, datatype FROM custom_columns")
+1 -1
View File
@@ -21,7 +21,7 @@ import zipfile
from lxml import etree from lxml import etree
import os import os
import uploader import uploader
from iso639 import languages as isoLanguages import isoLanguages
def extractCover(zipFile, coverFile, coverpath, tmp_file_name): def extractCover(zipFile, coverFile, coverpath, tmp_file_name):
+28 -32
View File
@@ -20,7 +20,7 @@
try: try:
from pydrive.auth import GoogleAuth from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError from pydrive.auth import RefreshError, InvalidConfigError
from apiclient import errors from apiclient import errors
gdrive_support = True gdrive_support = True
except ImportError: except ImportError:
@@ -31,12 +31,9 @@ from ub import config
import cli import cli
import shutil import shutil
from flask import Response, stream_with_context from flask import Response, stream_with_context
from sqlalchemy import * from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import * from sqlalchemy.orm import *
import web import web
class Singleton: class Singleton:
@@ -166,7 +163,10 @@ def getDrive(drive=None, gauth=None):
# Save the current credentials to a file # Save the current credentials to a file
return GoogleDrive(gauth) return GoogleDrive(gauth)
if drive.auth.access_token_expired: if drive.auth.access_token_expired:
drive.auth.Refresh() try:
drive.auth.Refresh()
except RefreshError as e:
web.app.logger.error("Google Drive error: " + e.message)
return drive return drive
def listRootFolders(): def listRootFolders():
@@ -184,7 +184,7 @@ def getFolderInFolder(parentId, folderName, drive):
# drive = getDrive(drive) # drive = getDrive(drive)
query="" query=""
if folderName: if folderName:
query = "title = '%s' and " % folderName.replace("'", "\\'") query = "title = '%s' and " % folderName.replace("'", r"\'")
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder'" \ folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder'" \
" and trashed = false" % parentId " and trashed = false" % parentId
fileList = drive.ListFile({'q': folder}).GetList() fileList = drive.ListFile({'q': folder}).GetList()
@@ -211,7 +211,7 @@ def getEbooksFolderId(drive=None):
def getFile(pathId, fileName, drive): def getFile(pathId, fileName, drive):
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'")) metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", r"\'"))
fileList = drive.ListFile({'q': metaDataFile}).GetList() fileList = drive.ListFile({'q': metaDataFile}).GetList()
if fileList.__len__() == 0: if fileList.__len__() == 0:
return None return None
@@ -246,7 +246,7 @@ def getFolderId(path, drive):
dbChange = True dbChange = True
currentFolderId = currentFolder['id'] currentFolderId = currentFolder['id']
else: else:
currentFolderId= None currentFolderId = None
break break
if dbChange: if dbChange:
session.commit() session.commit()
@@ -268,16 +268,9 @@ def getFileFromEbooksFolder(path, fileName):
return None return None
'''def copyDriveFileRemote(drive, origin_file_id, copy_title): def moveGdriveFileRemote(origin_file_id, new_title):
drive = getDrive(drive) origin_file_id['title']= new_title
copied_file = {'title': copy_title} origin_file_id.Upload()
try:
file_data = drive.auth.service.files().copy(
fileId = origin_file_id, body=copied_file).execute()
return drive.CreateFile({'id': file_data['id']})
except errors.HttpError as error:
print ('An error occurred: %s' % error)
return None'''
# Download metadata.db from gdrive # Download metadata.db from gdrive
@@ -289,9 +282,10 @@ def downloadFile(path, filename, output):
def moveGdriveFolderRemote(origin_file, target_folder): def moveGdriveFolderRemote(origin_file, target_folder):
drive = getDrive(Gdrive.Instance().drive) drive = getDrive(Gdrive.Instance().drive)
previous_parents = ",".join([parent["id"] for parent in origin_file.get('parents')]) previous_parents = ",".join([parent["id"] for parent in origin_file.get('parents')])
children = drive.auth.service.children().list(folderId=previous_parents).execute()
gFileTargetDir = getFileFromEbooksFolder(None, target_folder) gFileTargetDir = getFileFromEbooksFolder(None, target_folder)
if not gFileTargetDir: if not gFileTargetDir:
# Folder is not exisiting, create, and move folder # Folder is not existing, create, and move folder
gFileTargetDir = drive.CreateFile( gFileTargetDir = drive.CreateFile(
{'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}], {'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}],
"mimeType": "application/vnd.google-apps.folder"}) "mimeType": "application/vnd.google-apps.folder"})
@@ -301,13 +295,10 @@ def moveGdriveFolderRemote(origin_file, target_folder):
addParents=gFileTargetDir['id'], addParents=gFileTargetDir['id'],
removeParents=previous_parents, removeParents=previous_parents,
fields='id, parents').execute() fields='id, parents').execute()
# if previous_parents has no childs anymore, delete originfileparent # if previous_parents has no childs anymore, delete original fileparent
# is not working correctly, because of slow update on gdrive -> could cause trouble in gdrive.db if len(children['items']) == 1:
# (nonexisting folder has id) deleteDatabaseEntry(previous_parents)
# children = drive.auth.service.children().list(folderId=previous_parents).execute() drive.auth.service.files().delete(fileId=previous_parents).execute()
# if not len(children['items']):
# drive.auth.service.files().delete(fileId=previous_parents).execute()
def copyToDrive(drive, uploadFile, createRoot, replaceFiles, def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
@@ -320,7 +311,7 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
parent = getEbooksFolder(drive) parent = getEbooksFolder(drive)
if os.path.isdir(os.path.join(prevDir,uploadFile)): if os.path.isdir(os.path.join(prevDir,uploadFile)):
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(os.path.basename(uploadFile), parent['id'])}).GetList() (os.path.basename(uploadFile).replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFolder) == 0 and (not isInitial or createRoot): if len(existingFolder) == 0 and (not isInitial or createRoot):
parent = drive.CreateFile({'title': os.path.basename(uploadFile), parent = drive.CreateFile({'title': os.path.basename(uploadFile),
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
@@ -335,11 +326,11 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
else: else:
if os.path.basename(uploadFile) not in ignoreFiles: if os.path.basename(uploadFile) not in ignoreFiles:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(os.path.basename(uploadFile), parent['id'])}).GetList() (os.path.basename(uploadFile).replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFiles) > 0: if len(existingFiles) > 0:
driveFile = existingFiles[0] driveFile = existingFiles[0]
else: else:
driveFile = drive.CreateFile({'title': os.path.basename(uploadFile), driveFile = drive.CreateFile({'title': os.path.basename(uploadFile).replace("'", r"\'"),
'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], }) 'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], })
driveFile.SetContentFile(os.path.join(prevDir, uploadFile)) driveFile.SetContentFile(os.path.join(prevDir, uploadFile))
driveFile.Upload() driveFile.Upload()
@@ -352,7 +343,7 @@ def uploadFileToEbooksFolder(destFile, f):
for i, x in enumerate(splitDir): for i, x in enumerate(splitDir):
if i == len(splitDir)-1: if i == len(splitDir)-1:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x, parent['id'])}).GetList() (x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFiles) > 0: if len(existingFiles) > 0:
driveFile = existingFiles[0] driveFile = existingFiles[0]
else: else:
@@ -361,7 +352,7 @@ def uploadFileToEbooksFolder(destFile, f):
driveFile.Upload() driveFile.Upload()
else: else:
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x, parent['id'])}).GetList() (x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFolder) == 0: if len(existingFolder) == 0:
parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"}) "mimeType": "application/vnd.google-apps.folder"})
@@ -454,6 +445,10 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error: except (errors.HttpError) as error:
web.app.logger.info(error.message) web.app.logger.info(error.message)
return None return None
except Exception as e:
web.app.logger.info(e)
return None
# Deletes the local hashes database to force search for new folder names # Deletes the local hashes database to force search for new folder names
def deleteDatabaseOnChange(): def deleteDatabaseOnChange():
@@ -468,9 +463,10 @@ def updateGdriveCalibreFromLocal():
# update gdrive.db on edit of books title # update gdrive.db on edit of books title
def updateDatabaseOnEdit(ID,newPath): def updateDatabaseOnEdit(ID,newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first() storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName: if storedPathName:
storedPathName.path = newPath storedPathName.path = sqlCheckPath
session.commit() session.commit()
+101 -57
View File
@@ -22,13 +22,12 @@
import db import db
import ub import ub
from flask import current_app as app from flask import current_app as app
# import logging
from tempfile import gettempdir from tempfile import gettempdir
import sys import sys
import io
import os import os
import re import re
import unicodedata import unicodedata
# from io import BytesIO
import worker import worker
import time import time
from flask import send_from_directory, make_response, redirect, abort from flask import send_from_directory, make_response, redirect, abort
@@ -36,16 +35,13 @@ from flask_babel import gettext as _
from flask_login import current_user from flask_login import current_user
from babel.dates import format_datetime from babel.dates import format_datetime
from datetime import datetime from datetime import datetime
# import threading
import shutil import shutil
import requests import requests
# import zipfile
try: try:
import gdriveutils as gd import gdriveutils as gd
except ImportError: except ImportError:
pass pass
import web import web
# import server
import random import random
import subprocess import subprocess
@@ -55,6 +51,12 @@ try:
except ImportError: except ImportError:
use_unidecode = False use_unidecode = False
try:
from PIL import Image
use_PIL = True
except ImportError:
use_PIL = False
# Global variables # Global variables
# updater_thread = None # updater_thread = None
global_WorkerThread = worker.WorkerThread() global_WorkerThread = worker.WorkerThread()
@@ -128,7 +130,7 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
text += "Sincerely\r\n\r\n" text += "Sincerely\r\n\r\n"
text += "Your Calibre-Web team" text += "Your Calibre-Web team"
global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(), global_WorkerThread.add_email(_(u'Get Started with Calibre-Web'),None, None, ub.get_mail_settings(),
e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name), text) e_mail, None, _(u"Registration e-mail for user: %(name)s", name=user_name), text)
return return
def check_send_to_kindle(entry): def check_send_to_kindle(entry):
@@ -146,8 +148,8 @@ def check_send_to_kindle(entry):
bookformats.append({'format':'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')}) bookformats.append({'format':'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
if 'AZW' in ele.format: if 'AZW' in ele.format:
bookformats.append({'format':'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')}) bookformats.append({'format':'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
if 'AZW3' in ele.format: '''if 'AZW3' in ele.format:
bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')}) bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})'''
else: else:
formats = list() formats = list()
for ele in iter(entry.data): for ele in iter(entry.data):
@@ -156,18 +158,16 @@ def check_send_to_kindle(entry):
bookformats.append({'format': 'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')}) bookformats.append({'format': 'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
if 'AZW' in formats: if 'AZW' in formats:
bookformats.append({'format': 'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')}) bookformats.append({'format': 'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
if 'AZW3' in formats:
bookformats.append({'format': 'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})
if 'PDF' in formats: if 'PDF' in formats:
bookformats.append({'format': 'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')}) bookformats.append({'format': 'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
if ub.config.config_ebookconverter >= 1: if ub.config.config_ebookconverter >= 1:
if 'EPUB' in formats and not 'MOBI' in formats: if 'EPUB' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi','convert':1, bookformats.append({'format': 'Mobi','convert':1,
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Mobi')}) 'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Mobi')})
if ub.config.config_ebookconverter == 2: '''if ub.config.config_ebookconverter == 2:
if 'EPUB' in formats and not 'AZW3' in formats: if 'EPUB' in formats and not 'AZW3' in formats:
bookformats.append({'format': 'Azw3','convert':1, bookformats.append({'format': 'Azw3','convert':1,
'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Azw3')}) 'text':_('Convert %(orig)s to %(format)s and send to Kindle',orig='Epub',format='Azw3')})'''
return bookformats return bookformats
else: else:
app.logger.error(u'Cannot find book entry %d', entry.id) app.logger.error(u'Cannot find book entry %d', entry.id)
@@ -177,7 +177,7 @@ def check_send_to_kindle(entry):
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return # Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
# list with supported formats # list with supported formats
def check_read_formats(entry): def check_read_formats(entry):
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'ZIP', 'CBZ', 'TAR', 'CBT', 'RAR', 'CBR'} EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'}
bookformats = list() bookformats = list()
if len(entry.data): if len(entry.data):
for ele in iter(entry.data): for ele in iter(entry.data):
@@ -235,7 +235,10 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:128] value = value[:128]
if not value: if not value:
raise ValueError("Filename cannot be empty") raise ValueError("Filename cannot be empty")
return value if sys.version_info.major == 3:
return value
else:
return value.decode('utf-8')
def get_sorted_author(value): def get_sorted_author(value):
@@ -324,11 +327,11 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
# Rename all files from old names to new names # Rename all files from old names to new names
if authordir != new_authordir or titledir != new_titledir: if authordir != new_authordir or titledir != new_titledir:
try: try:
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
for file_format in localbook.data: for file_format in localbook.data:
path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
new_name = get_valid_filename(localbook.title) + ' - ' + get_valid_filename(new_authordir)
os.renames(os.path.join(path_name, file_format.name + '.' + file_format.format.lower()), os.renames(os.path.join(path_name, file_format.name + '.' + file_format.format.lower()),
os.path.join(path_name,new_name + '.' + file_format.format.lower())) os.path.join(path_name, new_name + '.' + file_format.format.lower()))
file_format.name = new_name file_format.name = new_name
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex)) web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex))
@@ -341,6 +344,7 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
def update_dir_structure_gdrive(book_id, first_author): def update_dir_structure_gdrive(book_id, first_author):
error = False error = False
book = db.session.query(db.Books).filter(db.Books.id == book_id).first() book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
path = book.path
authordir = book.path.split('/')[0] authordir = book.path.split('/')[0]
if first_author: if first_author:
@@ -348,40 +352,39 @@ def update_dir_structure_gdrive(book_id, first_author):
else: else:
new_authordir = get_valid_filename(book.authors[0].name) new_authordir = get_valid_filename(book.authors[0].name)
titledir = book.path.split('/')[1] titledir = book.path.split('/')[1]
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")" new_titledir = get_valid_filename(book.title) + u" (" + str(book_id) + u")"
if titledir != new_titledir: if titledir != new_titledir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir) gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if gFile: if gFile:
gFile['title'] = new_titledir gFile['title'] = new_titledir
gFile.Upload() gFile.Upload()
book.path = book.path.split('/')[0] + '/' + new_titledir book.path = book.path.split('/')[0] + u'/' + new_titledir
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
else: else:
error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
if authordir != new_authordir: if authordir != new_authordir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir) gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
if gFile: if gFile:
gd.moveGdriveFolderRemote(gFile,new_authordir) gd.moveGdriveFolderRemote(gFile, new_authordir)
book.path = new_authordir + '/' + book.path.split('/')[1] book.path = new_authordir + u'/' + book.path.split('/')[1]
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path) gd.updateDatabaseOnEdit(gFile['id'], book.path)
else: else:
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
# Rename all files from old names to new names # Rename all files from old names to new names
# ToDo: Rename also all bookfiles with new author name and new title name
'''
if authordir != new_authordir or titledir != new_titledir: if authordir != new_authordir or titledir != new_titledir:
for format in book.data: new_name = get_valid_filename(book.title) + u' - ' + get_valid_filename(new_authordir)
# path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path)) for file_format in book.data:
new_name = get_valid_filename(book.title) + ' - ' + get_valid_filename(book) gFile = gd.getFileFromEbooksFolder(path, file_format.name + u'.' + file_format.format.lower())
format.name = new_name if not gFile:
if gFile: error = _(u'File %(file)s not found on Google Drive', file=file_format.name) # file not found
pass break
else: gd.moveGdriveFileRemote(gFile, new_name + u'.' + file_format.format.lower())
error = _(u'File %(file)s not found on Google Drive', file=format.name) # file not found file_format.name = new_name
break'''
return error return error
@@ -427,6 +430,8 @@ def delete_book(book, calibrepath, book_format):
def get_book_cover(cover_path): def get_book_cover(cover_path):
if ub.config.config_use_google_drive: if ub.config.config_use_google_drive:
try: try:
if not web.is_gdrive_ready():
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
path=gd.get_cover_via_gdrive(cover_path) path=gd.get_cover_via_gdrive(cover_path)
if path: if path:
return redirect(path) return redirect(path)
@@ -434,7 +439,7 @@ def get_book_cover(cover_path):
web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive') web.app.logger.error(cover_path + '/cover.jpg not found on Google Drive')
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
except Exception as e: except Exception as e:
web.app.logger.error("Error Message: "+e.message) web.app.logger.error("Error Message: " + e.message)
web.app.logger.exception(e) web.app.logger.exception(e)
# traceback.print_exc() # traceback.print_exc()
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg") return send_from_directory(os.path.join(os.path.dirname(__file__), "static"),"generic_cover.jpg")
@@ -442,27 +447,71 @@ def get_book_cover(cover_path):
return send_from_directory(os.path.join(ub.config.config_calibre_dir, cover_path), "cover.jpg") return send_from_directory(os.path.join(ub.config.config_calibre_dir, cover_path), "cover.jpg")
# saves book cover to gdrive or locally # saves book cover from url
def save_cover(url, book_path): def save_cover_from_url(url, book_path):
img = requests.get(url) img = requests.get(url)
if img.headers.get('content-type') != 'image/jpeg': return save_cover(img, book_path)
web.app.logger.error("Cover is no jpg file, can't save")
return False
def save_cover_from_filestorage(filepath, saved_filename, img):
if hasattr(img,'_content'):
f = open(os.path.join(filepath, saved_filename), "wb")
f.write(img._content)
f.close()
else:
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
if not os.path.exists(filepath):
try:
os.makedirs(filepath)
except OSError:
web.app.logger.error(u"Failed to create path for cover")
return False
try:
img.save(os.path.join(filepath, saved_filename))
except OSError:
web.app.logger.error(u"Failed to store cover-file")
return False
except IOError:
web.app.logger.error(u"Cover-file is not a valid image file")
return False
return True
# saves book cover to gdrive or locally
def save_cover(img, book_path):
content_type = img.headers.get('content-type')
if use_PIL:
if content_type not in ('image/jpeg', 'image/png', 'image/webp'):
web.app.logger.error("Only jpg/jpeg/png/webp files are supported as coverfile")
return False
# convert to jpg because calibre only supports jpg
if content_type in ('image/png', 'image/webp'):
if hasattr(img,'stream'):
imgc = Image.open(img.stream)
else:
imgc = Image.open(io.BytesIO(img.content))
im = imgc.convert('RGB')
tmp_bytesio = io.BytesIO()
im.save(tmp_bytesio, format='JPEG')
img._content = tmp_bytesio.getvalue()
else:
if content_type not in ('image/jpeg'):
web.app.logger.error("Only jpg/jpeg files are supported as coverfile")
return False
if ub.config.config_use_google_drive: if ub.config.config_use_google_drive:
tmpDir = gettempdir() tmpDir = gettempdir()
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb") if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True:
f.write(img.content) gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
f.close() os.path.join(tmpDir, "uploaded_cover.jpg"))
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) web.app.logger.info("Cover is saved on Google Drive")
web.app.logger.info("Cover is saved on Google Drive") return True
return True else:
return False
else:
return save_cover_from_filestorage(os.path.join(ub.config.config_calibre_dir, book_path), "cover.jpg", img)
f = open(os.path.join(ub.config.config_calibre_dir, book_path, "cover.jpg"), "wb")
f.write(img.content)
f.close()
web.app.logger.info("Cover is saved")
return True
def do_download_file(book, book_format, data, headers): def do_download_file(book, book_format, data, headers):
@@ -487,7 +536,6 @@ def do_download_file(book, book_format, data, headers):
def check_unrar(unrarLocation): def check_unrar(unrarLocation):
error = False error = False
if os.path.exists(unrarLocation): if os.path.exists(unrarLocation):
@@ -520,17 +568,13 @@ def json_serial(obj):
return obj.isoformat() return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj)) raise TypeError ("Type %s not serializable" % type(obj))
# helper function to apply localize status information in tasklist entries
def render_task_status(tasklist): def render_task_status(tasklist):
#helper function to apply localize status information in tasklist entries
renderedtasklist=list() renderedtasklist=list()
# task2 = task
for task in tasklist: for task in tasklist:
if task['user'] == current_user.nickname or current_user.role_admin(): if task['user'] == current_user.nickname or current_user.role_admin():
# task2 = copy.deepcopy(task) # = task
if task['formStarttime']: if task['formStarttime']:
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale()) task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale())
# task2['formStarttime'] = ""
else: else:
if 'starttime' not in task: if 'starttime' not in task:
task['starttime'] = "" task['starttime'] = ""
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2019 pwr
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
try:
from iso639 import languages, __version__
get = languages.get
except ImportError:
from pycountry import languages as pyc_languages
try:
import pkg_resources
__version__ = pkg_resources.get_distribution('pycountry').version + ' (PyCountry)'
del pkg_resources
except (ImportError, Exception):
__version__ = "? (PyCountry)"
def _copy_fields(l):
l.part1 = l.alpha_2
l.part3 = l.alpha_3
return l
def get(name=None, part1=None, part3=None):
if (part3 is not None):
return _copy_fields(pyc_languages.get(alpha_3=part3))
if (part1 is not None):
return _copy_fields(pyc_languages.get(alpha_2=part1))
if (name is not None):
return _copy_fields(pyc_languages.get(name=name))
+33 -13
View File
@@ -1,21 +1,41 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) # Flask License
# Copyright (C) 2018 cervinko, janeczku, OzzieIsaacs
# #
# This program is free software: you can redistribute it and/or modify # Copyright © 2010 by the Pallets team, cervinko, janeczku, OzzieIsaacs
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # Some rights reserved.
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # Redistribution and use in source and binary forms of the software as
# along with this program. If not, see <http://www.gnu.org/licenses/>. # well as documentation, with or without modification, are permitted
# provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# Inspired by http://flask.pocoo.org/snippets/35/
class ReverseProxied(object): class ReverseProxied(object):
"""Wrap the application in this middleware and configure the """Wrap the application in this middleware and configure the
@@ -49,7 +69,7 @@ class ReverseProxied(object):
scheme = environ.get('HTTP_X_SCHEME', '') scheme = environ.get('HTTP_X_SCHEME', '')
if scheme: if scheme:
environ['wsgi.url_scheme'] = scheme environ['wsgi.url_scheme'] = scheme
servr = environ.get('HTTP_X_FORWARDED_SERVER', '') servr = environ.get('HTTP_X_FORWARDED_HOST', '')
if servr: if servr:
environ['HTTP_HOST'] = servr environ['HTTP_HOST'] = servr
return self.app(environ, start_response) return self.app(environ, start_response)
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -152,11 +152,11 @@ body {
max-width: 70%; max-width: 70%;
} }
#prev { #left {
left: 40px; left: 40px;
} }
#next { #right {
right: 40px; right: 40px;
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before

Width:  |  Height:  |  Size: 883 B

After

Width:  |  Height:  |  Size: 883 B

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 408 B

After

Width:  |  Height:  |  Size: 408 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 426 B

After

Width:  |  Height:  |  Size: 426 B

Before

Width:  |  Height:  |  Size: 158 B

After

Width:  |  Height:  |  Size: 158 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 304 B

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 193 B

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 296 B

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 193 B

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 296 B

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 199 B

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 304 B

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 403 B

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 933 B

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 179 B

Before

Width:  |  Height:  |  Size: 266 B

After

Width:  |  Height:  |  Size: 266 B

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 301 B

Before

Width:  |  Height:  |  Size: 583 B

After

Width:  |  Height:  |  Size: 583 B

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 360 B

Before

Width:  |  Height:  |  Size: 731 B

After

Width:  |  Height:  |  Size: 731 B

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 425 B

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 107 B

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 152 B

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 550 B

Before

Width:  |  Height:  |  Size: 242 B

After

Width:  |  Height:  |  Size: 242 B

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 398 B

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 238 B

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 396 B

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 405 B

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 246 B

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 403 B

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 321 B

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 586 B

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 464 B

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 309 B

Before

Width:  |  Height:  |  Size: 653 B

After

Width:  |  Height:  |  Size: 653 B

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 246 B

Before

Width:  |  Height:  |  Size: 456 B

After

Width:  |  Height:  |  Size: 456 B

Before

Width:  |  Height:  |  Size: 243 B

After

Width:  |  Height:  |  Size: 243 B

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 458 B

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 225 B

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 344 B

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 225 B

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

Before

Width:  |  Height:  |  Size: 384 B

After

Width:  |  Height:  |  Size: 384 B

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 859 B

Before

Width:  |  Height:  |  Size: 177 B

After

Width:  |  Height:  |  Size: 177 B

Before

Width:  |  Height:  |  Size: 394 B

After

Width:  |  Height:  |  Size: 394 B

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 178 B

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

Some files were not shown because too many files have changed in this diff Show More