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:
from wand.image import Image
from wand import version as ImageVersion
from wand.exceptions import PolicyError
use_generic_pdf_cover = False
except (ImportError, RuntimeError) as 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)
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):
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):
if use_pdf_meta:
pdf = PdfFileReader(open(tmp_file_path, 'rb'))
pdf = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
doc_info = pdf.getDocumentInfo()
else:
doc_info = None
@@ -130,12 +138,58 @@ def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover:
return None
else:
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
if use_PIL:
try:
input1 = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
page0 = input1.getPage(0)
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():
if not use_generic_pdf_cover:
@@ -152,4 +206,12 @@ def get_versions():
XVersion = 'v'+'.'.join(map(str, lxmlversion))
else:
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):
cover_data = None
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
compressed_name = cf.namelist()[0]
cover_data = cf.read(compressed_name)
for name in cf.namelist():
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':
cf = tarfile.TarFile(tmp_file_name)
compressed_name = cf.getnames()[0]
cover_data = cf.extractfile(compressed_name).read()
for name in cf.getnames():
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)
tmp_cover_name = prefix + '/cover' + os.path.splitext(compressed_name)[1]
image = open(tmp_cover_name, 'wb')
image.write(cover_data)
image.close()
if cover_data:
tmp_cover_name = prefix + '/cover' + extension
image = open(tmp_cover_name, 'wb')
image.write(cover_data)
image.close()
else:
tmp_cover_name = None
return tmp_cover_name
+8 -3
View File
@@ -27,6 +27,7 @@ import ast
from ub import config
import ub
import sys
import unidecode
session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
@@ -46,7 +47,7 @@ def title_sort(title):
def lcase(s):
return s.lower()
return unidecode.unidecode(s.lower())
def ucase(s):
@@ -112,6 +113,8 @@ class Identifiers(Base):
return u"Google Books"
elif self.type == "kobo":
return u"Kobo"
if self.type == "lubimyczytac":
return u"Lubimyczytac"
else:
return self.type
@@ -130,6 +133,8 @@ class Identifiers(Base):
return u"https://books.google.com/books?id={0}".format(self.val)
elif self.type == "kobo":
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":
return u"{0}".format(self.val)
else:
@@ -355,8 +360,8 @@ def setup_db():
ub.session.commit()
config.loadSettings()
conn.connection.create_function('title_sort', 1, title_sort)
conn.connection.create_function('lower', 1, lcase)
conn.connection.create_function('upper', 1, ucase)
# conn.connection.create_function('lower', 1, lcase)
# conn.connection.create_function('upper', 1, ucase)
if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
+1 -1
View File
@@ -21,7 +21,7 @@ import zipfile
from lxml import etree
import os
import uploader
from iso639 import languages as isoLanguages
import isoLanguages
def extractCover(zipFile, coverFile, coverpath, tmp_file_name):
+28 -32
View File
@@ -20,7 +20,7 @@
try:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError
from pydrive.auth import RefreshError, InvalidConfigError
from apiclient import errors
gdrive_support = True
except ImportError:
@@ -31,12 +31,9 @@ from ub import config
import cli
import shutil
from flask import Response, stream_with_context
from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import *
import web
class Singleton:
@@ -166,7 +163,10 @@ def getDrive(drive=None, gauth=None):
# Save the current credentials to a file
return GoogleDrive(gauth)
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
def listRootFolders():
@@ -184,7 +184,7 @@ def getFolderInFolder(parentId, folderName, drive):
# drive = getDrive(drive)
query=""
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'" \
" and trashed = false" % parentId
fileList = drive.ListFile({'q': folder}).GetList()
@@ -211,7 +211,7 @@ def getEbooksFolderId(drive=None):
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()
if fileList.__len__() == 0:
return None
@@ -246,7 +246,7 @@ def getFolderId(path, drive):
dbChange = True
currentFolderId = currentFolder['id']
else:
currentFolderId= None
currentFolderId = None
break
if dbChange:
session.commit()
@@ -268,16 +268,9 @@ def getFileFromEbooksFolder(path, fileName):
return None
'''def copyDriveFileRemote(drive, origin_file_id, copy_title):
drive = getDrive(drive)
copied_file = {'title': copy_title}
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'''
def moveGdriveFileRemote(origin_file_id, new_title):
origin_file_id['title']= new_title
origin_file_id.Upload()
# Download metadata.db from gdrive
@@ -289,9 +282,10 @@ def downloadFile(path, filename, output):
def moveGdriveFolderRemote(origin_file, target_folder):
drive = getDrive(Gdrive.Instance().drive)
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)
if not gFileTargetDir:
# Folder is not exisiting, create, and move folder
# Folder is not existing, create, and move folder
gFileTargetDir = drive.CreateFile(
{'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}],
"mimeType": "application/vnd.google-apps.folder"})
@@ -301,13 +295,10 @@ def moveGdriveFolderRemote(origin_file, target_folder):
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
# if previous_parents has no childs anymore, delete originfileparent
# is not working correctly, because of slow update on gdrive -> could cause trouble in gdrive.db
# (nonexisting folder has id)
# children = drive.auth.service.children().list(folderId=previous_parents).execute()
# if not len(children['items']):
# drive.auth.service.files().delete(fileId=previous_parents).execute()
# if previous_parents has no childs anymore, delete original fileparent
if len(children['items']) == 1:
deleteDatabaseEntry(previous_parents)
drive.auth.service.files().delete(fileId=previous_parents).execute()
def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
@@ -320,7 +311,7 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
parent = getEbooksFolder(drive)
if os.path.isdir(os.path.join(prevDir,uploadFile)):
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):
parent = drive.CreateFile({'title': os.path.basename(uploadFile),
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
@@ -335,11 +326,11 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
else:
if os.path.basename(uploadFile) not in ignoreFiles:
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:
driveFile = existingFiles[0]
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']}], })
driveFile.SetContentFile(os.path.join(prevDir, uploadFile))
driveFile.Upload()
@@ -352,7 +343,7 @@ def uploadFileToEbooksFolder(destFile, f):
for i, x in enumerate(splitDir):
if i == len(splitDir)-1:
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:
driveFile = existingFiles[0]
else:
@@ -361,7 +352,7 @@ def uploadFileToEbooksFolder(destFile, f):
driveFile.Upload()
else:
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:
parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"})
@@ -454,6 +445,10 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error:
web.app.logger.info(error.message)
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
def deleteDatabaseOnChange():
@@ -468,9 +463,10 @@ def updateGdriveCalibreFromLocal():
# update gdrive.db on edit of books title
def updateDatabaseOnEdit(ID,newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName:
storedPathName.path = newPath
storedPathName.path = sqlCheckPath
session.commit()
+101 -57
View File
@@ -22,13 +22,12 @@
import db
import ub
from flask import current_app as app
# import logging
from tempfile import gettempdir
import sys
import io
import os
import re
import unicodedata
# from io import BytesIO
import worker
import time
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 babel.dates import format_datetime
from datetime import datetime
# import threading
import shutil
import requests
# import zipfile
try:
import gdriveutils as gd
except ImportError:
pass
import web
# import server
import random
import subprocess
@@ -55,6 +51,12 @@ try:
except ImportError:
use_unidecode = False
try:
from PIL import Image
use_PIL = True
except ImportError:
use_PIL = False
# Global variables
# updater_thread = None
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 += "Your Calibre-Web team"
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
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')})
if 'AZW' in ele.format:
bookformats.append({'format':'Azw','convert':0,'text':_('Send %(format)s to Kindle',format='Azw')})
if 'AZW3' in ele.format:
bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})
'''if 'AZW3' in ele.format:
bookformats.append({'format':'Azw3','convert':0,'text':_('Send %(format)s to Kindle',format='Azw3')})'''
else:
formats = list()
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')})
if 'AZW' in formats:
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:
bookformats.append({'format': 'Pdf','convert':0,'text':_('Send %(format)s to Kindle',format='Pdf')})
if ub.config.config_ebookconverter >= 1:
if 'EPUB' in formats and not 'MOBI' in formats:
bookformats.append({'format': 'Mobi','convert':1,
'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:
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
else:
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
# list with supported formats
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()
if len(entry.data):
for ele in iter(entry.data):
@@ -235,7 +235,10 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:128]
if not value:
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):
@@ -324,11 +327,11 @@ def update_dir_structure_file(book_id, calibrepath, first_author):
# Rename all files from old names to new names
if authordir != new_authordir or titledir != new_titledir:
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:
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.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
except OSError as 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):
error = False
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
path = book.path
authordir = book.path.split('/')[0]
if first_author:
@@ -348,40 +352,39 @@ def update_dir_structure_gdrive(book_id, first_author):
else:
new_authordir = get_valid_filename(book.authors[0].name)
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:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if gFile:
gFile['title'] = new_titledir
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
else:
error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
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:
gd.moveGdriveFolderRemote(gFile,new_authordir)
book.path = new_authordir + '/' + book.path.split('/')[1]
gd.moveGdriveFolderRemote(gFile, new_authordir)
book.path = new_authordir + u'/' + book.path.split('/')[1]
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path)
else:
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
# 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:
for format in book.data:
# path_name = os.path.join(calibrepath, new_authordir, os.path.basename(path))
new_name = get_valid_filename(book.title) + ' - ' + get_valid_filename(book)
format.name = new_name
if gFile:
pass
else:
error = _(u'File %(file)s not found on Google Drive', file=format.name) # file not found
break'''
new_name = get_valid_filename(book.title) + u' - ' + get_valid_filename(new_authordir)
for file_format in book.data:
gFile = gd.getFileFromEbooksFolder(path, file_format.name + u'.' + file_format.format.lower())
if not gFile:
error = _(u'File %(file)s not found on Google Drive', file=file_format.name) # file not found
break
gd.moveGdriveFileRemote(gFile, new_name + u'.' + file_format.format.lower())
file_format.name = new_name
return error
@@ -427,6 +430,8 @@ def delete_book(book, calibrepath, book_format):
def get_book_cover(cover_path):
if ub.config.config_use_google_drive:
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)
if 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')
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
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)
# traceback.print_exc()
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")
# saves book cover to gdrive or locally
def save_cover(url, book_path):
# saves book cover from url
def save_cover_from_url(url, book_path):
img = requests.get(url)
if img.headers.get('content-type') != 'image/jpeg':
web.app.logger.error("Cover is no jpg file, can't save")
return False
return save_cover(img, book_path)
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:
tmpDir = gettempdir()
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
f.write(img.content)
f.close()
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")
return True
if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True:
gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'),
os.path.join(tmpDir, "uploaded_cover.jpg"))
web.app.logger.info("Cover is saved on Google Drive")
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):
@@ -487,7 +536,6 @@ def do_download_file(book, book_format, data, headers):
def check_unrar(unrarLocation):
error = False
if os.path.exists(unrarLocation):
@@ -520,17 +568,13 @@ def json_serial(obj):
return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj))
# helper function to apply localize status information in tasklist entries
def render_task_status(tasklist):
#helper function to apply localize status information in tasklist entries
renderedtasklist=list()
# task2 = task
for task in tasklist:
if task['user'] == current_user.nickname or current_user.role_admin():
# task2 = copy.deepcopy(task) # = task
if task['formStarttime']:
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale())
# task2['formStarttime'] = ""
else:
if 'starttime' not in task:
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
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 cervinko, janeczku, OzzieIsaacs
# Flask License
#
# 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.
# Copyright © 2010 by the Pallets team, cervinko, janeczku, OzzieIsaacs
#
# 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.
# Some rights reserved.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Redistribution and use in source and binary forms of the software as
# 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):
"""Wrap the application in this middleware and configure the
@@ -49,7 +69,7 @@ class ReverseProxied(object):
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
servr = environ.get('HTTP_X_FORWARDED_SERVER', '')
servr = environ.get('HTTP_X_FORWARDED_HOST', '')
if servr:
environ['HTTP_HOST'] = servr
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%;
}
#prev {
#left {
left: 40px;
}
#next {
#right {
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