Merge pull request #20 from janeczku/master

merge janeczku/master
pull/1097/head
Ethan Lin 5 years ago committed by GitHub
commit 8143bc7873
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

2
.gitattributes vendored

@ -1,4 +1,4 @@
helper.py ident export-subst updater.py ident export-subst
/test export-ignore /test export-ignore
cps/static/css/libs/* linguist-vendored cps/static/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored cps/static/js/libs/* linguist-vendored

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2016-2019 lemmsh cervinko Kennyl matthazinski OzzieIsaacs
#
# 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/>.
import logging import logging
import uploader import uploader
import os import os
@ -19,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)
@ -84,7 +101,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
@ -114,12 +131,18 @@ 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" try:
with Image(filename=tmp_file_path + "[0]", resolution=150) as img: cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
img.compression_quality = 88 with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
img.save(filename=os.path.join(tmp_dir, cover_file_name)) img.compression_quality = 88
return cover_file_name 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:

@ -1,3 +1,19 @@
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2016-2019 jkrehm andy29485 OzzieIsaacs
#
# 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/>.
# Inspired by https://github.com/ChrisTM/Flask-CacheBust # Inspired by https://github.com/ChrisTM/Flask-CacheBust
# Uses query strings so CSS font files are found without having to resort to absolute URLs # Uses query strings so CSS font files are found without having to resort to absolute URLs

@ -1,6 +1,23 @@
#!/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)
# Copyright (C) 2018 OzzieIsaacs
#
# 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/>.
import argparse import argparse
import os import os
import sys import sys

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2018 OzzieIsaacs
#
# 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/>.
import zipfile import zipfile
import tarfile import tarfile
import os import os
@ -8,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

@ -1,5 +1,23 @@
#!/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)
# Copyright (C) 2016-2019 Ben Bennett, OzzieIsaacs
#
# 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/>.
import os import os
import subprocess import subprocess
import ub import ub

@ -1,6 +1,23 @@
#!/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)
# Copyright (C) 2012-2019 mutschler, cervinko, ok11, jkrehm, nanu-c, Wineliva,
# pjeby, elelay, idalin, Ozzieisaacs
#
# 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/>.
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 *
@ -9,6 +26,7 @@ import re
import ast import ast
from ub import config from ub import config
import ub import ub
import sys
session = None session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
@ -301,6 +319,8 @@ class Custom_Columns(Base):
def get_display_dict(self): def get_display_dict(self):
display_dict = ast.literal_eval(self.display) display_dict = ast.literal_eval(self.display)
if sys.version_info < (3, 0):
display_dict['enum_values'] = [x.decode('unicode_escape') for x in display_dict['enum_values']]
return display_dict return display_dict

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2018 lemmsh, Kennyl, Kyosfonica, matthazinski
#
# 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/>.
import zipfile import zipfile
from lxml import etree from lxml import etree
import os import os

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2018 lemmsh, cervinko, OzzieIsaacs
#
# 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/>.
from lxml import etree from lxml import etree
import uploader import uploader

@ -1,7 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 idalin, OzzieIsaacs
#
# 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: 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:
@ -12,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:
@ -112,7 +128,8 @@ def migrate():
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)') sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2') sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
session.execute(sql) session.execute(sql)
session.execute('INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, gdrive_id, path FROM gdrive_ids;') session.execute("INSERT INTO gdrive_ids2 (id, gdrive_id, path) SELECT id, "
"gdrive_id, path FROM gdrive_ids;")
session.commit() session.commit()
session.execute('DROP TABLE %s' % 'gdrive_ids') session.execute('DROP TABLE %s' % 'gdrive_ids')
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids') session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
@ -146,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():
@ -164,8 +184,9 @@ 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' and trashed = false" % parentId folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder'" \
" and trashed = false" % parentId
fileList = drive.ListFile({'q': folder}).GetList() fileList = drive.ListFile({'q': folder}).GetList()
if fileList.__len__() == 0: if fileList.__len__() == 0:
return None return None
@ -190,8 +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
@ -226,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()
@ -248,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
@ -269,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"})
@ -281,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,
@ -299,9 +310,11 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
if not parent: if not parent:
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" % (os.path.basename(uploadFile), parent['id'])}).GetList() existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(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), 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}], parent = drive.CreateFile({'title': os.path.basename(uploadFile),
'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"}) "mimeType": "application/vnd.google-apps.folder"})
parent.Upload() parent.Upload()
else: else:
@ -312,11 +325,13 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
copyToDrive(drive, f, True, replaceFiles, ignoreFiles, parent, os.path.join(prevDir, uploadFile)) copyToDrive(drive, f, True, replaceFiles, ignoreFiles, parent, os.path.join(prevDir, uploadFile))
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" % (os.path.basename(uploadFile), parent['id'])}).GetList() existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(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), 'parents': [{"kind":"drive#fileLink", 'id': parent['id']}], }) 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.SetContentFile(os.path.join(prevDir, uploadFile))
driveFile.Upload() driveFile.Upload()
@ -327,7 +342,8 @@ def uploadFileToEbooksFolder(destFile, f):
splitDir = destFile.split('/') splitDir = destFile.split('/')
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" % (x, parent['id'])}).GetList() existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFiles) > 0: if len(existingFiles) > 0:
driveFile = existingFiles[0] driveFile = existingFiles[0]
else: else:
@ -335,7 +351,8 @@ def uploadFileToEbooksFolder(destFile, f):
driveFile.SetContentFile(f) driveFile.SetContentFile(f)
driveFile.Upload() driveFile.Upload()
else: else:
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (x, parent['id'])}).GetList() existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(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"})
@ -428,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():
@ -442,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()

@ -1,32 +1,46 @@
#!/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)
# Copyright (C) 2012-2019 cervinko, idalin, SiphonSquirrel, ouzklcn, akushsky,
# OzzieIsaacs, bodybybuddha, jkrehm, matthazinski, janeczku
#
# 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/>.
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 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
from flask_babel import gettext as _ 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
import threading from datetime import datetime
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
@ -37,7 +51,7 @@ except ImportError:
use_unidecode = False use_unidecode = False
# Global variables # Global variables
updater_thread = None # updater_thread = None
global_WorkerThread = worker.WorkerThread() global_WorkerThread = worker.WorkerThread()
global_WorkerThread.start() global_WorkerThread.start()
@ -112,42 +126,80 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False):
e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name), text) e_mail, user_name, _(u"Registration e-mail for user: %(name)s", name=user_name), text)
return return
def check_send_to_kindle(entry):
"""
returns all available book formats for sending to Kindle
"""
if len(entry.data):
bookformats=list()
if ub.config.config_ebookconverter == 0:
# no converter - only for mobi and pdf formats
for ele in iter(entry.data):
if 'MOBI' in ele.format:
bookformats.append({'format':'Mobi','convert':0,'text':_('Send %(format)s to Kindle',format='Mobi')})
if 'PDF' in ele.format:
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')})
else:
formats = list()
for ele in iter(entry.data):
formats.append(ele.format)
if 'MOBI' in formats:
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 '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')})
return bookformats
else:
app.logger.error(u'Cannot find book entry %d', entry.id)
return None
# 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'}
bookformats = list()
if len(entry.data):
for ele in iter(entry.data):
if ele.format in EXTENSIONS_READER:
bookformats.append(ele.format.lower())
return bookformats
# Files are processed in the following order/priority: # Files are processed in the following order/priority:
# 1: If Mobi file is exisiting, it's directly send to kindle email, # 1: If Mobi file is existing, it's directly send to kindle email,
# 2: If Epub file is exisiting, it's converted and send to kindle email # 2: If Epub file is existing, it's converted and send to kindle email,
# 3: If Pdf file is exisiting, it's directly send to kindle email, # 3: If Pdf file is existing, it's directly send to kindle email
def send_mail(book_id, kindle_mail, calibrepath, user_id): def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
"""Send email with attachments""" """Send email with attachments"""
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()
data = db.session.query(db.Data).filter(db.Data.book == book.id).all()
if convert:
formats = {}
for entry in data:
if entry.format == "MOBI":
formats["mobi"] = entry.name + ".mobi"
if entry.format == "EPUB":
formats["epub"] = entry.name + ".epub"
if entry.format == "PDF":
formats["pdf"] = entry.name + ".pdf"
if len(formats) == 0:
return _(u"Could not find any formats suitable for sending by e-mail")
if 'mobi' in formats:
result = formats['mobi']
elif 'epub' in formats:
# returns None if success, otherwise errormessage # returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'epub', u'mobi', user_id, kindle_mail) return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, kindle_mail)
elif 'pdf' in formats:
result = formats['pdf'] # worker.get_attachment()
else: else:
return _(u"Could not find any formats suitable for sending by e-mail") for entry in iter(book.data):
if result: if entry.format.upper() == book_format.upper():
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(), result = entry.name + '.' + book_format.lower()
global_WorkerThread.add_email(_(u"Send to Kindle"), book.path, result, ub.get_mail_settings(),
kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title), kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title),
_(u'This e-mail has been sent via Calibre-Web.')) _(u'This e-mail has been sent via Calibre-Web.'))
else: return
return _(u"The requested file could not be read. Maybe wrong permissions?") return _(u"The requested file could not be read. Maybe wrong permissions?")
@ -178,7 +230,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):
@ -223,12 +278,15 @@ def delete_book_file(book, calibrepath, book_format=None):
return False return False
def update_dir_structure_file(book_id, calibrepath): def update_dir_structure_file(book_id, calibrepath, first_author):
localbook = db.session.query(db.Books).filter(db.Books.id == book_id).first() localbook = db.session.query(db.Books).filter(db.Books.id == book_id).first()
path = os.path.join(calibrepath, localbook.path) path = os.path.join(calibrepath, localbook.path)
authordir = localbook.path.split('/')[0] authordir = localbook.path.split('/')[0]
new_authordir = get_valid_filename(localbook.authors[0].name) if first_author:
new_authordir = get_valid_filename(first_author)
else:
new_authordir = get_valid_filename(localbook.authors[0].name)
titledir = localbook.path.split('/')[1] titledir = localbook.path.split('/')[1]
new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")" new_titledir = get_valid_filename(localbook.title) + " (" + str(book_id) + ")"
@ -242,53 +300,86 @@ def update_dir_structure_file(book_id, calibrepath):
web.app.logger.info("Copying title: " + path + " into existing: " + new_title_path) web.app.logger.info("Copying title: " + path + " into existing: " + new_title_path)
for dir_name, subdir_list, file_list in os.walk(path): for dir_name, subdir_list, file_list in os.walk(path):
for file in file_list: for file in file_list:
os.renames(os.path.join(dir_name, file), os.path.join(new_title_path + dir_name[len(path):], file)) os.renames(os.path.join(dir_name, file),
os.path.join(new_title_path + dir_name[len(path):], file))
path = new_title_path path = new_title_path
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex)) web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True) web.app.logger.debug(ex, exc_info=True)
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_title_path, error=str(ex)) return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_title_path, error=str(ex))
if authordir != new_authordir: if authordir != new_authordir:
try: try:
new_author_path = os.path.join(os.path.join(calibrepath, new_authordir), os.path.basename(path)) new_author_path = os.path.join(calibrepath, new_authordir, os.path.basename(path))
os.renames(path, new_author_path) os.renames(path, new_author_path)
localbook.path = new_authordir + '/' + localbook.path.split('/')[1] localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex: except OSError as ex:
web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex)) web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True) web.app.logger.debug(ex, exc_info=True)
return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s", src=path, dest=new_author_path, error=str(ex)) return _("Rename author from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_author_path, error=str(ex))
# 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:
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()))
file_format.name = new_name
except OSError as ex:
web.app.logger.error("Rename file in path " + path + " to " + new_name + ": " + str(ex))
web.app.logger.debug(ex, exc_info=True)
return _("Rename file in path '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_name, error=str(ex))
return False return False
def update_dir_structure_gdrive(book_id): 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]
new_authordir = get_valid_filename(book.authors[0].name) if first_author:
new_authordir = get_valid_filename(first_author)
else:
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
if authordir != new_authordir or titledir != new_titledir:
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 return error
@ -309,6 +400,7 @@ def delete_book_gdrive(book, book_format):
error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found
return error return error
def generate_random_password(): def generate_random_password():
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8 passlen = 8
@ -316,11 +408,12 @@ def generate_random_password():
################################## External interface ################################## External interface
def update_dir_stucture(book_id, calibrepath): def update_dir_stucture(book_id, calibrepath, first_author = None):
if ub.config.config_use_google_drive: if ub.config.config_use_google_drive:
return update_dir_structure_gdrive(book_id) return update_dir_structure_gdrive(book_id, first_author)
else: else:
return update_dir_structure_file(book_id, calibrepath) return update_dir_structure_file(book_id, calibrepath, first_author)
def delete_book(book, calibrepath, book_format): def delete_book(book, calibrepath, book_format):
if ub.config.config_use_google_drive: if ub.config.config_use_google_drive:
@ -328,9 +421,12 @@ def delete_book(book, calibrepath, book_format):
else: else:
return delete_book_file(book, calibrepath, book_format) return delete_book_file(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)
@ -338,13 +434,14 @@ 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")
else: else:
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 to gdrive or locally
def save_cover(url, book_path): def save_cover(url, book_path):
img = requests.get(url) img = requests.get(url)
@ -357,7 +454,7 @@ def save_cover(url, book_path):
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb") f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
f.write(img.content) f.write(img.content)
f.close() f.close()
uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) 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
@ -367,6 +464,7 @@ def save_cover(url, book_path):
web.app.logger.info("Cover is saved") web.app.logger.info("Cover is saved")
return True return True
def do_download_file(book, book_format, data, headers): def do_download_file(book, book_format, data, headers):
if ub.config.config_use_google_drive: if ub.config.config_use_google_drive:
startTime = time.time() startTime = time.time()
@ -388,166 +486,6 @@ def do_download_file(book, book_format, data, headers):
################################## ##################################
class Updater(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.status = 0
def run(self):
try:
self.status = 1
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
r.raise_for_status()
fname = re.findall("filename=(.+)", r.headers['content-disposition'])[0]
self.status = 2
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
tmp_dir = gettempdir()
z.extractall(tmp_dir)
self.status = 4
self.update_source(os.path.join(tmp_dir, os.path.splitext(fname)[0]), ub.config.get_main_dir)
self.status = 6
time.sleep(2)
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
time.sleep(2)
except requests.exceptions.HTTPError as ex:
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
self.status = 8
except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
self.status = 10
except requests.exceptions.RequestException:
self.status = 11
logging.getLogger('cps.web').info(u'General error')
def get_update_status(self):
return self.status
@classmethod
def file_to_list(self, filelist):
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
@classmethod
def one_minus_two(self, one, two):
return [x for x in one if x not in set(two)]
@classmethod
def reduce_dirs(self, delete_files, new_list):
new_delete = []
for filename in delete_files:
parts = filename.split(os.sep)
sub = ''
for part in parts:
sub = os.path.join(sub, part)
if sub == '':
sub = os.sep
count = 0
for song in new_list:
if song.startswith(sub):
count += 1
break
if count == 0:
if sub != '\\':
new_delete.append(sub)
break
return list(set(new_delete))
@classmethod
def reduce_files(self, remove_items, exclude_items):
rf = []
for item in remove_items:
if not item.startswith(exclude_items):
rf.append(item)
return rf
@classmethod
def moveallfiles(self, root_src_dir, root_dst_dir):
change_permissions = True
if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False
else:
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
new_permissions = os.stat(root_dst_dir)
# print new_permissions
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if os.path.exists(dst_file):
if change_permissions:
permission = os.stat(dst_file)
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
os.remove(dst_file)
else:
if change_permissions:
permission = new_permissions
shutil.move(src_file, dst_dir)
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
if change_permissions:
try:
os.chown(dst_file, permission.st_uid, permission.st_gid)
except (Exception) as e:
# ex = sys.exc_info()
old_permissions = os.stat(dst_file)
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
return
def update_source(self, source, destination):
# destination files
old_list = list()
exclude = (
'vendor' + os.sep + 'kindlegen.exe', 'vendor' + os.sep + 'kindlegen', os.sep + 'app.db',
os.sep + 'vendor', os.sep + 'calibre-web.log')
for root, dirs, files in os.walk(destination, topdown=True):
for name in files:
old_list.append(os.path.join(root, name).replace(destination, ''))
for name in dirs:
old_list.append(os.path.join(root, name).replace(destination, ''))
# source files
new_list = list()
for root, dirs, files in os.walk(source, topdown=True):
for name in files:
new_list.append(os.path.join(root, name).replace(source, ''))
for name in dirs:
new_list.append(os.path.join(root, name).replace(source, ''))
delete_files = self.one_minus_two(old_list, new_list)
rf = self.reduce_files(delete_files, exclude)
remove_items = self.reduce_dirs(rf, new_list)
self.moveallfiles(source, destination)
for item in remove_items:
item_path = os.path.join(destination, item[1:])
if os.path.isdir(item_path):
logging.getLogger('cps.web').debug("Delete dir " + item_path)
shutil.rmtree(item_path)
else:
try:
logging.getLogger('cps.web').debug("Delete file " + item_path)
# log_from_thread("Delete file " + item_path)
os.remove(item_path)
except Exception:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
def check_unrar(unrarLocation): def check_unrar(unrarLocation):
@ -574,36 +512,25 @@ def check_unrar(unrarLocation):
return (error, version) return (error, version)
def is_sha1(sha1):
if len(sha1) != 40:
return False
try:
int(sha1, 16)
except ValueError:
return False
return True
def json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""
def get_current_version_info(): if isinstance(obj, (datetime)):
content = {} return obj.isoformat()
content[0] = '$Format:%H$' raise TypeError ("Type %s not serializable" % type(obj))
content[1] = '$Format:%cI$'
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# content[1] = '2018-09-09T10:13:08+02:00'
if is_sha1(content[0]) and len(content[1]) > 0:
return {'hash': content[0], 'datetime': content[1]}
return False
def render_task_status(tasklist): def render_task_status(tasklist):
#helper function to apply localize status information in tasklist entries #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())
task['formStarttime'] = "" # task2['formStarttime'] = ""
else: else:
if 'starttime' not in task: if 'starttime' not in task:
task['starttime'] = "" task['starttime'] = ""

@ -1,3 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Flask License
#
# Copyright © 2010 by the Pallets team.
#
# Some rights reserved.
# 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.
# http://flask.pocoo.org/snippets/62/ # http://flask.pocoo.org/snippets/62/
try: try:

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2018 cervinko, janeczku, OzzieIsaacs
#
# 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/>.
class ReverseProxied(object): class ReverseProxied(object):
"""Wrap the application in this middleware and configure the """Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind front-end server to add these headers, to let you quietly bind

@ -1,6 +1,23 @@
#!/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)
# Copyright (C) 2012-2019 janeczku, OzzieIsaacs, andrerfcsantos, idalin
#
# 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/>.
from socket import error as SocketError from socket import error as SocketError
import sys import sys
import os import os
@ -20,6 +37,7 @@ except ImportError:
gevent_present = False gevent_present = False
class server: class server:
wsgiserver = None wsgiserver = None
@ -32,18 +50,26 @@ class server:
def start_gevent(self): def start_gevent(self):
try: try:
ssl_args = dict() ssl_args = dict()
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile(): certfile_path = web.ub.config.get_config_certfile()
ssl_args = {"certfile": web.ub.config.get_config_certfile(), keyfile_path = web.ub.config.get_config_keyfile()
"keyfile": web.ub.config.get_config_keyfile()} if certfile_path and keyfile_path:
if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
ssl_args = {"certfile": certfile_path,
"keyfile": keyfile_path}
else:
web.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path))
if os.name == 'nt': if os.name == 'nt':
self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
else: else:
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
web.py3_gevent_link = self.wsgiserver
self.wsgiserver.serve_forever() self.wsgiserver.serve_forever()
except SocketError: except SocketError:
try: try:
web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...') web.app.logger.info('Unable to listen on \'\', trying on IPv4 only...')
self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args) self.wsgiserver = WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
web.py3_gevent_link = self.wsgiserver
self.wsgiserver.serve_forever() self.wsgiserver.serve_forever()
except (OSError, SocketError) as e: except (OSError, SocketError) as e:
web.app.logger.info("Error starting server: %s" % e.strerror) web.app.logger.info("Error starting server: %s" % e.strerror)
@ -60,12 +86,17 @@ class server:
self.start_gevent() self.start_gevent()
else: else:
try: try:
ssl = None
web.app.logger.info('Starting Tornado server') web.app.logger.info('Starting Tornado server')
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile(): certfile_path = web.ub.config.get_config_certfile()
ssl={"certfile": web.ub.config.get_config_certfile(), keyfile_path = web.ub.config.get_config_keyfile()
"keyfile": web.ub.config.get_config_keyfile()} if certfile_path and keyfile_path:
else: if os.path.isfile(certfile_path) and os.path.isfile(keyfile_path):
ssl=None ssl = {"certfile": certfile_path,
"keyfile": keyfile_path}
else:
web.app.logger.info('The specified paths for the ssl certificate file and/or key file seem to be broken. Ignoring ssl. Cert path: %s | Key path: %s' % (certfile_path, keyfile_path))
# Max Buffersize set to 200MB # Max Buffersize set to 200MB
http_server = HTTPServer(WSGIContainer(web.app), http_server = HTTPServer(WSGIContainer(web.app),
max_buffer_size = 209700000, max_buffer_size = 209700000,
@ -81,6 +112,9 @@ class server:
web.helper.global_WorkerThread.stop() web.helper.global_WorkerThread.stop()
sys.exit(1) sys.exit(1)
# ToDo: Somehow caused by circular import under python3 refactor
if sys.version_info > (3, 0):
self.restart = web.py3_restart_Typ
if self.restart == True: if self.restart == True:
web.app.logger.info("Performing restart of Calibre-Web") web.app.logger.info("Performing restart of Calibre-Web")
web.helper.global_WorkerThread.stop() web.helper.global_WorkerThread.stop()
@ -97,16 +131,26 @@ class server:
sys.exit(0) sys.exit(0)
def setRestartTyp(self,starttyp): def setRestartTyp(self,starttyp):
self.restart=starttyp self.restart = starttyp
# ToDo: Somehow caused by circular import under python3 refactor
web.py3_restart_Typ = starttyp
def killServer(self, signum, frame): def killServer(self, signum, frame):
self.stopServer() self.stopServer()
def stopServer(self): def stopServer(self):
if gevent_present: # ToDo: Somehow caused by circular import under python3 refactor
self.wsgiserver.close() if sys.version_info > (3, 0):
else: if not self.wsgiserver:
self.wsgiserver.add_callback(self.wsgiserver.stop) if gevent_present:
self.wsgiserver = web.py3_gevent_link
else:
self.wsgiserver = IOLoop.instance()
if self.wsgiserver:
if gevent_present:
self.wsgiserver.close()
else:
self.wsgiserver.add_callback(self.wsgiserver.stop)
@staticmethod @staticmethod
def getNameVersion(): def getNameVersion():

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

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

File diff suppressed because one or more lines are too long

@ -1,505 +1,349 @@
/*! normalize.css v1.0.1 | MIT License | git.io/normalize */ /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* ========================================================================== /* Document
HTML5 display definitions
========================================================================== */ ========================================================================== */
/* /**
* Corrects `block` display not defined in IE 6/7/8/9 and Firefox 3. * 1. Correct the line height in all browsers.
*/ * 2. Prevent adjustments of font size after orientation changes in iOS.
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section,
summary {
display: block;
}
/*
* Corrects `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
/*
* Prevents modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/ */
audio:not([controls]) { html {
display: none; line-height: 1.15; /* 1 */
height: 0; -webkit-text-size-adjust: 100%; /* 2 */
}
/*
* Addresses styling for `hidden` attribute not present in IE 7/8/9, Firefox 3,
* and Safari 4.
* Known issue: no IE 6 support.
*/
[hidden] {
display: none;
} }
/* ========================================================================== /* Sections
Base
========================================================================== */ ========================================================================== */
/* /**
* 1. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using * Remove the margin in all browsers.
* `em` units.
* 2. Prevents iOS text size adjust after orientation change, without disabling
* user zoom.
*/ */
html { body {
font-size: 100%; /* 1 */ margin: 0;
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
} }
/* /**
* Addresses `font-family` inconsistency between `textarea` and other form * Render the `main` element consistently in IE.
* elements.
*/ */
html, main {
button, display: block;
input,
select,
textarea {
font-family: sans-serif;
} }
/* /**
* Addresses margins handled incorrectly in IE 6/7. * Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/ */
body { h1 {
margin: 0; font-size: 2em;
margin: 0.67em 0;
} }
/* ========================================================================== /* Grouping content
Links
========================================================================== */ ========================================================================== */
/* /**
* Addresses `outline` inconsistency between Chrome and other browsers. * 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/ */
a:focus { hr {
outline: thin dotted; box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
} }
/* /**
* Improves readability when focused and also mouse hovered in all browsers. * 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/ */
a:active, pre {
a:hover { font-family: monospace, monospace; /* 1 */
outline: 0; font-size: 1em; /* 2 */
} }
/* ========================================================================== /* Text-level semantics
Typography
========================================================================== */ ========================================================================== */
/* /**
* Addresses font sizes and margins set differently in IE 6/7. * Remove the gray background on active links in IE 10.
* Addresses font sizes within `section` and `article` in Firefox 4+, Safari 5,
* and Chrome.
*/ */
h1 { a {
font-size: 2em; background-color: transparent;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: 0.83em 0;
} }
h3 { /**
font-size: 1.17em; * 1. Remove the bottom border in Chrome 57-
margin: 1em 0; * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
}
h4 {
font-size: 1em;
margin: 1.33em 0;
}
h5 {
font-size: 0.83em;
margin: 1.67em 0;
}
h6 {
font-size: 0.75em;
margin: 2.33em 0;
}
/*
* Addresses styling not present in IE 7/8/9, Safari 5, and Chrome.
*/ */
abbr[title] { abbr[title] {
border-bottom: 1px dotted; border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
} }
/* /**
* Addresses style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. * Add the correct font weight in Chrome, Edge, and Safari.
*/ */
b, b,
strong { strong {
font-weight: bold; font-weight: bolder;
}
blockquote {
margin: 1em 40px;
} }
/* /**
* Addresses styling not present in Safari 5 and Chrome. * 1. Correct the inheritance and scaling of font size in all browsers.
*/ * 2. Correct the odd `em` font sizing in all browsers.
dfn {
font-style: italic;
}
/*
* Addresses styling not present in IE 6/7/8/9.
*/
mark {
background: #ff0;
color: #000;
}
/*
* Addresses margins set differently in IE 6/7.
*/
p,
pre {
margin: 1em 0;
}
/*
* Corrects font family set oddly in IE 6, Safari 4/5, and Chrome.
*/ */
code, code,
kbd, kbd,
pre,
samp { samp {
font-family: monospace, serif; font-family: monospace, monospace; /* 1 */
_font-family: 'courier new', monospace; font-size: 1em; /* 2 */
font-size: 1em;
} }
/* /**
* Improves readability of pre-formatted text in all browsers. * Add the correct font size in all browsers.
*/
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
/*
* Addresses CSS quotes not supported in IE 6/7.
*/
q {
quotes: none;
}
/*
* Addresses `quotes` property not supported in Safari 4.
*/
q:before,
q:after {
content: '';
content: none;
}
/*
* Addresses inconsistent and variable font size in all browsers.
*/ */
small { small {
font-size: 80%; font-size: 80%;
} }
/* /**
* Prevents `sub` and `sup` affecting `line-height` in all browsers. * Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/ */
sub, sub,
sup { sup {
font-size: 75%; font-size: 75%;
line-height: 0; line-height: 0;
position: relative; position: relative;
vertical-align: baseline; vertical-align: baseline;
} }
sup { sub {
top: -0.5em; bottom: -0.25em;
} }
sub { sup {
bottom: -0.25em; top: -0.5em;
} }
/* ========================================================================== /* Embedded content
Lists
========================================================================== */ ========================================================================== */
/* /**
* Addresses margins set differently in IE 6/7. * Remove the border on images inside links in IE 10.
*/ */
dl, img {
menu, border-style: none;
ol,
ul {
margin: 1em 0;
} }
dd { /* Forms
margin: 0 0 0 40px; ========================================================================== */
}
/* /**
* Addresses paddings set differently in IE 6/7. * 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/ */
menu, button,
ol, input,
ul { optgroup,
padding: 0 0 0 40px; select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
} }
/* /**
* Corrects list images handled incorrectly in IE 7. * Show the overflow in IE.
* 1. Show the overflow in Edge.
*/ */
nav ul, button,
nav ol { input { /* 1 */
list-style: none; overflow: visible;
list-style-image: none;
} }
/* ========================================================================== /**
Embedded content * Remove the inheritance of text transform in Edge, Firefox, and IE.
========================================================================== */ * 1. Remove the inheritance of text transform in Firefox.
/*
* 1. Removes border when inside `a` element in IE 6/7/8/9 and Firefox 3.
* 2. Improves image quality when scaled in IE 7.
*/ */
img { button,
border: 0; /* 1 */ select { /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */ text-transform: none;
} }
/* /**
* Corrects overflow displayed oddly in IE 9. * Correct the inability to style clickable types in iOS and Safari.
*/ */
svg:not(:root) { button,
overflow: hidden; [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
} }
/* ========================================================================== /**
Figures * Remove the inner border and padding in Firefox.
========================================================================== */
/*
* Addresses margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
*/ */
figure { button::-moz-focus-inner,
margin: 0; [type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
} }
/* ========================================================================== /**
Forms * Restore the focus styles unset by the previous rule.
========================================================================== */
/*
* Corrects margin displayed oddly in IE 6/7.
*/ */
form { button:-moz-focusring,
margin: 0; [type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
} }
/* /**
* Define consistent border, margin, and padding. * Correct the padding in Firefox.
*/ */
fieldset { fieldset {
border: 1px solid #c0c0c0; padding: 0.35em 0.75em 0.625em;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
} }
/* /**
* 1. Corrects color not being inherited in IE 6/7/8/9. * 1. Correct the text wrapping in Edge and IE.
* 2. Corrects text not wrapping in Firefox 3. * 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Corrects alignment displayed oddly in IE 6/7. * 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/ */
legend { legend {
border: 0; /* 1 */ box-sizing: border-box; /* 1 */
padding: 0; color: inherit; /* 2 */
white-space: normal; /* 2 */ display: table; /* 1 */
*margin-left: -7px; /* 3 */ max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
} }
/* /**
* 1. Corrects font size not being inherited in all browsers. * Add the correct vertical alignment in Chrome, Firefox, and Opera.
* 2. Addresses margins set differently in IE 6/7, Firefox 3+, Safari 5,
* and Chrome.
* 3. Improves appearance and consistency in all browsers.
*/ */
button, progress {
input, vertical-align: baseline;
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
} }
/* /**
* Addresses Firefox 3+ setting `line-height` on `input` using `!important` in * Remove the default vertical scrollbar in IE 10+.
* the UA stylesheet.
*/ */
button, textarea {
input { overflow: auto;
line-height: normal;
} }
/* /**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` * 1. Add the correct box sizing in IE 10.
* and `video` controls. * 2. Remove the padding in IE 10.
* 2. Corrects inability to style clickable `input` types in iOS.
* 3. Improves usability and consistency of cursor style between image-type
* `input` and others.
* 4. Removes inner spacing in IE 7 without affecting normal text inputs.
* Known issue: inner spacing remains in IE 6.
*/ */
button, [type="checkbox"],
html input[type="button"], /* 1 */ [type="radio"] {
input[type="reset"], box-sizing: border-box; /* 1 */
input[type="submit"] { padding: 0; /* 2 */
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
*overflow: visible; /* 4 */
} }
/* /**
* Re-set default cursor for disabled elements. * Correct the cursor style of increment and decrement buttons in Chrome.
*/ */
button[disabled], [type="number"]::-webkit-inner-spin-button,
input[disabled] { [type="number"]::-webkit-outer-spin-button {
cursor: default; height: auto;
} }
/* /**
* 1. Addresses box sizing set to content-box in IE 8/9. * 1. Correct the odd appearance in Chrome and Safari.
* 2. Removes excess padding in IE 8/9. * 2. Correct the outline style in Safari.
* 3. Removes excess padding in IE 7.
* Known issue: excess padding remains in IE 6.
*/ */
input[type="checkbox"], [type="search"] {
input[type="radio"] { -webkit-appearance: textfield; /* 1 */
box-sizing: border-box; /* 1 */ outline-offset: -2px; /* 2 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
} }
/* /**
* 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. * Remove the inner padding in Chrome and Safari on macOS.
* 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/ */
/*
input[type="search"] { [type="search"]::-webkit-search-decoration {
-webkit-appearance: textfield; -webkit-appearance: none;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
} }
*/
/* /**
* Removes inner padding and search cancel button in Safari 5 and Chrome * 1. Correct the inability to style clickable types in iOS and Safari.
* on OS X. * 2. Change font properties to `inherit` in Safari.
*/ */
/* input[type="search"]::-webkit-search-cancel-button, ::-webkit-file-upload-button {
input[type="search"]::-webkit-search-decoration { -webkit-appearance: button; /* 1 */
-webkit-appearance: none; font: inherit; /* 2 */
} */ }
/* Interactive
========================================================================== */
/* /*
* Removes inner padding and border in Firefox 3+. * Add the correct display in Edge, IE 10+, and Firefox.
*/ */
button::-moz-focus-inner, details {
input::-moz-focus-inner { display: block;
border: 0;
padding: 0;
} }
/* /*
* 1. Removes default vertical scrollbar in IE 6/7/8/9. * Add the correct display in all browsers.
* 2. Improves readability and alignment in all browsers.
*/ */
textarea { summary {
overflow: auto; /* 1 */ display: list-item;
vertical-align: top; /* 2 */
} }
/* ========================================================================== /* Misc
Tables
========================================================================== */ ========================================================================== */
/* /**
* Remove most spacing between table cells. * Add the correct display in IE 10+.
*/ */
table { template {
border-collapse: collapse; display: none;
border-spacing: 0;
} }
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

