Merge pull request #1 from janeczku/master

Synch the master
pull/878/head
Barnabás Nagy 6 years ago committed by GitHub
commit 93720c9fdf
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
cps/static/css/libs/* linguist-vendored
cps/static/js/libs/* linguist-vendored

@ -1,6 +1,22 @@
#!/usr/bin/env python
# -*- 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 uploader
import os
@ -19,6 +35,7 @@ logger = logging.getLogger("book_formats")
try:
from wand.image import Image
from wand import version as ImageVersion
from wand.exceptions import PolicyError
use_generic_pdf_cover = False
except (ImportError, RuntimeError) as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
@ -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):
if use_pdf_meta:
pdf = PdfFileReader(open(tmp_file_path, 'rb'))
pdf = PdfFileReader(open(tmp_file_path, 'rb'), strict=False)
doc_info = pdf.getDocumentInfo()
else:
doc_info = None
@ -114,12 +131,18 @@ def pdf_preview(tmp_file_path, tmp_dir):
if use_generic_pdf_cover:
return None
else:
try:
cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg"
with Image(filename=tmp_file_path + "[0]", resolution=150) as img:
img.compression_quality = 88
img.save(filename=os.path.join(tmp_dir, cover_file_name))
return cover_file_name
except PolicyError as ex:
logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex)
return None
except Exception as ex:
logger.warning('Cannot extract cover image, using default: %s', ex)
return None
def get_versions():
if not use_generic_pdf_cover:

@ -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
# Uses query strings so CSS font files are found without having to resort to absolute URLs

@ -1,6 +1,23 @@
#!/usr/bin/env python
# -*- 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 os
import sys

@ -1,6 +1,22 @@
#!/usr/bin/env python
# -*- 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 tarfile
import os
@ -8,21 +24,34 @@ import uploader
def extractCover(tmp_file_name, original_file_extension):
cover_data = None
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
compressed_name = cf.namelist()[0]
cover_data = cf.read(compressed_name)
for name in cf.namelist():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension == '.jpg':
cover_data = cf.read(name)
break
elif original_file_extension.upper() == '.CBT':
cf = tarfile.TarFile(tmp_file_name)
compressed_name = cf.getnames()[0]
cover_data = cf.extractfile(compressed_name).read()
for name in cf.getnames():
ext = os.path.splitext(name)
if len(ext) > 1:
extension = ext[1].lower()
if extension == '.jpg':
cover_data = cf.extractfile(name).read()
break
prefix = os.path.dirname(tmp_file_name)
tmp_cover_name = prefix + '/cover' + os.path.splitext(compressed_name)[1]
if cover_data:
tmp_cover_name = prefix + '/cover' + extension
image = open(tmp_cover_name, 'wb')
image.write(cover_data)
image.close()
else:
tmp_cover_name = None
return tmp_cover_name

@ -1,5 +1,23 @@
#!/usr/bin/env python
# -*- 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 subprocess
import ub

@ -1,6 +1,23 @@
#!/usr/bin/env python
# -*- 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.ext.declarative import declarative_base
from sqlalchemy.orm import *
@ -9,6 +26,7 @@ import re
import ast
from ub import config
import ub
import sys
session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
@ -301,6 +319,8 @@ class Custom_Columns(Base):
def get_display_dict(self):
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

@ -1,6 +1,22 @@
#!/usr/bin/env python
# -*- 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
from lxml import etree
import os

@ -1,6 +1,22 @@
#!/usr/bin/env python
# -*- 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
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:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError
from pydrive.auth import RefreshError, InvalidConfigError
from apiclient import errors
gdrive_support = True
except ImportError:
@ -12,12 +31,9 @@ from ub import config
import cli
import shutil
from flask import Response, stream_with_context
from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import *
import web
class Singleton:
@ -112,7 +128,8 @@ def migrate():
sql=sql[0].replace(currUniqueConstraint, 'UNIQUE (gdrive_id, path)')
sql=sql.replace(GdriveId.__tablename__, GdriveId.__tablename__ + '2')
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.execute('DROP TABLE %s' % '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
return GoogleDrive(gauth)
if drive.auth.access_token_expired:
try:
drive.auth.Refresh()
except RefreshError as e:
web.app.logger.error("Google Drive error: " + e.message)
return drive
def listRootFolders():
@ -164,8 +184,9 @@ def getFolderInFolder(parentId, folderName, drive):
# drive = getDrive(drive)
query=""
if folderName:
query = "title = '%s' and " % folderName.replace("'", "\\'")
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % parentId
query = "title = '%s' and " % folderName.replace("'", r"\'")
folder = query + "'%s' in parents and mimeType = 'application/vnd.google-apps.folder'" \
" and trashed = false" % parentId
fileList = drive.ListFile({'q': folder}).GetList()
if fileList.__len__() == 0:
return None
@ -190,8 +211,7 @@ def getEbooksFolderId(drive=None):
def getFile(pathId, fileName, drive):
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'"))
metaDataFile = "'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", r"\'"))
fileList = drive.ListFile({'q': metaDataFile}).GetList()
if fileList.__len__() == 0:
return None
@ -248,16 +268,9 @@ def getFileFromEbooksFolder(path, fileName):
return None
'''def copyDriveFileRemote(drive, origin_file_id, copy_title):
drive = getDrive(drive)
copied_file = {'title': copy_title}
try:
file_data = drive.auth.service.files().copy(
fileId = origin_file_id, body=copied_file).execute()
return drive.CreateFile({'id': file_data['id']})
except errors.HttpError as error:
print ('An error occurred: %s' % error)
return None'''
def moveGdriveFileRemote(origin_file_id, new_title):
origin_file_id['title']= new_title
origin_file_id.Upload()
# Download metadata.db from gdrive
@ -269,9 +282,10 @@ def downloadFile(path, filename, output):
def moveGdriveFolderRemote(origin_file, target_folder):
drive = getDrive(Gdrive.Instance().drive)
previous_parents = ",".join([parent["id"] for parent in origin_file.get('parents')])
children = drive.auth.service.children().list(folderId=previous_parents).execute()
gFileTargetDir = getFileFromEbooksFolder(None, target_folder)
if not gFileTargetDir:
# Folder is not exisiting, create, and move folder
# Folder is not existing, create, and move folder
gFileTargetDir = drive.CreateFile(
{'title': target_folder, 'parents': [{"kind": "drive#fileLink", 'id': getEbooksFolderId()}],
"mimeType": "application/vnd.google-apps.folder"})
@ -281,13 +295,10 @@ def moveGdriveFolderRemote(origin_file, target_folder):
addParents=gFileTargetDir['id'],
removeParents=previous_parents,
fields='id, parents').execute()
# if previous_parents has no childs anymore, delete originfileparent
# is not working correctly, because of slow update on gdrive -> could cause trouble in gdrive.db
# (nonexisting folder has id)
# children = drive.auth.service.children().list(folderId=previous_parents).execute()
# if not len(children['items']):
# drive.auth.service.files().delete(fileId=previous_parents).execute()
# if previous_parents has no childs anymore, delete original fileparent
if len(children['items']) == 1:
deleteDatabaseEntry(previous_parents)
drive.auth.service.files().delete(fileId=previous_parents).execute()
def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
@ -299,9 +310,11 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
if not parent:
parent = getEbooksFolder(drive)
if os.path.isdir(os.path.join(prevDir,uploadFile)):
existingFolder = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (os.path.basename(uploadFile), parent['id'])}).GetList()
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):
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"})
parent.Upload()
else:
@ -312,11 +325,13 @@ def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
copyToDrive(drive, f, True, replaceFiles, ignoreFiles, parent, os.path.join(prevDir, uploadFile))
else:
if os.path.basename(uploadFile) not in ignoreFiles:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (os.path.basename(uploadFile), parent['id'])}).GetList()
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:
driveFile = existingFiles[0]
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.Upload()
@ -327,7 +342,8 @@ def uploadFileToEbooksFolder(destFile, f):
splitDir = destFile.split('/')
for i, x in enumerate(splitDir):
if i == len(splitDir)-1:
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" % (x, parent['id'])}).GetList()
existingFiles = drive.ListFile({'q': "title = '%s' and '%s' in parents and trashed = false" %
(x.replace("'", r"\'"), parent['id'])}).GetList()
if len(existingFiles) > 0:
driveFile = existingFiles[0]
else:
@ -335,7 +351,8 @@ def uploadFileToEbooksFolder(destFile, f):
driveFile.SetContentFile(f)
driveFile.Upload()
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:
parent = drive.CreateFile({'title': x, 'parents': [{"kind": "drive#fileLink", 'id': parent['id']}],
"mimeType": "application/vnd.google-apps.folder"})
@ -428,6 +445,10 @@ def getChangeById (drive, change_id):
except (errors.HttpError) as error:
web.app.logger.info(error.message)
return None
except Exception as e:
web.app.logger.info(e)
return None
# Deletes the local hashes database to force search for new folder names
def deleteDatabaseOnChange():
@ -442,9 +463,10 @@ def updateGdriveCalibreFromLocal():
# update gdrive.db on edit of books title
def updateDatabaseOnEdit(ID,newPath):
sqlCheckPath = newPath if newPath[-1] == '/' else newPath + u'/'
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
if storedPathName:
storedPathName.path = newPath
storedPathName.path = sqlCheckPath
session.commit()

@ -1,31 +1,46 @@
#!/usr/bin/env python
# -*- 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 ub
from flask import current_app as app
import logging
from tempfile import gettempdir
import sys
import os
import re
import unicodedata
from io import BytesIO
import worker
import time
from flask import send_from_directory, make_response, redirect, abort
from flask_babel import gettext as _
import threading
from flask_login import current_user
from babel.dates import format_datetime
from datetime import datetime
import shutil
import requests
import zipfile
try:
import gdriveutils as gd
except ImportError:
pass
import web
import server
import random
import subprocess
@ -36,7 +51,7 @@ except ImportError:
use_unidecode = False
# Global variables
updater_thread = None
# updater_thread = None
global_WorkerThread = worker.WorkerThread()
global_WorkerThread.start()
@ -73,10 +88,12 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
# read settings and append converter task to queue
if kindle_mail:
settings = ub.get_mail_settings()
text = _(u"Convert: %(book)s" , book=book.title)
settings['subject'] = _('Send to Kindle') # pretranslate Subject for e-mail
settings['body'] = _(u'This e-mail has been sent via Calibre-Web.')
# text = _(u"%(format)s: %(book)s", format=new_book_format, book=book.title)
else:
settings = dict()
text = _(u"Convert to %(format)s: %(book)s", format=new_book_format, book=book.title)
text = (u"%s -> %s: %s" % (old_book_format, new_book_format, book.title))
settings['old_book_format'] = old_book_format
settings['new_book_format'] = new_book_format
global_WorkerThread.add_convert(file_path, book.id, user_id, text, settings, kindle_mail)
@ -89,7 +106,8 @@ def convert_book_format(book_id, calibrepath, old_book_format, new_book_format,
def send_test_mail(kindle_mail, user_name):
global_WorkerThread.add_email(_(u'Calibre-Web test e-mail'),None, None, ub.get_mail_settings(),
kindle_mail, user_name, _(u"Test e-mail"))
kindle_mail, user_name, _(u"Test e-mail"),
_(u'This e-mail has been sent via Calibre-Web.'))
return
@ -108,41 +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)
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:
# 1: If Mobi file is exisiting, it's directly send to kindle email,
# 2: If Epub file is exisiting, it's converted and send to kindle email
# 3: If Pdf file is exisiting, it's directly send to kindle email,
def send_mail(book_id, kindle_mail, calibrepath, user_id):
# 1: If Mobi file is existing, it's directly send to kindle email,
# 2: If Epub file is existing, it's converted and send to kindle email,
# 3: If Pdf file is existing, it's directly send to kindle email
def send_mail(book_id, book_format, convert, kindle_mail, calibrepath, user_id):
"""Send email with attachments"""
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()
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 sucess, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'epub', u'mobi', user_id, kindle_mail)
elif 'pdf' in formats:
result = formats['pdf'] # worker.get_attachment()
if convert:
# returns None if success, otherwise errormessage
return convert_book_format(book_id, calibrepath, u'epub', book_format.lower(), user_id, kindle_mail)
else:
return _(u"Could not find any formats suitable for sending by e-mail")
if result:
for entry in iter(book.data):
if entry.format.upper() == book_format.upper():
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))
else:
kindle_mail, user_id, _(u"E-mail: %(book)s", book=book.title),
_(u'This e-mail has been sent via Calibre-Web.'))
return
return _(u"The requested file could not be read. Maybe wrong permissions?")
@ -173,18 +230,26 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:128]
if not value:
raise ValueError("Filename cannot be empty")
if sys.version_info.major == 3:
return value
else:
return value.decode('utf-8')
def get_sorted_author(value):
try:
if ',' not in value:
regexes = ["^(JR|SR)\.?$", "^I{1,3}\.?$", "^IV\.?$"]
combined = "(" + ")|(".join(regexes) + ")"
value = value.split(" ")
if re.match(combined, value[-1].upper()):
value2 = value[-2] + ", " + " ".join(value[:-2]) + " " + value[-1]
elif len(value) == 1:
value2 = value[0]
else:
value2 = value[-1] + ", " + " ".join(value[:-1])
else:
value2 = value
except Exception:
web.app.logger.error("Sorting author " + str(value) + "failed")
value2 = value
@ -213,11 +278,14 @@ def delete_book_file(book, calibrepath, book_format=None):
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()
path = os.path.join(calibrepath, localbook.path)
authordir = localbook.path.split('/')[0]
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]
@ -232,53 +300,86 @@ def update_dir_structure_file(book_id, calibrepath):
web.app.logger.info("Copying title: " + path + " into existing: " + new_title_path)
for dir_name, subdir_list, file_list in os.walk(path):
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
localbook.path = localbook.path.split('/')[0] + '/' + new_titledir
except OSError as ex:
web.app.logger.error("Rename title from: " + path + " to " + new_title_path + ": " + str(ex))
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:
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)
localbook.path = new_authordir + '/' + localbook.path.split('/')[1]
except OSError as ex:
web.app.logger.error("Rename author from: " + path + " to " + new_author_path + ": " + str(ex))
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
def update_dir_structure_gdrive(book_id):
def update_dir_structure_gdrive(book_id, first_author):
error = False
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
path = book.path
authordir = book.path.split('/')[0]
if first_author:
new_authordir = get_valid_filename(first_author)
else:
new_authordir = get_valid_filename(book.authors[0].name)
titledir = book.path.split('/')[1]
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
new_titledir = get_valid_filename(book.title) + u" (" + str(book_id) + u")"
if titledir != new_titledir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
if gFile:
gFile['title'] = new_titledir
gFile.Upload()
book.path = book.path.split('/')[0] + '/' + new_titledir
book.path = book.path.split('/')[0] + u'/' + new_titledir
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path) # only child folder affected
else:
error = _(u'File %(file)s not found on Google Drive', file=book.path) # file not found
if authordir != new_authordir:
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), titledir)
gFile = gd.getFileFromEbooksFolder(os.path.dirname(book.path), new_titledir)
if gFile:
gd.moveGdriveFolderRemote(gFile, new_authordir)
book.path = new_authordir + '/' + book.path.split('/')[1]
book.path = new_authordir + u'/' + book.path.split('/')[1]
path = book.path
gd.updateDatabaseOnEdit(gFile['id'], book.path)
else:
error = _(u'File %(file)s not found on Google Drive', file=authordir) # file not found
# Rename all files from old names to new names
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
@ -299,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
return error
def generate_random_password():
s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?"
passlen = 8
@ -306,11 +408,12 @@ def generate_random_password():
################################## 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:
return update_dir_structure_gdrive(book_id)
return update_dir_structure_gdrive(book_id, first_author)
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):
if ub.config.config_use_google_drive:
@ -318,9 +421,12 @@ def delete_book(book, calibrepath, book_format):
else:
return delete_book_file(book, calibrepath, book_format)
def get_book_cover(cover_path):
if ub.config.config_use_google_drive:
try:
if not web.is_gdrive_ready():
return send_from_directory(os.path.join(os.path.dirname(__file__), "static"), "generic_cover.jpg")
path=gd.get_cover_via_gdrive(cover_path)
if path:
return redirect(path)
@ -335,6 +441,7 @@ def get_book_cover(cover_path):
else:
return send_from_directory(os.path.join(ub.config.config_calibre_dir, cover_path), "cover.jpg")
# saves book cover to gdrive or locally
def save_cover(url, book_path):
img = requests.get(url)
@ -347,7 +454,7 @@ def save_cover(url, book_path):
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
f.write(img.content)
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")
return True
@ -357,6 +464,7 @@ def save_cover(url, book_path):
web.app.logger.info("Cover is saved")
return True
def do_download_file(book, book_format, data, headers):
if ub.config.config_use_google_drive:
startTime = time.time()
@ -367,161 +475,17 @@ def do_download_file(book, book_format, data, headers):
else:
abort(404)
else:
response = make_response(send_from_directory(os.path.join(ub.config.config_calibre_dir, book.path), data.name + "." + book_format))
filename = os.path.join(ub.config.config_calibre_dir, book.path)
if not os.path.isfile(os.path.join(filename, data.name + "." + book_format)):
# ToDo: improve error handling
web.app.logger.error('File not found: %s' % os.path.join(filename, data.name + "." + book_format))
response = make_response(send_from_directory(filename, data.name + "." + book_format))
response.headers = headers
return response
##################################
class Updater(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.status = 0
def run(self):
self.status = 1
r = requests.get('https://api.github.com/repos/janeczku/calibre-web/zipball/master', stream=True)
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 = 5
db.session.close()
db.engine.dispose()
ub.session.close()
ub.engine.dispose()
self.status = 6
server.Server.setRestartTyp(True)
server.Server.stopServer()
self.status = 7
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):
@ -548,22 +512,55 @@ def check_unrar(unrarLocation):
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():
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 {'hash': content[0], 'datetime': content[1]}
return False
if isinstance(obj, (datetime)):
return obj.isoformat()
raise TypeError ("Type %s not serializable" % type(obj))
def render_task_status(tasklist):
#helper function to apply localize status information in tasklist entries
renderedtasklist=list()
# task2 = task
for task in tasklist:
if task['user'] == current_user.nickname or current_user.role_admin():
# task2 = copy.deepcopy(task) # = task
if task['formStarttime']:
task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=web.get_locale())
# task2['formStarttime'] = ""
else:
if 'starttime' not in task:
task['starttime'] = ""
# localize the task status
if isinstance( task['stat'], int ):
if task['stat'] == worker.STAT_WAITING:
task['status'] = _(u'Waiting')
elif task['stat'] == worker.STAT_FAIL:
task['status'] = _(u'Failed')
elif task['stat'] == worker.STAT_STARTED:
task['status'] = _(u'Started')
elif task['stat'] == worker.STAT_FINISH_SUCCESS:
task['status'] = _(u'Finished')
else:
task['status'] = _(u'Unknown Status')
# localize the task type
if isinstance( task['taskType'], int ):
if task['taskType'] == worker.TASK_EMAIL:
task['taskMessage'] = _(u'E-mail: ') + task['taskMess']
elif task['taskType'] == worker.TASK_CONVERT:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
elif task['taskType'] == worker.TASK_UPLOAD:
task['taskMessage'] = _(u'Upload: ') + task['taskMess']
elif task['taskType'] == worker.TASK_CONVERT_ANY:
task['taskMessage'] = _(u'Convert: ') + task['taskMess']
else:
task['taskMessage'] = _(u'Unknown Task: ') + task['taskMess']
renderedtasklist.append(task)
return renderedtasklist

@ -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/
try:

@ -1,6 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2018 cervinko, janeczku, OzzieIsaacs
#
# 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):
"""Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind

@ -1,6 +1,23 @@
#!/usr/bin/env python
# -*- 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
import sys
import os
@ -20,6 +37,7 @@ except ImportError:
gevent_present = False
class server:
wsgiserver = None
@ -32,19 +50,32 @@ class server:
def start_gevent(self):
try:
ssl_args = dict()
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
ssl_args = {"certfile": web.ub.config.get_config_certfile(),
"keyfile": web.ub.config.get_config_keyfile()}
certfile_path = web.ub.config.get_config_certfile()
keyfile_path = 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':
self.wsgiserver= WSGIServer(('0.0.0.0', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
else:
self.wsgiserver = WSGIServer(('', web.ub.config.config_port), web.app, spawn=Pool(), **ssl_args)
web.py3_gevent_link = self.wsgiserver
self.wsgiserver.serve_forever()
except SocketError:
try:
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)
web.py3_gevent_link = self.wsgiserver
self.wsgiserver.serve_forever()
except (OSError, SocketError) as e:
web.app.logger.info("Error starting server: %s" % e.strerror)
print("Error starting server: %s" % e.strerror)
web.helper.global_WorkerThread.stop()
sys.exit(1)
except Exception:
web.app.logger.info("Unknown error while starting gevent")
@ -54,12 +85,18 @@ class server:
# leave subprocess out to allow forking for fetchers and processors
self.start_gevent()
else:
try:
ssl = None
web.app.logger.info('Starting Tornado server')
if web.ub.config.get_config_certfile() and web.ub.config.get_config_keyfile():
ssl={"certfile": web.ub.config.get_config_certfile(),
"keyfile": web.ub.config.get_config_keyfile()}
certfile_path = web.ub.config.get_config_certfile()
keyfile_path = 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 = {"certfile": certfile_path,
"keyfile": keyfile_path}
else:
ssl=None
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
http_server = HTTPServer(WSGIContainer(web.app),
max_buffer_size = 209700000,
@ -69,7 +106,15 @@ class server:
self.wsgiserver.start()
# wait for stop signal
self.wsgiserver.close(True)
except SocketError as e:
web.app.logger.info("Error starting server: %s" % e.strerror)
print("Error starting server: %s" % e.strerror)
web.helper.global_WorkerThread.stop()
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:
web.app.logger.info("Performing restart of Calibre-Web")
web.helper.global_WorkerThread.stop()
@ -87,11 +132,21 @@ class server:
def setRestartTyp(self,starttyp):
self.restart = starttyp
# ToDo: Somehow caused by circular import under python3 refactor
web.py3_restart_Typ = starttyp
def killServer(self, signum, frame):
self.stopServer()
def stopServer(self):
# ToDo: Somehow caused by circular import under python3 refactor
if sys.version_info > (3, 0):
if not self.wsgiserver:
if gevent_present:
self.wsgiserver = web.py3_gevent_link
else:
self.wsgiserver = IOLoop.instance()
if self.wsgiserver:
if gevent_present:
self.wsgiserver.close()
else:

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

@ -5,6 +5,22 @@
src: local('Grand Hotel'), local('GrandHotel-Regular'), url("fonts/GrandHotel-Regular.ttf") format('truetype');
}
html.http-error {
margin: 0;
height: 100%;
}
.http-error body {
margin: 0;
height: 100%;
display: table;
width: 100%;
}
.http-error body > div {
display: table-cell;
vertical-align: middle;
text-align: center;
}
body{background:#f2f2f2}body h2{font-weight:normal;color:#444}
body { margin-bottom: 40px;}
a{color: #45b29d}a:hover{color: #444;}
@ -20,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 .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 .author .author-hidden, .container-fluid .author .author-hidden-divider {
display: none;
}
.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>li{margin-bottom: 10px;}
@ -36,6 +57,7 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
-moz-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 {border-color: #000;}
.cover { margin-bottom: 10px;}
@ -87,6 +109,16 @@ input.pill:not(:checked) + label .glyphicon {
.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; }
.padded-bottom { margin-bottom: 15px; }
@ -104,3 +136,7 @@ input.pill:not(:checked) + label .glyphicon {
.editable-cancel { margin-bottom: 0px !important; margin-left: 7px !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) {
this.worker_.terminate();
}
} else {
console.log(e);
}
};
@ -292,15 +290,11 @@ bitjs.archive = bitjs.archive || {};
this.worker_ = new Worker(scriptFileName);
this.worker_.onerror = function(e) {
console.log("Worker error: message = " + e.message);
throw e;
};
this.worker_.onmessage = function(e) {
if (typeof e.data === "string") {
// Just log any strings the workers pump our way.
console.log(e.data);
} else {
if (typeof e.data !== "string") {
// Assume that it is an UnarchiveEvent. Some browsers preserve the 'type'
// so that instanceof UnarchiveEvent returns true, but others do not.
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 _ */
$(function() {

@ -142,6 +142,17 @@ var languages = new Bloodhound({
}
});
var publishers = new Bloodhound({
name: "publisher",
datumTokenizer: function datumTokenizer(datum) {
return [datum.name];
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: getPath() + "/get_publishers_json?q=%QUERY"
}
});
function sourceSplit(query, cb, split, source) {
var bhAdapter = source.ttAdapter();
@ -224,6 +235,20 @@ promiseLanguages.done(function() {
);
});
var promisePublishers = publishers.initialize();
promisePublishers.done(function() {
$("#publisher").typeahead(
{
highlight: true, minLength: 0,
hint: true
}, {
name: "publishers",
displayKey: "name",
source: publishers.ttAdapter()
}
);
});
$("#search").on("change input.typeahead:selected", function() {
var form = $("form").serialize();
$.getJSON( getPath() + "/get_matching_tags", form, function( data ) {

@ -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
* Created by idalin<dalin.lin@gmail.com>
* 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)
*/

@ -99,7 +99,8 @@ kthoom.setSettings = function() {
};
var createURLFromArray = function(array, mimeType) {
var offset = array.byteOffset, len = array.byteLength;
var offset = array.byteOffset;
var len = array.byteLength;
var url;
var blob;
@ -137,11 +138,13 @@ var createURLFromArray = function(array, mimeType) {
kthoom.ImageFile = function(file) {
this.filename = file.filename;
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 === "gif" ? "image/gif" : fileExtension == 'svg' ? 'image/xml+svg' : undefined;
this.dataURI = createURLFromArray(file.fileData, mimeType);
if ( this.mimeType !== undefined) {
this.dataURI = createURLFromArray(file.fileData, this.mimeType);
this.data = file;
}
};
@ -169,7 +172,9 @@ function loadFromArrayBuffer(ab) {
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS,
function(e) {
var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive;
if (totalImages === 0) {
totalImages = e.totalFilesInArchive;
}
updateProgress(percentage *100);
lastCompletion = percentage * 100;
});
@ -180,8 +185,10 @@ function loadFromArrayBuffer(ab) {
var f = e.unarchivedFile;
// add any new pages based on the filename
if (imageFilenames.indexOf(f.filename) === -1) {
var test = new kthoom.ImageFile(f);
if ( test.mimeType !== undefined) {
imageFilenames.push(f.filename);
imageFiles.push(new kthoom.ImageFile(f));
imageFiles.push(test);
// add thumbnails to the TOC list
$("#thumbnails").append(
"<li>" +
@ -191,12 +198,16 @@ function loadFromArrayBuffer(ab) {
"</a>" +
"</li>"
);
}
}
// display first page if we haven't yet
if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion);
}
}
else {
totalImages--;
}
}
}
});
unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH,
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
// e.g.
// <input type="checkbox" data-control="stuff-to-show">
@ -60,25 +77,20 @@ $(function() {
layoutMode : "fitRows"
});
$(".load-more .row").infinitescroll({
var $loadMore = $(".load-more .row").infiniteScroll({
debug: false,
navSelector : ".pagination",
// selector for the paged navigation (it will be hidden)
nextSelector : ".pagination a:last",
path : ".next",
// selector for the NEXT link (to page 2)
itemSelector : ".load-more .book",
animate : true,
extraScrollPx: 300
// selector for all items you'll retrieve
}, function(data) {
append : ".load-more .book"
//animate : true, # ToDo: Reenable function
//extraScrollPx: 300
});
$loadMore.on( "append.infiniteScroll", function( event, response, path, data ) {
$(".pagination").addClass("hidden");
$(".load-more .row").isotope( "appended", $(data), null );
});
$("#sendbtn").click(function() {
var $this = $(this);
$this.text("Please wait...");
$this.addClass("disabled");
});
$("#restart").click(function() {
$.ajax({
dataType: "json",
@ -104,15 +116,18 @@ $(function() {
var $this = $(this);
var buttonText = $this.html();
$this.html("...");
$("#update_error").addClass("hidden")
$("#update_error").addClass("hidden");
if ($("#message").length) {
$("#message").alert("close");
}
$.ajax({
dataType: "json",
url: window.location.pathname + "/../../get_update_status",
success: function success(data) {
$this.html(buttonText);
var cssClass = '';
var message = ''
var cssClass = "";
var message = "";
if (data.success === true) {
if (data.update === true) {
@ -122,19 +137,20 @@ $(function() {
.removeClass("hidden")
.find("span").html(data.commit);
data.history.reverse().forEach((entry, index) => {
data.history.forEach(function(entry) {
$("<tr><td>" + entry[0] + "</td><td>" + entry[1] + "</td></tr>").appendTo($("#update_table"));
});
cssClass = 'alert-warning'
cssClass = "alert-warning";
} else {
cssClass = 'alert-success'
cssClass = "alert-success";
}
} else {
cssClass = 'alert-danger'
cssClass = "alert-danger";
}
message = '<div class="alert ' + cssClass
+ ' fade in"><a href="#" class="close" data-dismiss="alert">&times;</a>' + data.message + '</div>';
message = "<div id=\"message\" class=\"alert " + cssClass
+ " fade in\"><a href=\"#\" class=\"close\" data-dismiss=\"alert\">&times;</a>"
+ data.message + "</div>";
$(message).insertAfter($("#update_table"));
}
@ -163,6 +179,7 @@ $(function() {
});
});
// Init all data control handlers to default
$("input[data-control]").trigger("change");
$("#bookDetailsModal")
@ -186,6 +203,14 @@ $(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 */
Sortable.create(sortTrue, {

@ -1,3 +1,20 @@
/* 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() {
$("#domain_submit").click(function(event) {
@ -10,48 +27,49 @@ $(function() {
async: true,
timeout: 900,
success:function(data){
$('#domain-table').bootstrapTable("load", data);
$("#domain-table").bootstrapTable("load", data);
}
});
});
$('#domain-table').bootstrapTable({
$("#domain-table").bootstrapTable({
formatNoMatches: function () {
return '';
return "";
},
striped: false
});
$("#btndeletedomain").click(function() {
//get data-id attribute of the clicked element
var domainId = $(this).data('domainId');
var domainId = $(this).data("domainId");
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deletedomain",
data: {"domainid":domainId}
});
$('#DeleteDomain').modal('hide');
$("#DeleteDomain").modal("hide");
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist",
async: true,
timeout: 900,
success:function(data) {
$('#domain-table').bootstrapTable("load", data);
$("#domain-table").bootstrapTable("load", data);
}
});
});
//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
var domainId = $(e.relatedTarget).data('domain-id');
$(e.currentTarget).find("#btndeletedomain").data('domainId',domainId);
var domainId = $(e.relatedTarget).data("domain-id");
$(e.currentTarget).find("#btndeletedomain").data("domainId", domainId);
});
});
function TableActions (value, row, index) {
/*function TableActions (value, row, index) {
return [
'<a class="danger remove" data-toggle="modal" data-target="#DeleteDomain" data-domain-id="'+row.id+'" title="Remove">',
'<i class="glyphicon glyphicon-trash"></i>',
'</a>'
].join('');
}
"<a class=\"danger remove\" data-toggle=\"modal\" data-target=\"#DeleteDomain\" data-domain-id=\"" + row.id
+ "\" title=\"Remove\">",
"<i class=\"glyphicon glyphicon-trash\"></i>",
"</a>"
].join("");
}*/

@ -269,7 +269,7 @@ var RD = { //rep decode
var rBuffer;
// read in Huffman tables for RAR
function RarReadTables(bstream) {
function rarReadTables(bstream) {
var BitLength = new Array(rBC),
Table = new Array(rHuffTableSize);
var i;
@ -480,7 +480,7 @@ function Unpack20(bstream) { //, Solid) {
continue;
}
if (num < 270) {
var Distance = rSDDecode[num -= 261] + 1;
Distance = rSDDecode[num -= 261] + 1;
if ((Bits = rSDBits[num]) > 0) {
Distance += bstream.readBits(Bits);
}
@ -513,9 +513,9 @@ function rarReadTables20(bstream) {
var BitLength = new Array(rBC20);
var Table = new Array(rMC20 * 4);
var TableSize, N, I;
var i;
bstream.readBits(1);
if (!bstream.readBits(1)) {
var i;
for (i = UnpOldTable20.length; i--;) UnpOldTable20[i] = 0;
}
TableSize = rNC20 + rDC20 + rRC20;
@ -553,12 +553,13 @@ function rarReadTables20(bstream) {
}
function Unpack29(bstream, Solid) {
function Unpack29(bstream) {
// lazy initialize rDDecode and rDBits
var DDecode = new Array(rDC);
var DBits = new Array(rDC);
var Distance = 0;
var Length = 0;
var Dist = 0, BitLength = 0, Slot = 0;
var I;
for (I = 0; I < rDBitLengthCounts.length; I++, BitLength++) {
@ -571,7 +572,7 @@ function Unpack29(bstream, Solid) {
var Bits;
//tablesRead = false;
rOldDist = [0, 0, 0, 0]
rOldDist = [0, 0, 0, 0];
lastDist = 0;
lastLength = 0;
@ -579,7 +580,7 @@ function Unpack29(bstream, Solid) {
for (i = UnpOldTable.length; i--;) UnpOldTable[i] = 0;
// read in Huffman tables
RarReadTables(bstream);
rarReadTables(bstream);
while (true) {
var num = rarDecodeNumber(bstream, LD);
@ -589,12 +590,12 @@ function Unpack29(bstream, Solid) {
continue;
}
if (num >= 271) {
var Length = rLDecode[num -= 271] + 3;
Length = rLDecode[num -= 271] + 3;
if ((Bits = rLBits[num]) > 0) {
Length += bstream.readBits(Bits);
}
var DistNumber = rarDecodeNumber(bstream, DD);
var Distance = DDecode[DistNumber]+1;
Distance = DDecode[DistNumber] + 1;
if ((Bits = DBits[DistNumber]) > 0) {
if (DistNumber > 9) {
if (Bits > 4) {
@ -625,18 +626,18 @@ function Unpack29(bstream, Solid) {
Length++;
}
}
RarInsertOldDist(Distance);
RarInsertLastMatch(Length, Distance);
rarInsertOldDist(Distance);
rarInsertLastMatch(Length, Distance);
rarCopyString(Length, Distance);
continue;
}
if (num === 256) {
if (!RarReadEndOfBlock(bstream)) break;
if (!rarReadEndOfBlock(bstream)) break;
continue;
}
if (num === 257) {
//console.log("READVMCODE");
if (!RarReadVMCode(bstream)) break;
if (!rarReadVMCode(bstream)) break;
continue;
}
if (num === 258) {
@ -647,7 +648,7 @@ function Unpack29(bstream, Solid) {
}
if (num < 263) {
var DistNum = num - 259;
var Distance = rOldDist[DistNum];
Distance = rOldDist[DistNum];
for (var I = DistNum; I > 0; I--) {
rOldDist[I] = rOldDist[I - 1];
@ -655,31 +656,31 @@ function Unpack29(bstream, Solid) {
rOldDist[0] = Distance;
var LengthNumber = rarDecodeNumber(bstream, RD);
var Length = rLDecode[LengthNumber] + 2;
Length = rLDecode[LengthNumber] + 2;
if ((Bits = rLBits[LengthNumber]) > 0) {
Length += bstream.readBits(Bits);
}
RarInsertLastMatch(Length, Distance);
rarInsertLastMatch(Length, Distance);
rarCopyString(Length, Distance);
continue;
}
if (num < 272) {
var Distance = rSDDecode[num -= 263] + 1;
Distance = rSDDecode[num -= 263] + 1;
if ((Bits = rSDBits[num]) > 0) {
Distance += bstream.readBits(Bits);
}
RarInsertOldDist(Distance);
RarInsertLastMatch(2, Distance);
rarInsertOldDist(Distance);
rarInsertLastMatch(2, Distance);
rarCopyString(2, Distance);
continue;
}
}
rarUpdateProgress()
rarUpdateProgress();
}
function RarReadEndOfBlock(bstream) {
function rarReadEndOfBlock(bstream) {
rarUpdateProgress()
rarUpdateProgress();
var NewTable = false, NewFile = false;
if (bstream.readBits(1)) {
@ -689,11 +690,11 @@ function RarReadEndOfBlock(bstream) {
NewTable = !!bstream.readBits(1);
}
//tablesRead = !NewTable;
return !(NewFile || NewTable && !RarReadTables(bstream));
return !(NewFile || NewTable && !rarReadTables(bstream));
}
function RarReadVMCode(bstream) {
function rarReadVMCode(bstream) {
var FirstByte = bstream.readBits(8);
var Length = (FirstByte & 7) + 1;
if (Length === 7) {
@ -717,12 +718,12 @@ function RarAddVMCode(firstByte, vmCode, length) {
return true;
}
function RarInsertLastMatch(length, distance) {
function rarInsertLastMatch(length, distance) {
lastDist = distance;
lastLength = length;
}
function RarInsertOldDist(distance) {
function rarInsertOldDist(distance) {
rOldDist.splice(3, 1);
rOldDist.splice(0, 0, distance);
}
@ -745,7 +746,7 @@ function rarCopyString(length, distance) {
}
}
var rOldBuffers = []
var rOldBuffers = [];
// v must be a valid RarVolume
function unpack(v) {
@ -768,7 +769,7 @@ function unpack(v) {
break;
case 29: // rar 3.x compression
case 36: // alternative hash
Unpack29(bstream, Solid);
Unpack29(bstream);
break;
} // switch(method)
@ -817,7 +818,7 @@ RarLocalFile.prototype.unrar = function() {
this.fileData = unpack(this);
}
}
}
};
var unrar = function(arrayBuffer) {
currentFilename = "";
@ -834,16 +835,16 @@ var unrar = function(arrayBuffer) {
if (header.crc === 0x6152 &&
header.headType === 0x72 &&
header.flags.value === 0x1A21 &&
header.headSize === 7)
{
header.headSize === 7) {
info("Found RAR signature");
var mhead = new RarVolumeHeader(bstream);
if (mhead.headType != MAIN_HEAD) {
info("Error! RAR did not include a MAIN_HEAD header");
} else {
var localFiles = [],
localFile = null;
var localFiles = [];
var localFile = null;
do {
try {
localFile = new RarLocalFile(bstream);
@ -869,7 +870,7 @@ var unrar = function(arrayBuffer) {
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) {
var localfile = localFiles[i];

@ -9,7 +9,7 @@
* ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* 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).
importScripts("io.js");
@ -44,8 +44,6 @@ var zLocalFileHeaderSignature = 0x04034b50;
var zArchiveExtraDataSignature = 0x08064b50;
var zCentralFileHeaderSignature = 0x02014b50;
var zDigitalSignatureSignature = 0x05054b50;
var zEndOfCentralDirSignature = 0x06064b50;
var zEndOfCentralDirLocatorSignature = 0x07064b50;
// takes a ByteStream and parses out the local file information
var ZipLocalFile = function(bstream) {
@ -115,6 +113,7 @@ ZipLocalFile.prototype.unzip = function() {
info("ZIP v" + this.version + ", store only: " + this.filename + " (" + this.compressedSize + " bytes)");
currentBytesUnarchivedInFile = this.compressedSize;
currentBytesUnarchived += this.compressedSize;
this.fileData = zeroCompression(this.fileData, this.uncompressedSize);
}
// version == 20, compression method == 8 (DEFLATE)
else if (this.compressionMethod == 8) {
@ -239,7 +238,7 @@ var unzip = function(arrayBuffer) {
postProgress();
postMessage(new bitjs.archive.UnarchiveFinishEvent());
}
}
};
// returns a table of Huffman codes
// each entry's index is its code and its value is a JavaScript object
@ -253,7 +252,7 @@ function getHuffmanCodes(bitLengths) {
// Reference: http://tools.ietf.org/html/rfc1951#page-8
var numLengths = bitLengths.length,
bl_count = [],
blCount = [],
MAX_BITS = 1;
// Step 1: count up how many codes of each length we have
@ -265,22 +264,22 @@ function getHuffmanCodes(bitLengths) {
return null;
}
// 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
if (length > 0) bl_count[length]++;
if (length > 0) blCount[length]++;
if (length > MAX_BITS) MAX_BITS = length;
}
// Step 2: Find the numerical value of the smallest code for each code length
var next_code = [],
var nextCode = [],
code = 0;
for (var bits = 1; bits <= MAX_BITS; ++bits) {
var length = bits - 1;
// ensure undefined lengths are zero
if (bl_count[length] == undefined) bl_count[length] = 0;
code = (code + bl_count[bits-1]) << 1;
next_code[bits] = code;
if (blCount[length] == undefined) blCount[length] = 0;
code = (code + blCount[bits - 1]) << 1;
nextCode [bits] = code;
}
// Step 3: Assign numerical values to all codes
@ -288,9 +287,9 @@ function getHuffmanCodes(bitLengths) {
for (var n = 0; n < numLengths; ++n) {
var len = bitLengths[n];
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++;
next_code[len]++;
nextCode [len]++;
}
}
table.maxLength = tableLength;
@ -321,7 +320,8 @@ function getFixedLiteralTable() {
// create once
if (!fixedHCtoLiteral) {
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 = 256; i <= 279; ++i) bitlengths[i] = 7;
for (i = 280; i <= 287; ++i) bitlengths[i] = 8;
@ -335,7 +335,9 @@ function getFixedDistanceTable() {
// create once
if (!fixedHCtoDistance) {
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
fixedHCtoDistance = getHuffmanCodes(bitlengths);
@ -347,7 +349,6 @@ function getFixedDistanceTable() {
// then return that symbol
function decodeSymbol(bstream, hcTable) {
var code = 0, len = 0;
var match = false;
// loop until we match
for (;;) {
@ -446,10 +447,9 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
stream, and copy length bytes from this
position to the output stream.
*/
var numSymbols = 0, blockSize = 0;
var blockSize = 0;
for (;;) {
var symbol = decodeSymbol(bstream, hcLiteralTable);
++numSymbols;
if (symbol < 256) {
// copy literal byte to output
buffer.insertByte(symbol);
@ -485,7 +485,7 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
buffer.insertByte(data[ch++]);
}
} else {
buffer.insertBytes(buffer.data.subarray(ch, ch + length))
buffer.insertBytes(buffer.data.subarray(ch, ch + length));
}
} // length-distance pair
@ -494,6 +494,16 @@ function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) {
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.
// compression method 8
// deflate: http://tools.ietf.org/html/rfc1951
@ -516,8 +526,8 @@ function inflate(compressedData, numDecompressedBytes) {
if (bType == 0) {
// skip remaining bits in this byte
while (bstream.bitPtr != 0) bstream.readBits(1);
var len = bstream.readBits(16),
nlen = bstream.readBits(16);
var len = bstream.readBits(16);
bstream.readBits(16);
// TODO: check if nlen is the ones-complement of len?
if (len > 0) buffer.insertBytes(bstream.readBytes(len));
@ -573,14 +583,13 @@ function inflate(compressedData, numDecompressedBytes) {
}
}
else if (symbol == 17) {
var repeat = bstream.readBits(3) + 3;
while (repeat--) {
var repeat1 = bstream.readBits(3) + 3;
while (repeat1--) {
literalCodeLengths.push(0);
}
}
else if (symbol == 18) {
var repeat = bstream.readBits(7) + 11;
while (repeat--) {
} else if (symbol == 18) {
var repeat2 = bstream.readBits(7) + 11;
while (repeat2--) {
literalCodeLengths.push(0);
}
}
@ -593,9 +602,8 @@ function inflate(compressedData, numDecompressedBytes) {
var hcLiteralTable = getHuffmanCodes(literalCodeLengths),
hcDistanceTable = getHuffmanCodes(distanceCodeLengths);
blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer);
}
} else {
// error
else {
err("Error! Encountered deflate block of type 3");
return null;
}

@ -0,0 +1,199 @@
/*
* 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");
// Write the error response to the document.
if (contentType || xhr.status === 422) {
var responseText = xhr.responseText;
if (contentType.indexOf("text/plain") !== -1) {
responseText = "<pre>" + responseText + "</pre>";
document.write(responseText);
}
else {
this.$modalBar.text(responseText);
}
}
else {
this.$modalBar.text(this.options.modalTitleFailed);
}
},
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);

@ -144,7 +144,7 @@
<div class="modal-body text-center">
<p>{{_('Do you really want to restart Calibre-Web?')}}</p>
<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>
<p></p>
<button type="button" class="btn btn-default" id="restart" >{{_('Ok')}}</button>
@ -176,7 +176,7 @@
</div>
<div class="modal-body text-center">
<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>
<p></p>
<div id="Updatecontent"></div>

@ -29,19 +29,31 @@
<div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}">
{% 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 %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" />
{% endif %}
</a>
</div>
<div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<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 %}
{% endfor %}
</p>
@ -79,11 +91,13 @@
<p class="title">{{entry.title|shortentitle}}</p>
<p class="author">
{% for author in entry.authors %}
<a href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">
{{author.name.replace('|',',')}}
</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
<a class="author-name author-hidden" href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">{{author.name.replace('|',',')}}</a>
{% if loop.last %}
<a href="#" class="author-expand" data-authors-max="{{g.config_authors_max}}" data-collapse-caption="({{_('reduce')}})">(...)</a>
{% 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 %}
</p>

@ -6,7 +6,7 @@
<div class="col-sm-3 col-lg-3 col-xs-12">
<div class="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 %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ book.title }}"/>
{% endif %}
@ -101,7 +101,7 @@
</div>
<div class="form-group">
<label for="publisher">{{_('Publisher')}}</label>
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}" disabled>
<input type="text" class="form-control typeahead" name="publisher" id="publisher" value="{% if book.publishers|length > 0 %}{{book.publishers[0].name}}{% endif %}">
</div>
<div class="form-group">
<label for="languages">{{_('Language')}}</label>
@ -219,7 +219,7 @@
</span>
</div>
</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 class="modal-body">
<div class="text-center padded-bottom">

@ -31,14 +31,18 @@
</label>
</div>
{% 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">
<a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div>
{% 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>
{% endif %}
{% if g.user.is_authenticated %}
{% if not show_authenticate_google_drive %}
<div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
@ -60,6 +64,7 @@
{% endif %}
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
</div>
@ -88,6 +93,15 @@
<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">
</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>

@ -27,6 +27,17 @@
<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">
</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">
<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">
@ -143,6 +154,10 @@
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<label for="show_author">{{_('Show author selection')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
<label for="show_publisher">{{_('Show publisher selection')}}</label>
</div>
<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 %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>

@ -5,7 +5,7 @@
<div class="col-sm-3 col-lg-3 col-xs-5">
<div class="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 %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %}
@ -18,7 +18,7 @@
{% if entry.data|length %}
<div class="btn-group" role="group">
{% if entry.data|length < 2 %}
<button type="button" class="btn btn-primary">
<button id="Download" type="button" class="btn btn-primary">
{{_('Download')}} :
</button>
{% for format in entry.data %}
@ -40,20 +40,32 @@
</div>
{% endif %}
{% endif %}
{% if g.user.kindle_mail and g.user.is_authenticated %}
<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 g.user.kindle_mail and g.user.is_authenticated and kindle_list %}
{% 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 %}
{% if entry.data|length %}
{% endif %}
{% if reader_list %}
<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">
<span class="glyphicon glyphicon-eye-open"></span> {{_('Read in browser')}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="read-in-browser">
{% for format in entry.data %}
{%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|lower) }}">{{format.format}}</a></li>
{% endif %}
{% for format in reader_list %}
<li><a target="_blank" href="{{ url_for('read_book', book_id=entry.id, book_format=format) }}">{{format}}</a></li>
{%endfor%}
</ul>
</div>
@ -120,15 +132,21 @@
</div>
{% endif %}
{% if entry.publishers|length > 0 %}
<div class="publishers">
<p>
<span>{{_('Publisher')}}:{% for publisher in entry.publishers %} {{publisher.name}}{% if not loop.last %},{% endif %}{% endfor %}</span>
<span>{{_('Publisher')}}:
<a href="{{url_for('publisher', book_id=entry.publishers[0].id ) }}">{{entry.publishers[0].name}}</a>
</span>
</p>
</div>
{% endif %}
{% if entry.pubdate[:10] != '0101-01-01' %}
<div class="publishing-date">
<p>{{_('Publishing date')}}: {{entry.pubdate|formatdate}} </p>
</div>
{% endif %}
{% if cc|length > 0 %}
@ -164,7 +182,7 @@
<p>
<form id="have_read_form" action="{{ url_for('toggle_read', book_id=entry.id)}}" method="POST">
<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>
</label>
</form>

@ -9,17 +9,29 @@
<div class="cover">
{% 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">
<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>
{% endif %}
</div>
<div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<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 %}
{% endfor %}
</p>

@ -27,7 +27,10 @@
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
{% endif %}
<link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
<link rel="search"
href="{{url_for('feed_osd')}}"
type="application/opensearchdescription+xml"/>
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
<title>{{instance}}</title>
<author>
<name>{{instance}}</name>
@ -40,9 +43,16 @@
<title>{{entry.title}}</title>
<id>{{entry.uuid}}</id>
<updated>{{entry.atom_timestamp}}</updated>
{% if entry.authors.__len__() > 0 %}
<author>
<name>{{entry.authors[0].name}}</name>
</author>
{% endif %}
{% if entry.publishers.__len__() > 0 %}
<publisher>
<name>{{entry.publishers[0].name}}</name>
</publisher>
{% endif %}
<dcterms:language>{{entry.language}}</dcterms:language>
{% for tag in entry.tags %}
<category scheme="http://www.bisg.org/standards/bisac_subject/index.html"

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html class="http-error" lang="{{ g.user.locale }}">
<head>
<title>{{ instance }} | HTTP Error ({{ error_code }})</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Bootstrap -->
<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 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">
{% if g.current_theme == 1 %}
<link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen">
{% endif %}
</head>
<body>
<div class="container text-center">
<h1>{{ error_code }}</h1>
<h3>{{ error_name }}</h3>
<a href="{{url_for('index')}}" title="{{ _('Back to home') }}">{{_('Back to home')}}</a>
</div>
</body>
</html>

@ -1,8 +1,8 @@
{% extends "layout.html" %}
{% block body %}
{% if g.user.show_detail_random() %}
<div class="discover">
<h2>{{_('Discover (Random Books)')}}</h2>
<div class="discover random-books">
<h2 class="random-books">{{_('Discover (Random Books)')}}</h2>
<div class="row">
{% for entry in random %}
@ -10,19 +10,31 @@
<div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% 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 %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %}
</a>
</div>
<div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<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 %}
{% endfor %}
</p>
@ -45,7 +57,7 @@
</div>
{% endif %}
<div class="discover load-more">
<h2>{{title}}</h2>
<h2 class="{{title}}">{{_(title)}}</h2>
<div class="row">
{% if entries[0] %}
{% for entry in entries %}
@ -53,19 +65,31 @@
<div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% 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 %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %}
</a>
</div>
<div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')|shortentitle(30)}}</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<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 %}
{% endfor %}
</p>

@ -5,7 +5,10 @@
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link title="{{_('Search')}}" type="application/opensearchdescription+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
<link rel="search"
href="{{url_for('feed_osd')}}"
type="application/opensearchdescription+xml"/>
<!--link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/-->
<title>{{instance}}</title>
<author>
<name>{{instance}}</name>
@ -62,6 +65,13 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Author')}}</content>
</entry>
<entry>
<title>{{_('Publishers')}}</title>
<link rel="subsection" href="{{url_for('feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
<id>{{url_for('feed_publisherindex')}}</id>
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by publisher')}}</content>
</entry>
<entry>
<title>{{_('Category list')}}</title>
<link rel="subsection" href="{{url_for('feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>

@ -12,8 +12,9 @@
<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/style.css') }}" rel="stylesheet" media="screen">
{% if g.user.get_theme == 1 %}
<link href="{{ url_for('static', filename='css/caliBlur-style.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/upload.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 %}
<!-- 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:// -->
@ -24,7 +25,7 @@
{% block header %}{% endblock %}
</head>
<body class="{{ page }}">
<body class="{{ page }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}">
<!-- Static navbar -->
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
@ -61,19 +62,20 @@
<li>
<form id="form-upload" class="navbar-form" action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data">
<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>
</form>
</li>
{% 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>
{% endif %}
{% 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 %}
<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 %}
<li><a id="logout" href="{{url_for('logout')}}"><span class="glyphicon glyphicon-log-out"></span><span class="hidden-sm">{{_('Logout')}}</span></a></li>
{% endif %}
@ -103,14 +105,14 @@
</div>
{%endif%}
{% endfor %}
{% if g.current_theme == 1 %}
<div id="loader" hidden="true">
<center>
<h3>{{_('Uploading...')}}</h3>
<span>{{_("please don't refresh the page")}}</span>.
<br />
<img src="{{ url_for('static', filename='img/loader.gif') }}">
</center>
</div>
{%endif%}
<div class="container-fluid">
<div class="row-fluid">
{% if g.user.is_authenticated or g.user.is_anonymous %}
@ -159,15 +161,18 @@
{% if g.user.show_author() %}
<li id="nav_author" {% if page == 'author' %}class="active"{% endif %}><a href="{{url_for('author_list')}}"><span class="glyphicon glyphicon-user"></span>{{_('Authors')}}</a></li>
{%endif%}
{% if g.user.show_publisher() %}
<li id="nav_publisher" {% if page == 'publisher' %}class="active"{% endif %}><a href="{{url_for('publisher_list')}}"><span class="glyphicon glyphicon-text-size"></span>{{_('Publishers')}}</a></li>
{%endif%}
{% if g.user.filter_language() == 'all' and g.user.show_language() %}
<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%}
{% 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 %}
<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 %}
<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 %}
<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 %}
@ -237,10 +242,23 @@
<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/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">
$(function() {
$("#form-upload").uploadprogress({
redirect_url: "{{ url_for('index')}}",
uploadedMsg: "{{_('Upload done, processing, please wait...')}}",
modalTitle: "{{_('Uploading...')}}",
modalFooter: "{{_('Close')}}",
modalTitleFailed: "{{_('Error')}}"
});
$("#btn-upload").change(function() {
$("#loader").show();
$("#form-upload").submit();
});
});

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

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

@ -7,6 +7,8 @@
<meta name="description" content="">
<meta name="viewport" content="width=device-width, user-scalable=no">
<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/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="{{ url_for('static', filename='locale/locale.properties') }}">
<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="pdf.js"></script>-->
<script type="text/javascript">
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 IMAGE_LOCATION="{{ url_for('static', filename='css/../images') }}";
var IMAGE_LOCATION="{{ url_for('static', filename='/images/') }}";
var PDFWORKER_LOCATION_JS="{{ url_for('static', filename='js/libs/pdf.worker') }}";
</script>
@ -420,8 +415,7 @@ See https://github.com/adobe-type-tools/cmap-resources
}
</style>
<div class="mozPrintCallback-dialog-box">
<!-- TODO: Localise the following strings -->
Preparing document for printing...
{{_('Preparing document for printing...')}}
<div class="progress-row">
<progress value="0" max="100"></progress>
<span class="relative-progress">0%</span>

@ -36,17 +36,29 @@
<div class="cover">
{% 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">
<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>
{% endif %}
</div>
<div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id ) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<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 %}
{% endfor %}
</p>

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

@ -16,19 +16,31 @@
<div class="cover">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
{% 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 %}
<img src="{{ url_for('static', filename='generic_cover.jpg') }}" alt="{{ entry.title }}" />
{% endif %}
</a>
</div>
<div class="meta">
<a href="{{ url_for('show_book', book_id=entry.id) }}" data-toggle="modal" data-target="#bookDetailsModal" data-remote="false">
<p class="title">{{entry.title|shortentitle}}</p>
</a>
<p class="author">
{% for author in entry.authors %}
<a href="{{url_for('author', book_id=author.id) }}">{{author.name.replace('|',',')}}</a>
{% if not loop.last %}
&amp;
{% if loop.index > g.config_authors_max and g.config_authors_max != 0 %}
{% if not loop.first %}
<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 %}
{% endfor %}
</p>

@ -11,7 +11,7 @@
{% if g.user.role_admin() %}
<th data-halign="right" data-align="right" data-field="user" data-sortable="true">{{_('User')}}</th>
{% endif %}
<th data-halign="right" data-align="right" data-field="type" data-sortable="true">{{_('Task')}}</th>
<th data-halign="right" data-align="right" data-field="taskMessage" data-sortable="true">{{_('Task')}}</th>
<th data-halign="right" data-align="right" data-field="status" data-sortable="true">{{_('Status')}}</th>
<th data-halign="right" data-align="right" data-field="progress" data-sortable="true" data-sorter="elementSorter">{{_('Progress')}}</th>
<th data-halign="right" data-align="right" data-field="runtime" data-sortable="true" data-sort-name="rt">{{_('Runtime')}}</th>

@ -35,14 +35,6 @@
{% endfor %}
</select>
</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">
<label for="default_language">{{_('Show books with language')}}</label>
<select name="default_language" id="default_language" class="form-control">
@ -89,10 +81,16 @@
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<label for="show_author">{{_('Show author selection')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="show_publisher" id="show_publisher" {% if content.show_publisher() %}checked{% endif %}>
<label for="show_publisher">{{_('Show publisher selection')}}</label>
</div>
{% if not content.role_anonymous() %}
<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 %}>
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
</div>
{% endif %}
<div class="form-group">
<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>
@ -159,7 +157,7 @@
{% for entry in downloads %}
<div class="col-sm-2">
<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>
</div>
{% 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
# -*- 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 exc
from sqlalchemy.ext.declarative import declarative_base
@ -41,10 +58,20 @@ SIDEBAR_READ_AND_UNREAD = 256
SIDEBAR_RECENT = 512
SIDEBAR_SORTED = 1024
MATURE_CONTENT = 2048
SIDEBAR_PUBLISHER = 4096
DEFAULT_PASS = "admin123"
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:
@ -102,10 +129,6 @@ class UserBase:
def is_anonymous(self):
return False
@property
def get_theme(self):
return self.theme
def get_id(self):
return str(self.id)
@ -136,6 +159,9 @@ class UserBase:
def show_author(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
def show_publisher(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
def show_best_rated_books(self):
return bool((self.sidebar_view is not None)and(self.sidebar_view & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
@ -166,7 +192,6 @@ class User(UserBase, Base):
sidebar_view = Column(Integer, default=1)
default_language = Column(String(3), default="all")
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
@ -290,6 +315,7 @@ class Settings(Base):
config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_books_per_page = Column(Integer, default=60)
config_random_books = Column(Integer, default=4)
config_authors_max = 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_log_level = Column(SmallInteger, default=logging.INFO)
@ -297,7 +323,7 @@ class Settings(Base):
config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0)
config_default_role = Column(SmallInteger, default=0)
config_default_show = Column(SmallInteger, default=2047)
config_default_show = Column(SmallInteger, default=6143)
config_columns_to_ignore = Column(String)
config_use_google_drive = Column(Boolean)
config_google_drive_folder = Column(String)
@ -312,6 +338,8 @@ class Settings(Base):
config_converterpath = Column(String)
config_calibre = Column(String)
config_rarfile_location = Column(String)
config_theme = Column(Integer, default=0)
config_updatechannel = Column(Integer, default=0)
def __repr__(self):
pass
@ -353,6 +381,7 @@ class Config:
self.config_calibre_web_title = data.config_calibre_web_title
self.config_books_per_page = data.config_books_per_page
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_read_column = data.config_read_column
self.config_log_level = data.config_log_level
@ -385,11 +414,17 @@ class Config:
if data.config_logfile:
self.config_logfile = data.config_logfile
self.config_rarfile_location = data.config_rarfile_location
self.config_theme = data.config_theme
self.config_updatechannel = data.config_updatechannel
@property
def get_main_dir(self):
return self.config_main_dir
@property
def get_update_channel(self):
return self.config_updatechannel
def get_config_certfile(self):
if cli.certfilepath:
return cli.certfilepath
@ -485,6 +520,10 @@ class Config:
return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_AUTHOR == SIDEBAR_AUTHOR))
def show_publisher(self):
return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_PUBLISHER == SIDEBAR_PUBLISHER))
def show_best_rated_books(self):
return bool((self.config_default_show is not None) and
(self.config_default_show & SIDEBAR_BEST_RATED == SIDEBAR_BEST_RATED))
@ -529,6 +568,8 @@ class Config:
# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
# rows with SQL commands
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"):
Bookmark.__table__.create(bind=engine)
if not engine.dialect.has_table(engine.connect(), "registration"):
@ -562,6 +603,12 @@ def migrate_Database():
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_default_role` SmallInteger DEFAULT 0")
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:
session.query(exists().where(BookShelf.order)).scalar()
except exc.OperationalError: # Database is not compatible, some rows are missing
@ -602,12 +649,7 @@ def migrate_Database():
except exc.OperationalError:
conn = engine.connect()
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:
create_anonymous_user()
try:
@ -660,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_calibre` String DEFAULT ''")
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
conn = engine.connect()
@ -740,7 +795,7 @@ def create_admin_user():
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_DELETE_BOOKS + ROLE_PASSWD
user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD + SIDEBAR_RECENT + \
SIDEBAR_SORTED + MATURE_CONTENT
SIDEBAR_SORTED + MATURE_CONTENT + SIDEBAR_PUBLISHER
user.password = generate_password_hash(DEFAULT_PASS)

@ -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()

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

Loading…
Cancel
Save