@ -36,6 +36,11 @@ a{color: #45b29d}a:hover{color: #444;}
.container-fluid .book .meta .title{font-weight:bold;font-size:15px;color:#444} .container-fluid .book .meta .title{font-weight:bold;font-size:15px;color:#444}
.container-fluid .book .meta .author{font-size:12px;color:#999} .container-fluid .book .meta .author{font-size:12px;color:#999}
.container-fluid .book .meta .rating{margin-top:5px}.rating .glyphicon-star{color:#999}.rating .glyphicon-star.good{color:#45b29d} .container-fluid .book .meta .rating{margin-top:5px}.rating .glyphicon-star{color:#999}.rating .glyphicon-star.good{color:#45b29d}
.container-fluid .author .author-hidden, .container-fluid .author .author-hidden-divider {
display: none;
}
.navbar-brand{font-family: 'Grand Hotel', cursive; font-size: 35px; color: #45b29d !important;} .navbar-brand{font-family: 'Grand Hotel', cursive; font-size: 35px; color: #45b29d !important;}
.more-stuff{margin-top: 20px; padding-top: 20px; border-top: 1px solid #ccc} .more-stuff{margin-top: 20px; padding-top: 20px; border-top: 1px solid #ccc}
.more-stuff>li{margin-bottom: 10px;} .more-stuff>li{margin-bottom: 10px;}
@ -52,6 +57,7 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
-moz-box-shadow: 0 5px 8px -6px #777; -moz-box-shadow: 0 5px 8px -6px #777;
box-shadow: 0 5px 8px -6px #777; box-shadow: 0 5px 8px -6px #777;
} }
.navbar-default .navbar-toggle .icon-bar {background-color: #000;} .navbar-default .navbar-toggle .icon-bar {background-color: #000;}
.navbar-default .navbar-toggle {border-color: #000;} .navbar-default .navbar-toggle {border-color: #000;}
.cover { margin-bottom: 10px;} .cover { margin-bottom: 10px;}
@ -103,6 +109,16 @@ input.pill:not(:checked) + label .glyphicon {
.tags_click, .serie_click, .language_click {margin-right: 5px;} .tags_click, .serie_click, .language_click {margin-right: 5px;}
#meta-info {
height:600px;
overflow-y:scroll;
}
.media-list {
padding-right:15px;
}
.media-body p {
text-align: justify;
}
#meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; } #meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; }
.padded-bottom { margin-bottom: 15px; } .padded-bottom { margin-bottom: 15px; }
@ -120,3 +136,7 @@ input.pill:not(:checked) + label .glyphicon {
.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;} .editable-cancel { margin-bottom: 0px !important; margin-left: 7px !important;}
.editable-submit { margin-bottom: 0px !important;} .editable-submit { margin-bottom: 0px !important;}
.modal-body .comments {
max-height:300px;
overflow-y: auto;
}

@ -0,0 +1,8 @@
@media (min-device-width: 768px) {
.upload-modal-dialog {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%) !important;
}
}

@ -277,8 +277,6 @@ bitjs.archive = bitjs.archive || {};
if (e.type === bitjs.archive.UnarchiveEvent.Type.FINISH) { if (e.type === bitjs.archive.UnarchiveEvent.Type.FINISH) {
this.worker_.terminate(); this.worker_.terminate();
} }
} else {
console.log(e);
} }
}; };
@ -292,15 +290,11 @@ bitjs.archive = bitjs.archive || {};
this.worker_ = new Worker(scriptFileName); this.worker_ = new Worker(scriptFileName);
this.worker_.onerror = function(e) { this.worker_.onerror = function(e) {
console.log("Worker error: message = " + e.message);
throw e; throw e;
}; };
this.worker_.onmessage = function(e) { this.worker_.onmessage = function(e) {
if (typeof e.data === "string") { if (typeof e.data !== "string") {
// Just log any strings the workers pump our way.
console.log(e.data);
} else {
// Assume that it is an UnarchiveEvent. Some browsers preserve the 'type' // Assume that it is an UnarchiveEvent. Some browsers preserve the 'type'
// so that instanceof UnarchiveEvent returns true, but others do not. // so that instanceof UnarchiveEvent returns true, but others do not.
me.handleWorkerEvent_(e.data); me.handleWorkerEvent_(e.data);

@ -0,0 +1,708 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018-2019 hexeth
*
* 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/>.
*/
// Move advanced search to side-menu
$( 'a[href*="advanced"]' ).parent().insertAfter( '#nav_new' );
$( 'body' ).addClass('blur');
$( 'body.stat' ).addClass( 'stats' );
$( 'body.config' ).addClass( 'admin');
$( 'body.uiconfig' ).addClass( 'admin');
$( 'body.advsearch' ).addClass( 'advanced_search' );
$( 'body.newuser' ).addClass( 'admin' );
$( 'body.mailset' ).addClass( 'admin' );
// Back button
curHref = window.location.href.split('/');
prevHref = document.referrer.split('/');
$( '.navbar-form.navbar-left' )
.before( '<div class="plexBack"><a href="' + document.referrer + '"></a></div>' );
if ( history.length === 1 ||
curHref[0] +
curHref[1] +
curHref[2] !=
prevHref[0] +
prevHref[1] +
prevHref[2] ||
$( 'body.root' )>length > 0 ) {
$( '.plexBack' ).addClass( 'noBack' );
}
//Weird missing a after pressing back from edit.
setTimeout(function() {
if ( $( '.plexBack a').length < 1 ) {
$( '.plexBack' ).append('<a href="' + document.referrer + '"></a>');
}
},10);
// Home button
$( '.plexBack' ).before( '<div class="home-btn"></div>' );
$( 'a.navbar-brand' ).clone().appendTo( '.home-btn' ).empty().removeClass('navbar-brand');
/////////////////////////////////
// Start of Book Details Work //
///////////////////////////////
// Wrap book description in div container
if ( $( 'body.book' ).length > 0 ) {
description = $( '.comments' );
bookInfo = $( '.author' ).nextUntil( 'h3:contains("Description")');
$( 'h3:contains("Description")' ).detach();
$( '.comments' ).detach();
$( bookInfo ).wrapAll( '<div class="bookinfo"></div>' );
// $( 'h3:contains("Description:")' ).after( '<div class="description"></div>' );
$( '.languages' ).appendTo( '.bookinfo' );
$('.hr').detach();
if ( $( '.identifiers ').length > 0 ) {
console.log(".identifiers length " + $( '.identifiers ').length );
$( '.identifiers' ).before( '<div class="hr"></div>' );
} else {
if ( $( '.bookinfo > p:first-child' ).length > 0 ) {
console.log(".bookinfo > p:first-child length " + $( '.bookinfo > p' ).length );
$( '.bookinfo > p:first-child' ).first().after( '<div class="hr"></div>' );
} else{
if ( $( '.bookinfo a[href*="/series/"]' ).length > 0 ) {
console.log( 'series text found; placing hr below series' );
$( '.bookinfo a[href*="/series/"]' ).parent().after( '<div class="hr"></div>' );
} else {
console.log("prepending hr div to top of .bookinfo");
$( '.bookinfo' ).prepend( '<div class="hr"></div>' );
}
}
}
$( '.rating' ).insertBefore( '.hr' );
$( '#remove-from-shelves' ).insertAfter( '.hr' );
$( description ).appendTo('.bookinfo')
/* if book description is not in html format, Remove extra line breaks
Remove blank lines/unnecessary spaces, split by line break to array
Push array into .description div. If there is still a wall of text,
find sentences and split wall into groups of three sentence paragraphs.
If the book format is in html format, Keep html, but strip away inline
styles and empty elements */
// If text is sitting in div as text node
if ( $('.comments:has(p)' ).length === 0 ) {
newdesc = description.text()
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split(/\n/);
$('.comments' ).empty();
$.each(newdesc, function(i, val) {
$( 'div.comments' ).append( '<p>' + newdesc[i] + '</p>' );
});
$( '.comments' ).fadeIn(100);
} //If still a wall of text create 3 sentence paragraphs.
if( $( '.comments p' ).length === 1 ) {
if ( description.context != undefined ) {
newdesc = description.text()
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm,"").split(/\n/);
}
else {
newdesc = description.text();
}
doc = nlp ( newdesc.toString() );
sentences = doc.map((m)=> m.out( 'text' ));
sentences[0] = sentences[0].replace(",","");
$( '.comments p' ).remove();
let size = 3; let sentenceChunks = [];
for (var i=0; i<sentences.length; i+=size) {
sentenceChunks.push(sentences.slice(i,i+size));
}
let output = '';
$.each(sentenceChunks, function(i, val) {
let preOutput = '';
$.each(val, function(i, val) {
preOutput += val;
});
output += '<p>' + preOutput + '</p>';
});
$( 'div.comments' ).append( output );
}
else {
$.each(description, function(i, val) {
// $( description[i].outerHTML ).appendTo( '.comments' );
$( 'div.comments :empty' ).remove();
$( 'div.comments ').attr( 'style', '' );
});
$( 'div.comments' ).fadeIn( 100 );
}
// Sexy blurred backgrounds
cover = $( '.cover img' ).attr( 'src' );
$( '#loader + .container-fluid' )
.prepend( '<div class="blur-wrapper"></div' );
$( '.blur-wrapper' )
.prepend( '<div><img class="bg-blur" src="' + cover + '"></div>' );
// Fix-up book detail headings
publisher = $( '.publishers p span' ).text().split( ':' );
$( '.publishers p span' ).remove();
$.each(publisher, function(i, val) {
$( '.publishers' ).append( '<span>' + publisher[i] + '</span>' );
});
$( '.publishers span:nth-child(3)' ).text(function() {
return $(this).text().replace(/^\s+|^\t+|\t+|\s+$/g, "");
});
published = $( '.publishing-date p' )
.text().split(': ');
$( '.publishing-date p' ).remove();
$.each(published, function(i, val) {
$( '.publishing-date' ).append( '<span>' + published[i] + '</span>' );
});
languages = $( '.languages p span' ).text().split( ': ' );
$( '.languages p span' ).remove();
$.each(languages, function(i, val) {
$( '.languages' ).append( '<span>' + languages[i] + '</span>' );
});
$( '.book-meta h2:first' ).clone()
.prependTo( '.book-meta > .btn-toolbar:first' );
// If only one download type exists still put the items into a drop-drown list.
downloads = $( 'a[id^=btnGroupDrop]' ).get();
if ( $( downloads ).length === 1 ) {
$( '<button id="btnGroupDrop1" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-download"></span>Download :<span class="caret"></span></button><ul class="dropdown-menu leramslist aria-labelledby="btnGroupDrop1"></ul>' ).insertBefore( downloads[downloads.length-1] );
$( downloads ).detach();
$.each(downloads, function(i, val) {
$( '<li>' + downloads[i].outerHTML + '</li>' ).appendTo( '.leramslist' );
});
$( '.leramslist' ).find( 'span' ).remove();
$( '.leramslist a' ).removeClass( 'btn btn-primary' ).removeAttr( 'role' );
}
// Add classes to buttons
$( '#sendbtn' ).parent().addClass( 'sendBtn' );
$( '[id*=btnGroupDrop]' ).parent().addClass( 'downloadBtn' );
$( 'read-in-browser' ).parent().addClass( 'readBtn' );
$( '.downloadBtn button:first' ).addClass( 'download-text' );
// Move all options in book details page to the same group
$( '[aria-label*="Delete book"]' )
.prependTo( '[aria-label^="Download, send"]' )
.children().removeClass( 'btn-sm' );
$( '.custom_columns' )
.addClass(' btn-group' )
.attr('role', 'group' )
.removeClass( 'custom_columns' )
.prependTo( '[aria-label^="Download, send"]' );
$( '#have_read_cb' )
.after( '<label class="block-label readLbl" for="#have_read_cb"></label>' );
$( '#shelf-actions' ).prependTo( '[aria-label^="Download, send"]' );
// Move dropdown lists higher in dom, replace bootstrap toggle with own toggle.
$( 'ul[aria-labelledby="read-in-browser"]' ).insertBefore( '.blur-wrapper' ).addClass('readinbrowser-drop');
$( 'ul[aria-labelledby="send-to-kindle"]' ).insertBefore( '.blur-wrapper' ).addClass('sendtokindle-drop');
$( '.leramslist' ).insertBefore( '.blur-wrapper' );
$( 'ul[aria-labelledby="btnGroupDrop1"]' ).insertBefore( '.blur-wrapper' ).addClass('leramslist');
$( '#add-to-shelves' ).insertBefore( '.blur-wrapper' );
$( '#read-in-browser' ).click( function() {
$( '.readinbrowser-drop' ).toggle();
});
$('.downloadBtn' ).click( function() {
$( '.leramslist' ).toggle();
});
$('#sendbtn2' ).click( function() {
$( '.sendtokindle-drop' ).toggle();
});
$('div[aria-label="Add to shelves"]' ).click( function() {
$( '#add-to-shelves' ).toggle();
});
// Fix formatting error on book detail languages
if ( !$( '.book-meta > .bookinfo > .languages > span:last-of-type' ).text().startsWith(" ") ) {
$( '.book-meta > .bookinfo > .languages > span:last-of-type' ).prepend(" ");
}
//Work to reposition dropdowns. Does not currently solve for
//screen resizing
function dropdownToggle() {
topPos = $( '.book-meta > .btn-toolbar:first' ).offset().top
if ( $( '#read-in-browser' ).length > 0 ) {
position = $( '#read-in-browser' ).offset().left
if ( position + $( '.readinbrowser-drop' ).width() > $( window ).width() ) {
positionOff = position + $( '.readinbrowser-drop' ).width() - $( window ).width();
ribPosition = position - positionOff - 5
$( '.readinbrowser-drop' ).attr("style", "left: " + ribPosition + "px !important; right: auto; top: " + topPos + "px");
} else {
$( '.readinbrowser-drop' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
}
}
if ( $( '#sendbtn2' ).length > 0 ) {
position = $( '#sendbtn2' ).offset().left
if ( position + $( '.sendtokindle-drop' ).width() > $( window ).width() ) {
positionOff = position + $( '.sendtokindle-drop' ).width() - $( window ).width();
ribPosition = position - positionOff - 5
$( '.sendtokindle-drop' ).attr("style", "left: " + ribPosition + "px !important; right: auto; top: " + topPos + "px");
} else {
$( '.sendtokindle-drop' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
}
}
if ( $( '.downloadBtn' ).length > 0 ) {
position = $( '#btnGroupDrop1' ).offset().left
if ( position + $( '.leramslist' ).width() > $( window ).width() ) {
positionOff = position + $( '.leramslist' ).width() - $( window ).width();
dlPosition = position - positionOff - 5
$( '.leramslist' ).attr("style", "left: " + dlPosition + "px !important; right: auto; top: " + topPos + "px");
} else {
$( '.leramslist' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
}
}
if ( $( 'div[aria-label="Add to shelves"]' ).length > 0 ) {
position = $( 'div[aria-label="Add to shelves"]' ).offset().left
if ( position + $( '#add-to-shelves' ).width() > $( window ).width() ) {
positionOff = position + $( '#add-to-shelves' ).width() - $( window ).width();
adsPosition = position - positionOff - 5
$( '#add-to-shelves' ).attr("style", "left: " + adsPosition + "px !important; right: auto; top: " + topPos + "px");
} else {
$( '#add-to-shelves' ).attr("style", "left: " + position + "px !important; right: auto; top: " + topPos + "px");
}
}
}
dropdownToggle();
$( window ).on( 'resize', function() {
dropdownToggle();
});
// Clone book rating for mobile view.
$( '.book-meta > .bookinfo > .rating' ).clone().insertBefore( '.book-meta > .description' ).addClass('rating-mobile');
}
///////////////////////////////
// End of Book Details Work //
/////////////////////////////
/////////////////////////////////
// Start of Global Work //
///////////////////////////////
// Hide dropdown and collapse menus on click-off
$(document).mouseup(function (e) {
var container = new Array();
container.push($('ul[aria-labelledby="read-in-browser"]'));
container.push($('.sendtokindle-drop'));
container.push($('.leramslist'));
container.push($('#add-to-shelves'));
container.push($('.navbar-collapse.collapse.in'));
$.each(container, function(key, value) {
if (!$(value).is(e.target) // if the target of the click isn't the container...
&& $(value).has(e.target).length === 0) // ... nor a descendant of the container
{
if ( $(value).hasClass('dropdown-menu') )
{
$(value).hide();
} else
{
if ( $(value).hasClass('collapse') )
{
$(value).collapse('toggle');
}
}
}
});
});
// Split path name to array and remove blanks
url = window.location.pathname
// Move create shelf
$( '#nav_createshelf' ).prependTo( '.your-shelves' );
// Create drop-down for profile and move elements to it
$( '#main-nav' )
.prepend( '<li class="dropdown"><a href="#" class="dropdown-toggle profileDrop" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-user"></span></a><ul class="dropdown-menu profileDropli"></ul></li>' );
$( '#top_user' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
$( '#nav_about' ).addClass( 'dropdown' ).appendTo( '.profileDropli' );
$( '#register' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
$( '#logout' ).parent().addClass( 'dropdown' ).appendTo( '.profileDropli' );
// Remove the modals except from some areas where they are needed
bodyClass = $( 'body' ).attr( 'class' ).split(' ');
modalWanted = ['admin', 'editbook', 'config', 'uiconfig'];
if ( $.inArray( bodyClass[0], modalWanted) != -1 ) {
} else {
$(' a:not(.dropdown-toggle) ')
.removeAttr( 'data-toggle', 'data-target', 'data-remote' );
}
// Add classes to global buttons
$( '#top_tasks' ).parent().addClass( 'top_tasks' );
$( '#top_admin' ).parent().addClass( 'top_admin' );
$( '#form-upload' ).parent().addClass( 'form-upload' );
// Search button work
$( 'input#query' ).focus(function() {
$( 'form[role="search"]' ).addClass( 'search-focus' );
});
$( 'input#query' ).focusout(function() {
setTimeout(function() {
$( 'form[role="search"]' ).removeClass( 'search-focus' );
}, 100);
});
// Check if dropdown goes out of viewport and add class
$(document).on('click','.dropdown-toggle',function() {
// Add .offscreen if part of container not visible
$('.dropdown-menu:visible').filter(function(){
return $(this).visible() === false;
}).each(function(){
$(this).addClass('offscreen');
});
});
// Fade out content on page unload
// delegate all clicks on "a" tag (links)
/*$(document).on("click", "a:not(.btn-toolbar a, a[href*='shelf/remove'], .identifiers a, .bookinfo , .btn-group > a, #add-to-shelves a, #book-list a, .stat.blur a )", function () {
// get the href attribute
var newUrl = $(this).attr("href");
// veryfy if the new url exists or is a hash
if (!newUrl || newUrl[0] === "#") {
// set that hash
location.hash = newUrl;
return;
}
now, fadeout the html (whole page)
$( '.blur-wrapper' ).fadeOut(250);
$(".row-fluid .col-sm-10").fadeOut(500,function () {
// when the animation is complete, set the new location
location = newUrl;
});
// prevent the default browser behavior.
return false;
});*/
// Collapse long text into read-more
$( 'div.comments' ).readmore( {
collapsedHeight: 134,
heightMargin: 45,
speed: 300,
moreLink: '<a href="#">READ MORE</a>', // ToDo: make translateable
lessLink: '<a href="#">READ LESS</a>', // ToDo: make translateable
});
/////////////////////////////////
// End of Global Work //
///////////////////////////////
// Author Page Background Blur
if ( $( 'body.author' ).length >0 ) {
cover = $( '.author-bio img' ).attr( 'src' );
$( '#loader + .container-fluid' )
.prepend( '<div class="blur-wrapper"></div>' );
$( '.blur-wrapper' ).prepend( '<img class="bg-blur" src="' + cover + '">' );
// Place undefined cover images inside container
if ( $( '.bg-blur[src="undefined"]' ).length > 0 ) {
$( '.bg-blur' ).before( '<div class="bg-blur undefined-img"></div>' );
$( 'img.bg-blur' ).appendTo( '.undefined-img' );
}
}
// Ereader Page - add class to iframe body on ereader page after it loads.
backurl = '../../book/' + url[2]
$( 'body.epub #title-controls' )
.append('<div class="epub-back"><input action="action" onclick="location.href=backurl; return false;" type="button" value="Back" /></div>')
$( 'body.stat .col-sm-10 p:first' ).insertAfter( '#libs' );
// Check if link is external and force _blank attribute
$(function(){ // document ready
$( 'a' ).filter(function () {
return this.hostname && this.hostname !== location.hostname;
}).each(function () {
$(this).addClass("external").attr( 'target', '_blank' );
});
});
// Check if lists are empty and add class to buttons
if ( $.trim( $('#add-to-shelves').html() ).length === 0 ) {
$( '#add-to-shelf' ).addClass( 'empty-ul' );
}
shelfLength = $('#add-to-shelves li').length
emptyLength = 0
$('#add-to-shelves').on('click','li a',function(){
console.log('#remove-from-shelves change registered' );
emptyLength++
setTimeout(function() {
if ( emptyLength >= shelfLength ) {
console.log('list is empty; adding empty-ul class' );
$( '#add-to-shelf' ).addClass( 'empty-ul' );
} else {
console.log('list is not empty; removing empty-ul class' );
$( '#add-to-shelf' ).removeClass( 'empty-ul' );
}
},100);
});
if ( $.trim( $( 'ul[aria-labelledby="read-in-browser"] li' ).html() ).length === 0 ) {
$('#read-in-browser').addClass('empty-ul');
}
// Shelf Buttons and Tooltips
if ( $( 'body.shelf' ).length > 0 ) {
$( 'div[data-target="#DeleteShelfDialog"]' )
.before( '<div class=".btn-group shelf-btn-group"></div>' )
.appendTo( '.shelf-btn-group' )
.addClass( 'delete-shelf-btn' );
$( 'a[href*="edit"]' )
.appendTo( '.shelf-btn-group' )
.addClass( 'edit-shelf-btn' );
$( 'a[href*="order"]' )
.appendTo( '.shelf-btn-group' )
.addClass( 'order-shelf-btn' );
$( '.delete-shelf-btn' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '.delete-shelf-btn' ).text(), // 'Delete Shelf'
'data-placement': 'bottom' })
.addClass('delete-btn-tooltip');
$( '.edit-shelf-btn' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '.edit-shelf-btn' ).text(), // 'Edit Shelf'
'data-placement': 'bottom' })
.addClass('edit-btn-tooltip');
$( '.order-shelf-btn' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '.order-shelf-btn' ).text(), //'Reorder Shelf'
'data-placement': 'bottom' })
.addClass('order-btn-tooltip');
}
// Rest of Tooltips
$( '.home-btn > a' ).attr({
'data-toggle': 'tooltip',
'title': $(document.body).attr('data-text'), // Home
'data-placement': 'bottom' })
.addClass('home-btn-tooltip');
$( '.plexBack > a' ).attr({
'data-toggle': 'tooltip',
'title': $(document.body).attr('data-textback'), // Back
'data-placement': 'bottom' })
.addClass('back-btn-tooltip');
$( '#top_tasks' ).attr({
'data-toggle': 'tooltip',
'title': $( '#top_tasks' ).text(), // 'Tasks'
'data-placement': 'bottom',
'data-viewport': '#main-nav' })
.addClass('tasks-btn-tooltip');
$( '#top_admin' ).attr({
'data-toggle': 'tooltip',
'title': $( '#top_admin' ).attr('data-text'), // Settings
'data-placement': 'bottom',
'data-viewport': '#main-nav' })
.addClass('admin-btn-tooltip');
$( '.profileDrop' ).attr({
'title': $( '#top_user' ).attr('data-text'), //Account
'data-placement': 'bottom',
'data-toggle-two': 'tooltip',
'data-viewport': '#main-nav' })
.addClass('send-btn-tooltip dropdown');
$( '#btn-upload' ).attr({
'data-toggle': 'tooltip',
'title': $( '#btn-upload' ).parent().text() , // 'Upload'
'data-placement': 'bottom',
'data-viewport': '#main-nav' })
.addClass('upload-btn-tooltip');
$( '#add-to-shelf' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '#add-to-shelf' ).text() , // 'Add to Shelf'
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('addtoshelf-btn-tooltip');
$( '#have_read_cb' ).attr({
'data-toggle': 'tooltip',
'title': $( '#have_read_cb').attr('data-unchecked'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( '#have_read_cb:checked' ).attr({
'data-toggle': 'tooltip',
'title': $( '#have_read_cb').attr('data-checked'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('readunread-btn-tooltip');
$( 'button#delete' ).attr({
'data-toggle-two': 'tooltip',
'title': $( 'button#delete' ).text(), //'Delete'
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('delete-book-btn-tooltip');
$( '#have_read_cb' ).click(function() {
if ( $( '#have_read_cb:checked' ).length > 0 ) {
$( this ).attr('data-original-title', $('#have_read_cb').attr('data-checked'));
} else {
$( this).attr('data-original-title', $('#have_read_cb').attr('data-unchecked'));
}
});
$( '.btn-group[aria-label="Edit/Delete book"] a' ).attr({
'data-toggle': 'tooltip',
'title': $( '#edit_book' ).text(), // 'Edit'
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('edit-btn-tooltip');
$( '#sendbtn' ).attr({
'data-toggle': 'tooltip',
'title': $( '#sendbtn' ).attr('data-text'),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('send-btn-tooltip');
$( '#sendbtn2' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '#sendbtn2' ).text(), // 'Send to Kindle',
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' })
.addClass('send-btn-tooltip');
$( '#read-in-browser' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '#read-in-browser' ).text(),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar'})
.addClass('send-btn-tooltip');
$( '#btnGroupDrop1' ).attr({
'data-toggle-two': 'tooltip',
'title': $( '#btnGroupDrop1' ).text(),
'data-placement': 'bottom',
'data-viewport': '.btn-toolbar' });
if ( $( 'body.epub').length === 0 ) {
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip({container: 'body', trigger: 'hover'});
$('[data-toggle-two="tooltip"]').tooltip({container: 'body', trigger: 'hover'});
$( '#btn-upload' ).attr('title', " ");
});
$( '[data-toggle-two="tooltip"]' ).click(function(){
$('[data-toggle-two="tooltip"]').tooltip('hide');
});
$( '[data-toggle="tooltip"]' ).click(function(){
$('[data-toggle="tooltip"]').tooltip('hide');
});
}
$( '#read-in-browser a' ).attr('target',"");
if ( $( '.edit-shelf-btn').length > 1 ) {
$( '.edit-shelf-btn:first').remove();
}
if ( $( '.order-shelf-btn').length > 1 ) {
$( '.order-shelf-btn:first').remove();
}
$( '#top_user > span.hidden-sm' ).clone().insertBefore( '.profileDropli' );
$( '.navbar-collapse.collapse.in').before('<div class="sidebar-backdrop"></div>');
// Get rid of leading white space
recentlyAdded = $( '#nav_new a:contains("Recently")' ).text().trim();
$('#nav_new a:contains("Recently")').contents().filter(function() {
return this.nodeType == 3
}).each(function(){
this.textContent = this.textContent.replace(' Recently Added',recentlyAdded);
});
// Change shelf textValue
shelfText = $( '.shelf .discover h2:first' ).text().replace(':',' —').replace(/\'/g,'');
$( '.shelf .discover h2:first' ).text(shelfText);
shelfText = $( '.shelforder .col-sm-10 .col-sm-6.col-lg-6.col-xs-6 h2:first' ).text().replace(':',' —').replace(/\'/g,'');
$( '.shelforder .col-sm-10 .col-sm-6.col-lg-6.col-xs-6 h2:first' ).text(shelfText);
function mobileSupport() {
if ( $( window ).width() <= 768 ) {
//Move menu to collapse
$( '.row-fluid > .col-sm-2:first' ).appendTo( '.navbar-collapse.collapse:first');
if ( $( '.sidebar-backdrop' ).length < 1 ) {
$( '.navbar-collapse.collapse:first' ).after( '<div class="sidebar-backdrop"></div>' );
}
} else {
//Move menu out of collapse
$( '.col-sm-2:first' ).insertBefore( '.col-sm-10:first');
$( '.sidebar-backdrop' ).remove();
}
}
// LayerCake plug
if ( $(' body.stat p').length > 0 ) {
$(' body.stat p').append(" and <a href='https://github.com/leram84/layer.Cake/tree/master/caliBlur' target='_blank'>layer.Cake</a>");
str = $(' body.stat p').html().replace("</a>.","</a>");
$(' body.stat p').html(str);
}
// Collect delete buttons in editbook to single dropdown
$( '.editbook .text-center.more-stuff' ).prepend( '<button id="deleteButton" type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-remove"></span>Delete Format<span class="caret"></span></button><ul class="dropdown-menu delete-dropdown"></ul>' );
deleteButtons = $( '.editbook .text-center.more-stuff a' ).removeClass('btn btn-danger' ).attr( 'type', '').get();
$( deleteButtons ).detach();
$( '.editbook .text-center.more-stuff h4' ).remove();
$.each(deleteButtons, function(i, val) {
$( '<li>' + deleteButtons[i].outerHTML + '</li>' ).appendTo( '.delete-dropdown' );
});
// Turn off bootstrap animations
$(function() { $.support.transition = false; })
mobileSupport();
// Only call function once resize is complete
//var id;
$( window ).on('resize',function() {
// clearTimeout(id);
// id = setTimeout(mobileSupport, 500);
mobileSupport();
});

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 jkrehm
*
* 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/>.
*/
/* global _ */ /* global _ */
$(function() { $(function() {

@ -1,6 +1,21 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 idalin<dalin.lin@gmail.com>
*
* 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/>.
*/
/* /*
* Get Metadata from Douban Books api and Google Books api * Get Metadata from Douban Books api and Google Books api
* Created by idalin<dalin.lin@gmail.com>
* Google Books api document: https://developers.google.com/books/docs/v1/using * Google Books api document: https://developers.google.com/books/docs/v1/using
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only) * Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
*/ */

@ -99,14 +99,15 @@ kthoom.setSettings = function() {
}; };
var createURLFromArray = function(array, mimeType) { var createURLFromArray = function(array, mimeType) {
var offset = array.byteOffset, len = array.byteLength; var offset = array.byteOffset;
var len = array.byteLength;
var url; var url;
var blob; var blob;
if (mimeType === 'image/xml+svg') { if (mimeType === 'image/xml+svg') {
const xmlStr = new TextDecoder('utf-8').decode(array); const xmlStr = new TextDecoder('utf-8').decode(array);
return 'data:image/svg+xml;UTF-8,' + encodeURIComponent(xmlStr); return 'data:image/svg+xml;UTF-8,' + encodeURIComponent(xmlStr);
} }
// TODO: Move all this browser support testing to a common place // TODO: Move all this browser support testing to a common place
// and do it just once. // and do it just once.
@ -137,11 +138,13 @@ var createURLFromArray = function(array, mimeType) {
kthoom.ImageFile = function(file) { kthoom.ImageFile = function(file) {
this.filename = file.filename; this.filename = file.filename;
var fileExtension = file.filename.split(".").pop().toLowerCase(); var fileExtension = file.filename.split(".").pop().toLowerCase();
var mimeType = fileExtension === "png" ? "image/png" : this.mimeType = fileExtension === "png" ? "image/png" :
(fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" : (fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" :
fileExtension === "gif" ? "image/gif" : fileExtension == 'svg' ? 'image/xml+svg' : undefined; fileExtension === "gif" ? "image/gif" : fileExtension == 'svg' ? 'image/xml+svg' : undefined;
this.dataURI = createURLFromArray(file.fileData, mimeType); if ( this.mimeType !== undefined) {
this.data = file; this.dataURI = createURLFromArray(file.fileData, this.mimeType);
this.data = file;
}
}; };
@ -169,34 +172,42 @@ function loadFromArrayBuffer(ab) {
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS, unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS,
function(e) { function(e) {
var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive; var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive;
totalImages = e.totalFilesInArchive; if (totalImages === 0) {
totalImages = e.totalFilesInArchive;
}
updateProgress(percentage *100); updateProgress(percentage *100);
lastCompletion = percentage * 100; lastCompletion = percentage * 100;
}); });
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT, unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT,
function(e) { function(e) {
// convert DecompressedFile into a bunch of ImageFiles // convert DecompressedFile into a bunch of ImageFiles
if (e.unarchivedFile) { if (e.unarchivedFile) {
var f = e.unarchivedFile; var f = e.unarchivedFile;
// add any new pages based on the filename // add any new pages based on the filename
if (imageFilenames.indexOf(f.filename) === -1) { if (imageFilenames.indexOf(f.filename) === -1) {
imageFilenames.push(f.filename); var test = new kthoom.ImageFile(f);
imageFiles.push(new kthoom.ImageFile(f)); if ( test.mimeType !== undefined) {
// add thumbnails to the TOC list imageFilenames.push(f.filename);
$("#thumbnails").append( imageFiles.push(test);
"<li>" + // add thumbnails to the TOC list
"<a data-page='" + imageFiles.length + "'>" + $("#thumbnails").append(
"<img src='" + imageFiles[imageFiles.length - 1].dataURI + "'/>" + "<li>" +
"<span>" + imageFiles.length + "</span>" + "<a data-page='" + imageFiles.length + "'>" +
"</a>" + "<img src='" + imageFiles[imageFiles.length - 1].dataURI + "'/>" +
"</li>" "<span>" + imageFiles.length + "</span>" +
); "</a>" +
"</li>"
);
// display first page if we haven't yet
if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion);
}
}
else {
totalImages--;
}
} }
} }
// display first page if we haven't yet
if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion);
}
}); });
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH, unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH,
function() { function() {

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
!function(a){a.fn.datepicker.dates.ja={days:["日曜","月曜","火曜","水曜","木曜","金曜","土曜"],daysShort:["日","月","火","水","木","金","土"],daysMin:["日","月","火","水","木","金","土"],months:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今日",format:"yyyy/mm/dd",titleFormat:"yyyy年mm月",clear:"クリア"}}(jQuery);

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

@ -0,0 +1 @@
!function(t){var i=t(window);t.fn.visible=function(t,e,o){if(!(this.length<1)){var r=this.length>1?this.eq(0):this,n=r.get(0),f=i.width(),h=i.height(),o=o?o:"both",l=e===!0?n.offsetWidth*n.offsetHeight:!0;if("function"==typeof n.getBoundingClientRect){var g=n.getBoundingClientRect(),u=g.top>=0&&g.top<h,s=g.bottom>0&&g.bottom<=h,c=g.left>=0&&g.left<f,a=g.right>0&&g.right<=f,v=t?u||s:u&&s,b=t?c||a:c&&a;if("both"===o)return l&&v&&b;if("vertical"===o)return l&&v;if("horizontal"===o)return l&&b}else{var d=i.scrollTop(),p=d+h,w=i.scrollLeft(),m=w+f,y=r.offset(),z=y.top,B=z+r.height(),C=y.left,R=C+r.width(),j=t===!0?B:z,q=t===!0?z:B,H=t===!0?R:C,L=t===!0?C:R;if("both"===o)return!!l&&p>=q&&j>=d&&m>=L&&H>=w;if("vertical"===o)return!!l&&p>=q&&j>=d;if("horizontal"===o)return!!l&&m>=L&&H>=w}}}}(jQuery);

File diff suppressed because one or more lines are too long

@ -0,0 +1,11 @@
/*!
* @preserve
*
* Readmore.js jQuery plugin
* Author: @jed_foster
* Project home: http://jedfoster.github.io/Readmore.js
* Licensed under the MIT license
*
* Debounce function from http://davidwalsh.name/javascript-debounce-function
*/
!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof exports?module.exports=t(require("jquery")):t(jQuery)}(function(t){"use strict";function e(t,e,i){var o;return function(){var n=this,a=arguments,s=function(){o=null,i||t.apply(n,a)},r=i&&!o;clearTimeout(o),o=setTimeout(s,e),r&&t.apply(n,a)}}function i(t){var e=++h;return String(null==t?"rmjs-":t)+e}function o(t){var e=t.clone().css({height:"auto",width:t.width(),maxHeight:"none",overflow:"hidden"}).insertAfter(t),i=e.outerHeight(),o=parseInt(e.css({maxHeight:""}).css("max-height").replace(/[^-\d\.]/g,""),10),n=t.data("defaultHeight");e.remove();var a=o||t.data("collapsedHeight")||n;t.data({expandedHeight:i,maxHeight:o,collapsedHeight:a}).css({maxHeight:"none"})}function n(t){if(!d[t.selector]){var e=" ";t.embedCSS&&""!==t.blockCSS&&(e+=t.selector+" + [data-readmore-toggle], "+t.selector+"[data-readmore]{"+t.blockCSS+"}"),e+=t.selector+"[data-readmore]{transition: height "+t.speed+"ms;overflow: hidden;}",function(t,e){var i=t.createElement("style");i.type="text/css",i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),t.getElementsByTagName("head")[0].appendChild(i)}(document,e),d[t.selector]=!0}}function a(e,i){this.element=e,this.options=t.extend({},r,i),n(this.options),this._defaults=r,this._name=s,this.init(),window.addEventListener?(window.addEventListener("load",c),window.addEventListener("resize",c)):(window.attachEvent("load",c),window.attachEvent("resize",c))}var s="readmore",r={speed:100,collapsedHeight:200,heightMargin:16,moreLink:'<a href="#">Read More</a>',lessLink:'<a href="#">Close</a>',embedCSS:!0,blockCSS:"display: block; width: 100%;",startOpen:!1,blockProcessed:function(){},beforeToggle:function(){},afterToggle:function(){}},d={},h=0,c=e(function(){t("[data-readmore]").each(function(){var e=t(this),i="true"===e.attr("aria-expanded");o(e),e.css({height:e.data(i?"expandedHeight":"collapsedHeight")})})},100);a.prototype={init:function(){var e=t(this.element);e.data({defaultHeight:this.options.collapsedHeight,heightMargin:this.options.heightMargin}),o(e);var n=e.data("collapsedHeight"),a=e.data("heightMargin");if(e.outerHeight(!0)<=n+a)return this.options.blockProcessed&&"function"==typeof this.options.blockProcessed&&this.options.blockProcessed(e,!1),!0;var s=e.attr("id")||i(),r=this.options.startOpen?this.options.lessLink:this.options.moreLink;e.attr({"data-readmore":"","aria-expanded":this.options.startOpen,id:s}),e.after(t(r).on("click",function(t){return function(i){t.toggle(this,e[0],i)}}(this)).attr({"data-readmore-toggle":s,"aria-controls":s})),this.options.startOpen||e.css({height:n}),this.options.blockProcessed&&"function"==typeof this.options.blockProcessed&&this.options.blockProcessed(e,!0)},toggle:function(e,i,o){o&&o.preventDefault(),e||(e=t('[aria-controls="'+this.element.id+'"]')[0]),i||(i=this.element);var n=t(i),a="",s="",r=!1,d=n.data("collapsedHeight");n.height()<=d?(a=n.data("expandedHeight")+"px",s="lessLink",r=!0):(a=d,s="moreLink"),this.options.beforeToggle&&"function"==typeof this.options.beforeToggle&&this.options.beforeToggle(e,n,!r),n.css({height:a}),n.on("transitionend",function(i){return function(){i.options.afterToggle&&"function"==typeof i.options.afterToggle&&i.options.afterToggle(e,n,r),t(this).attr({"aria-expanded":r}).off("transitionend")}}(this)),t(e).replaceWith(t(this.options[s]).on("click",function(t){return function(e){t.toggle(this,i,e)}}(this)).attr({"data-readmore-toggle":n.attr("id"),"aria-controls":n.attr("id")}))},destroy:function(){t(this.element).each(function(){var e=t(this);e.attr({"data-readmore":null,"aria-expanded":null}).css({maxHeight:"",height:""}).next("[data-readmore-toggle]").remove(),e.removeData()})}},t.fn.readmore=function(e){var i=arguments,o=this.selector;return e=e||{},"object"==typeof e?this.each(function(){if(t.data(this,"plugin_"+s)){var i=t.data(this,"plugin_"+s);i.destroy.apply(i)}e.selector=o,t.data(this,"plugin_"+s,new a(this,e))}):"string"==typeof e&&"_"!==e[0]&&"init"!==e?this.each(function(){var o=t.data(this,"plugin_"+s);o instanceof a&&"function"==typeof o[e]&&o[e].apply(o,Array.prototype.slice.call(i,1))}):void 0}});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2012-2019 mutschler, janeczku, jkrehm, OzzieIsaacs
*
* 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/>.
*/
// Generic control/related handler to show/hide fields based on a checkbox' value // Generic control/related handler to show/hide fields based on a checkbox' value
// e.g. // e.g.
// <input type="checkbox" data-control="stuff-to-show"> // <input type="checkbox" data-control="stuff-to-show">
@ -60,25 +77,20 @@ $(function() {
layoutMode : "fitRows" layoutMode : "fitRows"
}); });
$(".load-more .row").infinitescroll({ var $loadMore = $(".load-more .row").infiniteScroll({
debug: false, debug: false,
navSelector : ".pagination",
// selector for the paged navigation (it will be hidden) // selector for the paged navigation (it will be hidden)
nextSelector : ".pagination a:last", path : ".next",
// selector for the NEXT link (to page 2) // selector for the NEXT link (to page 2)
itemSelector : ".load-more .book", append : ".load-more .book"
animate : true, //animate : true, # ToDo: Reenable function
extraScrollPx: 300 //extraScrollPx: 300
// selector for all items you'll retrieve });
}, function(data) { $loadMore.on( "append.infiniteScroll", function( event, response, path, data ) {
$(".pagination").addClass("hidden");
$(".load-more .row").isotope( "appended", $(data), null ); $(".load-more .row").isotope( "appended", $(data), null );
}); });
$("#sendbtn").click(function() {
var $this = $(this);
$this.text("Please wait...");
$this.addClass("disabled");
});
$("#restart").click(function() { $("#restart").click(function() {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
@ -104,15 +116,18 @@ $(function() {
var $this = $(this); var $this = $(this);
var buttonText = $this.html(); var buttonText = $this.html();
$this.html("..."); $this.html("...");
$("#update_error").addClass("hidden") $("#update_error").addClass("hidden");
if ($("#message").length) {
$("#message").alert("close");
}
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../get_update_status", url: window.location.pathname + "/../../get_update_status",
success: function success(data) { success: function success(data) {
$this.html(buttonText); $this.html(buttonText);
var cssClass = ''; var cssClass = "";
var message = '' var message = "";
if (data.success === true) { if (data.success === true) {
if (data.update === true) { if (data.update === true) {
@ -122,19 +137,20 @@ $(function() {
.removeClass("hidden") .removeClass("hidden")
.find("span").html(data.commit); .find("span").html(data.commit);
data.history.reverse().forEach(function(entry, index) { data.history.forEach(function(entry) {
$("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table")); $("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table"));
}); });
cssClass = 'alert-warning' cssClass = "alert-warning";
} else { } else {
cssClass = 'alert-success' cssClass = "alert-success";
} }
} else { } else {
cssClass = 'alert-danger' cssClass = "alert-danger";
} }
message = '<div class="alert ' + cssClass message = "<div id=\"message\" class=\"alert " + cssClass
+ ' fade in"><a href="#" class="close" data-dismiss="alert">&times;</a>' + data.message + '</div>'; + " fade in\"><a href=\"#\" class=\"close\" data-dismiss=\"alert\">&times;</a>"
+ data.message + "</div>";
$(message).insertAfter($("#update_table")); $(message).insertAfter($("#update_table"));
} }
@ -163,6 +179,7 @@ $(function() {
}); });
}); });
// Init all data control handlers to default
$("input[data-control]").trigger("change"); $("input[data-control]").trigger("change");
$("#bookDetailsModal") $("#bookDetailsModal")
@ -186,6 +203,14 @@ $(function() {
}); });
$(window).resize(function() { $(window).resize(function() {
$(".discover .row").isotope("reLayout"); $(".discover .row").isotope("layout");
});
$(".author-expand").click(function() {
$(this).parent().find("a.author-name").slice($(this).data("authors-max")).toggle();
$(this).parent().find("span.author-hidden-divider").toggle();
$(this).html() === $(this).data("collapse-caption") ? $(this).html("(...)") : $(this).html($(this).data("collapse-caption"));
$(".discover .row").isotope("layout");
}); });
}); });

@ -1,3 +1,20 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 jkrehm, OzzieIsaacs
*
* 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/>.
*/
/* global Sortable,sortTrue */ /* global Sortable,sortTrue */
Sortable.create(sortTrue, { Sortable.create(sortTrue, {

@ -1,6 +1,23 @@
/* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
* Copyright (C) 2018 OzzieIsaacs
*
* 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/>.
*/
$(function() { $(function() {
$("#domain_submit").click(function(event){ $("#domain_submit").click(function(event) {
event.preventDefault(); event.preventDefault();
$("#domain_add").ajaxForm(); $("#domain_add").ajaxForm();
$(this).closest("form").submit(); $(this).closest("form").submit();
@ -10,48 +27,49 @@ $(function() {
async: true, async: true,
timeout: 900, timeout: 900,
success:function(data){ success:function(data){
$('#domain-table').bootstrapTable("load", data); $("#domain-table").bootstrapTable("load", data);
} }
}); });
}); });
$('#domain-table').bootstrapTable({ $("#domain-table").bootstrapTable({
formatNoMatches: function () { formatNoMatches: function () {
return ''; return "";
}, },
striped: false striped: false
}); });
$("#btndeletedomain").click(function() { $("#btndeletedomain").click(function() {
//get data-id attribute of the clicked element //get data-id attribute of the clicked element
var domainId = $(this).data('domainId'); var domainId = $(this).data("domainId");
$.ajax({ $.ajax({
method:"post", method:"post",
url: window.location.pathname + "/../../ajax/deletedomain", url: window.location.pathname + "/../../ajax/deletedomain",
data: {"domainid":domainId} data: {"domainid":domainId}
}); });
$('#DeleteDomain').modal('hide'); $("#DeleteDomain").modal("hide");
$.ajax({ $.ajax({
method:"get", method:"get",
url: window.location.pathname + "/../../ajax/domainlist", url: window.location.pathname + "/../../ajax/domainlist",
async: true, async: true,
timeout: 900, timeout: 900,
success:function(data){ success:function(data) {
$('#domain-table').bootstrapTable("load", data); $("#domain-table").bootstrapTable("load", data);
} }
}); });
}); });
//triggered when modal is about to be shown //triggered when modal is about to be shown
$('#DeleteDomain').on('show.bs.modal', function(e) { $("#DeleteDomain").on("show.bs.modal", function(e) {
//get data-id attribute of the clicked element and store in button //get data-id attribute of the clicked element and store in button
var domainId = $(e.relatedTarget).data('domain-id'); var domainId = $(e.relatedTarget).data("domain-id");
$(e.currentTarget).find("#btndeletedomain").data('domainId',domainId); $(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
}); });
}); });
function TableActions (value, row, index) { /*function TableActions (value, row, index) {
return [ return [
'<a class="danger remove" data-toggle="modal" data-target="#DeleteDomain" data-domain-id="'+row.id+'" title="Remove">', "<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
'<i class="glyphicon glyphicon-trash"></i>', + "\" title=\"Remove\">",
'</a>' "<i class=\"glyphicon glyphicon-trash\"></i>",
].join(''); "</a>"
} ].join("");
}*/

@ -79,7 +79,7 @@ var RarVolumeHeader = function(bstream) {
// bytes 4,5 // bytes 4,5
this.flags = {}; this.flags = {};
this.flags.value = bstream.peekBits(16); this.flags.value = bstream.peekBits(16);
info(" flags=" + twoByteValueToHexString(this.flags.value)); info(" flags=" + twoByteValueToHexString(this.flags.value));
switch (this.headType) { switch (this.headType) {
case MAIN_HEAD: case MAIN_HEAD:
@ -115,7 +115,7 @@ var RarVolumeHeader = function(bstream) {
default: default:
bstream.readBits(16); bstream.readBits(16);
} }
// byte 6,7 // byte 6,7
this.headSize = bstream.readBits(16); this.headSize = bstream.readBits(16);
info(" headSize=" + this.headSize); info(" headSize=" + this.headSize);
@ -212,12 +212,12 @@ var RarVolumeHeader = function(bstream) {
//var BLOCK_LZ = 0; //var BLOCK_LZ = 0;
var rLDecode = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224], var rLDecode = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224],
rLBits = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], rLBits = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5],
rDBitLengthCounts = [4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 14, 0, 12], rDBitLengthCounts = [4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 14, 0, 12],
rSDDecode = [0, 4, 8, 16, 32, 64, 128, 192], rSDDecode = [0, 4, 8, 16, 32, 64, 128, 192],
rSDBits = [2, 2, 3, 4, 5, 6, 6, 6]; rSDBits = [2, 2, 3, 4, 5, 6, 6, 6];
var rDDecode = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, var rDDecode = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32,
48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072,
4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304, 4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304,
@ -269,18 +269,18 @@ var RD = { //rep decode
var rBuffer; var rBuffer;
// read in Huffman tables for RAR // read in Huffman tables for RAR
function RarReadTables(bstream) { function rarReadTables(bstream) {
var BitLength = new Array(rBC), var BitLength = new Array(rBC),
Table = new Array(rHuffTableSize); Table = new Array(rHuffTableSize);
var i; var i;
// before we start anything we need to get byte-aligned // before we start anything we need to get byte-aligned
bstream.readBits( (8 - bstream.bitPtr) & 0x7 ); bstream.readBits( (8 - bstream.bitPtr) & 0x7 );
if (bstream.readBits(1)) { if (bstream.readBits(1)) {
info("Error! PPM not implemented yet"); info("Error! PPM not implemented yet");
return; return;
} }
if (!bstream.readBits(1)) { //discard old table if (!bstream.readBits(1)) { //discard old table
for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0; for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0;
} }
@ -308,7 +308,7 @@ function RarReadTables(bstream) {
// now all 20 bit lengths are obtained, we construct the Huffman Table: // now all 20 bit lengths are obtained, we construct the Huffman Table:
rarMakeDecodeTables(BitLength, 0, BD, rBC); rarMakeDecodeTables(BitLength, 0, BD, rBC);
var TableSize = rHuffTableSize; var TableSize = rHuffTableSize;
//console.log(DecodeLen, DecodePos, DecodeNum); //console.log(DecodeLen, DecodePos, DecodeNum);
for (i = 0; i < TableSize;) { for (i = 0; i < TableSize;) {
@ -332,12 +332,12 @@ function RarReadTables(bstream) {
} }
} }
} }
rarMakeDecodeTables(Table, 0, LD, rNC); rarMakeDecodeTables(Table, 0, LD, rNC);
rarMakeDecodeTables(Table, rNC, DD, rDC); rarMakeDecodeTables(Table, rNC, DD, rDC);
rarMakeDecodeTables(Table, rNC + rDC, LDD, rLDC); rarMakeDecodeTables(Table, rNC + rDC, LDD, rLDC);
rarMakeDecodeTables(Table, rNC + rDC + rLDC, RD, rRC); rarMakeDecodeTables(Table, rNC + rDC + rLDC, RD, rRC);
for (i = UnpOldTable.length; i--;) { for (i = UnpOldTable.length; i--;) {
UnpOldTable[i] = Table[i]; UnpOldTable[i] = Table[i];
} }
@ -366,7 +366,7 @@ function rarDecodeNumber(bstream, dec) {
: 15)); : 15));
bstream.readBits(bits); bstream.readBits(bits);
var N = DecodePos[bits] + ((bitField - DecodeLen[bits - 1]) >>> (16 - bits)); var N = DecodePos[bits] + ((bitField - DecodeLen[bits - 1]) >>> (16 - bits));
return DecodeNum[N]; return DecodeNum[N];
} }
@ -480,7 +480,7 @@ function Unpack20(bstream) { //, Solid) {
continue; continue;
} }
if (num < 270) { if (num < 270) {
var Distance = rSDDecode[num -= 261] + 1; Distance = rSDDecode[num -= 261] + 1;
if ((Bits = rSDBits[num]) > 0) { if ((Bits = rSDBits[num]) > 0) {
Distance += bstream.readBits(Bits); Distance += bstream.readBits(Bits);
} }
@ -513,9 +513,9 @@ function rarReadTables20(bstream) {
var BitLength = new Array(rBC20); var BitLength = new Array(rBC20);
var Table = new Array(rMC20 * 4); var Table = new Array(rMC20 * 4);
var TableSize, N, I; var TableSize, N, I;
var i;
bstream.readBits(1); bstream.readBits(1);
if (!bstream.readBits(1)) { if (!bstream.readBits(1)) {
var i;
for (i = UnpOldTable20.length; i--;) UnpOldTable20[i] = 0; for (i = UnpOldTable20.length; i--;) UnpOldTable20[i] = 0;
} }
TableSize = rNC20 + rDC20 + rRC20; TableSize = rNC20 + rDC20 + rRC20;
@ -553,48 +553,49 @@ function rarReadTables20(bstream) {
} }
function Unpack29(bstream, Solid) { function Unpack29(bstream) {
// lazy initialize rDDecode and rDBits // lazy initialize rDDecode and rDBits
var DDecode = new Array(rDC); var DDecode = new Array(rDC);
var DBits = new Array(rDC); var DBits = new Array(rDC);
var Distance = 0;
var Length = 0;
var Dist = 0, BitLength = 0, Slot = 0; var Dist = 0, BitLength = 0, Slot = 0;
var I; var I;
for (I = 0; I < rDBitLengthCounts.length; I++,BitLength++) { for (I = 0; I < rDBitLengthCounts.length; I++, BitLength++) {
for (var J = 0; J < rDBitLengthCounts[I]; J++,Slot++,Dist+=(1<<BitLength)) { for (var J = 0; J < rDBitLengthCounts[I]; J++, Slot++, Dist += (1 << BitLength)) {
DDecode[Slot]=Dist; DDecode[Slot] = Dist;
DBits[Slot]=BitLength; DBits[Slot] = BitLength;
} }
} }
var Bits; var Bits;
//tablesRead = false; //tablesRead = false;
rOldDist = [0, 0, 0, 0] rOldDist = [0, 0, 0, 0];
lastDist = 0; lastDist = 0;
lastLength = 0; lastLength = 0;
var i; var i;
for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0; for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0;
// read in Huffman tables // read in Huffman tables
RarReadTables(bstream); rarReadTables(bstream);
while (true) { while (true) {
var num = rarDecodeNumber(bstream, LD); var num = rarDecodeNumber(bstream, LD);
if (num < 256) { if (num < 256) {
rBuffer.insertByte(num); rBuffer.insertByte(num);
continue; continue;
} }
if (num >= 271) { if (num >= 271) {
var Length = rLDecode[num -= 271] + 3; Length = rLDecode[num -= 271] + 3;
if ((Bits = rLBits[num]) > 0) { if ((Bits = rLBits[num]) > 0) {
Length += bstream.readBits(Bits); Length += bstream.readBits(Bits);
} }
var DistNumber = rarDecodeNumber(bstream, DD); var DistNumber = rarDecodeNumber(bstream, DD);
var Distance = DDecode[DistNumber]+1; Distance = DDecode[DistNumber] + 1;
if ((Bits = DBits[DistNumber]) > 0) { if ((Bits = DBits[DistNumber]) > 0) {
if (DistNumber > 9) { if (DistNumber > 9) {
if (Bits > 4) { if (Bits > 4) {
@ -625,19 +626,19 @@ function Unpack29(bstream, Solid) {
Length++; Length++;
} }
} }
RarInsertOldDist(Distance); rarInsertOldDist(Distance);
RarInsertLastMatch(Length, Distance); rarInsertLastMatch(Length, Distance);
rarCopyString(Length, Distance); rarCopyString(Length, Distance);
continue; continue;
} }
if (num === 256) { if (num === 256) {
if (!RarReadEndOfBlock(bstream)) break; if (!rarReadEndOfBlock(bstream)) break;
continue; continue;
} }
if (num === 257) { if (num === 257) {
//console.log("READVMCODE"); //console.log("READVMCODE");
if (!RarReadVMCode(bstream)) break; if (!rarReadVMCode(bstream)) break;
continue; continue;
} }
if (num === 258) { if (num === 258) {
if (lastLength != 0) { if (lastLength != 0) {
@ -647,39 +648,39 @@ function Unpack29(bstream, Solid) {
} }
if (num < 263) { if (num < 263) {
var DistNum = num - 259; var DistNum = num - 259;
var Distance = rOldDist[DistNum]; Distance = rOldDist[DistNum];
for (var I = DistNum; I > 0; I--) { for (var I = DistNum; I > 0; I--) {
rOldDist[I] = rOldDist[I-1]; rOldDist[I] = rOldDist[I - 1];
} }
rOldDist[0] = Distance; rOldDist[0] = Distance;
var LengthNumber = rarDecodeNumber(bstream, RD); var LengthNumber = rarDecodeNumber(bstream, RD);
var Length = rLDecode[LengthNumber] + 2; Length = rLDecode[LengthNumber] + 2;
if ((Bits = rLBits[LengthNumber]) > 0) { if ((Bits = rLBits[LengthNumber]) > 0) {
Length += bstream.readBits(Bits); Length += bstream.readBits(Bits);
} }
RarInsertLastMatch(Length, Distance); rarInsertLastMatch(Length, Distance);
rarCopyString(Length, Distance); rarCopyString(Length, Distance);
continue; continue;
} }
if (num < 272) { if (num < 272) {
var Distance = rSDDecode[num -= 263] + 1; Distance = rSDDecode[num -= 263] + 1;
if ((Bits = rSDBits[num]) > 0) { if ((Bits = rSDBits[num]) > 0) {
Distance += bstream.readBits(Bits); Distance += bstream.readBits(Bits);
} }
RarInsertOldDist(Distance); rarInsertOldDist(Distance);
RarInsertLastMatch(2, Distance); rarInsertLastMatch(2, Distance);
rarCopyString(2, Distance); rarCopyString(2, Distance);
continue; continue;
} }
} }
rarUpdateProgress() rarUpdateProgress();
} }
function RarReadEndOfBlock(bstream) { function rarReadEndOfBlock(bstream) {
rarUpdateProgress() rarUpdateProgress();
var NewTable = false, NewFile = false; var NewTable = false, NewFile = false;
if (bstream.readBits(1)) { if (bstream.readBits(1)) {
@ -689,11 +690,11 @@ function RarReadEndOfBlock(bstream) {
NewTable = !!bstream.readBits(1); NewTable = !!bstream.readBits(1);
} }
//tablesRead = !NewTable; //tablesRead = !NewTable;
return !(NewFile || NewTable && !RarReadTables(bstream)); return !(NewFile || NewTable && !rarReadTables(bstream));
} }
function RarReadVMCode(bstream) { function rarReadVMCode(bstream) {
var FirstByte = bstream.readBits(8); var FirstByte = bstream.readBits(8);
var Length = (FirstByte & 7) + 1; var Length = (FirstByte & 7) + 1;
if (Length === 7) { if (Length === 7) {
@ -702,7 +703,7 @@ function RarReadVMCode(bstream) {
Length = bstream.readBits(16); Length = bstream.readBits(16);
} }
var vmCode = []; var vmCode = [];
for(var I = 0; I < Length; I++) { for (var I = 0; I < Length; I++) {
//do something here with cheking readbuf //do something here with cheking readbuf
vmCode.push(bstream.readBits(8)); vmCode.push(bstream.readBits(8));
} }
@ -717,14 +718,14 @@ function RarAddVMCode(firstByte, vmCode, length) {
return true; return true;
} }
function RarInsertLastMatch(length, distance) { function rarInsertLastMatch(length, distance) {
lastDist = distance; lastDist = distance;
lastLength = length; lastLength = length;
} }
function RarInsertOldDist(distance) { function rarInsertOldDist(distance) {
rOldDist.splice(3,1); rOldDist.splice(3, 1);
rOldDist.splice(0,0,distance); rOldDist.splice(0, 0, distance);
} }
//this is the real function, the other one is for debugging //this is the real function, the other one is for debugging
@ -736,28 +737,28 @@ function rarCopyString(length, distance) {
destPtr = rOldBuffers[--l].data.length + destPtr; destPtr = rOldBuffers[--l].data.length + destPtr;
} }
//TODO: lets hope that it never needs to read beyond file boundaries //TODO: lets hope that it never needs to read beyond file boundaries
while(length--) rBuffer.insertByte(rOldBuffers[l].data[destPtr++]); while (length--) rBuffer.insertByte(rOldBuffers[l].data[destPtr++]);
} }
if (length > distance) { if (length > distance) {
while(length--) rBuffer.insertByte(rBuffer.data[destPtr++]); while (length--) rBuffer.insertByte(rBuffer.data[destPtr++]);
} else { } else {
rBuffer.insertBytes(rBuffer.data.subarray(destPtr, destPtr + length)); rBuffer.insertBytes(rBuffer.data.subarray(destPtr, destPtr + length));
} }
} }
var rOldBuffers = [] var rOldBuffers = [];
// v must be a valid RarVolume // v must be a valid RarVolume
function unpack(v) { function unpack(v) {
// TODO: implement what happens when unpVer is < 15 // TODO: implement what happens when unpVer is < 15
var Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer, var Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer,
Solid = v.header.LHD_SOLID, Solid = v.header.LHD_SOLID,
bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength ); bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength );
rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize); rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize);
info("Unpacking " + v.filename+" RAR v" + Ver); info("Unpacking " + v.filename + " RAR v" + Ver);
switch(Ver) { switch(Ver) {
case 15: // rar 1.5 compression case 15: // rar 1.5 compression
Unpack15(); //(bstream, Solid); Unpack15(); //(bstream, Solid);
@ -768,10 +769,10 @@ function unpack(v) {
break; break;
case 29: // rar 3.x compression case 29: // rar 3.x compression
case 36: // alternative hash case 36: // alternative hash
Unpack29(bstream, Solid); Unpack29(bstream);
break; break;
} // switch(method) } // switch(method)
rOldBuffers.push(rBuffer); rOldBuffers.push(rBuffer);
//TODO: clear these old buffers when there's over 4MB of history //TODO: clear these old buffers when there's over 4MB of history
return rBuffer.data; return rBuffer.data;
@ -779,10 +780,10 @@ function unpack(v) {
// bstream is a bit stream // bstream is a bit stream
var RarLocalFile = function(bstream) { var RarLocalFile = function(bstream) {
this.header = new RarVolumeHeader(bstream); this.header = new RarVolumeHeader(bstream);
this.filename = this.header.filename; this.filename = this.header.filename;
if (this.header.headType != FILE_HEAD && this.header.headType != ENDARC_HEAD) { if (this.header.headType != FILE_HEAD && this.header.headType != ENDARC_HEAD) {
this.isValid = false; this.isValid = false;
info("Error! RAR Volume did not include a FILE_HEAD header "); info("Error! RAR Volume did not include a FILE_HEAD header ");
@ -803,7 +804,7 @@ RarLocalFile.prototype.unrar = function() {
if (this.header.method === 0x30) { if (this.header.method === 0x30) {
info("Unstore " + this.filename); info("Unstore " + this.filename);
this.isValid = true; this.isValid = true;
currentBytesUnarchivedInFile += this.fileData.length; currentBytesUnarchivedInFile += this.fileData.length;
currentBytesUnarchived += this.fileData.length; currentBytesUnarchived += this.fileData.length;
@ -817,7 +818,7 @@ RarLocalFile.prototype.unrar = function() {
this.fileData = unpack(this); this.fileData = unpack(this);
} }
} }
} };
var unrar = function(arrayBuffer) { var unrar = function(arrayBuffer) {
currentFilename = ""; currentFilename = "";
@ -834,16 +835,16 @@ var unrar = function(arrayBuffer) {
if (header.crc === 0x6152 && if (header.crc === 0x6152 &&
header.headType === 0x72 && header.headType === 0x72 &&
header.flags.value === 0x1A21 && header.flags.value === 0x1A21 &&
header.headSize === 7) header.headSize === 7) {
{
info("Found RAR signature"); info("Found RAR signature");
var mhead = new RarVolumeHeader(bstream); var mhead = new RarVolumeHeader(bstream);
if (mhead.headType != MAIN_HEAD) { if (mhead.headType != MAIN_HEAD) {
info("Error! RAR did not include a MAIN_HEAD header"); info("Error! RAR did not include a MAIN_HEAD header");
} else { } else {
var localFiles = [], var localFiles = [];
localFile = null; var localFile = null;
do { do {
try { try {
localFile = new RarLocalFile(bstream); localFile = new RarLocalFile(bstream);
@ -852,24 +853,24 @@ var unrar = function(arrayBuffer) {
totalUncompressedBytesInArchive += localFile.header.unpackedSize; totalUncompressedBytesInArchive += localFile.header.unpackedSize;
localFiles.push(localFile); localFiles.push(localFile);
} else if (localFile.header.packSize === 0 && localFile.header.unpackedSize === 0) { } else if (localFile.header.packSize === 0 && localFile.header.unpackedSize === 0) {
localFile.isValid = true; localFile.isValid = true;
} }
} catch(err) { } catch (err) {
break; break;
} }
//info("bstream" + bstream.bytePtr+"/"+bstream.bytes.length); //info("bstream" + bstream.bytePtr+"/"+bstream.bytes.length);
} while( localFile.isValid ); } while ( localFile.isValid );
totalFilesInArchive = localFiles.length; totalFilesInArchive = localFiles.length;
// now we have all information but things are unpacked // now we have all information but things are unpacked
// TODO: unpack // TODO: unpack
localFiles = localFiles.sort(function(a,b) { localFiles = localFiles.sort(function(a, b) {
var aname = a.filename.toLowerCase(); var aname = a.filename.toLowerCase();
var bname = b.filename.toLowerCase(); var bname = b.filename.toLowerCase();
return aname > bname ? 1 : -1; return aname > bname ? 1 : -1;
}); });
info(localFiles.map(function(a) {return a.filename}).join(', ')); info(localFiles.map(function(a) {return a.filename;}).join(", "));
for (var i = 0; i < localFiles.length; ++i) { for (var i = 0; i < localFiles.length; ++i) {
var localfile = localFiles[i]; var localfile = localFiles[i];

@ -9,7 +9,7 @@
* ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT * ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* DEFLATE format: http://tools.ietf.org/html/rfc1951 * DEFLATE format: http://tools.ietf.org/html/rfc1951
*/ */
/* global bitjs */ /* global bitjs, importScripts, Uint8Array*/
// This file expects to be invoked as a Worker (see onmessage below). // This file expects to be invoked as a Worker (see onmessage below).
importScripts("io.js"); importScripts("io.js");
@ -44,12 +44,10 @@ var zLocalFileHeaderSignature = 0x04034b50;
var zArchiveExtraDataSignature = 0x08064b50; var zArchiveExtraDataSignature = 0x08064b50;
var zCentralFileHeaderSignature = 0x02014b50; var zCentralFileHeaderSignature = 0x02014b50;
var zDigitalSignatureSignature = 0x05054b50; var zDigitalSignatureSignature = 0x05054b50;
var zEndOfCentralDirSignature = 0x06064b50;
var zEndOfCentralDirLocatorSignature = 0x07064b50;
// takes a ByteStream and parses out the local file information // takes a ByteStream and parses out the local file information
var ZipLocalFile = function(bstream) { var ZipLocalFile = function(bstream) {
if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function(){}) { if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function() {}) {
return null; return null;
} }
@ -112,9 +110,10 @@ ZipLocalFile.prototype.unzip = function() {
// Zip Version 1.0, no compression (store only) // Zip Version 1.0, no compression (store only)
if (this.compressionMethod == 0 ) { if (this.compressionMethod == 0 ) {
info("ZIP v"+this.version+", store only: " + this.filename + " (" + this.compressedSize + " bytes)"); info("ZIP v" + this.version + ", store only: " + this.filename + " (" + this.compressedSize + " bytes)");
currentBytesUnarchivedInFile = this.compressedSize; currentBytesUnarchivedInFile = this.compressedSize;
currentBytesUnarchived += this.compressedSize; currentBytesUnarchived += this.compressedSize;
this.fileData = zeroCompression(this.fileData, this.uncompressedSize);
} }
// version == 20, compression method == 8 (DEFLATE) // version == 20, compression method == 8 (DEFLATE)
else if (this.compressionMethod == 8) { else if (this.compressionMethod == 8) {
@ -158,7 +157,7 @@ var unzip = function(arrayBuffer) {
totalFilesInArchive = localFiles.length; totalFilesInArchive = localFiles.length;
// got all local files, now sort them // got all local files, now sort them
localFiles.sort(function(a,b) { localFiles.sort(function(a, b) {
var aname = a.filename.toLowerCase(); var aname = a.filename.toLowerCase();
var bname = b.filename.toLowerCase(); var bname = b.filename.toLowerCase();
return aname > bname ? 1 : -1; return aname > bname ? 1 : -1;
@ -238,11 +237,11 @@ var unzip = function(arrayBuffer) {
} }
postProgress(); postProgress();
postMessage(new bitjs.archive.UnarchiveFinishEvent()); postMessage(new bitjs.archive.UnarchiveFinishEvent());
} }
} };
// returns a table of Huffman codes // returns a table of Huffman codes
// each entry's index is its code and its value is a JavaScript object // each entry's index is its code and its value is a JavaScript object
// containing {length: 6, symbol: X} // containing {length: 6, symbol: X}
function getHuffmanCodes(bitLengths) { function getHuffmanCodes(bitLengths) {
// ensure bitLengths is an array containing at least one element // ensure bitLengths is an array containing at least one element
@ -253,7 +252,7 @@ function getHuffmanCodes(bitLengths) {
// Reference: http://tools.ietf.org/html/rfc1951#page-8 // Reference: http://tools.ietf.org/html/rfc1951#page-8
var numLengths = bitLengths.length, var numLengths = bitLengths.length,
bl_count = [], blCount = [],
MAX_BITS = 1; MAX_BITS = 1;
// Step 1: count up how many codes of each length we have // Step 1: count up how many codes of each length we have
@ -265,22 +264,22 @@ function getHuffmanCodes(bitLengths) {
return null; return null;
} }
// increment the appropriate bitlength count // increment the appropriate bitlength count
if (bl_count[length] == undefined) bl_count[length] = 0; if (blCount[length] == undefined) blCount[length] = 0;
// a length of zero means this symbol is not participating in the huffman coding // a length of zero means this symbol is not participating in the huffman coding
if (length > 0) bl_count[length]++; if (length > 0) blCount[length]++;
if (length > MAX_BITS) MAX_BITS = length; if (length > MAX_BITS) MAX_BITS = length;
} }
// Step 2: Find the numerical value of the smallest code for each code length // Step 2: Find the numerical value of the smallest code for each code length
var next_code = [], var nextCode = [],
code = 0; code = 0;
for (var bits = 1; bits <= MAX_BITS; ++bits) { for (var bits = 1; bits <= MAX_BITS; ++bits) {
var length = bits-1; var length = bits - 1;
// ensure undefined lengths are zero // ensure undefined lengths are zero
if (bl_count[length] == undefined) bl_count[length] = 0; if (blCount[length] == undefined) blCount[length] = 0;
code = (code + bl_count[bits-1]) << 1; code = (code + blCount[bits - 1]) << 1;
next_code[bits] = code; nextCode [bits] = code;
} }
// Step 3: Assign numerical values to all codes // Step 3: Assign numerical values to all codes
@ -288,9 +287,9 @@ function getHuffmanCodes(bitLengths) {
for (var n = 0; n < numLengths; ++n) { for (var n = 0; n < numLengths; ++n) {
var len = bitLengths[n]; var len = bitLengths[n];
if (len != 0) { if (len != 0) {
table[next_code[len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(next_code[len],len) }; table[nextCode [len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(nextCode [len],len) };
tableLength++; tableLength++;
next_code[len]++; nextCode [len]++;
} }
} }
table.maxLength = tableLength; table.maxLength = tableLength;
@ -321,7 +320,8 @@ function getFixedLiteralTable() {
// create once // create once
if (!fixedHCtoLiteral) { if (!fixedHCtoLiteral) {
var bitlengths = new Array(288); var bitlengths = new Array(288);
for (var i = 0; i <= 143; ++i) bitlengths[i] = 8; var i;
for (i = 0; i <= 143; ++i) bitlengths[i] = 8;
for (i = 144; i <= 255; ++i) bitlengths[i] = 9; for (i = 144; i <= 255; ++i) bitlengths[i] = 9;
for (i = 256; i <= 279; ++i) bitlengths[i] = 7; for (i = 256; i <= 279; ++i) bitlengths[i] = 7;
for (i = 280; i <= 287; ++i) bitlengths[i] = 8; for (i = 280; i <= 287; ++i) bitlengths[i] = 8;
@ -335,7 +335,9 @@ function getFixedDistanceTable() {
// create once // create once
if (!fixedHCtoDistance) { if (!fixedHCtoDistance) {
var bitlengths = new Array(32); var bitlengths = new Array(32);
for (var i = 0; i < 32; ++i) { bitlengths[i] = 5; } for (var i = 0; i < 32; ++i) {
bitlengths[i] = 5;
}
// get huffman code table // get huffman code table
fixedHCtoDistance = getHuffmanCodes(bitlengths); fixedHCtoDistance = getHuffmanCodes(bitlengths);
@ -347,13 +349,12 @@ function getFixedDistanceTable() {
// then return that symbol // then return that symbol
function decodeSymbol(bstream, hcTable) { function decodeSymbol(bstream, hcTable) {
var code = 0, len = 0; var code = 0, len = 0;
var match = false;
// loop until we match // loop until we match
for (;;) { for (;;) {
// read in next bit // read in next bit
var bit = bstream.readBits(1); var bit = bstream.readBits(1);
code = (code<<1) | bit; code = (code << 1) | bit;
++len; ++len;
// check against Huffman Code table and break if found // check against Huffman Code table and break if found
@ -372,62 +373,62 @@ function decodeSymbol(bstream, hcTable) {
var CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; var CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
/* /*
Extra Extra Extra Extra Extra Extra
Code Bits Length(s) Code Bits Lengths Code Bits Length(s) Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
---- ---- ------ ---- ---- ------- ---- ---- ------- ---- ---- ------ ---- ---- ------- ---- ---- -------
257 0 3 267 1 15,16 277 4 67-82 257 0 3 267 1 15,16 277 4 67-82
258 0 4 268 1 17,18 278 4 83-98 258 0 4 268 1 17,18 278 4 83-98
259 0 5 269 2 19-22 279 4 99-114 259 0 5 269 2 19-22 279 4 99-114
260 0 6 270 2 23-26 280 4 115-130 260 0 6 270 2 23-26 280 4 115-130
261 0 7 271 2 27-30 281 5 131-162 261 0 7 271 2 27-30 281 5 131-162
262 0 8 272 2 31-34 282 5 163-194 262 0 8 272 2 31-34 282 5 163-194
263 0 9 273 3 35-42 283 5 195-226 263 0 9 273 3 35-42 283 5 195-226
264 0 10 274 3 43-50 284 5 227-257 264 0 10 274 3 43-50 284 5 227-257
265 1 11,12 275 3 51-58 285 0 258 265 1 11,12 275 3 51-58 285 0 258
266 1 13,14 276 3 59-66 266 1 13,14 276 3 59-66
*/ */
var LengthLookupTable = [ var LengthLookupTable = [
[0,3], [0,4], [0,5], [0,6], [0, 3], [0, 4], [0, 5], [0, 6],
[0,7], [0,8], [0,9], [0,10], [0, 7], [0, 8], [0, 9], [0, 10],
[1,11], [1,13], [1,15], [1,17], [1, 11], [1, 13], [1, 15], [1, 17],
[2,19], [2,23], [2,27], [2,31], [2, 19], [2, 23], [2, 27], [2, 31],
[3,35], [3,43], [3,51], [3,59], [3, 35], [3, 43], [3, 51], [3, 59],
[4,67], [4,83], [4,99], [4,115], [4, 67], [4, 83], [4, 99], [4, 115],
[5,131], [5,163], [5,195], [5,227], [5, 131], [5, 163], [5, 195], [5, 227],
[0,258] [0, 258]
]; ];
/* /*
Extra Extra Extra Extra Extra Extra
Code Bits Dist Code Bits Dist Code Bits Distance Code Bits Dist Code Bits Dist Code Bits Distance
---- ---- ---- ---- ---- ------ ---- ---- -------- ---- ---- ---- ---- ---- ------ ---- ---- --------
0 0 1 10 4 33-48 20 9 1025-1536 0 0 1 10 4 33-48 20 9 1025-1536
1 0 2 11 4 49-64 21 9 1537-2048 1 0 2 11 4 49-64 21 9 1537-2048
2 0 3 12 5 65-96 22 10 2049-3072 2 0 3 12 5 65-96 22 10 2049-3072
3 0 4 13 5 97-128 23 10 3073-4096 3 0 4 13 5 97-128 23 10 3073-4096
4 1 5,6 14 6 129-192 24 11 4097-6144 4 1 5,6 14 6 129-192 24 11 4097-6144
5 1 7,8 15 6 193-256 25 11 6145-8192 5 1 7,8 15 6 193-256 25 11 6145-8192
6 2 9-12 16 7 257-384 26 12 8193-12288 6 2 9-12 16 7 257-384 26 12 8193-12288
7 2 13-16 17 7 385-512 27 12 12289-16384 7 2 13-16 17 7 385-512 27 12 12289-16384
8 3 17-24 18 8 513-768 28 13 16385-24576 8 3 17-24 18 8 513-768 28 13 16385-24576
9 3 25-32 19 8 769-1024 29 13 24577-32768 9 3 25-32 19 8 769-1024 29 13 24577-32768
*/ */
var DistLookupTable = [ var DistLookupTable = [
[0,1], [0,2], [0,3], [0,4], [0, 1], [0, 2], [0, 3], [0, 4],
[1,5], [1,7], [1, 5], [1, 7],
[2,9], [2,13], [2, 9], [2, 13],
[3,17], [3,25], [3, 17], [3, 25],
[4,33], [4,49], [4, 33], [4, 49],
[5,65], [5,97], [5, 65], [5, 97],
[6,129], [6,193], [6, 129], [6, 193],
[7,257], [7,385], [7, 257], [7, 385],
[8,513], [8,769], [8, 513], [8, 769],
[9,1025], [9,1537], [9, 1025], [9, 1537],
[10,2049], [10,3073], [10, 2049], [10, 3073],
[11,4097], [11,6145], [11, 4097], [11, 6145],
[12,8193], [12,12289], [12, 8193], [12, 12289],
[13,16385], [13,24577] [13, 16385], [13, 24577]
]; ];
function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
@ -446,10 +447,9 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
stream, and copy length bytes from this stream, and copy length bytes from this
position to the output stream. position to the output stream.
*/ */
var numSymbols = 0, blockSize = 0; var blockSize = 0;
for (;;) { for (;;) {
var symbol = decodeSymbol(bstream, hcLiteralTable); var symbol = decodeSymbol(bstream, hcLiteralTable);
++numSymbols;
if (symbol < 256) { if (symbol < 256) {
// copy literal byte to output // copy literal byte to output
buffer.insertByte(symbol); buffer.insertByte(symbol);
@ -461,7 +461,7 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
break; break;
} }
else { else {
var lengthLookup = LengthLookupTable[symbol-257], var lengthLookup = LengthLookupTable[symbol - 257],
length = lengthLookup[1] + bstream.readBits(lengthLookup[0]), length = lengthLookup[1] + bstream.readBits(lengthLookup[0]),
distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)], distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)],
distance = distLookup[1] + bstream.readBits(distLookup[0]); distance = distLookup[1] + bstream.readBits(distLookup[0]);
@ -479,13 +479,13 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
// loop for each character // loop for each character
var ch = buffer.ptr - distance; var ch = buffer.ptr - distance;
blockSize += length; blockSize += length;
if(length > distance) { if (length > distance) {
var data = buffer.data; var data = buffer.data;
while (length--) { while (length--) {
buffer.insertByte(data[ch++]); buffer.insertByte(data[ch++]);
} }
} else { } else {
buffer.insertBytes(buffer.data.subarray(ch, ch + length)) buffer.insertBytes(buffer.data.subarray(ch, ch + length));
} }
} // length-distance pair } // length-distance pair
@ -494,6 +494,16 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
return blockSize; return blockSize;
} }
function zeroCompression(compressedData, numDecompressedBytes) {
var bstream = new bitjs.io.BitStream(compressedData.buffer,
false /* rtl */,
compressedData.byteOffset,
compressedData.byteLength);
var buffer = new bitjs.io.ByteBuffer(numDecompressedBytes);
buffer.insertBytes(bstream.readBytes(numDecompressedBytes));
return buffer.data;
}
// {Uint8Array} compressedData A Uint8Array of the compressed file data. // {Uint8Array} compressedData A Uint8Array of the compressed file data.
// compression method 8 // compression method 8
// deflate: http://tools.ietf.org/html/rfc1951 // deflate: http://tools.ietf.org/html/rfc1951
@ -516,11 +526,11 @@ function inflate(compressedData, numDecompressedBytes) {
if (bType == 0) { if (bType == 0) {
// skip remaining bits in this byte // skip remaining bits in this byte
while (bstream.bitPtr != 0) bstream.readBits(1); while (bstream.bitPtr != 0) bstream.readBits(1);
var len = bstream.readBits(16), var len = bstream.readBits(16);
nlen = bstream.readBits(16); bstream.readBits(16);
// TODO: check if nlen is the ones-complement of len? // TODO: check if nlen is the ones-complement of len?
if(len > 0) buffer.insertBytes(bstream.readBytes(len)); if (len > 0) buffer.insertBytes(bstream.readBytes(len));
blockSize = len; blockSize = len;
} }
// fixed Huffman codes // fixed Huffman codes
@ -573,14 +583,13 @@ function inflate(compressedData, numDecompressedBytes) {
} }
} }
else if (symbol == 17) { else if (symbol == 17) {
var repeat = bstream.readBits(3) + 3; var repeat1 = bstream.readBits(3) + 3;
while (repeat--) { while (repeat1--) {
literalCodeLengths.push(0); literalCodeLengths.push(0);
} }
} } else if (symbol == 18) {
else if (symbol == 18) { var repeat2 = bstream.readBits(7) + 11;
var repeat = bstream.readBits(7) + 11; while (repeat2--) {
while (repeat--) {
literalCodeLengths.push(0); literalCodeLengths.push(0);
} }
} }
@ -593,9 +602,8 @@ function inflate(compressedData, numDecompressedBytes) {
var hcLiteralTable = getHuffmanCodes(literalCodeLengths), var hcLiteralTable = getHuffmanCodes(literalCodeLengths),
hcDistanceTable = getHuffmanCodes(distanceCodeLengths); hcDistanceTable = getHuffmanCodes(distanceCodeLengths);
blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer); blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer);
} } else {
// error // error
else {
err("Error! Encountered deflate block of type 3"); err("Error! Encountered deflate block of type 3");
return null; return null;
} }

@ -0,0 +1,202 @@
/*
* bootstrap-uploadprogress
* github: https://github.com/jakobadam/bootstrap-uploadprogress
*
* Copyright (c) 2015 Jakob Aarøe Dam
* Version 1.0.0
* Licensed under the MIT license.
*/
(function($) {
"use strict";
$.support.xhrFileUpload = !!(window.FileReader && window.ProgressEvent);
$.support.xhrFormData = !!window.FormData;
if (!$.support.xhrFileUpload || !$.support.xhrFormData) {
// skip decorating form
return;
}
var template = "<div class=\"modal fade\" id=\"file-progress-modal\">" +
"<div class=\"modal-dialog upload-modal-dialog\">" +
" <div class=\"modal-content\">" +
" <div class=\"modal-header\">" +
" <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>" +
" <h4 class=\"modal-title\">Uploading</h4>" +
" </div>" +
" <div class=\"modal-body\">" +
" <div class=\"modal-message\"></div>" +
" <div class=\"progress\">" +
" <div class=\"progress-bar progress-bar-striped active\" role=\"progressbar\" aria-valuenow=\"0\" aria-valuemin=\"0\"" +
" aria-valuemax=\"100\" style=\"width: 0%;min-width: 2em;\">" +
" 0%" +
" </div>" +
" </div>" +
" </div>" +
" <div class=\"modal-footer\" style=\"display:none\">" +
" <button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>" +
" </div>" +
" </div>" +
" </div>" +
"</div>";
var UploadProgress = function(element, options) {
this.options = options;
this.$element = $(element);
};
UploadProgress.prototype = {
constructor: function() {
this.$form = this.$element;
this.$form.on("submit", $.proxy(this.submit, this));
this.$modal = $(this.options.template);
this.$modalTitle = this.$modal.find(".modal-title");
this.$modalFooter = this.$modal.find(".modal-footer");
this.$modalBar = this.$modal.find(".progress-bar");
// Translate texts
this.$modalTitle.text(this.options.modalTitle)
this.$modalFooter.children("button").text(this.options.modalFooter);
this.$modal.on("hidden.bs.modal", $.proxy(this.reset, this));
},
reset: function() {
this.$modalTitle.text(this.options.modalTitle);
this.$modalFooter.hide();
this.$modalBar.addClass("progress-bar-success");
this.$modalBar.removeClass("progress-bar-danger");
if (this.xhr) {
this.xhr.abort();
}
},
submit: function(e) {
e.preventDefault();
this.$modal.modal({
backdrop: "static",
keyboard: false
});
// We need the native XMLHttpRequest for the progress event
var xhr = new XMLHttpRequest();
this.xhr = xhr;
xhr.addEventListener("load", $.proxy(this.success, this, xhr));
xhr.addEventListener("error", $.proxy(this.error, this, xhr));
xhr.upload.addEventListener("progress", $.proxy(this.progress, this));
var form = this.$form;
xhr.open(form.attr("method"), form.attr("action"));
xhr.setRequestHeader("X-REQUESTED-WITH", "XMLHttpRequest");
var data = new FormData(form.get(0));
xhr.send(data);
},
success: function(xhr) {
if (xhr.status === 0 || xhr.status >= 400) {
// HTTP 500 ends up here!?!
return this.error(xhr);
}
this.setProgress(100);
var url;
var contentType = xhr.getResponseHeader("Content-Type");
// make it possible to return the redirect URL in
// a JSON response
if (contentType.indexOf("application/json") !== -1) {
var response = $.parseJSON(xhr.responseText);
url = response.location;
}
else{
url = this.options.redirect_url;
}
window.location.href = url;
},
// handle form error
// we replace the form with the returned one
error: function(xhr) {
this.$modalTitle.text(this.options.modalTitleFailed);
this.$modalBar.removeClass("progress-bar-success");
this.$modalBar.addClass("progress-bar-danger");
this.$modalFooter.show();
var contentType = xhr.getResponseHeader("Content-Type");
// Replace the contents of the form, with the returned html
if (xhr.status === 422) {
var newHtml = $.parseHTML(xhr.responseText);
this.replaceForm(newHtml);
this.$modal.modal("hide");
}
// Write the error response to the document.
else{
// Handle no response error
if (contentType) {
var responseText = xhr.responseText;
if (contentType.indexOf("text/plain") !== -1) {
responseText = "<pre>" + responseText + "</pre>";
}
document.write(responseText);
}
}
},
setProgress: function(percent) {
var txt = percent + "%";
if (percent === 100) {
txt = this.options.uploadedMsg;
}
this.$modalBar.attr("aria-valuenow", percent);
this.$modalBar.text(txt);
this.$modalBar.css("width", percent + "%");
},
progress: function(/*ProgressEvent*/e) {
var percent = Math.round((e.loaded / e.total) * 100);
this.setProgress(percent);
},
// replaceForm replaces the contents of the current form
// with the form in the html argument.
// We use the id of the current form to find the new form in the html
replaceForm: function(html) {
var newForm;
var formId = this.$form.attr("id");
if ( typeof formId !== "undefined") {
newForm = $(html).find("#" + formId);
} else {
newForm = $(html).find("form");
}
// add the filestyle again
newForm.find(":file").filestyle({buttonBefore: true});
this.$form.html(newForm.children());
}
};
$.fn.uploadprogress = function(options) {
return this.each(function() {
var _options = $.extend({}, $.fn.uploadprogress.defaults, options);
var fileProgress = new UploadProgress(this, _options);
fileProgress.constructor();
});
};
$.fn.uploadprogress.defaults = {
template: template,
uploadedMsg: "Upload done, processing, please wait...",
modalTitle: "Uploading",
modalFooter: "Close",
modalTitleFailed: "Upload failed"
//redirect_url: ...
// need to customize stuff? Add here, and change code accordingly.
};
})(window.jQuery);

@ -105,7 +105,7 @@
<div class="col"> <div class="col">
<h2>{{_('Administration')}}</h2> <h2>{{_('Administration')}}</h2>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div> <div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" id="admin_restart"data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div> <div class="btn btn-default" id="admin_restart" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-Web')}}</div>
<div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div> <div class="btn btn-default" id="admin_stop" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-Web')}}</div>
</div> </div>
</div> </div>
@ -144,7 +144,7 @@
<div class="modal-body text-center"> <div class="modal-body text-center">
<p>{{_('Do you really want to restart Calibre-Web?')}}</p> <p>{{_('Do you really want to restart Calibre-Web?')}}</p>
<div id="spinner" class="spinner" style="display:none;"> <div id="spinner" class="spinner" style="display:none;">
<img id="img-spinner" src="/static/css/images/loading-icon.gif"/> <img id="img-spinner" src="{{ url_for('static', filename='css/images/loading-icon.gif') }}"/>
</div> </div>
<p></p> <p></p>
<button type="button" class="btn btn-default" id="restart" >{{_('Ok')}}</button> <button type="button" class="btn btn-default" id="restart" >{{_('Ok')}}</button>
@ -176,7 +176,7 @@
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">
<div id="spinner2" class="spinner2" style="display:none;"> <div id="spinner2" class="spinner2" style="display:none;">
<img id="img-spinner" src="/static/css/images/loading-icon.gif"/> <img id="img-spinner2" src="{{ url_for('static', filename='css/images/loading-icon.gif') }}"/>
</div> </div>
<p></p> <p></p>
<div id="Updatecontent"></div> <div id="Updatecontent"></div>

@ -29,7 +29,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}"> <a href="{{ url_for('show_book', book_id=entry.id) }}">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" />
{% endif %} {% endif %}
@ -41,10 +41,20 @@
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.last %} {% if not loop.first %}
&amp; <span class="author-hidden-divider">&amp;</span>
{% endif %} {% endif %}
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %}
{% endfor %} {% endfor %}
</p> </p>
{% if entry.ratings.__len__() > 0 %} {% if entry.ratings.__len__() > 0 %}
@ -80,13 +90,15 @@
<div class="meta"> <div class="meta">
<p class="title">{{entry.title|shortentitle}}</p> <p class="title">{{entry.title|shortentitle}}</p>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener"> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{{author.name.replace('|',',')}} <a class="author-name author-hidden" href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">{{author.name.replace('|',',')}}</a>
</a> {% if loop.last %}
{% if not loop.last %} <a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
&amp; {% endif %}
{% endif %} {% else %}
<a class="author-name" href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">{{author.name.replace('|',',')}}</a>
{% endif %}
{% endfor %} {% endfor %}
</p> </p>
<div class="rating"> <div class="rating">

@ -6,7 +6,7 @@
<div class="col-sm-3 col-lg-3 col-xs-12"> <div class="col-sm-3 col-lg-3 col-xs-12">
<div class="cover"> <div class="cover">
{% if book.has_cover %} {% if book.has_cover %}
<img src="{{ url_for('get_cover', cover_path=book.path.replace('\\','/')) }}" alt="{{ book.title }}"/> <img src="{{ url_for('get_cover', book_id=book.id) }}" alt="{{ book.title }}"/>
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
{% endif %} {% endif %}
@ -219,7 +219,7 @@
</span> </span>
</div> </div>
</form> </form>
<div>{{_('Click the cover to load metadata to the form')}}</div> <div class="text-center"><strong>{{_('Click the cover to load metadata to the form')}}</strong></div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="text-center padded-bottom"> <div class="text-center padded-bottom">

@ -31,15 +31,19 @@
</label> </label>
</div> </div>
{% else %} {% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated %} {% if show_authenticate_google_drive and g.user.is_authenticated and content.config_use_google_drive %}
<div class="form-group required"> <div class="form-group required">
<a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a> <a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div> </div>
{% else %} {% else %}
{% if show_authenticate_google_drive and not g.user.is_authenticated %} {% if show_authenticate_google_drive and g.user.is_authenticated and not content.config_use_google_drive %}
<div >{{_('Please hit submit to continue with setup')}}</div>
{% endif %}
{% if not g.user.is_authenticated %}
<div >{{_('Please finish Google Drive setup after login')}}</div> <div >{{_('Please finish Google Drive setup after login')}}</div>
{% endif %} {% endif %}
{% if not show_authenticate_google_drive %} {% if g.user.is_authenticated %}
{% if not show_authenticate_google_drive %}
<div class="form-group required"> <div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label> <label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
<select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control"> <select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control">
@ -58,6 +62,7 @@
<a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a> <a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@ -76,7 +81,7 @@
</div> </div>
<div id="collapsetwo" class="panel-collapse collapse"> <div id="collapsetwo" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<div class="form-group"> <div class="form-group">
<label for="config_port">{{_('Server Port')}}</label> <label for="config_port">{{_('Server Port')}}</label>
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required> <input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required>
</div> </div>
@ -88,6 +93,15 @@
<label for="config_keyfile">{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label> <label for="config_keyfile">{{_('SSL Keyfile location (leave it empty for non-SSL Servers)')}}</label>
<input type="text" class="form-control" name="config_keyfile" id="config_keyfile" value="{% if content.config_keyfile != None %}{{ content.config_keyfile }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" name="config_keyfile" id="config_keyfile" value="{% if content.config_keyfile != None %}{{ content.config_keyfile }}{% endif %}" autocomplete="off">
</div> </div>
<div class="form-group">
<label for="config_updater">{{_('Update channel')}}</label>
<select name="config_updater" id="config_updater" class="form-control">
<option value="0" {% if content.config_updatechannel == 0 %}selected{% endif %}>{{_('Stable')}}</option>
<!--option value="1" {% if content.config_updatechannel == 1 %}selected{% endif %}>{{_('Stable (Automatic)')}}</option-->
<option value="2" {% if content.config_updatechannel == 2 %}selected{% endif %}>{{_('Nightly')}}</option>
<!--option-- value="3" {% if content.config_updatechannel == 3 %}selected{% endif %}>{{_('Nightly (Automatic)')}}</option-->
</select>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -27,6 +27,17 @@
<label for="config_random_books">{{_('No. of random books to show')}}</label> <label for="config_random_books">{{_('No. of random books to show')}}</label>
<input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off"> <input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off">
</div> </div>
<div class="form-group">
<label for="config_authors_max">{{_('No. of authors to show before hiding (0=disable hiding)')}}</label>
<input type="number" min="0" max="999" class="form-control" name="config_authors_max" id="config_authors_max" value="{% if content.config_authors_max != None %}{{ content.config_authors_max }}{% endif %}" autocomplete="off">
</div>
<div class="form-group">
<label for="config_theme">{{_('Theme')}}</label>
<select name="config_theme" id="config_theme" class="form-control">
<option value="0" {% if content.config_theme == 0 %}selected{% endif %}>{{ _("Standard Theme") }}</option>
<option value="1" {% if content.config_theme == 1 %}selected{% endif %}>{{ _("caliBlur! Dark Theme") }}</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="config_columns_to_ignore">{{_('Regular expression for ignoring columns')}}</label> <label for="config_columns_to_ignore">{{_('Regular expression for ignoring columns')}}</label>
<input type="text" class="form-control" name="config_columns_to_ignore" id="config_columns_to_ignore" value="{% if content.config_columns_to_ignore != None %}{{ content.config_columns_to_ignore }}{% endif %}" autocomplete="off"> <input type="text" class="form-control" name="config_columns_to_ignore" id="config_columns_to_ignore" value="{% if content.config_columns_to_ignore != None %}{{ content.config_columns_to_ignore }}{% endif %}" autocomplete="off">

@ -5,7 +5,7 @@
<div class="col-sm-3 col-lg-3 col-xs-5"> <div class="col-sm-3 col-lg-3 col-xs-5">
<div class="cover"> <div class="cover">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}
@ -18,7 +18,7 @@
{% if entry.data|length %} {% if entry.data|length %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if entry.data|length < 2 %} {% if entry.data|length < 2 %}
<button type="button" class="btn btn-primary"> <button id="Download" type="button" class="btn btn-primary">
{{_('Download')}} : {{_('Download')}} :
</button> </button>
{% for format in entry.data %} {% for format in entry.data %}
@ -40,20 +40,32 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if g.user.kindle_mail and g.user.is_authenticated %} {% if g.user.kindle_mail and g.user.is_authenticated and kindle_list %}
<a href="{{url_for('send_to_kindle', book_id=entry.id)}}" id="sendbtn" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{_('Send to Kindle')}}</a> {% if kindle_list.__len__() == 1 %}
<a href="{{url_for('send_to_kindle', book_id=entry.id, book_format=kindle_list[0]['format'], convert=kindle_list[0]['convert'])}}" id="sendbtn" data-text="{{_('Send to Kindle')}}" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{kindle_list[0]['text']}}</a>
{% else %}
<div class="btn-group" role="group">
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-send"></span>{{_('Send to Kindle')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="send-to-kindle">
{% for format in kindle_list %}
<li><a href="{{url_for('send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
{%endfor%}
</ul>
</div>
{% endif %}
{% endif %} {% endif %}
{% if entry.data|length %} {% if reader_list %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button id="read-in-browser" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in browser')}} <span class="glyphicon glyphicon-eye-open"></span> {{_('Read in browser')}}
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="read-in-browser"> <ul class="dropdown-menu" aria-labelledby="read-in-browser">
{% for format in entry.data %} {% for format in reader_list %}
{%if format.format|lower == 'epub' or format.format|lower == 'txt' or format.format|lower == 'pdf' or format.format|lower == 'cbr' or format.format|lower == 'cbt' or format.format|lower == 'cbz' %} <li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
<li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format.format|lower) }}">{{format.format}}</a></li>
{% endif %}
{%endfor%} {%endfor%}
</ul> </ul>
</div> </div>
@ -91,7 +103,7 @@
{% if entry.languages.__len__() > 0 %} {% if entry.languages.__len__() > 0 %}
<div class="languages"> <div class="languages">
<p> <p>
<span class="label label-default">{{_('language')}}: {% for language in entry.languages %} {{language.language_name}}{% if not loop.last %},{% endif %}{% endfor %} </span> <span class="label label-default">{{_('language')}}: {% for language in entry.languages %}{{language.language_name}}{% if not loop.last %}, {% endif %}{% endfor %}</span>
</p> </p>
</div> </div>
{% endif %} {% endif %}
@ -132,7 +144,9 @@
{% endif %} {% endif %}
{% if entry.pubdate[:10] != '0101-01-01' %} {% if entry.pubdate[:10] != '0101-01-01' %}
<div class="publishing-date">
<p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p> <p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p>
</div>
{% endif %} {% endif %}
{% if cc|length > 0 %} {% if cc|length > 0 %}
@ -168,7 +182,7 @@
<p> <p>
<form id="have_read_form" action="{{ url_for('toggle_read', book_id=entry.id)}}" method="POST"> <form id="have_read_form" action="{{ url_for('toggle_read', book_id=entry.id)}}" method="POST">
<label class="block-label"> <label class="block-label">
<input id="have_read_cb" type="checkbox" {% if have_read %}checked{% endif %} > <input id="have_read_cb" data-checked="{{_('Mark As Unread')}}" data-unchecked="{{_('Mark As Read')}}" type="checkbox" {% if have_read %}checked{% endif %} >
<span>{{_('Read')}}</span> <span>{{_('Read')}}</span>
</label> </label>
</form> </form>

@ -9,7 +9,7 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -19,9 +19,19 @@
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.last %} {% if not loop.first %}
&amp; <span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -12,8 +12,8 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
{% if g.user.get_theme == 1 %} {% if g.current_theme == 1 %}
<link href="{{ url_for('static', filename='css/caliBlur-style.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen">
{% endif %} {% endif %}
</head> </head>
<body> <body>

@ -1,8 +1,8 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
{% if g.user.show_detail_random() %} {% if g.user.show_detail_random() %}
<div class="discover"> <div class="discover random-books">
<h2>{{_('Discover (Random Books)')}}</h2> <h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
<div class="row"> <div class="row">
{% for entry in random %} {% for entry in random %}
@ -10,7 +10,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}
@ -22,9 +22,19 @@
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.last %} {% if not loop.first %}
&amp; <span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>
@ -47,7 +57,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="discover load-more"> <div class="discover load-more">
<h2>{{title}}</h2> <h2 class="{{title}}">{{_(title)}}</h2>
<div class="row"> <div class="row">
{% if entries[0] %} {% if entries[0] %}
{% for entry in entries %} {% for entry in entries %}
@ -55,7 +65,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}"/> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}"/>
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}
@ -67,9 +77,19 @@
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.last %} {% if not loop.first %}
&amp; <span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -12,8 +12,9 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/libs/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" media="screen">
{% if g.user.get_theme == 1 %} <link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/caliBlur-style.css') }}" rel="stylesheet" media="screen"> {% if g.current_theme == 1 %}
<link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen">
{% endif %} {% endif %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
@ -24,7 +25,7 @@
{% block header %}{% endblock %} {% block header %}{% endblock %}
</head> </head>
<body class="{{ page }}"> <body class="{{ page }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}">
<!-- Static navbar --> <!-- Static navbar -->
<div class="navbar navbar-default navbar-static-top" role="navigation"> <div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid"> <div class="container-fluid">
@ -61,21 +62,22 @@
<li> <li>
<form id="form-upload" class="navbar-form" action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data"> <form id="form-upload" class="navbar-form" action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="form-group">
<span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload" type="file" multiple></span> <span class="btn btn-default btn-file">{{_('Upload')}}<input id="btn-upload" name="btn-upload"
type="file" accept="{% for format in accept %}.{{format}}{{ ',' if not loop.last }}{% endfor %}" multiple></span>
</div> </div>
</form> </form>
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if g.user.role_admin() %} {% if not g.user.is_anonymous %}
<li><a id="top_tasks" href="{{url_for('get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span><span class="hidden-sm"> {{_('Tasks')}}</span></a></li> <li><a id="top_tasks" href="{{url_for('get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span><span class="hidden-sm">{{_('Tasks')}}</span></a></li>
{% endif %} {% endif %}
{% if g.user.role_admin() %} {% if g.user.role_admin() %}
<li><a id="top_admin" href="{{url_for('admin')}}"><span class="glyphicon glyphicon-dashboard"></span><span class="hidden-sm"> {{_('Admin')}}</span></a></li> <li><a id="top_admin" data-text="{{_('Settings')}}" href="{{url_for('admin')}}"><span class="glyphicon glyphicon-dashboard"></span><span class="hidden-sm">{{_('Admin')}}</span></a></li>
{% endif %} {% endif %}
<li><a id="top_user" href="{{url_for('profile')}}"><span class="glyphicon glyphicon-user"></span><span class="hidden-sm"> {{g.user.nickname}}</span></a></li> <li><a id="top_user" data-text="{{_('Account')}}" href="{{url_for('profile')}}"><span class="glyphicon glyphicon-user"></span><span class="hidden-sm">{{g.user.nickname}}</span></a></li>
{% if not g.user.is_anonymous %} {% if not g.user.is_anonymous %}
<li><a id="logout" href="{{url_for('logout')}}"><span class="glyphicon glyphicon-log-out"></span><span class="hidden-sm"> {{_('Logout')}}</span></a></li> <li><a id="logout" href="{{url_for('logout')}}"><span class="glyphicon glyphicon-log-out"></span><span class="hidden-sm">{{_('Logout')}}</span></a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if g.allow_registration and not g.user.is_authenticated %} {% if g.allow_registration and not g.user.is_authenticated %}
@ -103,14 +105,14 @@
</div> </div>
{%endif%} {%endif%}
{% endfor %} {% endfor %}
<div id="loader" hidden="true"> {% if g.current_theme == 1 %}
<center> <div id="loader" hidden="true">
<h3>{{_('Uploading...')}}</h3> <center>
<span>{{_("please don't refresh the page")}}</span>. <h3>{{_('Uploading...')}}</h3>
<br /> <span>{{_("please don't refresh the page")}}</span>.
<img src="{{ url_for('static', filename='img/loader.gif') }}"> </center>
</center> </div>
</div> {%endif%}
<div class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row-fluid">
{% if g.user.is_authenticated or g.user.is_anonymous %} {% if g.user.is_authenticated or g.user.is_anonymous %}
@ -166,11 +168,11 @@
<li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span>{{_('Languages')}} </a></li> <li id="nav_lang" {% if page == 'language' %}class="active"{% endif %}><a href="{{url_for('language_overview')}}"><span class="glyphicon glyphicon-flag"></span>{{_('Languages')}} </a></li>
{%endif%} {%endif%}
{% if g.user.is_authenticated or g.user.is_anonymous %} {% if g.user.is_authenticated or g.user.is_anonymous %}
<li class="nav-head hidden-xs">{{_('Public Shelves')}}</li> <li class="nav-head hidden-xs public-shelves">{{_('Public Shelves')}}</li>
{% for shelf in g.public_shelfes %} {% for shelf in g.public_shelfes %}
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li> <li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list public_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
{% endfor %} {% endfor %}
<li class="nav-head hidden-xs">{{_('Your Shelves')}}</li> <li class="nav-head hidden-xs your-shelves">{{_('Your Shelves')}}</li>
{% for shelf in g.user.shelf %} {% for shelf in g.user.shelf %}
<li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li> <li><a href="{{url_for('show_shelf', shelf_id=shelf.id)}}"><span class="glyphicon glyphicon-list private_shelf"></span>{{shelf.name|shortentitle(40)}}</a></li>
{% endfor %} {% endfor %}
@ -240,13 +242,26 @@
<script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/plugins.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/uploadprogress.js') }}"> </script>
{% if g.current_theme == 1 %}
<script src="{{ url_for('static', filename='js/libs/jquery.visible.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/compromise.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/readmore.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/caliBlur.js') }}"></script>
{% endif %}
<script type="text/javascript"> <script type="text/javascript">
$(function () { $(function() {
$("#btn-upload").change(function () { $("#form-upload").uploadprogress({
$("#loader").show(); redirect_url: "{{ url_for('index')}}",
$("#form-upload").submit(); uploadedMsg: "{{_('Upload done, processing, please wait...')}}",
}); modalTitle: "{{_('Uploading...')}}",
}); modalFooter: "{{_('Close')}}",
modalTitleFailed: "{{_('Error')}}"
});
$("#btn-upload").change(function() {
$("#form-upload").submit();
});
});
</script> </script>
{% block js %}{% endblock %} {% block js %}{% endblock %}

@ -1,6 +1,6 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h1>{{title}}</h1> <h1 class="{{page}}">{{_(title)}}</h1>
<div class="container"> <div class="container">
<div class="col-xs-12 col-sm-6"> <div class="col-xs-12 col-sm-6">
{% for entry in entries %} {% for entry in entries %}

@ -6,7 +6,7 @@
<Developer>Janeczku</Developer> <Developer>Janeczku</Developer>
<Contact>https://github.com/janeczku/calibre-web</Contact> <Contact>https://github.com/janeczku/calibre-web</Contact>
<Url type="text/html" <Url type="text/html"
template="{{url_for('search')}}?query={searchTerms}"/> template="{{url_for('feed_cc_search')}}{searchTerms}"/>
<Url type="application/atom+xml" <Url type="application/atom+xml"
template="{{url_for('feed_normal_search')}}?query={searchTerms}"/> template="{{url_for('feed_normal_search')}}?query={searchTerms}"/>
<SyndicationRight>open</SyndicationRight> <SyndicationRight>open</SyndicationRight>

@ -7,6 +7,8 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, user-scalable=no"> <meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="140x140" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/libs/normalize.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">

@ -41,16 +41,11 @@ See https://github.com/adobe-type-tools/cmap-resources
<!--<link rel="resource" type="application/l10n" href="locale/locale.properties">--> <!--<link rel="resource" type="application/l10n" href="locale/locale.properties">-->
<link rel="resource" type="application/l10n" href="{{ url_for('static', filename='locale/locale.properties') }}"> <link rel="resource" type="application/l10n" href="{{ url_for('static', filename='locale/locale.properties') }}">
<script src="{{ url_for('static', filename='js/libs/l10n.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/l10n.js') }}"></script>
<!--<script src="l10n.js"></script>-->
<!--script src="{{ url_for('static', filename='js/libs/debugger.js') }}"></script-->
<!--<script src="debugger.js"></script>-->
<script src="{{ url_for('static', filename='js/libs/pdf.js') }}"></script> <script src="{{ url_for('static', filename='js/libs/pdf.js') }}"></script>
<!--<script src="pdf.js"></script>-->
<script type="text/javascript"> <script type="text/javascript">
var DEFAULT_URL = "{{ url_for('serve_book', book_id=pdffile, book_format='pdf') }}"; var DEFAULT_URL = "{{ url_for('serve_book', book_id=pdffile, book_format='pdf') }}";
var PDFWORKER_LOCATION="{{ url_for('static', filename='js/libs/pdf.worker.js') }}"; var PDFWORKER_LOCATION="{{ url_for('static', filename='js/libs/pdf.worker.js') }}";
// var IMAGE_LOCATION="{{ url_for('static', filename='css/../images') }}";
var IMAGE_LOCATION="{{ url_for('static', filename='/images/') }}"; var IMAGE_LOCATION="{{ url_for('static', filename='/images/') }}";
var PDFWORKER_LOCATION_JS="{{ url_for('static', filename='js/libs/pdf.worker') }}"; var PDFWORKER_LOCATION_JS="{{ url_for('static', filename='js/libs/pdf.worker') }}";
</script> </script>
@ -420,8 +415,7 @@ See https://github.com/adobe-type-tools/cmap-resources
} }
</style> </style>
<div class="mozPrintCallback-dialog-box"> <div class="mozPrintCallback-dialog-box">
<!-- TODO: Localise the following strings --> {{_('Preparing document for printing...')}}
Preparing document for printing...
<div class="progress-row"> <div class="progress-row">
<progress value="0" max="100"></progress> <progress value="0" max="100"></progress>
<span class="relative-progress">0%</span> <span class="relative-progress">0%</span>

@ -36,7 +36,7 @@
<div class="cover"> <div class="cover">
{% if entry.has_cover is defined %} {% if entry.has_cover is defined %}
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -46,9 +46,19 @@
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.last %} {% if not loop.first %}
&amp; <span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -141,10 +141,7 @@
{% endif %} {% endif %}
{% if c.datatype == 'rating' %} {% if c.datatype == 'rating' %}
<input type="number" min="1" max="5" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}" <input type="number" min="1" max="5" step="1" class="form-control" name="{{ 'custom_column_' ~ c.id }}" id="{{ 'custom_column_' ~ c.id }}">
{% if book['custom_column_' ~ c.id]|length > 0 %}
value="{{ '%d' % (book['custom_column_' ~ c.id][0].value / 2) }}"
{% endif %}>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}

@ -16,7 +16,7 @@
<div class="cover"> <div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false"> <a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="{{ entry.title }}" /> <img src="{{ url_for('get_cover', book_id=entry.id) }}" alt="{{ entry.title }}" />
{% else %} {% else %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" /> <img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %} {% endif %}
@ -28,9 +28,19 @@
</a> </a>
<p class="author"> <p class="author">
{% for author in entry.authors %} {% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a> {% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.last %} {% if not loop.first %}
&amp; <span class="author-hidden-divider">&amp;</span>
{% endif %}
<a class="author-name author-hidden" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% endif %}
{% else %}
{% if not loop.first %}
<span>&amp;</span>
{% endif %}
<a class="author-name" href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>

@ -16,7 +16,7 @@
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %} {% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
{% if g.user and g.user.role_admin() and g.allow_registration and not new_user and not profile %} {% if g.user and g.user.role_admin() and g.allow_registration and not new_user and not profile %}
<div class="btn btn-default" id="resend_password"><a href="{{url_for('reset_password', user_id = content.id) }}">{{_('Reset user Password')}}</a></div> <div class="btn btn-default" id="resend_password"><a href="{{url_for('reset_password', user_id = content.id) }}">{{_('Reset user Password')}}</a></div>
{% else %} {% else %}
<div class="form-group"> <div class="form-group">
<label for="password">{{_('Password')}}</label> <label for="password">{{_('Password')}}</label>
<input type="password" class="form-control" name="password" id="password" value="" autocomplete="off"> <input type="password" class="form-control" name="password" id="password" value="" autocomplete="off">
@ -35,14 +35,6 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-group">
<label for="theme">{{_('Theme')}}</label>
<select name="theme" id="theme" class="form-control">
<option value="0" {% if content.get_theme == 0 %}selected{% endif %}>{{ _("Standard Theme") }}</option>
<option value="1" {% if content.get_theme == 1 %}selected{% endif %}>{{ _("caliBlur! Dark Theme (Beta)") }}</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="default_language">{{_('Show books with language')}}</label> <label for="default_language">{{_('Show books with language')}}</label>
<select name="default_language" id="default_language" class="form-control"> <select name="default_language" id="default_language" class="form-control">
@ -93,10 +85,12 @@
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}> <input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
<label for="show_publisher">{{_('Show publisher selection')}}</label> <label for="show_publisher">{{_('Show publisher selection')}}</label>
</div> </div>
{% if not content.role_anonymous() %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}> <input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label> <label for="show_read_and_unread">{{_('Show read and unread')}}</label>
</div> </div>
{% endif %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}> <input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="show_detail_random">{{_('Show random books in detail view')}}</label> <label for="show_detail_random">{{_('Show random books in detail view')}}</label>
@ -163,7 +157,7 @@
{% for entry in downloads %} {% for entry in downloads %}
<div class="col-sm-2"> <div class="col-sm-2">
<a class="pull-left" href="{{ url_for('show_book', book_id=entry.id) }}"> <a class="pull-left" href="{{ url_for('show_book', book_id=entry.id) }}">
<img class="media-object" width="100" src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" alt="..."> <img class="media-object" width="100" src="{{ url_for('get_cover', book_id=entry.id) }}" alt="...">
</a> </a>
</div> </div>
{% endfor %} {% endfor %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,6 +1,23 @@
#!/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)
# Copyright (C) 2012-2019 mutschler, jkrehm, cervinko, janeczku, OzzieIsaacs, csitko
# ok11, issmirnov, idalin
#
# 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/>.
from sqlalchemy import * from sqlalchemy import *
from sqlalchemy import exc from sqlalchemy import exc
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
@ -44,8 +61,17 @@ MATURE_CONTENT = 2048
SIDEBAR_PUBLISHER = 4096 SIDEBAR_PUBLISHER = 4096
DEFAULT_PASS = "admin123" DEFAULT_PASS = "admin123"
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083)) try:
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
except ValueError:
print ('Environmentvariable CALIBRE_PORT is set to an invalid value: ' +
os.environ.get("CALIBRE_PORT", 8083) + ', faling back to default (8083)')
DEFAULT_PORT = 8083
UPDATE_STABLE = 0
AUTO_UPDATE_STABLE = 1
UPDATE_NIGHTLY = 2
AUTO_UPDATE_NIGHTLY = 4
class UserBase: class UserBase:
@ -103,10 +129,6 @@ class UserBase:
def is_anonymous(self): def is_anonymous(self):
return False return False
@property
def get_theme(self):
return self.theme
def get_id(self): def get_id(self):
return str(self.id) return str(self.id)
@ -170,7 +192,6 @@ class User(UserBase, Base):
sidebar_view = Column(Integer, default=1) sidebar_view = Column(Integer, default=1)
default_language = Column(String(3), default="all") default_language = Column(String(3), default="all")
mature_content = Column(Boolean, default=True) mature_content = Column(Boolean, default=True)
theme = Column(Integer, default=0)
# Class for anonymous user is derived from User base and completly overrides methods and properties for the # Class for anonymous user is derived from User base and completly overrides methods and properties for the
@ -263,7 +284,7 @@ class Downloads(Base):
def __repr__(self): def __repr__(self):
return '<Download %r' % self.book_id return '<Download %r' % self.book_id
# Baseclass representing allowed domains for registration # Baseclass representing allowed domains for registration
class Registration(Base): class Registration(Base):
__tablename__ = 'registration' __tablename__ = 'registration'
@ -272,8 +293,8 @@ class Registration(Base):
domain = Column(String) domain = Column(String)
def __repr__(self): def __repr__(self):
return u"<Registration('{0}')>".format(self.domain) return u"<Registration('{0}')>".format(self.domain)
# Baseclass for representing settings in app.db with email server settings and Calibre database settings # Baseclass for representing settings in app.db with email server settings and Calibre database settings
# (application settings) # (application settings)
@ -294,6 +315,7 @@ class Settings(Base):
config_calibre_web_title = Column(String, default=u'Calibre-Web') config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_books_per_page = Column(Integer, default=60) config_books_per_page = Column(Integer, default=60)
config_random_books = Column(Integer, default=4) config_random_books = Column(Integer, default=4)
config_authors_max = Column(Integer, default=0)
config_read_column = Column(Integer, default=0) config_read_column = Column(Integer, default=0)
config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+') config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
config_log_level = Column(SmallInteger, default=logging.INFO) config_log_level = Column(SmallInteger, default=logging.INFO)
@ -316,6 +338,8 @@ class Settings(Base):
config_converterpath = Column(String) config_converterpath = Column(String)
config_calibre = Column(String) config_calibre = Column(String)
config_rarfile_location = Column(String) config_rarfile_location = Column(String)
config_theme = Column(Integer, default=0)
config_updatechannel = Column(Integer, default=0)
def __repr__(self): def __repr__(self):
pass pass
@ -357,6 +381,7 @@ class Config:
self.config_calibre_web_title = data.config_calibre_web_title self.config_calibre_web_title = data.config_calibre_web_title
self.config_books_per_page = data.config_books_per_page self.config_books_per_page = data.config_books_per_page
self.config_random_books = data.config_random_books self.config_random_books = data.config_random_books
self.config_authors_max = data.config_authors_max
self.config_title_regex = data.config_title_regex self.config_title_regex = data.config_title_regex
self.config_read_column = data.config_read_column self.config_read_column = data.config_read_column
self.config_log_level = data.config_log_level self.config_log_level = data.config_log_level
@ -389,11 +414,17 @@ class Config:
if data.config_logfile: if data.config_logfile:
self.config_logfile = data.config_logfile self.config_logfile = data.config_logfile
self.config_rarfile_location = data.config_rarfile_location self.config_rarfile_location = data.config_rarfile_location
self.config_theme = data.config_theme
self.config_updatechannel = data.config_updatechannel
@property @property
def get_main_dir(self): def get_main_dir(self):
return self.config_main_dir return self.config_main_dir
@property
def get_update_channel(self):
return self.config_updatechannel
def get_config_certfile(self): def get_config_certfile(self):
if cli.certfilepath: if cli.certfilepath:
return cli.certfilepath return cli.certfilepath
@ -537,6 +568,8 @@ class Config:
# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding # everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
# rows with SQL commands # rows with SQL commands
def migrate_Database(): def migrate_Database():
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
ReadBook.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "bookmark"): if not engine.dialect.has_table(engine.connect(), "bookmark"):
Bookmark.__table__.create(bind=engine) Bookmark.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"): if not engine.dialect.has_table(engine.connect(), "registration"):
@ -557,7 +590,7 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
session.commit() session.commit()
try: try:
session.query(exists().where(Settings.config_columns_to_ignore)).scalar() session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
except exc.OperationalError: except exc.OperationalError:
@ -570,6 +603,12 @@ def migrate_Database():
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0")
session.commit() session.commit()
try:
session.query(exists().where(Settings.config_authors_max)).scalar()
except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_authors_max` INTEGER DEFAULT 0")
session.commit()
try: try:
session.query(exists().where(BookShelf.order)).scalar() session.query(exists().where(BookShelf.order)).scalar()
except exc.OperationalError: # Database is not compatible, some rows are missing except exc.OperationalError: # Database is not compatible, some rows are missing
@ -610,12 +649,7 @@ def migrate_Database():
except exc.OperationalError: except exc.OperationalError:
conn = engine.connect() conn = engine.connect()
conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1") conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")
try:
session.query(exists().where(User.theme)).scalar()
except exc.OperationalError:
conn = engine.connect()
conn.execute("ALTER TABLE user ADD column `theme` INTEGER DEFAULT 0")
session.commit()
if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None: if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
create_anonymous_user() create_anonymous_user()
try: try:
@ -668,6 +702,19 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_converterpath` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_converterpath` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''") conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''")
session.commit() session.commit()
try:
session.query(exists().where(Settings.config_theme)).scalar()
except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_theme` INTEGER DEFAULT 0")
session.commit()
try:
session.query(exists().where(Settings.config_updatechannel)).scalar()
except exc.OperationalError: # Database is not compatible, some rows are missing
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_updatechannel` INTEGER DEFAULT 0")
session.commit()
# Remove login capability of user Guest # Remove login capability of user Guest
conn = engine.connect() conn = engine.connect()

@ -0,0 +1,514 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018-2019 OzzieIsaacs
#
# 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/>.
import threading
import zipfile
import requests
import re
import logging
import server
import time
from io import BytesIO
import os
import sys
import shutil
from ub import config, UPDATE_STABLE
from tempfile import gettempdir
import datetime
import json
from flask_babel import gettext as _
from babel.dates import format_datetime
import web
def is_sha1(sha1):
if len(sha1) != 40:
return False
try:
int(sha1, 16)
except ValueError:
return False
return True
class Updater(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.status = -1
self.updateIndex = None
def get_current_version_info(self):
if config.get_update_channel == UPDATE_STABLE:
return self._stable_version_info()
else:
return self._nightly_version_info()
def get_available_updates(self, request_method):
if config.get_update_channel == UPDATE_STABLE:
return self._stable_available_updates(request_method)
else:
return self._nightly_available_updates(request_method)
def run(self):
try:
self.status = 1
r = requests.get(self._get_request_path(), stream=True)
r.raise_for_status()
self.status = 2
z = zipfile.ZipFile(BytesIO(r.content))
self.status = 3
tmp_dir = gettempdir()
z.extractall(tmp_dir)
foldername = os.path.join(tmp_dir, z.namelist()[0])[:-1]
if not os.path.isdir(foldername):
self.status = 11
logging.getLogger('cps.web').info(u'Extracted contents of zipfile not found in temp folder')
return
self.status = 4
self.update_source(foldername, config.get_main_dir)
self.status = 6
time.sleep(2)
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
time.sleep(2)
except requests.exceptions.HTTPError as ex:
logging.getLogger('cps.web').info( u'HTTP Error' + ' ' + str(ex))
self.status = 8
except requests.exceptions.ConnectionError:
logging.getLogger('cps.web').info(u'Connection error')
self.status = 9
except requests.exceptions.Timeout:
logging.getLogger('cps.web').info(u'Timeout while establishing connection')
self.status = 10
except requests.exceptions.RequestException:
self.status = 11
logging.getLogger('cps.web').info(u'General error')
def get_update_status(self):
return self.status
@classmethod
def file_to_list(self, filelist):
return [x.strip() for x in open(filelist, 'r') if not x.startswith('#EXT')]
@classmethod
def one_minus_two(self, one, two):
return [x for x in one if x not in set(two)]
@classmethod
def reduce_dirs(self, delete_files, new_list):
new_delete = []
for filename in delete_files:
parts = filename.split(os.sep)
sub = ''
for part in parts:
sub = os.path.join(sub, part)
if sub == '':
sub = os.sep
count = 0
for song in new_list:
if song.startswith(sub):
count += 1
break
if count == 0:
if sub != '\\':
new_delete.append(sub)
break
return list(set(new_delete))
@classmethod
def reduce_files(self, remove_items, exclude_items):
rf = []
for item in remove_items:
if not item.startswith(exclude_items):
rf.append(item)
return rf
@classmethod
def moveallfiles(self, root_src_dir, root_dst_dir):
change_permissions = True
if sys.platform == "win32" or sys.platform == "darwin":
change_permissions = False
else:
logging.getLogger('cps.web').debug('Update on OS-System : ' + sys.platform)
new_permissions = os.stat(root_dst_dir)
# print new_permissions
for src_dir, __, files in os.walk(root_src_dir):
dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
logging.getLogger('cps.web').debug('Create-Dir: '+dst_dir)
if change_permissions:
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
os.chown(dst_dir, new_permissions.st_uid, new_permissions.st_gid)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if os.path.exists(dst_file):
if change_permissions:
permission = os.stat(dst_file)
logging.getLogger('cps.web').debug('Remove file before copy: '+dst_file)
os.remove(dst_file)
else:
if change_permissions:
permission = new_permissions
shutil.move(src_file, dst_dir)
logging.getLogger('cps.web').debug('Move File '+src_file+' to '+dst_dir)
if change_permissions:
try:
os.chown(dst_file, permission.st_uid, permission.st_gid)
except (Exception) as e:
# ex = sys.exc_info()
old_permissions = os.stat(dst_file)
logging.getLogger('cps.web').debug('Fail change permissions of ' + str(dst_file) + '. Before: '
+ str(old_permissions.st_uid) + ':' + str(old_permissions.st_gid) + ' After: '
+ str(permission.st_uid) + ':' + str(permission.st_gid) + ' error: '+str(e))
return
def update_source(self, source, destination):
# destination files
old_list = list()
exclude = (
os.sep + 'app.db', os.sep + 'calibre-web.log1', os.sep + 'calibre-web.log2', os.sep + 'gdrive.db',
os.sep + 'vendor', os.sep + 'calibre-web.log', os.sep + '.git', os.sep +'client_secrets.json',
os.sep + 'gdrive_credentials', os.sep + 'settings.yaml')
for root, dirs, files in os.walk(destination, topdown=True):
for name in files:
old_list.append(os.path.join(root, name).replace(destination, ''))
for name in dirs:
old_list.append(os.path.join(root, name).replace(destination, ''))
# source files
new_list = list()
for root, dirs, files in os.walk(source, topdown=True):
for name in files:
new_list.append(os.path.join(root, name).replace(source, ''))
for name in dirs:
new_list.append(os.path.join(root, name).replace(source, ''))
delete_files = self.one_minus_two(old_list, new_list)
rf = self.reduce_files(delete_files, exclude)
remove_items = self.reduce_dirs(rf, new_list)
self.moveallfiles(source, destination)
for item in remove_items:
item_path = os.path.join(destination, item[1:])
if os.path.isdir(item_path):
logging.getLogger('cps.web').debug("Delete dir " + item_path)
shutil.rmtree(item_path, ignore_errors=True)
else:
try:
logging.getLogger('cps.web').debug("Delete file " + item_path)
# log_from_thread("Delete file " + item_path)
os.remove(item_path)
except Exception:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)
def _nightly_version_info(self):
content = {}
content[0] = '$Format:%H$'
content[1] = '$Format:%cI$'
# content[0] = 'bb7d2c6273ae4560e83950d36d64533343623a57'
# content[1] = '2018-09-09T10:13:08+02:00'
if is_sha1(content[0]) and len(content[1]) > 0:
return {'version': content[0], 'datetime': content[1]}
return False
def _stable_version_info(self):
return {'version': '0.6.1'} # Current version
def _nightly_available_updates(self, request_method):
tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone)
if request_method == "GET":
repository_url = 'https://api.github.com/repos/janeczku/calibre-web'
status, commit = self._load_remote_data(repository_url +'/git/refs/heads/master')
parents = []
if status['message'] != '':
return json.dumps(status)
if 'object' not in commit:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
if commit['object']['sha'] == status['current_commit_hash']:
status.update({
'update': False,
'success': True,
'message': _(u'No update available. You already have the latest version installed')
})
return json.dumps(status)
# a new update is available
status['update'] = True
try:
r = requests.get(repository_url + '/git/commits/' + commit['object']['sha'])
r.raise_for_status()
update_data = r.json()
except requests.exceptions.HTTPError as e:
status['error'] = _(u'HTTP Error') + ' ' + str(e)
except requests.exceptions.ConnectionError:
status['error'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['error'] = _(u'Timeout while establishing connection')
except requests.exceptions.RequestException:
status['error'] = _(u'General error')
if status['message'] != '':
return json.dumps(status)
if 'committer' in update_data and 'message' in update_data:
status['success'] = True
status['message'] = _(
u'A new update is available. Click on the button below to update to the latest version.')
new_commit_date = datetime.datetime.strptime(
update_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append(
[
format_datetime(new_commit_date, format='short', locale=web.get_locale()),
update_data['message'],
update_data['sha']
]
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = update_data['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except IndexError:
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if parent_commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=web.get_locale())
parents.append([parent_commit_date,
parent_data['message'].replace('\r\n','<p>').replace('\n','<p>')])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
else:
status['success'] = False
status['message'] = _(u'Could not fetch update information')
# a new update is available
status['update'] = True
if 'body' in commit:
status['success'] = True
status['message'] = _(
u'A new update is available. Click on the button below to update to the latest version.')
new_commit_date = datetime.datetime.strptime(
commit['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parents.append(
[
format_datetime(new_commit_date, format='short', locale=web.get_locale()),
commit['message'],
commit['sha']
]
)
# it only makes sense to analyze the parents if we know the current commit hash
if status['current_commit_hash'] != '':
try:
parent_commit = commit['parents'][0]
# limit the maximum search depth
remaining_parents_cnt = 10
except IndexError:
remaining_parents_cnt = None
if remaining_parents_cnt is not None:
while True:
if remaining_parents_cnt == 0:
break
# check if we are more than one update behind if so, go up the tree
if commit['sha'] != status['current_commit_hash']:
try:
r = requests.get(parent_commit['url'])
r.raise_for_status()
parent_data = r.json()
parent_commit_date = datetime.datetime.strptime(
parent_data['committer']['date'], '%Y-%m-%dT%H:%M:%SZ') - tz
parent_commit_date = format_datetime(
parent_commit_date, format='short', locale=web.get_locale())
parents.append([parent_commit_date, parent_data['message'], parent_data['sha']])
parent_commit = parent_data['parents'][0]
remaining_parents_cnt -= 1
except Exception:
# it isn't crucial if we can't get information about the parent
break
else:
# parent is our current version
break
status['history'] = parents[::-1]
return json.dumps(status)
return ''
def _stable_available_updates(self, request_method):
if request_method == "GET":
parents = []
# repository_url = 'https://api.github.com/repos/flatpak/flatpak/releases' # test URL
repository_url = 'https://api.github.com/repos/janeczku/calibre-web/releases'
status, commit = self._load_remote_data(repository_url)
if status['message'] != '':
return json.dumps(status)
if not commit:
status['success'] = True
status['message'] = _(u'No release information available')
return json.dumps(status)
version = status['current_commit_hash']
current_version = status['current_commit_hash'].split('.')
# we are already on newest version, no update available
if 'tag_name' not in commit[0]:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
if commit[0]['tag_name'] == version:
status.update({
'update': False,
'success': True,
'message': _(u'No update available. You already have the latest version installed')
})
return json.dumps(status)
i = len(commit) - 1
while i >= 0:
if 'tag_name' not in commit[i] or 'body' not in commit[i]:
status['message'] = _(u'Unexpected data while reading update information')
return json.dumps(status)
major_version_update = int(commit[i]['tag_name'].split('.')[0])
minor_version_update = int(commit[i]['tag_name'].split('.')[1])
patch_version_update = int(commit[i]['tag_name'].split('.')[2])
# Check if major versions are identical search for newest nonenqual commit and update to this one
if major_version_update == int(current_version[0]):
if (minor_version_update == int(current_version[1]) and
patch_version_update > int(current_version[2])) or \
minor_version_update > int(current_version[1]):
parents.append([commit[i]['tag_name'],commit[i]['body'].replace('\r\n', '<p>')])
i -= 1
continue
if major_version_update < int(current_version[0]):
i -= 1
continue
if major_version_update > int(current_version[0]):
# found update update to last version before major update, unless current version is on last version
# before major update
if commit[i+1]['tag_name'].split('.')[1] == current_version[1]:
parents.append([commit[i]['tag_name'],
commit[i]['body'].replace('\r\n', '<p>').replace('\n', '<p>')])
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i]['tag_name']),
'history': parents
})
self.updateFile = commit[i]['zipball_url']
else:
status.update({
'update': True,
'success': True,
'message': _(u'A new update is available. Click on the button below to '
u'update to version: %(version)s', version=commit[i]['tag_name']),
'history': parents
})
self.updateFile = commit[i +1]['zipball_url']
break
if i == -1:
status.update({
'update': True,
'success': True,
'message': _(
u'A new update is available. Click on the button below to update to the latest version.'),
'history': parents
})
self.updateFile = commit[0]['zipball_url']
return json.dumps(status)
def _get_request_path(self):
if config.get_update_channel == UPDATE_STABLE:
return self.updateFile
else:
return 'https://api.github.com/repos/janeczku/calibre-web/zipball/master'
def _load_remote_data(self, repository_url):
status = {
'update': False,
'success': False,
'message': '',
'current_commit_hash': ''
}
commit = None
version = self.get_current_version_info()
if version is False:
status['current_commit_hash'] = _(u'Unknown')
else:
status['current_commit_hash'] = version['version']
try:
r = requests.get(repository_url)
commit = r.json()
r.raise_for_status()
except requests.exceptions.HTTPError as e:
if commit:
if 'message' in commit:
status['message'] = _(u'HTTP Error') + ': ' + commit['message']
else:
status['message'] = _(u'HTTP Error') + ': ' + str(e)
except requests.exceptions.ConnectionError:
status['message'] = _(u'Connection error')
except requests.exceptions.Timeout:
status['message'] = _(u'Timeout while establishing connection')
except requests.exceptions.RequestException:
status['message'] = _(u'General error')
return status, commit
updater_thread = Updater()

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2012-2019 lemmsh, OzzieIsaacs
#
# 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/>.
import os import os
from tempfile import gettempdir from tempfile import gettempdir
import hashlib import hashlib

File diff suppressed because it is too large Load Diff

@ -1,6 +1,22 @@
#!/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)
# Copyright (C) 2018-2019 OzzieIsaacs, bodybybuddha, janeczku
#
# 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/>.
from __future__ import print_function from __future__ import print_function
import smtplib import smtplib
import threading import threading
@ -71,7 +87,7 @@ def get_attachment(bookpath, filename):
file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb') file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb')
data = file_.read() data = file_.read()
file_.close() file_.close()
except IOError: except IOError as e:
web.app.logger.exception(e) # traceback.print_exc() web.app.logger.exception(e) # traceback.print_exc()
web.app.logger.error(u'The requested file could not be read. Maybe wrong permissions?') web.app.logger.error(u'The requested file could not be read. Maybe wrong permissions?')
return None return None
@ -99,7 +115,7 @@ class emailbase():
def send(self, strg): def send(self, strg):
"""Send `strg' to the server.""" """Send `strg' to the server."""
if self.debuglevel > 0: if self.debuglevel > 0:
print('send:', repr(strg), file=sys.stderr) print('send:', repr(strg[:300]), file=sys.stderr)
if hasattr(self, 'sock') and self.sock: if hasattr(self, 'sock') and self.sock:
try: try:
if self.transferSize: if self.transferSize:
@ -108,7 +124,7 @@ class emailbase():
self.transferSize = len(strg) self.transferSize = len(strg)
lock.release() lock.release()
for i in range(0, self.transferSize, chunksize): for i in range(0, self.transferSize, chunksize):
if type(strg) == bytes: if isinstance(strg, bytes):
self.sock.send((strg[i:i+chunksize])) self.sock.send((strg[i:i+chunksize]))
else: else:
self.sock.send((strg[i:i + chunksize]).encode('utf-8')) self.sock.send((strg[i:i + chunksize]).encode('utf-8'))
@ -278,7 +294,9 @@ class WorkerThread(threading.Thread):
command = [web.ub.config.config_converterpath, (file_path + format_old_ext), command = [web.ub.config.config_converterpath, (file_path + format_old_ext),
(file_path + format_new_ext)] (file_path + format_new_ext)]
if web.ub.config.config_calibre: if web.ub.config.config_calibre:
command.append(web.ub.config.config_calibre) parameters = web.ub.config.config_calibre.split(" ")
for param in parameters:
command.append(param)
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
command = [x.encode(sys.getfilesystemencoding()) for x in command] command = [x.encode(sys.getfilesystemencoding()) for x in command]
@ -320,7 +338,7 @@ class WorkerThread(threading.Thread):
cur_book = web.db.session.query(web.db.Books).filter(web.db.Books.id == bookid).first() cur_book = web.db.session.query(web.db.Books).filter(web.db.Books.id == bookid).first()
if os.path.isfile(file_path + format_new_ext): if os.path.isfile(file_path + format_new_ext):
new_format = web.db.Data(name=cur_book.data[0].name, new_format = web.db.Data(name=cur_book.data[0].name,
book_format=self.queue[self.current]['settings']['new_book_format'], book_format=self.queue[self.current]['settings']['new_book_format'].upper(),
book=bookid, uncompressed_size=os.path.getsize(file_path + format_new_ext)) book=bookid, uncompressed_size=os.path.getsize(file_path + format_new_ext))
cur_book.data.append(new_format) cur_book.data.append(new_format)
web.db.session.commit() web.db.session.commit()
@ -453,6 +471,8 @@ class WorkerThread(threading.Thread):
except (smtplib.SMTPException) as e: except (smtplib.SMTPException) as e:
if hasattr(e, "smtp_error"): if hasattr(e, "smtp_error"):
text = e.smtp_error.replace("\n",'. ') text = e.smtp_error.replace("\n",'. ')
elif hasattr(e, "message"):
text = e.message
else: else:
text = '' text = ''
self._handleError(u'Error sending email: ' + text) self._handleError(u'Error sending email: ' + text)
@ -499,10 +519,13 @@ class StderrLogger(object):
self.logger = web.app.logger self.logger = web.app.logger
def write(self, message): def write(self, message):
if message == '\n': try:
self.logger.debug(self.buffer) if message == '\n':
print(self.buffer) self.logger.debug(self.buffer)
self.buffer = '' print(self.buffer)
else: self.buffer = ''
self.buffer += message else:
self.buffer += message
except:
pass

File diff suppressed because it is too large Load Diff

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

Loading…
Cancel
Save