Merge remote-tracking branch 'refs/remotes/janeczku/master'

# Conflicts:
#	cps/translations/pl/LC_MESSAGES/messages.mo
#	cps/translations/pl/LC_MESSAGES/messages.po
This commit is contained in:
Radosław Kierznowski 2017-03-20 22:01:25 +01:00
commit 55ee323ca1
1765 changed files with 4324 additions and 316944 deletions

6
.gitignore vendored
View File

@ -23,3 +23,9 @@ cps/static/[0-9]*
*.bak
*.log.*
tags
settings.yaml
gdrive_credentials
#kindlegen
vendor/kindlegen

27
cps.py
View File

@ -6,20 +6,33 @@ import sys
base_path = os.path.dirname(os.path.abspath(__file__))
# Insert local directories into path
sys.path.insert(0, os.path.join(base_path, 'vendor'))
sys.path.append(base_path)
sys.path.append(os.path.join(base_path, 'cps'))
sys.path.append(os.path.join(base_path, 'vendor'))
from cps import web
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
try:
from gevent.wsgi import WSGIServer
gevent_present = True
except ImportError:
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
gevent_present = False
if __name__ == '__main__':
if web.ub.DEVELOPMENT:
web.app.run(host="0.0.0.0", port=web.ub.config.config_port, debug=True)
else:
http_server = HTTPServer(WSGIContainer(web.app))
http_server.listen(web.ub.config.config_port)
IOLoop.instance().start()
if gevent_present:
web.app.logger.info('Attempting to start gevent')
web.start_gevent()
else:
web.app.logger.info('Falling back to Tornado')
http_server = HTTPServer(WSGIContainer(web.app))
http_server.listen(web.ub.config.config_port)
IOLoop.instance().start()
IOLoop.instance().close(True)
if web.helper.global_task == 0:
web.app.logger.info("Performing restart of Calibre-web")

View File

@ -14,28 +14,28 @@ try:
from wand.image import Image
from wand import version as ImageVersion
use_generic_pdf_cover = False
except ImportError, e:
except ImportError as e:
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
use_generic_pdf_cover = True
try:
from PyPDF2 import PdfFileReader
from PyPDF2 import __version__ as PyPdfVersion
use_pdf_meta = True
except ImportError, e:
except ImportError as e:
logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e)
use_pdf_meta = False
try:
import epub
use_epub_meta = True
except ImportError, e:
except ImportError as e:
logger.warning('cannot import epub, extracting epub metadata will not work: %s', e)
use_epub_meta = False
try:
import fb2
use_fb2_meta = True
except ImportError, e:
except ImportError as e:
logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False
@ -48,7 +48,7 @@ def process(tmp_file_path, original_file_name, original_file_extension):
return epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension)
if ".FB2" == original_file_extension.upper() and use_fb2_meta is True:
return fb2.get_fb2_info(tmp_file_path, original_file_extension)
except Exception, e:
except Exception as e:
logger.warning('cannot parse metadata, using default: %s', e)
return default_meta(tmp_file_path, original_file_name, original_file_extension)
@ -63,7 +63,8 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
description="",
tags="",
series="",
series_id="")
series_id="",
languages="")
def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
@ -91,7 +92,8 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
description=subject,
tags="",
series="",
series_id="")
series_id="",
languages="")
def pdf_preview(tmp_file_path, tmp_dir):

View File

@ -11,10 +11,8 @@ from ub import config
import ub
session = None
cc_exceptions = None
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
cc_classes = None
cc_ids = None
books_custom_column_links = None
engine = None
@ -247,7 +245,7 @@ class Books(Base):
identifiers = relationship('Identifiers', backref='books')
def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover,
authors, tags):
authors, tags, languages = None):
self.title = title
self.sort = sort
self.author_sort = author_sort
@ -283,22 +281,19 @@ class Custom_Columns(Base):
def setup_db():
global session
global cc_exceptions
global cc_classes
global cc_ids
global books_custom_column_links
global engine
global session
global cc_classes
if config.config_calibre_dir is None or config.config_calibre_dir == u'':
return False
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False, isolation_level="SERIALIZABLE")
#engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False, isolation_level="SERIALIZABLE")
engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE")
try:
conn = engine.connect()
except:
except Exception as e:
content = ub.session.query(ub.Settings).first()
content.config_calibre_dir = None
content.db_configured = False
@ -311,43 +306,43 @@ def setup_db():
config.loadSettings()
conn.connection.create_function('title_sort', 1, title_sort)
cc = conn.execute("SELECT id, datatype FROM custom_columns")
if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns")
cc_ids = []
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
books_custom_column_links = {}
cc_classes = {}
for row in cc:
if row.datatype not in cc_exceptions:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' + str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
if row.datatype == 'bool':
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')),
'value': Column(Boolean)}
cc_ids = []
books_custom_column_links = {}
cc_classes = {}
for row in cc:
if row.datatype not in cc_exceptions:
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
Column('book', Integer, ForeignKey('books.id'),
primary_key=True),
Column('value', Integer,
ForeignKey('custom_column_' + str(row.id) + '.id'),
primary_key=True)
)
cc_ids.append([row.id, row.datatype])
if row.datatype == 'bool':
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')),
'value': Column(Boolean)}
else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'value': Column(String)}
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
for id in cc_ids:
if id[1] == 'bool':
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
primaryjoin=(
Books.id == cc_classes[id[0]].book),
backref='books'))
else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'value': Column(String)}
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
for id in cc_ids:
if id[1] == 'bool':
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
primaryjoin=(
Books.id == cc_classes[id[0]].book),
backref='books'))
else:
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
secondary=books_custom_column_links[id[0]],
backref='books'))
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
secondary=books_custom_column_links[id[0]],
backref='books'))
# Base.metadata.create_all(engine)
Session = sessionmaker()

View File

@ -5,7 +5,7 @@ import zipfile
from lxml import etree
import os
import uploader
from iso639 import languages as isoLanguages
def extractCover(zip, coverFile, coverpath, tmp_file_name):
if coverFile is None:
@ -41,22 +41,52 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
epub_metadata = {}
for s in ['title', 'description', 'creator']:
for s in ['title', 'description', 'creator', 'language']:
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
if len(tmp) > 0:
epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0]
else:
epub_metadata[s] = "Unknown"
if epub_metadata['description'] == "Unknown":
description = tree.xpath("//*[local-name() = 'description']/text()")
if len(description) > 0:
epub_metadata['description'] = description
else:
epub_metadata['description'] = ""
if epub_metadata['language'] == "Unknown":
epub_metadata['language'] == ""
else:
lang = epub_metadata['language'].split('-', 1)[0].lower()
if len(lang) == 2:
epub_metadata['language'] = isoLanguages.get(part1=lang).name
elif len(lang) == 3:
epub_metadata['language'] = isoLanguages.get(part3=lang).name
else:
epub_metadata['language'] = ""
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
coverfile = None
if len(coversection) > 0:
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
else:
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover']/@href", namespaces=ns)
if len(coversection) > 0:
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
else:
coverfile = None
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0:
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
if len(coversection) > 0:
filetype = coversection[0].rsplit('.',1)[-1]
if filetype == "xhtml" or filetype == "html": #if cover is (x)html format
markup = zip.read(os.path.join(coverpath,coversection[0]))
markupTree = etree.fromstring(markup)
#no matter xhtml or html with no namespace
imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src")
#imgsrc maybe startwith "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])), imgsrc[0]))
coverfile = extractCover(zip, filename, "", tmp_file_path)
else:
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
if epub_metadata['title'] is None:
title = original_file_name
@ -72,4 +102,5 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
description=epub_metadata['description'],
tags="",
series="",
series_id="")
series_id="",
languages=epub_metadata['language'])

View File

@ -4,8 +4,10 @@
from lxml import etree
import os
import uploader
import StringIO
try:
from io import StringIO
except ImportError as e:
import StringIO
def get_fb2_info(tmp_file_path, original_file_extension):
@ -37,16 +39,16 @@ def get_fb2_info(tmp_file_path, original_file_extension):
first_name = u''
return first_name + ' ' + middle_name + ' ' + last_name
author = unicode(", ".join(map(get_author, authors)))
author = str(", ".join(map(get_author, authors)))
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
if len(title):
title = unicode(title[0])
title = str(title[0])
else:
title = u''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description):
description = unicode(description[0])
description = str(description[0])
else:
description = u''
@ -59,4 +61,5 @@ def get_fb2_info(tmp_file_path, original_file_extension):
description=description,
tags="",
series="",
series_id="")
series_id="",
languages="")

370
cps/gdriveutils.py Normal file
View File

@ -0,0 +1,370 @@
try:
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from apiclient import errors
except ImportError:
pass
import os, time
from ub import config
from sqlalchemy import *
from sqlalchemy import exc
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import *
import web
dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep), "gdrive.db")
engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False)
Base = declarative_base()
# Open session for database connection
Session = sessionmaker()
Session.configure(bind=engine)
session = scoped_session(Session)
class GdriveId(Base):
__tablename__='gdrive_ids'
id = Column(Integer, primary_key=True)
gdrive_id = Column(Integer, unique=True)
path = Column(String)
__table_args__ = (UniqueConstraint('gdrive_id', 'path', name='_gdrive_path_uc'),)
def __repr__(self):
return str(self.path)
class PermissionAdded(Base):
__tablename__='permissions_added'
id = Column(Integer, primary_key=True)
gdrive_id = Column(Integer, unique=True)
def __repr__(self):
return str(self.gdrive_id)
def migrate():
if not engine.dialect.has_table(engine.connect(), "permissions_added"):
PermissionAdded.__table__.create(bind = engine)
for sql in session.execute("select sql from sqlite_master where type='table'"):
if 'CREATE TABLE gdrive_ids' in sql[0]:
currUniqueConstraint='UNIQUE (gdrive_id)'
if currUniqueConstraint in sql[0]:
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.commit()
session.execute('DROP TABLE %s' % 'gdrive_ids')
session.execute('ALTER TABLE gdrive_ids2 RENAME to gdrive_ids')
break
if not os.path.exists(dbpath):
try:
Base.metadata.create_all(engine)
except Exception:
raise
migrate()
def getDrive(gauth=None):
if not gauth:
gauth=GoogleAuth(settings_file='settings.yaml')
# Try to load saved client credentials
gauth.LoadCredentialsFile("gdrive_credentials")
if gauth.access_token_expired:
# Refresh them if expired
gauth.Refresh()
else:
# Initialize the saved creds
gauth.Authorize()
# Save the current credentials to a file
return GoogleDrive(gauth)
def getEbooksFolder(drive=None):
if not drive:
drive = getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
ebooksFolder= "title = '%s' and 'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % config.config_google_drive_folder
fileList = drive.ListFile({'q': ebooksFolder}).GetList()
return fileList[0]
def getEbooksFolderId(drive=None):
storedPathName=session.query(GdriveId).filter(GdriveId.path == '/').first()
if storedPathName:
return storedPathName.gdrive_id
else:
gDriveId=GdriveId()
gDriveId.gdrive_id=getEbooksFolder(drive)['id']
gDriveId.path='/'
session.merge(gDriveId)
session.commit()
return
def getFolderInFolder(parentId, folderName, drive=None):
if not drive:
drive = getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
folder= "title = '%s' and '%s' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false" % (folderName.replace("'", "\\'"), parentId)
fileList = drive.ListFile({'q': folder}).GetList()
return fileList[0]
def getFile(pathId, fileName, drive=None):
if not drive:
drive = getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
metaDataFile="'%s' in parents and trashed = false and title = '%s'" % (pathId, fileName.replace("'", "\\'"))
fileList = drive.ListFile({'q': metaDataFile}).GetList()
return fileList[0]
def getFolderId(path, drive=None):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
currentFolderId=getEbooksFolderId(drive)
sqlCheckPath=path if path[-1] =='/' else path + '/'
storedPathName=session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
if not storedPathName:
dbChange=False
s=path.split('/')
for i, x in enumerate(s):
if len(x) > 0:
currentPath="/".join(s[:i+1])
if currentPath[-1] != '/':
currentPath = currentPath + '/'
storedPathName=session.query(GdriveId).filter(GdriveId.path == currentPath).first()
if storedPathName:
currentFolderId=storedPathName.gdrive_id
else:
currentFolderId=getFolderInFolder(currentFolderId, x, drive)['id']
gDriveId=GdriveId()
gDriveId.gdrive_id=currentFolderId
gDriveId.path=currentPath
session.merge(gDriveId)
dbChange=True
if dbChange:
session.commit()
else:
currentFolderId=storedPathName.gdrive_id
return currentFolderId
def getFileFromEbooksFolder(drive, path, fileName):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
if path:
sqlCheckPath=path if path[-1] =='/' else path + '/'
folderId=getFolderId(path, drive)
else:
folderId=getEbooksFolderId(drive)
return getFile(folderId, fileName, drive)
def copyDriveFileRemote(drive, origin_file_id, copy_title):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
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 downloadFile(drive, path, filename, output):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
f=getFileFromEbooksFolder(drive, path, filename)
f.GetContentFile(output)
def backupCalibreDbAndOptionalDownload(drive, f=None):
pass
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
metaDataFile="'%s' in parents and title = 'metadata.db' and trashed = false" % getEbooksFolderId()
fileList = drive.ListFile({'q': metaDataFile}).GetList()
databaseFile=fileList[0]
if f:
databaseFile.GetContentFile(f)
def copyToDrive(drive, uploadFile, createRoot, replaceFiles,
ignoreFiles=[],
parent=None, prevDir=''):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
isInitial=not bool(parent)
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()
if len(existingFolder) == 0 and (not isInitial or createRoot):
parent = drive.CreateFile({'title': os.path.basename(uploadFile), 'parents' : [{"kind": "drive#fileLink", 'id' : parent['id']}],
"mimeType": "application/vnd.google-apps.folder" })
parent.Upload()
else:
if (not isInitial or createRoot) and len(existingFolder) > 0:
parent=existingFolder[0]
for f in os.listdir(os.path.join(prevDir,uploadFile)):
if f not in ignoreFiles:
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()
if len(existingFiles) > 0:
driveFile=existingFiles[0]
else:
driveFile = drive.CreateFile({'title': os.path.basename(uploadFile), 'parents' : [{"kind": "drive#fileLink", 'id' : parent['id']}], })
driveFile.SetContentFile(os.path.join(prevDir,uploadFile))
driveFile.Upload()
def uploadFileToEbooksFolder(drive, destFile, f):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
parent=getEbooksFolder(drive)
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()
if len(existingFiles) > 0:
driveFile=existingFiles[0]
else:
driveFile = drive.CreateFile({'title': x, 'parents' : [{"kind": "drive#fileLink", 'id' : parent['id']}], })
driveFile.SetContentFile(f)
driveFile.Upload()
else:
existingFolder=drive.ListFile({'q' : "title = '%s' and '%s' in parents and trashed = false" % (x, 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" })
parent.Upload()
else:
parent=existingFolder[0]
def watchChange(drive, channel_id, channel_type, channel_address,
channel_token=None, expiration=None):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
"""Watch for all changes to a user's Drive.
Args:
service: Drive API service instance.
channel_id: Unique string that identifies this channel.
channel_type: Type of delivery mechanism used for this channel.
channel_address: Address where notifications are delivered.
channel_token: An arbitrary string delivered to the target address with
each notification delivered over this channel. Optional.
channel_address: Address where notifications are delivered. Optional.
Returns:
The created channel if successful
Raises:
apiclient.errors.HttpError: if http request to create channel fails.
"""
body = {
'id': channel_id,
'type': channel_type,
'address': channel_address
}
if channel_token:
body['token'] = channel_token
if expiration:
body['expiration'] = expiration
return drive.auth.service.changes().watch(body=body).execute()
def watchFile(drive, file_id, channel_id, channel_type, channel_address,
channel_token=None, expiration=None):
"""Watch for any changes to a specific file.
Args:
service: Drive API service instance.
file_id: ID of the file to watch.
channel_id: Unique string that identifies this channel.
channel_type: Type of delivery mechanism used for this channel.
channel_address: Address where notifications are delivered.
channel_token: An arbitrary string delivered to the target address with
each notification delivered over this channel. Optional.
channel_address: Address where notifications are delivered. Optional.
Returns:
The created channel if successful
Raises:
apiclient.errors.HttpError: if http request to create channel fails.
"""
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
body = {
'id': channel_id,
'type': channel_type,
'address': channel_address
}
if channel_token:
body['token'] = channel_token
if expiration:
body['expiration'] = expiration
return drive.auth.service.files().watch(fileId=file_id, body=body).execute()
def stopChannel(drive, channel_id, resource_id):
"""Stop watching to a specific channel.
Args:
service: Drive API service instance.
channel_id: ID of the channel to stop.
resource_id: Resource ID of the channel to stop.
Raises:
apiclient.errors.HttpError: if http request to create channel fails.
"""
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
service=drive.auth.service
body = {
'id': channel_id,
'resourceId': resource_id
}
return drive.auth.service.channels().stop(body=body).execute()
def getChangeById (drive, change_id):
if not drive:
drive=getDrive()
if drive.auth.access_token_expired:
drive.auth.Refresh()
"""Print a single Change resource information.
Args:
service: Drive API service instance.
change_id: ID of the Change resource to retrieve.
"""
try:
change = drive.auth.service.changes().get(changeId=change_id).execute()
return change
except errors.HttpError, error:
web.app.logger.exception(error)
return None

View File

@ -13,11 +13,18 @@ import os
import traceback
import re
import unicodedata
from StringIO import StringIO
try:
from StringIO import StringIO
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
except ImportError as e:
from io import StringIO
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import encoders
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.generator import Generator
from email.utils import formatdate
from email.utils import make_msgid
@ -28,11 +35,16 @@ import shutil
import requests
import zipfile
from tornado.ioloop import IOLoop
try:
import gdriveutils as gd
except ImportError:
pass
import web
try:
import unidecode
use_unidecode=True
except:
except Exception as e:
use_unidecode=False
# Global variables
@ -147,7 +159,7 @@ def send_raw_email(kindle_mail, msg):
smtplib.stderr = org_stderr
except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException), e:
except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException) as e:
app.logger.error(traceback.print_exc())
return _("Failed to send mail: %s" % str(e))
@ -239,7 +251,10 @@ def get_valid_filename(value, replace_whitespace=True):
value=value.replace(u'ß',u'ss')
value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile('[\W\s-]', re.UNICODE)
value = unicode(re_slugify.sub('', value).strip())
if type(value) is str: #Python3 str, Python2 unicode
value = re_slugify.sub('', value).strip()
else:
value = unicode(re_slugify.sub('', value).strip())
if replace_whitespace:
#*+:\"/<>? werden durch _ ersetzt
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U)
@ -280,6 +295,30 @@ def update_dir_stucture(book_id, calibrepath):
book.path = new_authordir + '/' + book.path.split('/')[1]
db.session.commit()
def update_dir_structure_gdrive(book_id):
db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
authordir = book.path.split('/')[0]
new_authordir = get_valid_filename(book.authors[0].name)
titledir = book.path.split('/')[1]
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
if titledir != new_titledir:
print (titledir)
gFile=gd.getFileFromEbooksFolder(web.Gdrive.Instance().drive,os.path.dirname(book.path),titledir)
gFile['title']= new_titledir
gFile.Upload()
book.path = book.path.split('/')[0] + '/' + new_titledir
if authordir != new_authordir:
gFile=gd.getFileFromEbooksFolder(web.Gdrive.Instance().drive,None,authordir)
gFile['title']= new_authordir
gFile.Upload()
book.path = new_authordir + '/' + book.path.split('/')[1]
db.session.commit()
class Updater(threading.Thread):
def __init__(self):
@ -305,9 +344,13 @@ class Updater(threading.Thread):
ub.session.close()
ub.engine.dispose()
self.status=6
# stop tornado server
server = IOLoop.instance()
server.add_callback(server.stop)
if web.gevent_server:
web.gevent_server.stop()
else:
# stop tornado server
server = IOLoop.instance()
server.add_callback(server.stop)
self.status=7
def get_update_status(self):
@ -379,7 +422,7 @@ class Updater(threading.Thread):
try:
os.chown(dst_file, permission.st_uid, permission.st_uid)
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
except:
except Exception as e:
e = sys.exc_info()
logging.getLogger('cps.web').debug('Fail '+str(dst_file)+' error: '+str(e))
return
@ -421,7 +464,7 @@ class Updater(threading.Thread):
logging.getLogger('cps.web').debug("Delete file " + item_path)
log_from_thread("Delete file " + item_path)
os.remove(item_path)
except:
except Exception as e:
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True)

180
cps/static/js/get_meta.js Normal file
View File

@ -0,0 +1,180 @@
/*
* 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)
*/
$(document).ready(function () {
var msg = i18n_msg;
var douban = 'https://api.douban.com';
var db_search = '/v2/book/search';
var db_get_info = '/v2/book/';
var db_get_info_by_isbn = '/v2/book/isbn/ ';
var db_done = false;
var google = 'https://www.googleapis.com/';
var gg_search = '/books/v1/volumes';
var gg_get_info = '/books/v1/volumes/';
var gg_done = false;
var db_results = [];
var gg_results = [];
var show_flag = 0;
String.prototype.replaceAll = function (s1, s2) {  
return this.replace(new RegExp(s1, "gm"), s2);  
}
gg_search_book = function (title) {
title = title.replaceAll(/\s+/, '+');
var url = google + gg_search + '?q=' + title;
$.ajax({
url: url,
type: "GET",
dataType: "jsonp",
jsonp: 'callback',
success: function (data) {
gg_results = data.items;
},
complete: function () {
gg_done = true;
show_result();
}
});
}
get_meta = function (source, id) {
var meta;
if (source == 'google') {;
meta = gg_results[id];
$('#description').val(meta.volumeInfo.description);
$('#bookAuthor').val(meta.volumeInfo.authors.join(' & '));
$('#book_title').val(meta.volumeInfo.title);
if (meta.volumeInfo.categories) {
var tags = meta.volumeInfo.categories.join(',');
$('#tags').val(tags);
}
if (meta.volumeInfo.averageRating) {
$('#rating').val(Math.round(meta.volumeInfo.averageRating));
}
return;
}
if (source == 'douban') {
meta = db_results[id];
$('#description').val(meta.summary);
$('#bookAuthor').val(meta.author.join(' & '));
$('#book_title').val(meta.title);
var tags = '';
for (var i = 0; i < meta.tags.length; i++) {
tags = tags + meta.tags[i].title + ',';
}
$('#tags').val(tags);
$('#rating').val(Math.round(meta.rating.average / 2));
return;
}
}
do_search = function (keyword) {
show_flag = 0;
$('#meta-info').text(msg.loading);
var keyword = $('#keyword').val();
if (keyword) {
db_search_book(keyword);
gg_search_book(keyword);
}
}
db_search_book = function (title) {
var url = douban + db_search + '?q=' + title + '&fields=all&count=10';
$.ajax({
url: url,
type: "GET",
dataType: "jsonp",
jsonp: 'callback',
success: function (data) {
db_results = data.books;
},
error: function () {
$('#meta-info').html('<p class="text-danger">'+ msg.search_error+'!</p>');
},
complete: function () {
db_done = true;
show_result();
}
});
}
show_result = function () {
show_flag++;
if (show_flag == 1) {
$('#meta-info').html('<ul id="book-list" class="media-list"></ul>');
}
if (gg_done && db_done) {
if (!gg_results && !db_results) {
$('#meta-info').html('<p class="text-danger">'+ msg.no_result +'</p>');
return;
}
}
if (gg_done && gg_results.length > 0) {
for (var i = 0; i < gg_results.length; i++) {
var book = gg_results[i];
var book_cover;
if (book.volumeInfo.imageLinks) {
book_cover = book.volumeInfo.imageLinks.thumbnail;
} else {
book_cover = '/static/generic_cover.jpg';
}
var book_html = '<li class="media">' +
'<img class="pull-left img-responsive" data-toggle="modal" data-target="#metaModal" src="' +
book_cover + '" alt="Cover" style="width:100px;height:150px" onclick=\'javascript:get_meta("google",' +
i + ')\'>' +
'<div class="media-body">' +
'<h4 class="media-heading"><a href="https://books.google.com/books?id=' +
book.id + '" target="_blank">' + book.volumeInfo.title + '</a></h4>' +
'<p>'+ msg.author +'' + book.volumeInfo.authors + '</p>' +
'<p>'+ msg.publisher + '' + book.volumeInfo.publisher + '</p>' +
'<p>'+ msg.description + ':' + book.volumeInfo.description + '</p>' +
'<p>'+ msg.source + ':<a href="https://books.google.com" target="_blank">Google Books</a></p>' +
'</div>' +
'</li>';
$("#book-list").append(book_html);
}
gg_done = false;
}
if (db_done && db_results.length > 0) {
for (var i = 0; i < db_results.length; i++) {
var book = db_results[i];
var book_html = '<li class="media">' +
'<img class="pull-left img-responsive" data-toggle="modal" data-target="#metaModal" src="' +
book.image + '" alt="Cover" style="width:100px;height: 150px" onclick=\'javascript:get_meta("douban",' +
i + ')\'>' +
'<div class="media-body">' +
'<h4 class="media-heading"><a href="https://book.douban.com/subject/' +
book.id + '" target="_blank">' + book.title + '</a></h4>' +
'<p>' + msg.author + '' + book.author + '</p>' +
'<p>' + msg.publisher + '' + book.publisher + '</p>' +
'<p>' + msg.description + ':' + book.summary + '</p>' +
'<p>' + msg.source + ':<a href="https://book.douban.com" target="_blank">Douban Books</a></p>' +
'</div>' +
'</li>';
$("#book-list").append(book_html);
}
db_done = false;
}
}
$('#do-search').click(function () {
var keyword = $('#keyword').val();
if (keyword) {
do_search(keyword);
}
});
$('#get_meta').click(function () {
var book_title = $('#book_title').val();
if (book_title) {
$('#keyword').val(book_title);
do_search(book_title);
}
});
});

View File

@ -0,0 +1 @@
!function(a){"use strict";function b(a){return"[data-value"+(a?"="+a:"")+"]"}function c(a,b,c){var d=c.activeIcon,e=c.inactiveIcon;a.removeClass(b?e:d).addClass(b?d:e)}function d(b,c){var d=a.extend({},i,b.data(),c);return d.inline=""===d.inline||d.inline,d.readonly=""===d.readonly||d.readonly,d.clearable===!1?d.clearableLabel="":d.clearableLabel=d.clearable,d.clearable=""===d.clearable||d.clearable,d}function e(b,c){if(c.inline)var d=a('<span class="rating-input"></span>');else var d=a('<div class="rating-input"></div>');d.addClass(b.attr("class")),d.removeClass("rating");for(var e=c.min;e<=c.max;e++)d.append('<i class="'+c.iconLib+'" data-value="'+e+'"></i>');return c.clearable&&!c.readonly&&d.append("&nbsp;").append('<a class="'+f+'"><i class="'+c.iconLib+" "+c.clearableIcon+'"/>'+c.clearableLabel+"</a>"),d}var f="rating-clear",g="."+f,h="hidden",i={min:1,max:5,"empty-value":0,iconLib:"glyphicon",activeIcon:"glyphicon-star",inactiveIcon:"glyphicon-star-empty",clearable:!1,clearableIcon:"glyphicon-remove",clearableRemain:!1,inline:!1,readonly:!1},j=function(a,b){var c=this.$input=a;this.options=d(c,b);var f=this.$el=e(c,this.options);c.addClass(h).before(f),c.attr("type","hidden"),this.highlight(c.val())};j.VERSION="0.4.0",j.DEFAULTS=i,j.prototype={clear:function(){this.setValue(this.options["empty-value"])},setValue:function(a){this.highlight(a),this.updateInput(a)},highlight:function(a,d){var e=this.options,f=this.$el;if(a>=this.options.min&&a<=this.options.max){var i=f.find(b(a));c(i.prevAll("i").andSelf(),!0,e),c(i.nextAll("i"),!1,e)}else c(f.find(b()),!1,e);d||(this.options.clearableRemain?f.find(g).removeClass(h):a&&a!=this.options["empty-value"]?f.find(g).removeClass(h):f.find(g).addClass(h))},updateInput:function(a){var b=this.$input;b.val()!=a&&b.val(a).change()}};var k=a.fn.rating=function(c){return this.filter("input[type=number]").each(function(){var d=a(this),e="object"==typeof c&&c||{},f=new j(d,e);f.options.readonly||f.$el.on("mouseenter",b(),function(){f.highlight(a(this).data("value"),!0)}).on("mouseleave",b(),function(){f.highlight(d.val(),!0)}).on("click",b(),function(){f.setValue(a(this).data("value"))}).on("click",g,function(){f.clear()})})};k.Constructor=j,a(function(){a("input.rating[type=number]").each(function(){a(this).rating()})})}(jQuery);

1277
cps/static/js/libs/jquery.form.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,13 @@ $(function() {
}
});
});
$("#restart_database").click(function() {
$.ajax({
dataType: 'json',
url: window.location.pathname+"/../../shutdown",
data: {"parameter":2}
});
});
$("#perform_update").click(function() {
$('#spinner2').show();
$.ajax({

View File

@ -80,6 +80,7 @@
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
<p></p>
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-web')}}</div>
<div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-web')}}</div>
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>

View File

@ -39,7 +39,7 @@
</div>
<div class="form-group">
<label for="rating">{{_('Rating')}}</label>
<input type="number" min="0" max="5" step="1" class="form-control" name="rating" id="rating" value="{% if book.ratings %}{{book.ratings[0].rating / 2}}{% endif %}">
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
</div>
<div class="form-group">
<label for="cover_url">{{_('Cover URL (jpg)')}}</label>
@ -104,16 +104,54 @@
<input name="detail_view" type="checkbox" checked> {{_('view book after edit')}}
</label>
</div>
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Get metadata')}}</a>
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
<a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
</form>
</div>
{% endif %}
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
<div class="modal-dialog" role="document">
<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" id="metaModalLabel">{{_('Get metadata')}}</h4>
<form class="form-inline">
<div class="form-group">
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
<input type="text" class="form-control" id="keyword" placeholder="{{_(" Search keyword ")}}">
</div>
<button type="button" class="btn btn-default" id="do-search">{{_("Go!")}}</button>
<span>{{_('Click the cover to load metadata to the form')}}</span>
</form>
</div>
<div class="modal-body" id="meta-info">
{{_("Loading...")}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script>
var i18n_msg = {
'loading': {{_('Loading...')|safe|tojson}},
'search_error': {{_('Search error!')|safe|tojson}},
'no_result': {{_('No Result! Please try anonther keyword.')|safe|tojson}},
'author': {{_('Author')|safe|tojson}},
'publisher': {{_('Publisher')|safe|tojson}},
'description': {{_('Description')|safe|tojson}},
'source': {{_('Source')|safe|tojson}},
};
</script>
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/get_meta.js') }}"></script>
{% endblock %}
{% block header %}
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">

View File

@ -7,6 +7,47 @@
<label for="config_calibre_dir">{{_('Location of Calibre database')}}</label>
<input type="text" class="form-control" name="config_calibre_dir" id="config_calibre_dir" value="{% if content.config_calibre_dir != None %}{{ content.config_calibre_dir }}{% endif %}" autocomplete="off">
</div>
{% if gdrive %}
<div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" {% if content.config_use_google_drive %}checked{% endif %} >
<label for="config_use_google_drive">{{_('Use google drive?')}}</label>
</div>
<div id="gdrive_settings">
<div class="form-group required">
<label for="config_google_drive_client_id">{{_('Client id')}}</label>
<input type="text" class="form-control" name="config_google_drive_client_id" id="config_google_drive_client_id" value="{% if content.config_google_drive_client_id %}{{content.config_google_drive_client_id}}{% endif %}" autocomplete="off">
</div>
<div class="form-group required">
<label for="config_google_drive_client_secret">{{_('Client secret')}}</label>
<input type="text" class="form-control" name="config_google_drive_client_secret" id="config_google_drive_client_secret" value="{% if content.config_google_drive_client_secret %}{{content.config_google_drive_client_secret}}{% endif %}" autocomplete="off">
</div>
<div class="form-group required">
<label for="config_google_drive_calibre_url_base">{{_('Calibre Base URL')}}</label>
<input type="text" class="form-control" name="config_google_drive_calibre_url_base" id="config_google_drive_calibre_url_base" value="{% if content.config_google_drive_calibre_url_base %}{{content.config_google_drive_calibre_url_base}}{% endif %}" autocomplete="off">
</div>
<div class="form-group required">
<label for="config_google_drive_folder">{{_('Google drive Calibre folder')}}</label>
<input type="text" class="form-control" name="config_google_drive_folder" id="config_google_drive_folder" value="{% if content.config_google_drive_folder %}{{content.config_google_drive_folder}}{% endif %}" autocomplete="off" required>
</div>
{% if show_authenticate_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 content.config_google_drive_watch_changes_response %}
<label for="config_google_drive_watch_changes_response">{{_('Metadata Watch Channel ID')}}</label>
<div class="form-group input-group required">
<input type="text" class="form-control" name="config_google_drive_watch_changes_response" id="config_google_drive_watch_changes_response" value="{{ content.config_google_drive_watch_changes_response['id'] }} expires on {{ content.config_google_drive_watch_changes_response['expiration'] | strftime }}" autocomplete="off" disabled="">
<span class="input-group-btn">
<a href="{{ url_for('revoke_watch_gdrive') }}" class="btn btn-primary">Revoke</a>
</span>
{% else %}
<a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
<div class="form-group">
<label for="config_port">{{_('Server Port')}}</label>
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required>
@ -23,7 +64,10 @@
<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_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">
</div>
<div class="form-group">
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if content.config_title_regex != None %}{{ content.config_title_regex }}{% endif %}" autocomplete="off">
@ -70,6 +114,10 @@
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Changing Password')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="edit_shelf_role" id="edit_shelf_role" {% if content.role_edit_shelfs() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Editing Public Shelfs')}}</label>
</div>
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if not origin %}
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
@ -80,3 +128,22 @@
</form>
</div>
{% endblock %}
{% block js %}
<script>
$(document).ready(function() {
$('#config_use_google_drive').trigger("change");
});
$('#config_use_google_drive').change(function(){
formInputs=$("#gdrive_settings :input");
isChecked=this.checked;
formInputs.each(function(formInput) {
$(this).prop('required',isChecked);
});
if (this.checked) {
$('#gdrive_settings').show();
} else {
$('#gdrive_settings').hide();
}
});
</script>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% block body %}
<div class="single">
<div class="row">
<div class="col-sm-3 col-lg-3 col-xs-12">
<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('\\','/')) }}" />
@ -107,6 +107,16 @@
</div>
</p>
{% endif %}
{% if not g.user.is_anonymous() %}
<p>
<div class="custom_columns" id="have_read_container">
<form id="have_read_form" action="{{ url_for('toggle_read', id=entry.id)}}" method="POST") >
<input id="have_read_cb" type="checkbox" {% if have_read %}checked{% endif %} >
<label for="have_read_cb">{{_('Read')}}</label>
</form>
</div>
</p>
{% endif %}
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
@ -126,7 +136,7 @@
</button>
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
{% for format in entry.data %}
<li><a href="{{ url_for('get_download_link', book_id=entry.id, format=format.format|lower) }}">{{format.format}}</a></li>
<li><a href="{{ url_for('get_download_link_ext', book_id=entry.id, format=format.format|lower, anyname=entry.id|string+'.'+format.format) }}">{{format.format}}</a></li>
{%endfor%}
</ul>
</div>
@ -208,3 +218,34 @@
</div>
{% endblock %}
{% block js %}
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
<script>
$( document ).ready(function() {
var haveReadForm = $('#have_read_form');
haveReadForm.ajaxForm();
});
$("#have_read_container").attr('unselectable','on')
.css({'-moz-user-select':'-moz-none',
'-moz-user-select':'none',
'-o-user-select':'none',
'-khtml-user-select':'none', /* you could also put this in a class */
'-webkit-user-select':'none',/* and add the CSS class here instead */
'-ms-user-select':'none',
'user-select':'none'
}).bind('selectstart', function(){ return false; });
$("#have_read_container").click(function() {
var haveReadForm = $('#have_read_form');
if ($("#have_read").find('span').hasClass('glyphicon-ok')) {
$("#have_read").find('span').removeClass('glyphicon-ok');
$("#have_read").find('span').addClass('glyphicon-remove');
} else {
$("#have_read").find('span').removeClass('glyphicon-remove');
$("#have_read").find('span').addClass('glyphicon-ok');
}
haveReadForm.submit()
});
</script>
{% endblock %}

View File

@ -35,6 +35,7 @@
<uri>https://github.com/janeczku/calibre-web</uri>
</author>
{% if entries and entries[0] %}
{% for entry in entries %}
<entry>
<title>{{entry.title}}</title>
@ -60,6 +61,7 @@
{% endfor %}
</entry>
{% endfor %}
{% endif %}
{% for entry in listelements %}
<entry>
<title>{{entry.name}}</title>

View File

@ -40,6 +40,7 @@
<div class="discover load-more">
<h2>{{title}}</h2>
<div class="row">
{% if entries[0] %}
{% for entry in entries %}
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover">
@ -76,6 +77,7 @@
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -39,6 +39,20 @@
<id>{{url_for('feed_discover')}}</id>
<content type="text">{{_('Show Random Books')}}</content>
</entry>
<entry>
<title>{{_('Read Books')}}</title>
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_read_books')}}" />
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition"/>
<id>{{url_for('feed_read_books')}}</id>
<content type="text">{{_('Read Books')}}</content>
</entry>
<entry>
<title>{{_('Unread Books')}}</title>
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_unread_books')}}" />
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition"/>
<id>{{url_for('feed_unread_books')}}</id>
<content type="text">{{_('Unread Books')}}</content>
</entry>
<entry>
<title>{{_('Authors')}}</title>
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_authorindex')}}"/>

View File

@ -128,6 +128,10 @@
{% if g.user.show_best_rated_books() %}
<li><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span> {{_('Best rated Books')}}</a></li>
{%endif%}
{% if g.user.show_read_and_unread() %}
<li><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span> {{_('Read Books')}}</a></li>
<li><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span> {{_('Unread Books')}}</a></li>
{%endif%}
{% if g.user.show_random_books() %}
<li id="nav_rand"><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span> {{_('Discover')}}</a></li>
{%endif%}

View File

@ -3,10 +3,11 @@
<div class="discover">
<h2>{{title}}</h2>
{% if g.user.is_authenticated %}
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{ _('Delete this Shelf') }} </a>
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf name') }} </a>
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<div data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf name') }} </a>
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
{% endif %}
{% endif %}
<div class="row">
@ -39,4 +40,20 @@
{% endfor %}
</div>
</div>
<div id="DeleteShelfDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Do you really want to delete the shelf?')}}</span>
</div>
<div class="modal-body text-center">
<span>{{_('Shelf will be lost for everybody and forever!')}}</span>
<p></p>
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -7,11 +7,13 @@
<label for="title">{{_('Title')}}</label>
<input type="text" class="form-control" name="title" id="title" value="{{ shelf.name if shelf.name != None }}">
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="is_public" {% if shelf.is_public == 1 %}checked{% endif %}> {{_('should the shelf be public?')}}
</label>
</div>
{% if g.user.role_edit_shelfs() %}
<div class="checkbox">
<label>
<input type="checkbox" name="is_public" {% if shelf.is_public == 1 %}checked{% endif %}> {{_('should the shelf be public?')}}
</label>
</div>
{% endif %}
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if shelf.id != None %}
<a href="{{ url_for('show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a>

View File

@ -70,6 +70,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_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>
<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>
@ -100,6 +104,10 @@
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Changing Password')}}</label>
</div>
<div class="form-group">
<input type="checkbox" name="edit_shelf_role" id="edit_shelf_role" {% if content.role_edit_shelfs() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Editing Public Shelfs')}}</label>
</div>
{% endif %}
{% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}

View File

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Calibre-web\n"
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
"PO-Revision-Date: 2016-07-12 19:54+0200\n"
"Last-Translator: Ozzie Isaacs\n"
"Language: de\n"
@ -32,347 +32,360 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
msgid "not installed"
msgstr "Nicht installiert"
#: cps/helper.py:150
#: cps/helper.py:164
#, python-format
msgid "Failed to send mail: %s"
msgstr "E-Mail: %s konnte nicht gesendet werden"
#: cps/helper.py:157
#: cps/helper.py:171
msgid "Calibre-web test email"
msgstr "Calibre-web Test E-Mail"
#: cps/helper.py:158 cps/helper.py:168
#: cps/helper.py:172 cps/helper.py:184
msgid "This email has been sent via calibre web."
msgstr "Die E-Mail wurde via calibre-web versendet"
#: cps/helper.py:167 cps/templates/detail.html:130
#: cps/helper.py:181 cps/templates/detail.html:146
msgid "Send to Kindle"
msgstr "An Kindle senden"
#: cps/helper.py:185 cps/helper.py:200
#: cps/helper.py:201 cps/helper.py:216
msgid "Could not find any formats suitable for sending by email"
msgstr ""
"Konnte keine Formate finden welche für das versenden per E-Mail geeignet "
"sind"
#: cps/helper.py:194
#: cps/helper.py:210
msgid "Could not convert epub to mobi"
msgstr "Konnte .epub nicht nach .mobi konvertieren"
#: cps/ub.py:434
#: cps/ub.py:488
msgid "Guest"
msgstr "Gast"
#: cps/web.py:734
#: cps/web.py:904
msgid "Requesting update package"
msgstr "Frage Update Paket an"
#: cps/web.py:735
#: cps/web.py:905
msgid "Downloading update package"
msgstr "Lade Update Paket herunter"
#: cps/web.py:736
#: cps/web.py:906
msgid "Unzipping update package"
msgstr "Entpacke Update Paket"
#: cps/web.py:737
#: cps/web.py:907
msgid "Files are replaced"
msgstr "Ersetze Dateien"
#: cps/web.py:738
#: cps/web.py:908
msgid "Database connections are closed"
msgstr "Schließe Datenbankverbindungen"
#: cps/web.py:739
#: cps/web.py:909
msgid "Server is stopped"
msgstr "Stoppe Server"
#: cps/web.py:740
#: cps/web.py:910
msgid "Update finished, please press okay and reload page"
msgstr "Update abgeschlossen, bitte okay drücken und Seite neu laden"
#: cps/web.py:810
#: cps/web.py:983
msgid "Latest Books"
msgstr "Letzte Bücher"
#: cps/web.py:835
#: cps/web.py:1014
msgid "Hot Books (most downloaded)"
msgstr "Beliebte Bücher (die meisten Downloads)"
#: cps/web.py:845
#: cps/web.py:1024
msgid "Best rated books"
msgstr "Best bewertete Bücher"
#: cps/templates/index.xml:36 cps/web.py:854
#: cps/templates/index.xml:36 cps/web.py:1033
msgid "Random Books"
msgstr "Zufällige Bücher"
#: cps/web.py:867
#: cps/web.py:1046
msgid "Author list"
msgstr "Autorenliste"
#: cps/web.py:878
#: cps/web.py:1057
#, python-format
msgid "Author: %(name)s"
msgstr "Autor: %(name)s"
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
#: cps/web.py:2115
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
#: cps/web.py:2579
msgid "Error opening eBook. File does not exist or file is not accessible:"
msgstr ""
"Buch öffnen fehlgeschlagen. Datei existiert nicht, oder ist nicht "
"zugänglich."
#: cps/templates/index.xml:57 cps/web.py:894
#: cps/templates/index.xml:71 cps/web.py:1073
msgid "Series list"
msgstr "Liste Serien"
#: cps/web.py:906
#: cps/web.py:1085
#, python-format
msgid "Series: %(serie)s"
msgstr "Serie: %(serie)s"
#: cps/web.py:939
#: cps/web.py:1118
msgid "Available languages"
msgstr "Verfügbare Sprachen"
#: cps/web.py:954
#: cps/web.py:1133
#, python-format
msgid "Language: %(name)s"
msgstr "Sprache: %(name)s"
#: cps/templates/index.xml:50 cps/web.py:967
#: cps/templates/index.xml:64 cps/web.py:1146
msgid "Category list"
msgstr "Kategorieliste"
#: cps/web.py:979
#: cps/web.py:1158
#, python-format
msgid "Category: %(name)s"
msgstr "Kategorie: %(name)s"
#: cps/web.py:1040
#: cps/web.py:1267
msgid "Statistics"
msgstr "Statistiken"
#: cps/web.py:1061
#: cps/web.py:1375
msgid "Server restarted, please reload page"
msgstr "Server neu gestartet,bitte Seite neu laden"
#: cps/web.py:1063
#: cps/web.py:1377
msgid "Performing shutdown of server, please close window"
msgstr "Server wird runtergefahren, bitte Fenster schließen"
#: cps/web.py:1073
#: cps/web.py:1392
msgid "Update done"
msgstr "Update durchgeführt"
#: cps/web.py:1147 cps/web.py:1160
#: cps/web.py:1470 cps/web.py:1483
msgid "search"
msgstr "Suche"
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
msgid "Read a Book"
msgstr "Lese ein Buch"
#: cps/web.py:1276 cps/web.py:1713
#: cps/web.py:1676 cps/web.py:2152
msgid "Please fill out all fields!"
msgstr "Bitte alle Felder ausfüllen!"
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
msgid "register"
msgstr "Registieren"
#: cps/web.py:1292
#: cps/web.py:1692
msgid "An unknown error occured. Please try again later."
msgstr "Es ist ein unbekannter Fehler aufgetreten. Bitte später erneut versuchen."
#: cps/web.py:1297
#: cps/web.py:1697
msgid "This username or email address is already in use."
msgstr "Der Benutzername oder die E-Mailadresse ist in bereits in Benutzung."
#: cps/web.py:1315
#: cps/web.py:1715
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr "Du bist nun eingeloggt als '%(nickname)s'"
#: cps/web.py:1320
#: cps/web.py:1720
msgid "Wrong Username or Password"
msgstr "Falscher Benutzername oder Passwort"
#: cps/web.py:1322
#: cps/web.py:1722
msgid "login"
msgstr "Login"
#: cps/web.py:1339
#: cps/web.py:1739
msgid "Please configure the SMTP mail settings first..."
msgstr "Bitte zuerst die SMTP Mail Einstellung konfigurieren ..."
#: cps/web.py:1343
#: cps/web.py:1743
#, python-format
msgid "Book successfully send to %(kindlemail)s"
msgstr "Buch erfolgreich versandt an %(kindlemail)s"
#: cps/web.py:1347
#: cps/web.py:1747
#, python-format
msgid "There was an error sending this book: %(res)s"
msgstr "Beim Senden des Buchs trat ein Fehler auf: %(res)s"
#: cps/web.py:1349
#: cps/web.py:1749 cps/web.py:2232
msgid "Please configure your kindle email address first..."
msgstr "Bitte die Kindle E-Mail Adresse zuuerst konfigurieren..."
#: cps/web.py:1369
#: cps/web.py:1774
#, python-format
msgid "Book has been added to shelf: %(sname)s"
msgstr "Das Buch wurde dem Bücherregal: %(sname)s hinzugefügt"
#: cps/web.py:1390
#: cps/web.py:1793
#, python-format
msgid "Book has been removed from shelf: %(sname)s"
msgstr "Das Buch wurde aus dem Bücherregal: %(sname)s entfernt"
#: cps/web.py:1409 cps/web.py:1433
#: cps/web.py:1812 cps/web.py:1836
#, python-format
msgid "A shelf with the name '%(title)s' already exists."
msgstr "Es existiert bereits ein Bücheregal mit dem Titel '%(title)s'"
#: cps/web.py:1414
#: cps/web.py:1817
#, python-format
msgid "Shelf %(title)s created"
msgstr "Bücherregal %(title)s erzeugt"
#: cps/web.py:1416 cps/web.py:1444
#: cps/web.py:1819 cps/web.py:1847
msgid "There was an error"
msgstr "Es trat ein Fehler auf"
#: cps/web.py:1417 cps/web.py:1419
#: cps/web.py:1820 cps/web.py:1822
msgid "create a shelf"
msgstr "Bücherregal erzeugen"
#: cps/web.py:1442
#: cps/web.py:1845
#, python-format
msgid "Shelf %(title)s changed"
msgstr "Bücherregal %(title)s verändert"
#: cps/web.py:1445 cps/web.py:1447
#: cps/web.py:1848 cps/web.py:1850
msgid "Edit a shelf"
msgstr "Bücherregal editieren"
#: cps/web.py:1465
#: cps/web.py:1868
#, python-format
msgid "successfully deleted shelf %(name)s"
msgstr "Bücherregal %(name)s erfolgreich gelöscht"
#: cps/web.py:1487
#: cps/web.py:1890
#, python-format
msgid "Shelf: '%(name)s'"
msgstr "Bücherregal: '%(name)s'"
#: cps/web.py:1518
#: cps/web.py:1921
#, python-format
msgid "Change order of Shelf: '%(name)s'"
msgstr "Reihenfolge in Bücherregal '%(name)s' verändern"
#: cps/web.py:1580
#: cps/web.py:1985
msgid "Found an existing account for this email address."
msgstr "Es existiert ein Benutzerkonto für diese E-Mailadresse"
#: cps/web.py:1582 cps/web.py:1586
#: cps/web.py:1987 cps/web.py:1991
#, python-format
msgid "%(name)s's profile"
msgstr "%(name)s's Profil"
#: cps/web.py:1583
#: cps/web.py:1988
msgid "Profile updated"
msgstr "Profil aktualisiert"
#: cps/web.py:1597
#: cps/web.py:2002
msgid "Admin page"
msgstr "Admin Seite"
#: cps/web.py:1668
#: cps/web.py:2106
msgid "Calibre-web configuration updated"
msgstr "Calibre-web Konfiguration wurde aktualisiert"
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
msgid "Basic Configuration"
msgstr "Basis Konfiguration"
#: cps/web.py:1679
#: cps/web.py:2117
msgid "DB location is not valid, please enter correct path"
msgstr "DB Speicherort ist ungültig, bitte Pfad korrigieren"
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
msgid "Add new user"
msgstr "Neuen Benutzer hinzufügen"
#: cps/web.py:1753
#: cps/web.py:2194
#, python-format
msgid "User '%(user)s' created"
msgstr "Benutzer '%(user)s' angelegt"
#: cps/web.py:1757
#: cps/web.py:2198
msgid "Found an existing account for this email address or nickname."
msgstr ""
"Es existiert ein Benutzerkonto für diese Emailadresse oder den "
"Benutzernamen."
#: cps/web.py:1779
#: cps/web.py:2220
msgid "Mail settings updated"
msgstr "E-Mail Einstellungen aktualisiert"
#: cps/web.py:1785
#: cps/web.py:2227
#, python-format
msgid "Test E-Mail successfully send to %(kindlemail)s"
msgstr "Test E-Mail erfolgreich an %(kindlemail)s versendet"
#: cps/web.py:1788
#: cps/web.py:2230
#, python-format
msgid "There was an error sending the Test E-Mail: %(res)s"
msgstr "Fehler beim versenden der Test E-Mail: %(res)s"
#: cps/web.py:1789
#: cps/web.py:2234
msgid "E-Mail settings updated"
msgstr "E-Mail Einstellungen wurde aktualisiert"
#: cps/web.py:2235
msgid "Edit mail settings"
msgstr "E-Mail Einstellungen editieren"
#: cps/web.py:1817
#: cps/web.py:2263
#, python-format
msgid "User '%(nick)s' deleted"
msgstr "Benutzer '%(nick)s' gelöscht"
#: cps/web.py:1898
#: cps/web.py:2349
#, python-format
msgid "User '%(nick)s' updated"
msgstr "Benutzer '%(nick)s' aktualisiert"
#: cps/web.py:1901
#: cps/web.py:2352
msgid "An unknown error occured."
msgstr "Es ist ein unbekanter Fehler aufgetreten"
#: cps/web.py:1904
#: cps/web.py:2355
#, python-format
msgid "Edit User %(nick)s"
msgstr "Benutzer %(nick)s bearbeiten"
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
msgid "edit metadata"
msgstr "Metadaten editieren"
#: cps/web.py:2145
#: cps/web.py:2598
#, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr "Die Dateiendung \"%s\" kann nicht auf diesen Server hochgeladen werden"
#: cps/web.py:2604
msgid "File to be uploaded must have an extension"
msgstr "Datei müssen eine Erweiterung haben, um hochgeladen zu werden"
#: cps/web.py:2621
#, python-format
msgid "Failed to create path %s (Permission denied)."
msgstr "Fehler beim Erzeugen des Pfads %s (Zugriff verweigert)"
#: cps/web.py:2150
#: cps/web.py:2626
#, python-format
msgid "Failed to store file %s (Permission denied)."
msgstr "Fehler beim speichern der Datei %s (Zugriff verweigert)"
#: cps/web.py:2155
#: cps/web.py:2631
#, python-format
msgid "Failed to delete file %s (Permission denied)."
msgstr "Fehler beim Löschen von Datei %s (Zugriff verweigert)"
@ -401,7 +414,7 @@ msgstr "DLS"
msgid "Admin"
msgstr "Admin"
#: cps/templates/admin.html:13 cps/templates/detail.html:117
#: cps/templates/admin.html:13 cps/templates/detail.html:134
msgid "Download"
msgstr "Download"
@ -457,7 +470,7 @@ msgstr "Konfiguration"
msgid "Calibre DB dir"
msgstr "Calibre DB Pfad"
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
msgid "Log Level"
msgstr "Log Level"
@ -465,7 +478,7 @@ msgstr "Log Level"
msgid "Port"
msgstr "Port"
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
msgid "Books per page"
msgstr "Bücher pro Seite"
@ -494,42 +507,46 @@ msgid "Newest commit timestamp"
msgstr "Neuestes Commit Datum"
#: cps/templates/admin.html:83
msgid "Reconnect to Calibre DB"
msgstr "Calibre-DB neu verbinden"
#: cps/templates/admin.html:84
msgid "Restart Calibre-web"
msgstr "Calibre-web Neustarten"
#: cps/templates/admin.html:84
#: cps/templates/admin.html:85
msgid "Stop Calibre-web"
msgstr "Stoppe Calibre-web"
#: cps/templates/admin.html:85
#: cps/templates/admin.html:86
msgid "Check for update"
msgstr "Suche nach Update"
#: cps/templates/admin.html:86
#: cps/templates/admin.html:87
msgid "Perform Update"
msgstr "Update durchführen"
#: cps/templates/admin.html:96
#: cps/templates/admin.html:97
msgid "Do you really want to restart Calibre-web?"
msgstr "Calibre-web wirklich neustarten?"
#: cps/templates/admin.html:101 cps/templates/admin.html:115
#: cps/templates/admin.html:136
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/admin.html:137
msgid "Ok"
msgstr "Ok"
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
#: cps/templates/admin.html:103 cps/templates/admin.html:117
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
msgid "Back"
msgstr "Zurück"
#: cps/templates/admin.html:114
#: cps/templates/admin.html:115
msgid "Do you really want to stop Calibre-web?"
msgstr "Calibre-web wirklich stoppen"
#: cps/templates/admin.html:127
#: cps/templates/admin.html:128
msgid "Updating, please do not reload page"
msgstr "Updatevorgang, bitte Seite nicht neu laden"
@ -537,20 +554,21 @@ msgstr "Updatevorgang, bitte Seite nicht neu laden"
msgid "Book Title"
msgstr "Buchtitel"
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
#: cps/templates/search_form.html:10
msgid "Author"
msgstr "Autor"
#: cps/templates/book_edit.html:24
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
msgid "Description"
msgstr "Beschreibung"
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
msgid "Tags"
msgstr "Tags"
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
#: cps/templates/search_form.html:33
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
#: cps/templates/search_form.html:37
msgid "Series"
msgstr "Serien"
@ -582,69 +600,142 @@ msgstr "Nein"
msgid "view book after edit"
msgstr "Buch nach Bearbeitung ansehen"
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
#: cps/templates/login.html:19 cps/templates/search_form.html:75
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
msgid "Get metadata"
msgstr "Metadaten laden"
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
#: cps/templates/login.html:19 cps/templates/search_form.html:79
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
msgid "Submit"
msgstr "Abschicken"
#: cps/templates/book_edit.html:121
msgid "Keyword"
msgstr "Suchbegriff"
#: cps/templates/book_edit.html:122
msgid " Search keyword "
msgstr "Suchbegriff"
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
msgid "Go!"
msgstr "Los!"
#: cps/templates/book_edit.html:125
msgid "Click the cover to load metadata to the form"
msgstr "Klicke auf das Bild um die Metadaten zu übertragen"
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
msgid "Loading..."
msgstr "Lade..."
#: cps/templates/book_edit.html:132
msgid "Close"
msgstr "Schließen"
#: cps/templates/book_edit.html:143
msgid "Search error!"
msgstr "Fehler bei Suche!"
#: cps/templates/book_edit.html:144
msgid "No Result! Please try anonther keyword."
msgstr "Kein Ergebniss! Bitte anderen Begriff versuchen"
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
#: cps/templates/search_form.html:14
msgid "Publisher"
msgstr "Herausgeber"
#: cps/templates/book_edit.html:148
msgid "Source"
msgstr "Quelle"
#: cps/templates/config_edit.html:7
msgid "Location of Calibre database"
msgstr "Speicherort der Calibre Datenbank"
#: cps/templates/config_edit.html:11
#: cps/templates/config_edit.html:13
msgid "Use google drive?"
msgstr "Google Drive benutzen"
#: cps/templates/config_edit.html:17
msgid "Client id"
msgstr "Benutzer Id"
#: cps/templates/config_edit.html:21
msgid "Client secret"
msgstr "Benutzer Secret"
#: cps/templates/config_edit.html:25
msgid "Calibre Base URL"
msgstr "Calibnre Basis URL"
#: cps/templates/config_edit.html:29
msgid "Google drive Calibre folder"
msgstr "Google Drive Calibre Ordner"
#: cps/templates/config_edit.html:38
msgid "Metadata Watch Channel ID"
msgstr "Matadata Überwachungs-ID"
#: cps/templates/config_edit.html:52
msgid "Server Port"
msgstr "Server Port"
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr "Titel"
#: cps/templates/config_edit.html:23
#: cps/templates/config_edit.html:64
msgid "No. of random books to show"
msgstr "Anzahl Anzeige zufällige Bücher"
#: cps/templates/config_edit.html:28
#: cps/templates/config_edit.html:68
msgid "Regular expression for ignoring columns"
msgstr "Regulärer Ausdruck um Spalten zu ignorien"
#: cps/templates/config_edit.html:72
msgid "Regular expression for title sorting"
msgstr "Regulärer Ausdruck für Titelsortierung"
#: cps/templates/config_edit.html:42
#: cps/templates/config_edit.html:86
msgid "Enable uploading"
msgstr "Hochladen aktivieren"
#: cps/templates/config_edit.html:46
#: cps/templates/config_edit.html:90
msgid "Enable anonymous browsing"
msgstr "Anonymes Browsen aktivieren"
#: cps/templates/config_edit.html:50
#: cps/templates/config_edit.html:94
msgid "Enable public registration"
msgstr "Öffentliche Registrierung aktivieren"
#: cps/templates/config_edit.html:52
#: cps/templates/config_edit.html:96
msgid "Default Settings for new users"
msgstr "Default Einstellungen für neue Benutzer"
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
msgid "Admin user"
msgstr "Admin Benutzer"
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
msgid "Allow Downloads"
msgstr "Downloads erlauben"
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
msgid "Allow Uploads"
msgstr "Uploads erlauben"
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
msgid "Allow Edit"
msgstr "Bearbeiten erlauben"
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
msgid "Allow Changing Password"
msgstr "Passwort ändern erlauben"
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
#: cps/templates/login.html:4
msgid "Login"
msgstr "Login"
@ -661,23 +752,27 @@ msgstr "von"
msgid "language"
msgstr "Sprache"
#: cps/templates/detail.html:74
#: cps/templates/detail.html:81
msgid "Publishing date"
msgstr "Herausgabedatum"
#: cps/templates/detail.html:106
#: cps/templates/detail.html:115
msgid "Read"
msgstr "Gelesen"
#: cps/templates/detail.html:123
msgid "Description:"
msgstr "Beschreibung"
#: cps/templates/detail.html:134
#: cps/templates/detail.html:151
msgid "Read in browser"
msgstr "Im Browser lesen"
#: cps/templates/detail.html:154
#: cps/templates/detail.html:171
msgid "Add to shelf"
msgstr "Zu Bücherregal hinzufügen"
#: cps/templates/detail.html:194
#: cps/templates/detail.html:211
msgid "Edit metadata"
msgstr "Metadaten bearbeiten"
@ -759,19 +854,29 @@ msgstr "Die neuesten Bücher"
msgid "Show Random Books"
msgstr "Zeige zufällige Bücher"
#: cps/templates/index.xml:43 cps/templates/layout.html:140
#: cps/templates/index.xml:43 cps/templates/index.xml:47
#: cps/templates/layout.html:132
msgid "Read Books"
msgstr "Gelesene Bücher"
#: cps/templates/index.xml:50 cps/templates/index.xml:54
#: cps/templates/layout.html:133
msgid "Unread Books"
msgstr "Ungelesene Bücher"
#: cps/templates/index.xml:57 cps/templates/layout.html:144
msgid "Authors"
msgstr "Autoren"
#: cps/templates/index.xml:47
#: cps/templates/index.xml:61
msgid "Books ordered by Author"
msgstr "Bücher nach Autoren sortiert"
#: cps/templates/index.xml:54
#: cps/templates/index.xml:68
msgid "Books ordered by category"
msgstr "Bücher nach Kategorien sortiert"
#: cps/templates/index.xml:61
#: cps/templates/index.xml:75
msgid "Books ordered by series"
msgstr "Bücher nach Reihen geordnet"
@ -779,10 +884,6 @@ msgstr "Bücher nach Reihen geordnet"
msgid "Toggle navigation"
msgstr "Nagivation umschalten"
#: cps/templates/layout.html:60
msgid "Go!"
msgstr "Los!"
#: cps/templates/layout.html:68
msgid "Advanced Search"
msgstr "Erweiterte Suche"
@ -799,31 +900,31 @@ msgstr "Registrieren"
msgid "Browse"
msgstr "Browsen"
#: cps/templates/layout.html:132
#: cps/templates/layout.html:136
msgid "Discover"
msgstr "Entdecke"
#: cps/templates/layout.html:135
#: cps/templates/layout.html:139
msgid "Categories"
msgstr "Kategorien"
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
msgid "Languages"
msgstr "Sprachen"
#: cps/templates/layout.html:145
#: cps/templates/layout.html:149
msgid "Public Shelves"
msgstr "Öffentiche Bücherregale"
#: cps/templates/layout.html:149
#: cps/templates/layout.html:153
msgid "Your Shelves"
msgstr "Deine Bücherregale"
#: cps/templates/layout.html:154
#: cps/templates/layout.html:158
msgid "Create a Shelf"
msgstr "Bücherregal erzeugen"
#: cps/templates/layout.html:155
#: cps/templates/layout.html:159
msgid "About"
msgstr "Über"
@ -889,15 +990,15 @@ msgstr "Versuche eine andere Suche"
msgid "Results for:"
msgstr "Ergebnisse für:"
#: cps/templates/search_form.html:23
#: cps/templates/search_form.html:27
msgid "Exclude Tags"
msgstr "Tags ausschließen"
#: cps/templates/search_form.html:43
#: cps/templates/search_form.html:47
msgid "Exclude Series"
msgstr "Serie ausschließen"
#: cps/templates/search_form.html:64
#: cps/templates/search_form.html:68
msgid "Exclude Languages"
msgstr "Sprache ausschließen"
@ -922,37 +1023,37 @@ msgid "Drag 'n drop to rearrange order"
msgstr "Drag 'n drop um Reihenfolge zu ändern"
#: cps/templates/stats.html:3
msgid "Linked libraries"
msgstr "Dynamische Bibliotheken"
#: cps/templates/stats.html:8
msgid "Program library"
msgstr "Programm Bibliotheken"
#: cps/templates/stats.html:9
msgid "Installed Version"
msgstr "Installierte Version"
#: cps/templates/stats.html:32
msgid "Calibre library statistics"
msgstr "Calibre Bibliothek Statistiken"
#: cps/templates/stats.html:37
#: cps/templates/stats.html:8
msgid "Books in this Library"
msgstr "Bücher in dieser Bibliothek"
#: cps/templates/stats.html:41
#: cps/templates/stats.html:12
msgid "Authors in this Library"
msgstr "Autoren in dieser Bibliothek"
#: cps/templates/stats.html:45
#: cps/templates/stats.html:16
msgid "Categories in this Library"
msgstr "Kategorien in dieser Bibliothek"
#: cps/templates/stats.html:49
#: cps/templates/stats.html:20
msgid "Series in this Library"
msgstr "Serien in dieser Bibliothek"
#: cps/templates/stats.html:24
msgid "Linked libraries"
msgstr "Dynamische Bibliotheken"
#: cps/templates/stats.html:28
msgid "Program library"
msgstr "Programm Bibliotheken"
#: cps/templates/stats.html:29
msgid "Installed Version"
msgstr "Installierte Version"
#: cps/templates/user_edit.html:23
msgid "Kindle E-Mail"
msgstr "Kindle E-Mail"
@ -965,43 +1066,47 @@ msgstr "Zeige nur Bücher mit dieser Sprache"
msgid "Show all"
msgstr "Zeige alle"
#: cps/templates/user_edit.html:45
#: cps/templates/user_edit.html:47
msgid "Show random books"
msgstr "Zeige Zufällige Bücher"
#: cps/templates/user_edit.html:49
#: cps/templates/user_edit.html:51
msgid "Show hot books"
msgstr "Zeige Auswahl Beliebte Bücher"
#: cps/templates/user_edit.html:53
#: cps/templates/user_edit.html:55
msgid "Show best rated books"
msgstr "Zeige am besten bewertete Bücher"
#: cps/templates/user_edit.html:57
#: cps/templates/user_edit.html:59
msgid "Show language selection"
msgstr "Zeige Sprachauswahl"
#: cps/templates/user_edit.html:61
#: cps/templates/user_edit.html:63
msgid "Show series selection"
msgstr "Zeige Serienauswahl"
#: cps/templates/user_edit.html:65
#: cps/templates/user_edit.html:67
msgid "Show category selection"
msgstr "Zeige Kategorienauswahl"
#: cps/templates/user_edit.html:69
#: cps/templates/user_edit.html:71
msgid "Show author selection"
msgstr "Zeige Autorenauswahl"
#: cps/templates/user_edit.html:73
#: cps/templates/user_edit.html:75
msgid "Show read and unread"
msgstr "Zeige Gelesen/Ungelesen Auswahl"
#: cps/templates/user_edit.html:79
msgid "Show random books in detail view"
msgstr "Zeige zufällige Bücher in der Detailansicht"
#: cps/templates/user_edit.html:105
#: cps/templates/user_edit.html:112
msgid "Delete this user"
msgstr "Benutzer löschen"
#: cps/templates/user_edit.html:116
#: cps/templates/user_edit.html:127
msgid "Recent Downloads"
msgstr "Letzte Downloads"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Calibre-web\n"
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
"PO-Revision-Date: 2016-11-13 18:35+0100\n"
"Last-Translator: Juan F. Villa <juan.villa@paisdelconocimiento.org>\n"
"Language: es\n"
@ -25,341 +25,354 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
msgid "not installed"
msgstr "No instalado"
#: cps/helper.py:150
#: cps/helper.py:164
#, python-format
msgid "Failed to send mail: %s"
msgstr "Fallo al enviar el correo : %s"
#: cps/helper.py:157
#: cps/helper.py:171
msgid "Calibre-web test email"
msgstr "Prueba de Correo Calibre-web"
#: cps/helper.py:158 cps/helper.py:168
#: cps/helper.py:172 cps/helper.py:184
msgid "This email has been sent via calibre web."
msgstr "Este mensaje ha sido enviado via Calibre Web."
#: cps/helper.py:167 cps/templates/detail.html:130
#: cps/helper.py:181 cps/templates/detail.html:146
msgid "Send to Kindle"
msgstr "Enviar a Kindle"
#: cps/helper.py:185 cps/helper.py:200
#: cps/helper.py:201 cps/helper.py:216
msgid "Could not find any formats suitable for sending by email"
msgstr "Formato no compatible para enviar por correo electronico"
#: cps/helper.py:194
#: cps/helper.py:210
msgid "Could not convert epub to mobi"
msgstr "No fue posible convertir de epub a mobi"
#: cps/ub.py:434
#: cps/ub.py:488
msgid "Guest"
msgstr ""
#: cps/web.py:734
#: cps/web.py:904
msgid "Requesting update package"
msgstr ""
#: cps/web.py:735
#: cps/web.py:905
msgid "Downloading update package"
msgstr ""
#: cps/web.py:736
#: cps/web.py:906
msgid "Unzipping update package"
msgstr ""
#: cps/web.py:737
#: cps/web.py:907
msgid "Files are replaced"
msgstr ""
#: cps/web.py:738
#: cps/web.py:908
msgid "Database connections are closed"
msgstr ""
#: cps/web.py:739
#: cps/web.py:909
msgid "Server is stopped"
msgstr ""
#: cps/web.py:740
#: cps/web.py:910
msgid "Update finished, please press okay and reload page"
msgstr ""
#: cps/web.py:810
#: cps/web.py:983
msgid "Latest Books"
msgstr "Libros recientes"
#: cps/web.py:835
#: cps/web.py:1014
msgid "Hot Books (most downloaded)"
msgstr "Libros Populares (los mas descargados)"
#: cps/web.py:845
#: cps/web.py:1024
msgid "Best rated books"
msgstr ""
#: cps/templates/index.xml:36 cps/web.py:854
#: cps/templates/index.xml:36 cps/web.py:1033
msgid "Random Books"
msgstr "Libros al Azar"
#: cps/web.py:867
#: cps/web.py:1046
msgid "Author list"
msgstr "Lista de Autores"
#: cps/web.py:878
#: cps/web.py:1057
#, python-format
msgid "Author: %(name)s"
msgstr ""
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
#: cps/web.py:2115
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
#: cps/web.py:2579
msgid "Error opening eBook. File does not exist or file is not accessible:"
msgstr "Error en apertura del Objeto. El archivo no existe o no es accesible"
#: cps/templates/index.xml:57 cps/web.py:894
#: cps/templates/index.xml:71 cps/web.py:1073
msgid "Series list"
msgstr "lista de Series"
#: cps/web.py:906
#: cps/web.py:1085
#, python-format
msgid "Series: %(serie)s"
msgstr "Series : %(serie)s"
#: cps/web.py:939
#: cps/web.py:1118
msgid "Available languages"
msgstr "Lenguajes disponibles"
#: cps/web.py:954
#: cps/web.py:1133
#, python-format
msgid "Language: %(name)s"
msgstr "Lenguaje: %(name)s"
#: cps/templates/index.xml:50 cps/web.py:967
#: cps/templates/index.xml:64 cps/web.py:1146
msgid "Category list"
msgstr "Lista de Categorias"
#: cps/web.py:979
#: cps/web.py:1158
#, python-format
msgid "Category: %(name)s"
msgstr "Categoria : %(name)s"
#: cps/web.py:1040
#: cps/web.py:1267
msgid "Statistics"
msgstr "Estadisticas"
#: cps/web.py:1061
#: cps/web.py:1375
msgid "Server restarted, please reload page"
msgstr ""
#: cps/web.py:1063
#: cps/web.py:1377
msgid "Performing shutdown of server, please close window"
msgstr ""
#: cps/web.py:1073
#: cps/web.py:1392
msgid "Update done"
msgstr ""
#: cps/web.py:1147 cps/web.py:1160
#: cps/web.py:1470 cps/web.py:1483
msgid "search"
msgstr ""
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
msgid "Read a Book"
msgstr "Leer un Libro"
#: cps/web.py:1276 cps/web.py:1713
#: cps/web.py:1676 cps/web.py:2152
msgid "Please fill out all fields!"
msgstr "Por favor llenar todos los campos!"
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
msgid "register"
msgstr "Registrarse"
#: cps/web.py:1292
#: cps/web.py:1692
msgid "An unknown error occured. Please try again later."
msgstr "Ocurrio un error. Intentar de nuevo mas tarde."
#: cps/web.py:1297
#: cps/web.py:1697
msgid "This username or email address is already in use."
msgstr "Usuario o direccion de correo en uso."
#: cps/web.py:1315
#: cps/web.py:1715
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr "Sesion iniciada como : '%(nickname)s'"
#: cps/web.py:1320
#: cps/web.py:1720
msgid "Wrong Username or Password"
msgstr "Usuario o contraseña invalido"
#: cps/web.py:1322
#: cps/web.py:1722
msgid "login"
msgstr "Iniciar Sesion"
#: cps/web.py:1339
#: cps/web.py:1739
msgid "Please configure the SMTP mail settings first..."
msgstr "Configurar primero los parametros SMTP por favor..."
#: cps/web.py:1343
#: cps/web.py:1743
#, python-format
msgid "Book successfully send to %(kindlemail)s"
msgstr "Envio de Libro a %(kindlemail)s correctamente"
#: cps/web.py:1347
#: cps/web.py:1747
#, python-format
msgid "There was an error sending this book: %(res)s"
msgstr "Ha sucedido un error en el envio del Libro: %(res)s"
#: cps/web.py:1349
#: cps/web.py:1749 cps/web.py:2232
msgid "Please configure your kindle email address first..."
msgstr "Configurar primero la dirección de correo Kindle por favor..."
#: cps/web.py:1369
#: cps/web.py:1774
#, python-format
msgid "Book has been added to shelf: %(sname)s"
msgstr "El libro fue agregado a el estante: %(sname)s"
#: cps/web.py:1390
#: cps/web.py:1793
#, python-format
msgid "Book has been removed from shelf: %(sname)s"
msgstr "El libro fue removido del estante: %(sname)s"
#: cps/web.py:1409 cps/web.py:1433
#: cps/web.py:1812 cps/web.py:1836
#, python-format
msgid "A shelf with the name '%(title)s' already exists."
msgstr "Une étagère de ce nom '%(title)s' existe déjà."
#: cps/web.py:1414
#: cps/web.py:1817
#, python-format
msgid "Shelf %(title)s created"
msgstr "Estante %(title)s creado"
#: cps/web.py:1416 cps/web.py:1444
#: cps/web.py:1819 cps/web.py:1847
msgid "There was an error"
msgstr "Hemos tenido un error"
#: cps/web.py:1417 cps/web.py:1419
#: cps/web.py:1820 cps/web.py:1822
msgid "create a shelf"
msgstr "Crear un Estante"
#: cps/web.py:1442
#: cps/web.py:1845
#, python-format
msgid "Shelf %(title)s changed"
msgstr ""
#: cps/web.py:1445 cps/web.py:1447
#: cps/web.py:1848 cps/web.py:1850
msgid "Edit a shelf"
msgstr ""
#: cps/web.py:1465
#: cps/web.py:1868
#, python-format
msgid "successfully deleted shelf %(name)s"
msgstr "Estante %(name)s fue borrado correctamente"
#: cps/web.py:1487
#: cps/web.py:1890
#, python-format
msgid "Shelf: '%(name)s'"
msgstr "Estante: '%(name)s'"
#: cps/web.py:1518
#: cps/web.py:1921
#, python-format
msgid "Change order of Shelf: '%(name)s'"
msgstr ""
#: cps/web.py:1580
#: cps/web.py:1985
msgid "Found an existing account for this email address."
msgstr "Existe una cuenta vinculada a esta cuenta de correo."
#: cps/web.py:1582 cps/web.py:1586
#: cps/web.py:1987 cps/web.py:1991
#, python-format
msgid "%(name)s's profile"
msgstr "Perfil de %(name)s"
#: cps/web.py:1583
#: cps/web.py:1988
msgid "Profile updated"
msgstr "Perfil actualizado"
#: cps/web.py:1597
#: cps/web.py:2002
msgid "Admin page"
msgstr ""
#: cps/web.py:1668
#: cps/web.py:2106
msgid "Calibre-web configuration updated"
msgstr ""
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
msgid "Basic Configuration"
msgstr ""
#: cps/web.py:1679
#: cps/web.py:2117
msgid "DB location is not valid, please enter correct path"
msgstr ""
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
msgid "Add new user"
msgstr "Agregar un nuevo usuario"
#: cps/web.py:1753
#: cps/web.py:2194
#, python-format
msgid "User '%(user)s' created"
msgstr "Usuario '%(user)s' creado"
#: cps/web.py:1757
#: cps/web.py:2198
msgid "Found an existing account for this email address or nickname."
msgstr "Se ha encontrado una cuenta vinculada a esta cuenta de correo o usuario."
#: cps/web.py:1779
#: cps/web.py:2220
msgid "Mail settings updated"
msgstr "Parametros de correo actualizados"
#: cps/web.py:1785
#: cps/web.py:2227
#, python-format
msgid "Test E-Mail successfully send to %(kindlemail)s"
msgstr "Exito al realizar envio de prueba a %(kindlemail)s"
#: cps/web.py:1788
#: cps/web.py:2230
#, python-format
msgid "There was an error sending the Test E-Mail: %(res)s"
msgstr "Error al realizar envio de prueba a E-Mail: %(res)s"
#: cps/web.py:1789
#: cps/web.py:2234
msgid "E-Mail settings updated"
msgstr ""
#: cps/web.py:2235
msgid "Edit mail settings"
msgstr "Editar parametros de correo"
#: cps/web.py:1817
#: cps/web.py:2263
#, python-format
msgid "User '%(nick)s' deleted"
msgstr "Usuario '%(nick)s' borrado"
#: cps/web.py:1898
#: cps/web.py:2349
#, python-format
msgid "User '%(nick)s' updated"
msgstr "Usuario '%(nick)s' Actualizado"
#: cps/web.py:1901
#: cps/web.py:2352
msgid "An unknown error occured."
msgstr "Oups ! Error inesperado."
#: cps/web.py:1904
#: cps/web.py:2355
#, python-format
msgid "Edit User %(nick)s"
msgstr "Editar Usuario %(nick)s"
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
msgid "edit metadata"
msgstr ""
#: cps/web.py:2145
#: cps/web.py:2598
#, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr ""
#: cps/web.py:2604
msgid "File to be uploaded must have an extension"
msgstr ""
#: cps/web.py:2621
#, python-format
msgid "Failed to create path %s (Permission denied)."
msgstr "Fallo al crear la ruta %s (permiso negado)"
#: cps/web.py:2150
#: cps/web.py:2626
#, python-format
msgid "Failed to store file %s (Permission denied)."
msgstr "Fallo al almacenar el archivo %s (permiso negado)"
#: cps/web.py:2155
#: cps/web.py:2631
#, python-format
msgid "Failed to delete file %s (Permission denied)."
msgstr "Fallo al borrar el archivo %s (permiso negado)"
@ -388,7 +401,7 @@ msgstr "DLS"
msgid "Admin"
msgstr "Administracion"
#: cps/templates/admin.html:13 cps/templates/detail.html:117
#: cps/templates/admin.html:13 cps/templates/detail.html:134
msgid "Download"
msgstr "Descarga"
@ -444,7 +457,7 @@ msgstr ""
msgid "Calibre DB dir"
msgstr ""
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
msgid "Log Level"
msgstr ""
@ -452,7 +465,7 @@ msgstr ""
msgid "Port"
msgstr ""
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
msgid "Books per page"
msgstr ""
@ -481,42 +494,46 @@ msgid "Newest commit timestamp"
msgstr ""
#: cps/templates/admin.html:83
msgid "Restart Calibre-web"
msgid "Reconnect to Calibre DB"
msgstr ""
#: cps/templates/admin.html:84
msgid "Stop Calibre-web"
msgid "Restart Calibre-web"
msgstr ""
#: cps/templates/admin.html:85
msgid "Check for update"
msgid "Stop Calibre-web"
msgstr ""
#: cps/templates/admin.html:86
msgid "Check for update"
msgstr ""
#: cps/templates/admin.html:87
msgid "Perform Update"
msgstr ""
#: cps/templates/admin.html:96
#: cps/templates/admin.html:97
msgid "Do you really want to restart Calibre-web?"
msgstr ""
#: cps/templates/admin.html:101 cps/templates/admin.html:115
#: cps/templates/admin.html:136
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/admin.html:137
msgid "Ok"
msgstr ""
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
#: cps/templates/admin.html:103 cps/templates/admin.html:117
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
msgid "Back"
msgstr "Regresar"
#: cps/templates/admin.html:114
#: cps/templates/admin.html:115
msgid "Do you really want to stop Calibre-web?"
msgstr ""
#: cps/templates/admin.html:127
#: cps/templates/admin.html:128
msgid "Updating, please do not reload page"
msgstr ""
@ -524,20 +541,21 @@ msgstr ""
msgid "Book Title"
msgstr "Titulo del Libro"
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
#: cps/templates/search_form.html:10
msgid "Author"
msgstr "Autor"
#: cps/templates/book_edit.html:24
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
msgid "Description"
msgstr "Descripcion"
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
msgid "Tags"
msgstr "Etiqueta"
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
#: cps/templates/search_form.html:33
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
#: cps/templates/search_form.html:37
msgid "Series"
msgstr "Series"
@ -569,69 +587,142 @@ msgstr "No"
msgid "view book after edit"
msgstr "Ver libro tras la edicion"
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
#: cps/templates/login.html:19 cps/templates/search_form.html:75
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
msgid "Get metadata"
msgstr ""
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
#: cps/templates/login.html:19 cps/templates/search_form.html:79
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
msgid "Submit"
msgstr "Enviar"
#: cps/templates/book_edit.html:121
msgid "Keyword"
msgstr ""
#: cps/templates/book_edit.html:122
msgid " Search keyword "
msgstr ""
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
msgid "Go!"
msgstr "Vamos!"
#: cps/templates/book_edit.html:125
msgid "Click the cover to load metadata to the form"
msgstr ""
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
msgid "Loading..."
msgstr ""
#: cps/templates/book_edit.html:132
msgid "Close"
msgstr ""
#: cps/templates/book_edit.html:143
msgid "Search error!"
msgstr ""
#: cps/templates/book_edit.html:144
msgid "No Result! Please try anonther keyword."
msgstr ""
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
#: cps/templates/search_form.html:14
msgid "Publisher"
msgstr ""
#: cps/templates/book_edit.html:148
msgid "Source"
msgstr ""
#: cps/templates/config_edit.html:7
msgid "Location of Calibre database"
msgstr ""
#: cps/templates/config_edit.html:11
msgid "Server Port"
#: cps/templates/config_edit.html:13
msgid "Use google drive?"
msgstr ""
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr "Titulo"
#: cps/templates/config_edit.html:23
msgid "No. of random books to show"
#: cps/templates/config_edit.html:17
msgid "Client id"
msgstr ""
#: cps/templates/config_edit.html:28
msgid "Regular expression for title sorting"
#: cps/templates/config_edit.html:21
msgid "Client secret"
msgstr ""
#: cps/templates/config_edit.html:42
msgid "Enable uploading"
#: cps/templates/config_edit.html:25
msgid "Calibre Base URL"
msgstr ""
#: cps/templates/config_edit.html:46
msgid "Enable anonymous browsing"
#: cps/templates/config_edit.html:29
msgid "Google drive Calibre folder"
msgstr ""
#: cps/templates/config_edit.html:50
msgid "Enable public registration"
#: cps/templates/config_edit.html:38
msgid "Metadata Watch Channel ID"
msgstr ""
#: cps/templates/config_edit.html:52
msgid "Server Port"
msgstr ""
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr "Titulo"
#: cps/templates/config_edit.html:64
msgid "No. of random books to show"
msgstr ""
#: cps/templates/config_edit.html:68
msgid "Regular expression for ignoring columns"
msgstr ""
#: cps/templates/config_edit.html:72
msgid "Regular expression for title sorting"
msgstr ""
#: cps/templates/config_edit.html:86
msgid "Enable uploading"
msgstr ""
#: cps/templates/config_edit.html:90
msgid "Enable anonymous browsing"
msgstr ""
#: cps/templates/config_edit.html:94
msgid "Enable public registration"
msgstr ""
#: cps/templates/config_edit.html:96
msgid "Default Settings for new users"
msgstr ""
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
msgid "Admin user"
msgstr "Usuario Administrador"
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
msgid "Allow Downloads"
msgstr "Permitir descargas"
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
msgid "Allow Uploads"
msgstr "Permitir subidas de archivos"
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
msgid "Allow Edit"
msgstr "Permitir editar"
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
msgid "Allow Changing Password"
msgstr "Permitir cambiar la clave"
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
#: cps/templates/login.html:4
msgid "Login"
msgstr "Inicio de Sesion"
@ -648,23 +739,27 @@ msgstr "de"
msgid "language"
msgstr "Lenguaje"
#: cps/templates/detail.html:74
#: cps/templates/detail.html:81
msgid "Publishing date"
msgstr ""
#: cps/templates/detail.html:106
#: cps/templates/detail.html:115
msgid "Read"
msgstr ""
#: cps/templates/detail.html:123
msgid "Description:"
msgstr "Descripcion :"
#: cps/templates/detail.html:134
#: cps/templates/detail.html:151
msgid "Read in browser"
msgstr "Ver en el navegador"
#: cps/templates/detail.html:154
#: cps/templates/detail.html:171
msgid "Add to shelf"
msgstr "Agregar al estante"
#: cps/templates/detail.html:194
#: cps/templates/detail.html:211
msgid "Edit metadata"
msgstr "Editar la metadata"
@ -744,19 +839,29 @@ msgstr "Libros Recientes"
msgid "Show Random Books"
msgstr "Mostrar libros al azar"
#: cps/templates/index.xml:43 cps/templates/layout.html:140
#: cps/templates/index.xml:43 cps/templates/index.xml:47
#: cps/templates/layout.html:132
msgid "Read Books"
msgstr ""
#: cps/templates/index.xml:50 cps/templates/index.xml:54
#: cps/templates/layout.html:133
msgid "Unread Books"
msgstr ""
#: cps/templates/index.xml:57 cps/templates/layout.html:144
msgid "Authors"
msgstr "Autores"
#: cps/templates/index.xml:47
#: cps/templates/index.xml:61
msgid "Books ordered by Author"
msgstr "Libros ordenados por Autor"
#: cps/templates/index.xml:54
#: cps/templates/index.xml:68
msgid "Books ordered by category"
msgstr "Libros ordenados por Categorias"
#: cps/templates/index.xml:61
#: cps/templates/index.xml:75
msgid "Books ordered by series"
msgstr "Libros ordenados por Series"
@ -764,10 +869,6 @@ msgstr "Libros ordenados por Series"
msgid "Toggle navigation"
msgstr ""
#: cps/templates/layout.html:60
msgid "Go!"
msgstr "Vamos!"
#: cps/templates/layout.html:68
msgid "Advanced Search"
msgstr "Busqueda avanzada"
@ -784,31 +885,31 @@ msgstr "Registro"
msgid "Browse"
msgstr "Explorar"
#: cps/templates/layout.html:132
#: cps/templates/layout.html:136
msgid "Discover"
msgstr "Descubrir"
#: cps/templates/layout.html:135
#: cps/templates/layout.html:139
msgid "Categories"
msgstr "Categoria"
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
msgid "Languages"
msgstr "Lenguaje"
#: cps/templates/layout.html:145
#: cps/templates/layout.html:149
msgid "Public Shelves"
msgstr "Estantes Publicos"
#: cps/templates/layout.html:149
#: cps/templates/layout.html:153
msgid "Your Shelves"
msgstr "Sus Estantes"
#: cps/templates/layout.html:154
#: cps/templates/layout.html:158
msgid "Create a Shelf"
msgstr "Crear un estante"
#: cps/templates/layout.html:155
#: cps/templates/layout.html:159
msgid "About"
msgstr "Acerca de"
@ -874,15 +975,15 @@ msgstr "Intente una busqueda diferente"
msgid "Results for:"
msgstr "Resultados para:"
#: cps/templates/search_form.html:23
#: cps/templates/search_form.html:27
msgid "Exclude Tags"
msgstr "Excluir etiquetas"
#: cps/templates/search_form.html:43
#: cps/templates/search_form.html:47
msgid "Exclude Series"
msgstr ""
#: cps/templates/search_form.html:64
#: cps/templates/search_form.html:68
msgid "Exclude Languages"
msgstr ""
@ -907,37 +1008,37 @@ msgid "Drag 'n drop to rearrange order"
msgstr ""
#: cps/templates/stats.html:3
msgid "Linked libraries"
msgstr "Librerias vinculadas"
#: cps/templates/stats.html:8
msgid "Program library"
msgstr "Librerias del programa"
#: cps/templates/stats.html:9
msgid "Installed Version"
msgstr "Version instalada"
#: cps/templates/stats.html:32
msgid "Calibre library statistics"
msgstr "Estadisticas de la Biblioteca"
#: cps/templates/stats.html:37
#: cps/templates/stats.html:8
msgid "Books in this Library"
msgstr "Libros en esta Biblioteca"
#: cps/templates/stats.html:41
#: cps/templates/stats.html:12
msgid "Authors in this Library"
msgstr "Autores en esta Biblioteca"
#: cps/templates/stats.html:45
#: cps/templates/stats.html:16
msgid "Categories in this Library"
msgstr ""
#: cps/templates/stats.html:49
#: cps/templates/stats.html:20
msgid "Series in this Library"
msgstr ""
#: cps/templates/stats.html:24
msgid "Linked libraries"
msgstr "Librerias vinculadas"
#: cps/templates/stats.html:28
msgid "Program library"
msgstr "Librerias del programa"
#: cps/templates/stats.html:29
msgid "Installed Version"
msgstr "Version instalada"
#: cps/templates/user_edit.html:23
msgid "Kindle E-Mail"
msgstr "Correo del Kindle"
@ -950,43 +1051,47 @@ msgstr "Mostrar lenguaje de los libros"
msgid "Show all"
msgstr "Mostrar Todo"
#: cps/templates/user_edit.html:45
#: cps/templates/user_edit.html:47
msgid "Show random books"
msgstr "Mostrar libros al azar"
#: cps/templates/user_edit.html:49
#: cps/templates/user_edit.html:51
msgid "Show hot books"
msgstr "Mostrar libros populares"
#: cps/templates/user_edit.html:53
#: cps/templates/user_edit.html:55
msgid "Show best rated books"
msgstr ""
#: cps/templates/user_edit.html:57
#: cps/templates/user_edit.html:59
msgid "Show language selection"
msgstr "Mostrar lenguaje seleccionado"
#: cps/templates/user_edit.html:61
#: cps/templates/user_edit.html:63
msgid "Show series selection"
msgstr "Mostrar series seleccionadas"
#: cps/templates/user_edit.html:65
#: cps/templates/user_edit.html:67
msgid "Show category selection"
msgstr "Mostrar categorias elegidas"
#: cps/templates/user_edit.html:69
#: cps/templates/user_edit.html:71
msgid "Show author selection"
msgstr ""
#: cps/templates/user_edit.html:73
#: cps/templates/user_edit.html:75
msgid "Show read and unread"
msgstr ""
#: cps/templates/user_edit.html:79
msgid "Show random books in detail view"
msgstr ""
#: cps/templates/user_edit.html:105
#: cps/templates/user_edit.html:112
msgid "Delete this user"
msgstr "Borrar este usuario"
#: cps/templates/user_edit.html:116
#: cps/templates/user_edit.html:127
msgid "Recent Downloads"
msgstr "Descargas Recientes"

View File

@ -20,7 +20,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Calibre-web\n"
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
"PO-Revision-Date: 2016-11-13 18:35+0100\n"
"Last-Translator: Nicolas Roudninski <nicoroud@gmail.com>\n"
"Language: fr\n"
@ -31,343 +31,356 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
msgid "not installed"
msgstr ""
#: cps/helper.py:150
#: cps/helper.py:164
#, python-format
msgid "Failed to send mail: %s"
msgstr "Impossible d'envoyer le courriel : %s"
#: cps/helper.py:157
#: cps/helper.py:171
msgid "Calibre-web test email"
msgstr ""
#: cps/helper.py:158 cps/helper.py:168
#: cps/helper.py:172 cps/helper.py:184
msgid "This email has been sent via calibre web."
msgstr "Ce message a été envoyé depuis calibre web."
#: cps/helper.py:167 cps/templates/detail.html:130
#: cps/helper.py:181 cps/templates/detail.html:146
msgid "Send to Kindle"
msgstr "Envoyer ver Kindle"
#: cps/helper.py:185 cps/helper.py:200
#: cps/helper.py:201 cps/helper.py:216
msgid "Could not find any formats suitable for sending by email"
msgstr "Impossible de trouver un format adapté à envoyer par courriel"
#: cps/helper.py:194
#: cps/helper.py:210
msgid "Could not convert epub to mobi"
msgstr "Impossible de convertir epub vers mobi"
#: cps/ub.py:434
#: cps/ub.py:488
msgid "Guest"
msgstr ""
#: cps/web.py:734
#: cps/web.py:904
msgid "Requesting update package"
msgstr ""
#: cps/web.py:735
#: cps/web.py:905
msgid "Downloading update package"
msgstr ""
#: cps/web.py:736
#: cps/web.py:906
msgid "Unzipping update package"
msgstr ""
#: cps/web.py:737
#: cps/web.py:907
msgid "Files are replaced"
msgstr ""
#: cps/web.py:738
#: cps/web.py:908
msgid "Database connections are closed"
msgstr ""
#: cps/web.py:739
#: cps/web.py:909
msgid "Server is stopped"
msgstr ""
#: cps/web.py:740
#: cps/web.py:910
msgid "Update finished, please press okay and reload page"
msgstr ""
#: cps/web.py:810
#: cps/web.py:983
msgid "Latest Books"
msgstr "Derniers livres"
#: cps/web.py:835
#: cps/web.py:1014
msgid "Hot Books (most downloaded)"
msgstr "Livres populaires (les plus téléchargés)"
#: cps/web.py:845
#: cps/web.py:1024
msgid "Best rated books"
msgstr ""
#: cps/templates/index.xml:36 cps/web.py:854
#: cps/templates/index.xml:36 cps/web.py:1033
msgid "Random Books"
msgstr "Livres au hasard"
#: cps/web.py:867
#: cps/web.py:1046
msgid "Author list"
msgstr "Liste des auteurs"
#: cps/web.py:878
#: cps/web.py:1057
#, python-format
msgid "Author: %(name)s"
msgstr ""
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
#: cps/web.py:2115
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
#: cps/web.py:2579
msgid "Error opening eBook. File does not exist or file is not accessible:"
msgstr ""
"Erreur d'ouverture du livre numérique. Le fichier n'existe pas ou n'est "
"pas accessible :"
#: cps/templates/index.xml:57 cps/web.py:894
#: cps/templates/index.xml:71 cps/web.py:1073
msgid "Series list"
msgstr "Liste des séries"
#: cps/web.py:906
#: cps/web.py:1085
#, python-format
msgid "Series: %(serie)s"
msgstr "Séries : %(serie)s"
#: cps/web.py:939
#: cps/web.py:1118
msgid "Available languages"
msgstr "Langues disponibles"
#: cps/web.py:954
#: cps/web.py:1133
#, python-format
msgid "Language: %(name)s"
msgstr "Langue : %(name)s"
#: cps/templates/index.xml:50 cps/web.py:967
#: cps/templates/index.xml:64 cps/web.py:1146
msgid "Category list"
msgstr "Liste des catégories"
#: cps/web.py:979
#: cps/web.py:1158
#, python-format
msgid "Category: %(name)s"
msgstr "Catégorie : %(name)s"
#: cps/web.py:1040
#: cps/web.py:1267
msgid "Statistics"
msgstr "Statistiques"
#: cps/web.py:1061
#: cps/web.py:1375
msgid "Server restarted, please reload page"
msgstr ""
#: cps/web.py:1063
#: cps/web.py:1377
msgid "Performing shutdown of server, please close window"
msgstr ""
#: cps/web.py:1073
#: cps/web.py:1392
msgid "Update done"
msgstr ""
#: cps/web.py:1147 cps/web.py:1160
#: cps/web.py:1470 cps/web.py:1483
msgid "search"
msgstr ""
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
msgid "Read a Book"
msgstr "Lire un livre"
#: cps/web.py:1276 cps/web.py:1713
#: cps/web.py:1676 cps/web.py:2152
msgid "Please fill out all fields!"
msgstr "SVP, complétez tous les champs !"
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
msgid "register"
msgstr "S'enregistrer"
#: cps/web.py:1292
#: cps/web.py:1692
msgid "An unknown error occured. Please try again later."
msgstr "Une erreur a eu lieu. Merci de réessayez plus tard."
#: cps/web.py:1297
#: cps/web.py:1697
msgid "This username or email address is already in use."
msgstr "Ce nom d'utilisateur ou cette adresse de courriel est déjà utilisée."
#: cps/web.py:1315
#: cps/web.py:1715
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr "Vous êtes maintenant connecté sous : '%(nickname)s'"
#: cps/web.py:1320
#: cps/web.py:1720
msgid "Wrong Username or Password"
msgstr "Mauvais nom d'utilisateur ou mot de passe"
#: cps/web.py:1322
#: cps/web.py:1722
msgid "login"
msgstr "Connexion"
#: cps/web.py:1339
#: cps/web.py:1739
msgid "Please configure the SMTP mail settings first..."
msgstr "Veillez configurer les paramètres smtp d'abord..."
#: cps/web.py:1343
#: cps/web.py:1743
#, python-format
msgid "Book successfully send to %(kindlemail)s"
msgstr "Livres envoyés à %(kindlemail)s avec succès"
#: cps/web.py:1347
#: cps/web.py:1747
#, python-format
msgid "There was an error sending this book: %(res)s"
msgstr "Il y a eu une erreur en envoyant ce livre : %(res)s"
#: cps/web.py:1349
#: cps/web.py:1749 cps/web.py:2232
msgid "Please configure your kindle email address first..."
msgstr "Veuillez configurer votre adresse kindle d'abord..."
#: cps/web.py:1369
#: cps/web.py:1774
#, python-format
msgid "Book has been added to shelf: %(sname)s"
msgstr "Le livre a bien été ajouté à l'étagère : %(sname)s"
#: cps/web.py:1390
#: cps/web.py:1793
#, python-format
msgid "Book has been removed from shelf: %(sname)s"
msgstr "Le livre a été supprimé de l'étagère %(sname)s"
#: cps/web.py:1409 cps/web.py:1433
#: cps/web.py:1812 cps/web.py:1836
#, python-format
msgid "A shelf with the name '%(title)s' already exists."
msgstr "Une étagère de ce nom '%(title)s' existe déjà."
#: cps/web.py:1414
#: cps/web.py:1817
#, python-format
msgid "Shelf %(title)s created"
msgstr "Étagère %(title)s créée"
#: cps/web.py:1416 cps/web.py:1444
#: cps/web.py:1819 cps/web.py:1847
msgid "There was an error"
msgstr "Il y a eu une erreur"
#: cps/web.py:1417 cps/web.py:1419
#: cps/web.py:1820 cps/web.py:1822
msgid "create a shelf"
msgstr "Créer une étagère"
#: cps/web.py:1442
#: cps/web.py:1845
#, python-format
msgid "Shelf %(title)s changed"
msgstr ""
#: cps/web.py:1445 cps/web.py:1447
#: cps/web.py:1848 cps/web.py:1850
msgid "Edit a shelf"
msgstr ""
#: cps/web.py:1465
#: cps/web.py:1868
#, python-format
msgid "successfully deleted shelf %(name)s"
msgstr "L'étagère %(name)s a été supprimé avec succès"
#: cps/web.py:1487
#: cps/web.py:1890
#, python-format
msgid "Shelf: '%(name)s'"
msgstr "Étagère : '%(name)s'"
#: cps/web.py:1518
#: cps/web.py:1921
#, python-format
msgid "Change order of Shelf: '%(name)s'"
msgstr ""
#: cps/web.py:1580
#: cps/web.py:1985
msgid "Found an existing account for this email address."
msgstr "Un compte avec cette adresse de courriel existe déjà."
#: cps/web.py:1582 cps/web.py:1586
#: cps/web.py:1987 cps/web.py:1991
#, python-format
msgid "%(name)s's profile"
msgstr "Profil de %(name)s"
#: cps/web.py:1583
#: cps/web.py:1988
msgid "Profile updated"
msgstr "Profil mis à jour"
#: cps/web.py:1597
#: cps/web.py:2002
msgid "Admin page"
msgstr ""
#: cps/web.py:1668
#: cps/web.py:2106
msgid "Calibre-web configuration updated"
msgstr ""
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
msgid "Basic Configuration"
msgstr ""
#: cps/web.py:1679
#: cps/web.py:2117
msgid "DB location is not valid, please enter correct path"
msgstr ""
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
msgid "Add new user"
msgstr "Ajouter un nouvel utilisateur"
#: cps/web.py:1753
#: cps/web.py:2194
#, python-format
msgid "User '%(user)s' created"
msgstr "Utilisateur '%(user)s' créé"
#: cps/web.py:1757
#: cps/web.py:2198
msgid "Found an existing account for this email address or nickname."
msgstr "Un compte avec cette adresse de courriel ou ce surnom existe déjà."
#: cps/web.py:1779
#: cps/web.py:2220
msgid "Mail settings updated"
msgstr "Paramètres de courriel mis à jour"
#: cps/web.py:1785
#: cps/web.py:2227
#, python-format
msgid "Test E-Mail successfully send to %(kindlemail)s"
msgstr ""
#: cps/web.py:1788
#: cps/web.py:2230
#, python-format
msgid "There was an error sending the Test E-Mail: %(res)s"
msgstr ""
#: cps/web.py:1789
#: cps/web.py:2234
msgid "E-Mail settings updated"
msgstr ""
#: cps/web.py:2235
msgid "Edit mail settings"
msgstr "Éditer les paramètres de courriel"
#: cps/web.py:1817
#: cps/web.py:2263
#, python-format
msgid "User '%(nick)s' deleted"
msgstr "Utilisateur '%(nick)s' supprimé"
#: cps/web.py:1898
#: cps/web.py:2349
#, python-format
msgid "User '%(nick)s' updated"
msgstr "Utilisateur '%(nick)s' mis à jour"
#: cps/web.py:1901
#: cps/web.py:2352
msgid "An unknown error occured."
msgstr "Oups ! Une erreur inconnue a eu lieu."
#: cps/web.py:1904
#: cps/web.py:2355
#, python-format
msgid "Edit User %(nick)s"
msgstr "Éditer l'utilisateur %(nick)s"
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
msgid "edit metadata"
msgstr ""
#: cps/web.py:2145
#: cps/web.py:2598
#, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr ""
#: cps/web.py:2604
msgid "File to be uploaded must have an extension"
msgstr ""
#: cps/web.py:2621
#, python-format
msgid "Failed to create path %s (Permission denied)."
msgstr "Impossible de créer le chemin %s (permission refusée)"
#: cps/web.py:2150
#: cps/web.py:2626
#, python-format
msgid "Failed to store file %s (Permission denied)."
msgstr "Impossible d'enregistrer le fichier %s (permission refusée)"
#: cps/web.py:2155
#: cps/web.py:2631
#, python-format
msgid "Failed to delete file %s (Permission denied)."
msgstr "Impossible de supprimer le fichier %s (permission refusée)"
@ -396,7 +409,7 @@ msgstr "DLS"
msgid "Admin"
msgstr "Administration"
#: cps/templates/admin.html:13 cps/templates/detail.html:117
#: cps/templates/admin.html:13 cps/templates/detail.html:134
msgid "Download"
msgstr "Télécharger"
@ -452,7 +465,7 @@ msgstr ""
msgid "Calibre DB dir"
msgstr ""
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
msgid "Log Level"
msgstr ""
@ -460,7 +473,7 @@ msgstr ""
msgid "Port"
msgstr ""
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
msgid "Books per page"
msgstr ""
@ -489,42 +502,46 @@ msgid "Newest commit timestamp"
msgstr ""
#: cps/templates/admin.html:83
msgid "Restart Calibre-web"
msgid "Reconnect to Calibre DB"
msgstr ""
#: cps/templates/admin.html:84
msgid "Stop Calibre-web"
msgid "Restart Calibre-web"
msgstr ""
#: cps/templates/admin.html:85
msgid "Check for update"
msgid "Stop Calibre-web"
msgstr ""
#: cps/templates/admin.html:86
msgid "Check for update"
msgstr ""
#: cps/templates/admin.html:87
msgid "Perform Update"
msgstr ""
#: cps/templates/admin.html:96
#: cps/templates/admin.html:97
msgid "Do you really want to restart Calibre-web?"
msgstr ""
#: cps/templates/admin.html:101 cps/templates/admin.html:115
#: cps/templates/admin.html:136
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/admin.html:137
msgid "Ok"
msgstr ""
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
#: cps/templates/admin.html:103 cps/templates/admin.html:117
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
msgid "Back"
msgstr "Retour"
#: cps/templates/admin.html:114
#: cps/templates/admin.html:115
msgid "Do you really want to stop Calibre-web?"
msgstr ""
#: cps/templates/admin.html:127
#: cps/templates/admin.html:128
msgid "Updating, please do not reload page"
msgstr ""
@ -532,20 +549,21 @@ msgstr ""
msgid "Book Title"
msgstr "Titre du livre"
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
#: cps/templates/search_form.html:10
msgid "Author"
msgstr "Auteur"
#: cps/templates/book_edit.html:24
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
msgid "Description"
msgstr "Description"
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
msgid "Tags"
msgstr "Étiquette"
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
#: cps/templates/search_form.html:33
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
#: cps/templates/search_form.html:37
msgid "Series"
msgstr "Séries"
@ -577,69 +595,142 @@ msgstr "Non"
msgid "view book after edit"
msgstr "Voir le livre après l'édition"
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
#: cps/templates/login.html:19 cps/templates/search_form.html:75
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
msgid "Get metadata"
msgstr ""
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
#: cps/templates/login.html:19 cps/templates/search_form.html:79
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
msgid "Submit"
msgstr "Soumettre"
#: cps/templates/book_edit.html:121
msgid "Keyword"
msgstr ""
#: cps/templates/book_edit.html:122
msgid " Search keyword "
msgstr ""
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
msgid "Go!"
msgstr "Allez !"
#: cps/templates/book_edit.html:125
msgid "Click the cover to load metadata to the form"
msgstr ""
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
msgid "Loading..."
msgstr ""
#: cps/templates/book_edit.html:132
msgid "Close"
msgstr ""
#: cps/templates/book_edit.html:143
msgid "Search error!"
msgstr ""
#: cps/templates/book_edit.html:144
msgid "No Result! Please try anonther keyword."
msgstr ""
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
#: cps/templates/search_form.html:14
msgid "Publisher"
msgstr ""
#: cps/templates/book_edit.html:148
msgid "Source"
msgstr ""
#: cps/templates/config_edit.html:7
msgid "Location of Calibre database"
msgstr ""
#: cps/templates/config_edit.html:11
msgid "Server Port"
#: cps/templates/config_edit.html:13
msgid "Use google drive?"
msgstr ""
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr "Titre"
#: cps/templates/config_edit.html:23
msgid "No. of random books to show"
#: cps/templates/config_edit.html:17
msgid "Client id"
msgstr ""
#: cps/templates/config_edit.html:28
msgid "Regular expression for title sorting"
#: cps/templates/config_edit.html:21
msgid "Client secret"
msgstr ""
#: cps/templates/config_edit.html:42
msgid "Enable uploading"
#: cps/templates/config_edit.html:25
msgid "Calibre Base URL"
msgstr ""
#: cps/templates/config_edit.html:46
msgid "Enable anonymous browsing"
#: cps/templates/config_edit.html:29
msgid "Google drive Calibre folder"
msgstr ""
#: cps/templates/config_edit.html:50
msgid "Enable public registration"
#: cps/templates/config_edit.html:38
msgid "Metadata Watch Channel ID"
msgstr ""
#: cps/templates/config_edit.html:52
msgid "Server Port"
msgstr ""
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr "Titre"
#: cps/templates/config_edit.html:64
msgid "No. of random books to show"
msgstr ""
#: cps/templates/config_edit.html:68
msgid "Regular expression for ignoring columns"
msgstr ""
#: cps/templates/config_edit.html:72
msgid "Regular expression for title sorting"
msgstr ""
#: cps/templates/config_edit.html:86
msgid "Enable uploading"
msgstr ""
#: cps/templates/config_edit.html:90
msgid "Enable anonymous browsing"
msgstr ""
#: cps/templates/config_edit.html:94
msgid "Enable public registration"
msgstr ""
#: cps/templates/config_edit.html:96
msgid "Default Settings for new users"
msgstr ""
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
msgid "Admin user"
msgstr "Utilisateur admin"
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
msgid "Allow Downloads"
msgstr "Permettre les téléchargements"
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
msgid "Allow Uploads"
msgstr "Permettre les téléversements"
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
msgid "Allow Edit"
msgstr "Permettre l'édition"
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
msgid "Allow Changing Password"
msgstr "Permettre le changement de mot de passe"
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
#: cps/templates/login.html:4
msgid "Login"
msgstr "Connexion"
@ -656,23 +747,27 @@ msgstr ""
msgid "language"
msgstr "Langue"
#: cps/templates/detail.html:74
#: cps/templates/detail.html:81
msgid "Publishing date"
msgstr ""
#: cps/templates/detail.html:106
#: cps/templates/detail.html:115
msgid "Read"
msgstr ""
#: cps/templates/detail.html:123
msgid "Description:"
msgstr "Description :"
#: cps/templates/detail.html:134
#: cps/templates/detail.html:151
msgid "Read in browser"
msgstr "Lire dans le navigateur"
#: cps/templates/detail.html:154
#: cps/templates/detail.html:171
msgid "Add to shelf"
msgstr "Ajouter à l'étagère"
#: cps/templates/detail.html:194
#: cps/templates/detail.html:211
msgid "Edit metadata"
msgstr "Éditer les métadonnées"
@ -752,19 +847,29 @@ msgstr "Les derniers livres"
msgid "Show Random Books"
msgstr "Montrer des livres au hasard"
#: cps/templates/index.xml:43 cps/templates/layout.html:140
#: cps/templates/index.xml:43 cps/templates/index.xml:47
#: cps/templates/layout.html:132
msgid "Read Books"
msgstr ""
#: cps/templates/index.xml:50 cps/templates/index.xml:54
#: cps/templates/layout.html:133
msgid "Unread Books"
msgstr ""
#: cps/templates/index.xml:57 cps/templates/layout.html:144
msgid "Authors"
msgstr "Auteurs"
#: cps/templates/index.xml:47
#: cps/templates/index.xml:61
msgid "Books ordered by Author"
msgstr "Livres classés par auteur"
#: cps/templates/index.xml:54
#: cps/templates/index.xml:68
msgid "Books ordered by category"
msgstr "Livres classés par catégorie"
#: cps/templates/index.xml:61
#: cps/templates/index.xml:75
msgid "Books ordered by series"
msgstr "Livres classés par série"
@ -772,10 +877,6 @@ msgstr "Livres classés par série"
msgid "Toggle navigation"
msgstr "Basculer la navigation"
#: cps/templates/layout.html:60
msgid "Go!"
msgstr "Allez !"
#: cps/templates/layout.html:68
msgid "Advanced Search"
msgstr "Recherche avancée"
@ -792,31 +893,31 @@ msgstr "S'enregistrer"
msgid "Browse"
msgstr "Explorer"
#: cps/templates/layout.html:132
#: cps/templates/layout.html:136
msgid "Discover"
msgstr "Découvrir"
#: cps/templates/layout.html:135
#: cps/templates/layout.html:139
msgid "Categories"
msgstr "Catégories"
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
msgid "Languages"
msgstr "Langues"
#: cps/templates/layout.html:145
#: cps/templates/layout.html:149
msgid "Public Shelves"
msgstr "Étagères publiques"
#: cps/templates/layout.html:149
#: cps/templates/layout.html:153
msgid "Your Shelves"
msgstr "Vos étagères"
#: cps/templates/layout.html:154
#: cps/templates/layout.html:158
msgid "Create a Shelf"
msgstr "Créer une étagère"
#: cps/templates/layout.html:155
#: cps/templates/layout.html:159
msgid "About"
msgstr "À popos"
@ -882,15 +983,15 @@ msgstr "Essayer une recherche différente"
msgid "Results for:"
msgstr "Résultats pour :"
#: cps/templates/search_form.html:23
#: cps/templates/search_form.html:27
msgid "Exclude Tags"
msgstr "Exclure des étiquettes"
#: cps/templates/search_form.html:43
#: cps/templates/search_form.html:47
msgid "Exclude Series"
msgstr ""
#: cps/templates/search_form.html:64
#: cps/templates/search_form.html:68
msgid "Exclude Languages"
msgstr ""
@ -915,37 +1016,37 @@ msgid "Drag 'n drop to rearrange order"
msgstr ""
#: cps/templates/stats.html:3
msgid "Linked libraries"
msgstr ""
#: cps/templates/stats.html:8
msgid "Program library"
msgstr ""
#: cps/templates/stats.html:9
msgid "Installed Version"
msgstr ""
#: cps/templates/stats.html:32
msgid "Calibre library statistics"
msgstr ""
#: cps/templates/stats.html:37
#: cps/templates/stats.html:8
msgid "Books in this Library"
msgstr "Livres dans la bibiothèque"
#: cps/templates/stats.html:41
#: cps/templates/stats.html:12
msgid "Authors in this Library"
msgstr "Auteurs dans la bibliothèque"
#: cps/templates/stats.html:45
#: cps/templates/stats.html:16
msgid "Categories in this Library"
msgstr ""
#: cps/templates/stats.html:49
#: cps/templates/stats.html:20
msgid "Series in this Library"
msgstr ""
#: cps/templates/stats.html:24
msgid "Linked libraries"
msgstr ""
#: cps/templates/stats.html:28
msgid "Program library"
msgstr ""
#: cps/templates/stats.html:29
msgid "Installed Version"
msgstr ""
#: cps/templates/user_edit.html:23
msgid "Kindle E-Mail"
msgstr "Courriel Kindle"
@ -958,43 +1059,47 @@ msgstr "Montrer les livres dans la langue"
msgid "Show all"
msgstr "Montrer tout"
#: cps/templates/user_edit.html:45
#: cps/templates/user_edit.html:47
msgid "Show random books"
msgstr "Montrer des livres au hasard"
#: cps/templates/user_edit.html:49
#: cps/templates/user_edit.html:51
msgid "Show hot books"
msgstr "Montrer les livres populaires"
#: cps/templates/user_edit.html:53
#: cps/templates/user_edit.html:55
msgid "Show best rated books"
msgstr ""
#: cps/templates/user_edit.html:57
#: cps/templates/user_edit.html:59
msgid "Show language selection"
msgstr "Montrer la sélection de la langue"
#: cps/templates/user_edit.html:61
#: cps/templates/user_edit.html:63
msgid "Show series selection"
msgstr "Montrer la sélection des séries"
#: cps/templates/user_edit.html:65
#: cps/templates/user_edit.html:67
msgid "Show category selection"
msgstr "Montrer la sélection des catégories"
#: cps/templates/user_edit.html:69
#: cps/templates/user_edit.html:71
msgid "Show author selection"
msgstr ""
#: cps/templates/user_edit.html:73
#: cps/templates/user_edit.html:75
msgid "Show read and unread"
msgstr ""
#: cps/templates/user_edit.html:79
msgid "Show random books in detail view"
msgstr ""
#: cps/templates/user_edit.html:105
#: cps/templates/user_edit.html:112
msgid "Delete this user"
msgstr "Supprimer cet utilisateur"
#: cps/templates/user_edit.html:116
#: cps/templates/user_edit.html:127
msgid "Recent Downloads"
msgstr "Téléchargements récents"

View File

@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Calibre-web\n"
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
"PO-Revision-Date: 2017-01-06 17:00+0000\n"
"Last-Translator: dalin <dalin.lin@gmail.com>\n"
"Language: zh_Hans_CN\n"
@ -26,341 +26,354 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
msgid "not installed"
msgstr "未安装"
#: cps/helper.py:150
#: cps/helper.py:164
#, python-format
msgid "Failed to send mail: %s"
msgstr "发送邮件失败: %s"
#: cps/helper.py:157
#: cps/helper.py:171
msgid "Calibre-web test email"
msgstr "Calibre-web 测试邮件"
#: cps/helper.py:158 cps/helper.py:168
#: cps/helper.py:172 cps/helper.py:184
msgid "This email has been sent via calibre web."
msgstr "此邮件由calibre web发送"
#: cps/helper.py:167 cps/templates/detail.html:130
#: cps/helper.py:181 cps/templates/detail.html:146
msgid "Send to Kindle"
msgstr "发送到Kindle"
#: cps/helper.py:185 cps/helper.py:200
#: cps/helper.py:201 cps/helper.py:216
msgid "Could not find any formats suitable for sending by email"
msgstr "无法找到适合邮件发送的格式"
#: cps/helper.py:194
#: cps/helper.py:210
msgid "Could not convert epub to mobi"
msgstr "无法转换epub到mobi"
#: cps/ub.py:434
#: cps/ub.py:488
msgid "Guest"
msgstr "游客"
#: cps/web.py:734
#: cps/web.py:904
msgid "Requesting update package"
msgstr "正在请求更新包"
#: cps/web.py:735
#: cps/web.py:905
msgid "Downloading update package"
msgstr "正在下载更新包"
#: cps/web.py:736
#: cps/web.py:906
msgid "Unzipping update package"
msgstr "正在解压更新包"
#: cps/web.py:737
#: cps/web.py:907
msgid "Files are replaced"
msgstr "文件已替换"
#: cps/web.py:738
#: cps/web.py:908
msgid "Database connections are closed"
msgstr "数据库连接已关闭"
#: cps/web.py:739
#: cps/web.py:909
msgid "Server is stopped"
msgstr "服务器已停止"
#: cps/web.py:740
#: cps/web.py:910
msgid "Update finished, please press okay and reload page"
msgstr "更新完成,请按确定并刷新页面"
#: cps/web.py:810
#: cps/web.py:983
msgid "Latest Books"
msgstr "最新书籍"
#: cps/web.py:835
#: cps/web.py:1014
msgid "Hot Books (most downloaded)"
msgstr "热门书籍(最多下载)"
#: cps/web.py:845
#: cps/web.py:1024
msgid "Best rated books"
msgstr "最高评分书籍"
#: cps/templates/index.xml:36 cps/web.py:854
#: cps/templates/index.xml:36 cps/web.py:1033
msgid "Random Books"
msgstr "随机书籍"
#: cps/web.py:867
#: cps/web.py:1046
msgid "Author list"
msgstr "作者列表"
#: cps/web.py:878
#, python-forma
#: cps/web.py:1057
#, python-forma, python-format
msgid "Author: %(name)s"
msgstr "作者: %(name)s"
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
#: cps/web.py:2115
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
#: cps/web.py:2579
msgid "Error opening eBook. File does not exist or file is not accessible:"
msgstr "无法打开电子书。 文件不存在或者文件不可访问:"
#: cps/templates/index.xml:57 cps/web.py:894
#: cps/templates/index.xml:71 cps/web.py:1073
msgid "Series list"
msgstr "丛书列表"
#: cps/web.py:906
#: cps/web.py:1085
#, python-format
msgid "Series: %(serie)s"
msgstr "丛书: %(serie)s"
#: cps/web.py:939
#: cps/web.py:1118
msgid "Available languages"
msgstr "可用语言"
#: cps/web.py:954
#: cps/web.py:1133
#, python-format
msgid "Language: %(name)s"
msgstr "语言: %(name)s"
#: cps/templates/index.xml:50 cps/web.py:967
#: cps/templates/index.xml:64 cps/web.py:1146
msgid "Category list"
msgstr "分类列表"
#: cps/web.py:979
#: cps/web.py:1158
#, python-format
msgid "Category: %(name)s"
msgstr "分类: %(name)s"
#: cps/web.py:1040
#: cps/web.py:1267
msgid "Statistics"
msgstr "统计"
#: cps/web.py:1061
#: cps/web.py:1375
msgid "Server restarted, please reload page"
msgstr "服务器已重启,请刷新页面"
#: cps/web.py:1063
#: cps/web.py:1377
msgid "Performing shutdown of server, please close window"
msgstr "正在关闭服务器,请关闭窗口"
#: cps/web.py:1073
#: cps/web.py:1392
msgid "Update done"
msgstr "更新完成"
#: cps/web.py:1147 cps/web.py:1160
#: cps/web.py:1470 cps/web.py:1483
msgid "search"
msgstr "搜索"
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
msgid "Read a Book"
msgstr "阅读一本书"
#: cps/web.py:1276 cps/web.py:1713
#: cps/web.py:1676 cps/web.py:2152
msgid "Please fill out all fields!"
msgstr "请填写所有字段"
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
msgid "register"
msgstr "注册"
#: cps/web.py:1292
#: cps/web.py:1692
msgid "An unknown error occured. Please try again later."
msgstr "发生一个未知错误。请稍后再试。"
#: cps/web.py:1297
#: cps/web.py:1697
msgid "This username or email address is already in use."
msgstr "此用户名或邮箱已被使用。"
#: cps/web.py:1315
#: cps/web.py:1715
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr "您现在已以'%(nickname)s'身份登录"
#: cps/web.py:1320
#: cps/web.py:1720
msgid "Wrong Username or Password"
msgstr "用户名或密码错误"
#: cps/web.py:1322
#: cps/web.py:1722
msgid "login"
msgstr "登录"
#: cps/web.py:1339
#: cps/web.py:1739
msgid "Please configure the SMTP mail settings first..."
msgstr "请先配置SMTP邮箱..."
#: cps/web.py:1343
#: cps/web.py:1743
#, python-format
msgid "Book successfully send to %(kindlemail)s"
msgstr "此书已被成功发给 %(kindlemail)s"
#: cps/web.py:1347
#: cps/web.py:1747
#, python-format
msgid "There was an error sending this book: %(res)s"
msgstr "发送这本书的时候出现错误: %(res)s"
#: cps/web.py:1349
#: cps/web.py:1749 cps/web.py:2232
msgid "Please configure your kindle email address first..."
msgstr "请先配置您的kindle电子邮箱地址..."
#: cps/web.py:1369
#: cps/web.py:1774
#, python-format
msgid "Book has been added to shelf: %(sname)s"
msgstr "此书已被添加到书架: %(sname)s"
#: cps/web.py:1390
#: cps/web.py:1793
#, python-format
msgid "Book has been removed from shelf: %(sname)s"
msgstr "此书已从书架 %(sname)s 中删除"
#: cps/web.py:1409 cps/web.py:1433
#: cps/web.py:1812 cps/web.py:1836
#, python-format
msgid "A shelf with the name '%(title)s' already exists."
msgstr "已存在书架 '%(title)s'。"
#: cps/web.py:1414
#: cps/web.py:1817
#, python-format
msgid "Shelf %(title)s created"
msgstr "书架 %(title)s 已被创建"
#: cps/web.py:1416 cps/web.py:1444
#: cps/web.py:1819 cps/web.py:1847
msgid "There was an error"
msgstr "发生错误"
#: cps/web.py:1417 cps/web.py:1419
#: cps/web.py:1820 cps/web.py:1822
msgid "create a shelf"
msgstr "创建书架"
#: cps/web.py:1442
#: cps/web.py:1845
#, python-format
msgid "Shelf %(title)s changed"
msgstr "书架 %(title)s 已被修改"
#: cps/web.py:1445 cps/web.py:1447
#: cps/web.py:1848 cps/web.py:1850
msgid "Edit a shelf"
msgstr "编辑书架"
#: cps/web.py:1465
#: cps/web.py:1868
#, python-format
msgid "successfully deleted shelf %(name)s"
msgstr "成功删除书架 %(name)s"
#: cps/web.py:1487
#: cps/web.py:1890
#, python-format
msgid "Shelf: '%(name)s'"
msgstr "书架: '%(name)s'"
#: cps/web.py:1518
#: cps/web.py:1921
#, python-format
msgid "Change order of Shelf: '%(name)s'"
msgstr "修改书架 '%(name)s' 顺序"
#: cps/web.py:1580
#: cps/web.py:1985
msgid "Found an existing account for this email address."
msgstr "找到已使用此邮箱的账号。"
#: cps/web.py:1582 cps/web.py:1586
#: cps/web.py:1987 cps/web.py:1991
#, python-format
msgid "%(name)s's profile"
msgstr "%(name)s 的资料"
#: cps/web.py:1583
#: cps/web.py:1988
msgid "Profile updated"
msgstr "资料已更新"
#: cps/web.py:1597
#: cps/web.py:2002
msgid "Admin page"
msgstr "管理页"
#: cps/web.py:1668
#: cps/web.py:2106
msgid "Calibre-web configuration updated"
msgstr "Calibre-web配置已更新"
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
msgid "Basic Configuration"
msgstr "基本配置"
#: cps/web.py:1679
#: cps/web.py:2117
msgid "DB location is not valid, please enter correct path"
msgstr "DB位置无效请输入正确路径"
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
msgid "Add new user"
msgstr "添加新用户"
#: cps/web.py:1753
#: cps/web.py:2194
#, python-format
msgid "User '%(user)s' created"
msgstr "用户 '%(user)s' 已被创建"
#: cps/web.py:1757
#: cps/web.py:2198
msgid "Found an existing account for this email address or nickname."
msgstr "已找到使用此邮箱或昵称的账号。"
#: cps/web.py:1779
#: cps/web.py:2220
msgid "Mail settings updated"
msgstr "邮箱设置已更新"
#: cps/web.py:1785
#: cps/web.py:2227
#, python-format
msgid "Test E-Mail successfully send to %(kindlemail)s"
msgstr "测试邮件已成功发送到 %(kindlemail)s"
#: cps/web.py:1788
#: cps/web.py:2230
#, python-format
msgid "There was an error sending the Test E-Mail: %(res)s"
msgstr "发送测试邮件时发生错误: %(res)s"
#: cps/web.py:1789
#: cps/web.py:2234
msgid "E-Mail settings updated"
msgstr "E-Mail 设置已更新"
#: cps/web.py:2235
msgid "Edit mail settings"
msgstr "编辑邮箱设置"
#: cps/web.py:1817
#: cps/web.py:2263
#, python-format
msgid "User '%(nick)s' deleted"
msgstr "用户 '%(nick)s' 已被删除"
#: cps/web.py:1898
#: cps/web.py:2349
#, python-format
msgid "User '%(nick)s' updated"
msgstr "用户 '%(nick)s' 已被更新"
#: cps/web.py:1901
#: cps/web.py:2352
msgid "An unknown error occured."
msgstr "发生未知错误。"
#: cps/web.py:1904
#: cps/web.py:2355
#, python-format
msgid "Edit User %(nick)s"
msgstr "编辑用户 %(nick)s"
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
msgid "edit metadata"
msgstr "编辑元数据"
#: cps/web.py:2145
#: cps/web.py:2598
#, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr "不能上传后缀为 \"%s\" 的文件到此服务器"
#: cps/web.py:2604
msgid "File to be uploaded must have an extension"
msgstr "要上传的文件必须有一个后缀"
#: cps/web.py:2621
#, python-format
msgid "Failed to create path %s (Permission denied)."
msgstr "创建路径 %s 失败(权限拒绝)。"
#: cps/web.py:2150
#: cps/web.py:2626
#, python-format
msgid "Failed to store file %s (Permission denied)."
msgstr "存储文件 %s 失败(权限拒绝)。"
#: cps/web.py:2155
#: cps/web.py:2631
#, python-format
msgid "Failed to delete file %s (Permission denied)."
msgstr "删除文件 %s 失败(权限拒绝)。"
@ -389,7 +402,7 @@ msgstr ""
msgid "Admin"
msgstr "管理"
#: cps/templates/admin.html:13 cps/templates/detail.html:117
#: cps/templates/admin.html:13 cps/templates/detail.html:134
msgid "Download"
msgstr "下载"
@ -445,7 +458,7 @@ msgstr "配置"
msgid "Calibre DB dir"
msgstr "Calibre DB目录"
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
msgid "Log Level"
msgstr "日志级别"
@ -453,7 +466,7 @@ msgstr "日志级别"
msgid "Port"
msgstr "端口"
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
msgid "Books per page"
msgstr "每页书籍数"
@ -482,42 +495,46 @@ msgid "Newest commit timestamp"
msgstr "最新提交时间戳"
#: cps/templates/admin.html:83
msgid "Reconnect to Calibre DB"
msgstr ""
#: cps/templates/admin.html:84
msgid "Restart Calibre-web"
msgstr "重启 Calibre-web"
#: cps/templates/admin.html:84
#: cps/templates/admin.html:85
msgid "Stop Calibre-web"
msgstr "停止 Calibre-web"
#: cps/templates/admin.html:85
#: cps/templates/admin.html:86
msgid "Check for update"
msgstr "检查更新"
#: cps/templates/admin.html:86
#: cps/templates/admin.html:87
msgid "Perform Update"
msgstr "执行更新"
#: cps/templates/admin.html:96
#: cps/templates/admin.html:97
msgid "Do you really want to restart Calibre-web?"
msgstr "您确定要重启 Calibre-web 吗?"
#: cps/templates/admin.html:101 cps/templates/admin.html:115
#: cps/templates/admin.html:136
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/admin.html:137
msgid "Ok"
msgstr "确定"
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
#: cps/templates/admin.html:103 cps/templates/admin.html:117
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
msgid "Back"
msgstr "后退"
#: cps/templates/admin.html:114
#: cps/templates/admin.html:115
msgid "Do you really want to stop Calibre-web?"
msgstr "您确定要关闭 Calibre-web 吗?"
#: cps/templates/admin.html:127
#: cps/templates/admin.html:128
msgid "Updating, please do not reload page"
msgstr "正在更新,请不要刷新页面"
@ -525,20 +542,21 @@ msgstr "正在更新,请不要刷新页面"
msgid "Book Title"
msgstr "书名"
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
#: cps/templates/search_form.html:10
msgid "Author"
msgstr "作者"
#: cps/templates/book_edit.html:24
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
msgid "Description"
msgstr "简介"
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
msgid "Tags"
msgstr "标签"
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
#: cps/templates/search_form.html:33
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
#: cps/templates/search_form.html:37
msgid "Series"
msgstr "丛书"
@ -570,69 +588,142 @@ msgstr ""
msgid "view book after edit"
msgstr "编辑后查看书籍"
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
#: cps/templates/login.html:19 cps/templates/search_form.html:75
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
msgid "Get metadata"
msgstr ""
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
#: cps/templates/login.html:19 cps/templates/search_form.html:79
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
msgid "Submit"
msgstr "提交"
#: cps/templates/book_edit.html:121
msgid "Keyword"
msgstr ""
#: cps/templates/book_edit.html:122
msgid " Search keyword "
msgstr ""
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
msgid "Go!"
msgstr "走起!"
#: cps/templates/book_edit.html:125
msgid "Click the cover to load metadata to the form"
msgstr ""
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
msgid "Loading..."
msgstr ""
#: cps/templates/book_edit.html:132
msgid "Close"
msgstr ""
#: cps/templates/book_edit.html:143
msgid "Search error!"
msgstr ""
#: cps/templates/book_edit.html:144
msgid "No Result! Please try anonther keyword."
msgstr ""
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
#: cps/templates/search_form.html:14
msgid "Publisher"
msgstr ""
#: cps/templates/book_edit.html:148
msgid "Source"
msgstr ""
#: cps/templates/config_edit.html:7
msgid "Location of Calibre database"
msgstr "Calibre 数据库位置"
#: cps/templates/config_edit.html:11
#: cps/templates/config_edit.html:13
msgid "Use google drive?"
msgstr ""
#: cps/templates/config_edit.html:17
msgid "Client id"
msgstr ""
#: cps/templates/config_edit.html:21
msgid "Client secret"
msgstr ""
#: cps/templates/config_edit.html:25
msgid "Calibre Base URL"
msgstr ""
#: cps/templates/config_edit.html:29
msgid "Google drive Calibre folder"
msgstr ""
#: cps/templates/config_edit.html:38
msgid "Metadata Watch Channel ID"
msgstr ""
#: cps/templates/config_edit.html:52
msgid "Server Port"
msgstr "服务器端口"
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr "标题"
#: cps/templates/config_edit.html:23
#: cps/templates/config_edit.html:64
msgid "No. of random books to show"
msgstr "随机书籍显示数量"
#: cps/templates/config_edit.html:28
#: cps/templates/config_edit.html:68
msgid "Regular expression for ignoring columns"
msgstr ""
#: cps/templates/config_edit.html:72
msgid "Regular expression for title sorting"
msgstr "标题排序的正则表达式"
#: cps/templates/config_edit.html:42
#: cps/templates/config_edit.html:86
msgid "Enable uploading"
msgstr "启用上传"
#: cps/templates/config_edit.html:46
#: cps/templates/config_edit.html:90
msgid "Enable anonymous browsing"
msgstr "启用匿名浏览"
#: cps/templates/config_edit.html:50
#: cps/templates/config_edit.html:94
msgid "Enable public registration"
msgstr "启用注册"
#: cps/templates/config_edit.html:52
#: cps/templates/config_edit.html:96
msgid "Default Settings for new users"
msgstr "新用户默认设置"
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
msgid "Admin user"
msgstr "管理用户"
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
msgid "Allow Downloads"
msgstr "允许下载"
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
msgid "Allow Uploads"
msgstr "允许上传"
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
msgid "Allow Edit"
msgstr "允许编辑"
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
msgid "Allow Changing Password"
msgstr "允许修改密码"
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
#: cps/templates/login.html:4
msgid "Login"
msgstr "登录"
@ -649,23 +740,27 @@ msgstr ""
msgid "language"
msgstr "语言"
#: cps/templates/detail.html:74
#: cps/templates/detail.html:81
msgid "Publishing date"
msgstr "出版日期"
#: cps/templates/detail.html:106
#: cps/templates/detail.html:115
msgid "Read"
msgstr ""
#: cps/templates/detail.html:123
msgid "Description:"
msgstr "简介:"
#: cps/templates/detail.html:134
#: cps/templates/detail.html:151
msgid "Read in browser"
msgstr "在浏览器中阅读"
#: cps/templates/detail.html:154
#: cps/templates/detail.html:171
msgid "Add to shelf"
msgstr "添加到书架"
#: cps/templates/detail.html:194
#: cps/templates/detail.html:211
msgid "Edit metadata"
msgstr "编辑元数据"
@ -745,19 +840,29 @@ msgstr "最新书籍"
msgid "Show Random Books"
msgstr "显示随机书籍"
#: cps/templates/index.xml:43 cps/templates/layout.html:140
#: cps/templates/index.xml:43 cps/templates/index.xml:47
#: cps/templates/layout.html:132
msgid "Read Books"
msgstr ""
#: cps/templates/index.xml:50 cps/templates/index.xml:54
#: cps/templates/layout.html:133
msgid "Unread Books"
msgstr ""
#: cps/templates/index.xml:57 cps/templates/layout.html:144
msgid "Authors"
msgstr "作者"
#: cps/templates/index.xml:47
#: cps/templates/index.xml:61
msgid "Books ordered by Author"
msgstr "书籍按作者排序"
#: cps/templates/index.xml:54
#: cps/templates/index.xml:68
msgid "Books ordered by category"
msgstr "书籍按分类排序"
#: cps/templates/index.xml:61
#: cps/templates/index.xml:75
msgid "Books ordered by series"
msgstr "书籍按丛书排序"
@ -765,10 +870,6 @@ msgstr "书籍按丛书排序"
msgid "Toggle navigation"
msgstr "切换导航"
#: cps/templates/layout.html:60
msgid "Go!"
msgstr "走起!"
#: cps/templates/layout.html:68
msgid "Advanced Search"
msgstr "高级搜索"
@ -785,31 +886,31 @@ msgstr "注册"
msgid "Browse"
msgstr "浏览"
#: cps/templates/layout.html:132
#: cps/templates/layout.html:136
msgid "Discover"
msgstr "发现"
#: cps/templates/layout.html:135
#: cps/templates/layout.html:139
msgid "Categories"
msgstr "分类"
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
msgid "Languages"
msgstr "语言"
#: cps/templates/layout.html:145
#: cps/templates/layout.html:149
msgid "Public Shelves"
msgstr "公开书架"
#: cps/templates/layout.html:149
#: cps/templates/layout.html:153
msgid "Your Shelves"
msgstr "您的书架"
#: cps/templates/layout.html:154
#: cps/templates/layout.html:158
msgid "Create a Shelf"
msgstr "创建书架"
#: cps/templates/layout.html:155
#: cps/templates/layout.html:159
msgid "About"
msgstr "关于"
@ -875,15 +976,15 @@ msgstr "请尝试别的关键字"
msgid "Results for:"
msgstr "结果:"
#: cps/templates/search_form.html:23
#: cps/templates/search_form.html:27
msgid "Exclude Tags"
msgstr "排除标签"
#: cps/templates/search_form.html:43
#: cps/templates/search_form.html:47
msgid "Exclude Series"
msgstr "排除丛书"
#: cps/templates/search_form.html:64
#: cps/templates/search_form.html:68
msgid "Exclude Languages"
msgstr "排除语言"
@ -908,37 +1009,37 @@ msgid "Drag 'n drop to rearrange order"
msgstr "拖拽以重新排序"
#: cps/templates/stats.html:3
msgid "Linked libraries"
msgstr "链接库"
#: cps/templates/stats.html:8
msgid "Program library"
msgstr "程序库"
#: cps/templates/stats.html:9
msgid "Installed Version"
msgstr "已安装版本"
#: cps/templates/stats.html:32
msgid "Calibre library statistics"
msgstr "Calibre书库统计"
#: cps/templates/stats.html:37
#: cps/templates/stats.html:8
msgid "Books in this Library"
msgstr "本书在此书库"
#: cps/templates/stats.html:41
#: cps/templates/stats.html:12
msgid "Authors in this Library"
msgstr "个作者在此书库"
#: cps/templates/stats.html:45
#: cps/templates/stats.html:16
msgid "Categories in this Library"
msgstr "个分类在此书库"
#: cps/templates/stats.html:49
#: cps/templates/stats.html:20
msgid "Series in this Library"
msgstr "个丛书在此书库"
#: cps/templates/stats.html:24
msgid "Linked libraries"
msgstr "链接库"
#: cps/templates/stats.html:28
msgid "Program library"
msgstr "程序库"
#: cps/templates/stats.html:29
msgid "Installed Version"
msgstr "已安装版本"
#: cps/templates/user_edit.html:23
msgid "Kindle E-Mail"
msgstr ""
@ -951,43 +1052,47 @@ msgstr "按语言显示书籍"
msgid "Show all"
msgstr "显示全部"
#: cps/templates/user_edit.html:45
#: cps/templates/user_edit.html:47
msgid "Show random books"
msgstr "显示随机书籍"
#: cps/templates/user_edit.html:49
#: cps/templates/user_edit.html:51
msgid "Show hot books"
msgstr "显示热门书籍"
#: cps/templates/user_edit.html:53
#: cps/templates/user_edit.html:55
msgid "Show best rated books"
msgstr "显示最高评分书籍"
#: cps/templates/user_edit.html:57
#: cps/templates/user_edit.html:59
msgid "Show language selection"
msgstr "显示语言选择"
#: cps/templates/user_edit.html:61
#: cps/templates/user_edit.html:63
msgid "Show series selection"
msgstr "显示丛书选择"
#: cps/templates/user_edit.html:65
#: cps/templates/user_edit.html:67
msgid "Show category selection"
msgstr "显示分类选择"
#: cps/templates/user_edit.html:69
#: cps/templates/user_edit.html:71
msgid "Show author selection"
msgstr "显示作者选择"
#: cps/templates/user_edit.html:73
#: cps/templates/user_edit.html:75
msgid "Show read and unread"
msgstr ""
#: cps/templates/user_edit.html:79
msgid "Show random books in detail view"
msgstr "在详情页显示随机书籍"
#: cps/templates/user_edit.html:105
#: cps/templates/user_edit.html:112
msgid "Delete this user"
msgstr "删除此用户"
#: cps/templates/user_edit.html:116
#: cps/templates/user_edit.html:127
msgid "Recent Downloads"
msgstr "最近下载"

View File

@ -10,6 +10,8 @@ import os
import logging
from werkzeug.security import generate_password_hash
from flask_babel import gettext as _
import json
#from builtins import str
dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep), "app.db")
engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False)
@ -22,6 +24,7 @@ ROLE_UPLOAD = 4
ROLE_EDIT = 8
ROLE_PASSWD = 16
ROLE_ANONYMOUS = 32
ROLE_EDIT_SHELFS = 64
DETAIL_RANDOM = 1
SIDEBAR_LANGUAGE = 2
@ -31,6 +34,7 @@ SIDEBAR_HOT = 16
SIDEBAR_RANDOM = 32
SIDEBAR_AUTHOR = 64
SIDEBAR_BEST_RATED = 128
SIDEBAR_READ_AND_UNREAD = 256
DEFAULT_PASS = "admin123"
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
@ -83,6 +87,12 @@ class UserBase:
else:
return False
def role_edit_shelfs(self):
if self.role is not None:
return True if self.role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False
else:
return False
def is_active(self):
return True
@ -90,7 +100,7 @@ class UserBase:
return False
def get_id(self):
return unicode(self.id)
return str(self.id)
def filter_language(self):
return self.default_language
@ -137,6 +147,12 @@ class UserBase:
else:
return False
def show_read_and_unread(self):
if self.sidebar_view is not None:
return True if self.sidebar_view & SIDEBAR_READ_AND_UNREAD == SIDEBAR_READ_AND_UNREAD else False
else:
return False
def show_detail_random(self):
if self.sidebar_view is not None:
return True if self.sidebar_view & DETAIL_RANDOM == DETAIL_RANDOM else False
@ -178,7 +194,6 @@ class Anonymous(AnonymousUserMixin, UserBase):
self.role = data.role
self.sidebar_view = data.sidebar_view
self.default_language = data.default_language
self.default_language = data.default_language
self.locale = data.locale
self.anon_browse = settings.config_anonbrowse
@ -217,6 +232,14 @@ class BookShelf(Base):
def __repr__(self):
return '<Book %r>' % self.id
class ReadBook(Base):
__tablename__ = 'book_read_link'
id=Column(Integer, primary_key=True)
book_id = Column(Integer, unique=False)
user_id =Column(Integer, ForeignKey('user.id'), unique=False)
is_read = Column(Boolean, unique=False)
# Baseclass representing Downloads from calibre-web in app.db
class Downloads(Base):
@ -253,6 +276,14 @@ class Settings(Base):
config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0)
config_default_role = Column(SmallInteger, default=0)
config_columns_to_ignore = Column(String)
config_use_google_drive = Column(Boolean)
config_google_drive_client_id = Column(String)
config_google_drive_client_secret = Column(String)
config_google_drive_folder = Column(String)
config_google_drive_calibre_url_base = Column(String)
config_google_drive_watch_changes_response = Column(String)
config_columns_to_ignore = Column(String)
def __repr__(self):
pass
@ -279,7 +310,18 @@ class Config:
self.config_anonbrowse = data.config_anonbrowse
self.config_public_reg = data.config_public_reg
self.config_default_role = data.config_default_role
if self.config_calibre_dir is not None:
self.config_columns_to_ignore = data.config_columns_to_ignore
self.config_use_google_drive = data.config_use_google_drive
self.config_google_drive_client_id = data.config_google_drive_client_id
self.config_google_drive_client_secret = data.config_google_drive_client_secret
self.config_google_drive_calibre_url_base = data.config_google_drive_calibre_url_base
self.config_google_drive_folder = data.config_google_drive_folder
if data.config_google_drive_watch_changes_response:
self.config_google_drive_watch_changes_response = json.loads(data.config_google_drive_watch_changes_response)
else:
self.config_google_drive_watch_changes_response=None
self.config_columns_to_ignore = data.config_columns_to_ignore
if self.config_calibre_dir is not None and (not self.config_use_google_drive or os.path.exists(self.config_calibre_dir + '/metadata.db')):
self.db_configured = True
else:
self.db_configured = False
@ -318,6 +360,12 @@ class Config:
else:
return False
def role_edit_shelfs(self):
if self.config_default_role is not None:
return True if self.config_default_role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False
else:
return False
def get_Log_Level(self):
ret_value=""
if self.config_log_level == logging.INFO:
@ -335,6 +383,9 @@ 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)
try:
session.query(exists().where(User.locale)).scalar()
session.commit()
@ -360,6 +411,23 @@ def migrate_Database():
conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0")
session.commit()
try:
session.query(exists().where(Settings.config_use_google_drive)).scalar()
except exc.OperationalError:
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_client_id` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_client_secret` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_calibre_url_base` INTEGER DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
try:
session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
except exc.OperationalError:
conn = engine.connect()
conn.execute("ALTER TABLE Settings ADD column `config_columns_to_ignore` String DEFAULT ''")
session.commit()
try:
session.query(exists().where(Settings.config_default_role)).scalar()
session.commit()
@ -438,7 +506,7 @@ def create_anonymous_user():
session.add(user)
try:
session.commit()
except:
except Exception as e:
session.rollback()
pass
@ -449,14 +517,15 @@ def create_admin_user():
user.nickname = "admin"
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_PASSWD
user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD
user.password = generate_password_hash(DEFAULT_PASS)
session.add(user)
try:
session.commit()
except:
except Exception as e:
session.rollback()
pass

View File

@ -7,7 +7,7 @@ import hashlib
from collections import namedtuple
import book_formats
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id')
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages')
"""
:rtype: BookMeta

File diff suppressed because it is too large Load Diff

14
gdrive_template.yaml Normal file
View File

@ -0,0 +1,14 @@
client_config_backend: settings
client_config:
client_id: %(client_id)s
client_secret: %(client_secret)s
redirect_uri: %(redirect_uri)s
save_credentials: True
save_credentials_backend: file
save_credentials_file: gdrive_credentials
get_refresh_token: True
oauth_scope:
- https://www.googleapis.com/auth/drive

3
getVendor.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash -e
pip install --target ./vendor -r requirements.txt

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,341 +17,354 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
msgid "not installed"
msgstr ""
#: cps/helper.py:150
#: cps/helper.py:164
#, python-format
msgid "Failed to send mail: %s"
msgstr ""
#: cps/helper.py:157
#: cps/helper.py:171
msgid "Calibre-web test email"
msgstr ""
#: cps/helper.py:158 cps/helper.py:168
#: cps/helper.py:172 cps/helper.py:184
msgid "This email has been sent via calibre web."
msgstr ""
#: cps/helper.py:167 cps/templates/detail.html:130
#: cps/helper.py:181 cps/templates/detail.html:146
msgid "Send to Kindle"
msgstr ""
#: cps/helper.py:185 cps/helper.py:200
#: cps/helper.py:201 cps/helper.py:216
msgid "Could not find any formats suitable for sending by email"
msgstr ""
#: cps/helper.py:194
#: cps/helper.py:210
msgid "Could not convert epub to mobi"
msgstr ""
#: cps/ub.py:434
#: cps/ub.py:488
msgid "Guest"
msgstr ""
#: cps/web.py:734
#: cps/web.py:904
msgid "Requesting update package"
msgstr ""
#: cps/web.py:735
#: cps/web.py:905
msgid "Downloading update package"
msgstr ""
#: cps/web.py:736
#: cps/web.py:906
msgid "Unzipping update package"
msgstr ""
#: cps/web.py:737
#: cps/web.py:907
msgid "Files are replaced"
msgstr ""
#: cps/web.py:738
#: cps/web.py:908
msgid "Database connections are closed"
msgstr ""
#: cps/web.py:739
#: cps/web.py:909
msgid "Server is stopped"
msgstr ""
#: cps/web.py:740
#: cps/web.py:910
msgid "Update finished, please press okay and reload page"
msgstr ""
#: cps/web.py:810
#: cps/web.py:983
msgid "Latest Books"
msgstr ""
#: cps/web.py:835
#: cps/web.py:1014
msgid "Hot Books (most downloaded)"
msgstr ""
#: cps/web.py:845
#: cps/web.py:1024
msgid "Best rated books"
msgstr ""
#: cps/templates/index.xml:36 cps/web.py:854
#: cps/templates/index.xml:36 cps/web.py:1033
msgid "Random Books"
msgstr ""
#: cps/web.py:867
#: cps/web.py:1046
msgid "Author list"
msgstr ""
#: cps/web.py:878
#: cps/web.py:1057
#, python-format
msgid "Author: %(name)s"
msgstr ""
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
#: cps/web.py:2115
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
#: cps/web.py:2579
msgid "Error opening eBook. File does not exist or file is not accessible:"
msgstr ""
#: cps/templates/index.xml:57 cps/web.py:894
#: cps/templates/index.xml:71 cps/web.py:1073
msgid "Series list"
msgstr ""
#: cps/web.py:906
#: cps/web.py:1085
#, python-format
msgid "Series: %(serie)s"
msgstr ""
#: cps/web.py:939
#: cps/web.py:1118
msgid "Available languages"
msgstr ""
#: cps/web.py:954
#: cps/web.py:1133
#, python-format
msgid "Language: %(name)s"
msgstr ""
#: cps/templates/index.xml:50 cps/web.py:967
#: cps/templates/index.xml:64 cps/web.py:1146
msgid "Category list"
msgstr ""
#: cps/web.py:979
#: cps/web.py:1158
#, python-format
msgid "Category: %(name)s"
msgstr ""
#: cps/web.py:1040
#: cps/web.py:1267
msgid "Statistics"
msgstr ""
#: cps/web.py:1061
#: cps/web.py:1375
msgid "Server restarted, please reload page"
msgstr ""
#: cps/web.py:1063
#: cps/web.py:1377
msgid "Performing shutdown of server, please close window"
msgstr ""
#: cps/web.py:1073
#: cps/web.py:1392
msgid "Update done"
msgstr ""
#: cps/web.py:1147 cps/web.py:1160
#: cps/web.py:1470 cps/web.py:1483
msgid "search"
msgstr ""
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
msgid "Read a Book"
msgstr ""
#: cps/web.py:1276 cps/web.py:1713
#: cps/web.py:1676 cps/web.py:2152
msgid "Please fill out all fields!"
msgstr ""
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
msgid "register"
msgstr ""
#: cps/web.py:1292
#: cps/web.py:1692
msgid "An unknown error occured. Please try again later."
msgstr ""
#: cps/web.py:1297
#: cps/web.py:1697
msgid "This username or email address is already in use."
msgstr ""
#: cps/web.py:1315
#: cps/web.py:1715
#, python-format
msgid "you are now logged in as: '%(nickname)s'"
msgstr ""
#: cps/web.py:1320
#: cps/web.py:1720
msgid "Wrong Username or Password"
msgstr ""
#: cps/web.py:1322
#: cps/web.py:1722
msgid "login"
msgstr ""
#: cps/web.py:1339
#: cps/web.py:1739
msgid "Please configure the SMTP mail settings first..."
msgstr ""
#: cps/web.py:1343
#: cps/web.py:1743
#, python-format
msgid "Book successfully send to %(kindlemail)s"
msgstr ""
#: cps/web.py:1347
#: cps/web.py:1747
#, python-format
msgid "There was an error sending this book: %(res)s"
msgstr ""
#: cps/web.py:1349
#: cps/web.py:1749 cps/web.py:2232
msgid "Please configure your kindle email address first..."
msgstr ""
#: cps/web.py:1369
#: cps/web.py:1774
#, python-format
msgid "Book has been added to shelf: %(sname)s"
msgstr ""
#: cps/web.py:1390
#: cps/web.py:1793
#, python-format
msgid "Book has been removed from shelf: %(sname)s"
msgstr ""
#: cps/web.py:1409 cps/web.py:1433
#: cps/web.py:1812 cps/web.py:1836
#, python-format
msgid "A shelf with the name '%(title)s' already exists."
msgstr ""
#: cps/web.py:1414
#: cps/web.py:1817
#, python-format
msgid "Shelf %(title)s created"
msgstr ""
#: cps/web.py:1416 cps/web.py:1444
#: cps/web.py:1819 cps/web.py:1847
msgid "There was an error"
msgstr ""
#: cps/web.py:1417 cps/web.py:1419
#: cps/web.py:1820 cps/web.py:1822
msgid "create a shelf"
msgstr ""
#: cps/web.py:1442
#: cps/web.py:1845
#, python-format
msgid "Shelf %(title)s changed"
msgstr ""
#: cps/web.py:1445 cps/web.py:1447
#: cps/web.py:1848 cps/web.py:1850
msgid "Edit a shelf"
msgstr ""
#: cps/web.py:1465
#: cps/web.py:1868
#, python-format
msgid "successfully deleted shelf %(name)s"
msgstr ""
#: cps/web.py:1487
#: cps/web.py:1890
#, python-format
msgid "Shelf: '%(name)s'"
msgstr ""
#: cps/web.py:1518
#: cps/web.py:1921
#, python-format
msgid "Change order of Shelf: '%(name)s'"
msgstr ""
#: cps/web.py:1580
#: cps/web.py:1985
msgid "Found an existing account for this email address."
msgstr ""
#: cps/web.py:1582 cps/web.py:1586
#: cps/web.py:1987 cps/web.py:1991
#, python-format
msgid "%(name)s's profile"
msgstr ""
#: cps/web.py:1583
#: cps/web.py:1988
msgid "Profile updated"
msgstr ""
#: cps/web.py:1597
#: cps/web.py:2002
msgid "Admin page"
msgstr ""
#: cps/web.py:1668
#: cps/web.py:2106
msgid "Calibre-web configuration updated"
msgstr ""
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
msgid "Basic Configuration"
msgstr ""
#: cps/web.py:1679
#: cps/web.py:2117
msgid "DB location is not valid, please enter correct path"
msgstr ""
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
msgid "Add new user"
msgstr ""
#: cps/web.py:1753
#: cps/web.py:2194
#, python-format
msgid "User '%(user)s' created"
msgstr ""
#: cps/web.py:1757
#: cps/web.py:2198
msgid "Found an existing account for this email address or nickname."
msgstr ""
#: cps/web.py:1779
#: cps/web.py:2220
msgid "Mail settings updated"
msgstr ""
#: cps/web.py:1785
#: cps/web.py:2227
#, python-format
msgid "Test E-Mail successfully send to %(kindlemail)s"
msgstr ""
#: cps/web.py:1788
#: cps/web.py:2230
#, python-format
msgid "There was an error sending the Test E-Mail: %(res)s"
msgstr ""
#: cps/web.py:1789
#: cps/web.py:2234
msgid "E-Mail settings updated"
msgstr ""
#: cps/web.py:2235
msgid "Edit mail settings"
msgstr ""
#: cps/web.py:1817
#: cps/web.py:2263
#, python-format
msgid "User '%(nick)s' deleted"
msgstr ""
#: cps/web.py:1898
#: cps/web.py:2349
#, python-format
msgid "User '%(nick)s' updated"
msgstr ""
#: cps/web.py:1901
#: cps/web.py:2352
msgid "An unknown error occured."
msgstr ""
#: cps/web.py:1904
#: cps/web.py:2355
#, python-format
msgid "Edit User %(nick)s"
msgstr ""
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
msgid "edit metadata"
msgstr ""
#: cps/web.py:2145
#: cps/web.py:2598
#, python-format
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
msgstr ""
#: cps/web.py:2604
msgid "File to be uploaded must have an extension"
msgstr ""
#: cps/web.py:2621
#, python-format
msgid "Failed to create path %s (Permission denied)."
msgstr ""
#: cps/web.py:2150
#: cps/web.py:2626
#, python-format
msgid "Failed to store file %s (Permission denied)."
msgstr ""
#: cps/web.py:2155
#: cps/web.py:2631
#, python-format
msgid "Failed to delete file %s (Permission denied)."
msgstr ""
@ -380,7 +393,7 @@ msgstr ""
msgid "Admin"
msgstr ""
#: cps/templates/admin.html:13 cps/templates/detail.html:117
#: cps/templates/admin.html:13 cps/templates/detail.html:134
msgid "Download"
msgstr ""
@ -436,7 +449,7 @@ msgstr ""
msgid "Calibre DB dir"
msgstr ""
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
msgid "Log Level"
msgstr ""
@ -444,7 +457,7 @@ msgstr ""
msgid "Port"
msgstr ""
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
msgid "Books per page"
msgstr ""
@ -473,42 +486,46 @@ msgid "Newest commit timestamp"
msgstr ""
#: cps/templates/admin.html:83
msgid "Restart Calibre-web"
msgid "Reconnect to Calibre DB"
msgstr ""
#: cps/templates/admin.html:84
msgid "Stop Calibre-web"
msgid "Restart Calibre-web"
msgstr ""
#: cps/templates/admin.html:85
msgid "Check for update"
msgid "Stop Calibre-web"
msgstr ""
#: cps/templates/admin.html:86
msgid "Check for update"
msgstr ""
#: cps/templates/admin.html:87
msgid "Perform Update"
msgstr ""
#: cps/templates/admin.html:96
#: cps/templates/admin.html:97
msgid "Do you really want to restart Calibre-web?"
msgstr ""
#: cps/templates/admin.html:101 cps/templates/admin.html:115
#: cps/templates/admin.html:136
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/admin.html:137
msgid "Ok"
msgstr ""
#: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
#: cps/templates/admin.html:103 cps/templates/admin.html:117
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
msgid "Back"
msgstr ""
#: cps/templates/admin.html:114
#: cps/templates/admin.html:115
msgid "Do you really want to stop Calibre-web?"
msgstr ""
#: cps/templates/admin.html:127
#: cps/templates/admin.html:128
msgid "Updating, please do not reload page"
msgstr ""
@ -516,20 +533,21 @@ msgstr ""
msgid "Book Title"
msgstr ""
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
#: cps/templates/search_form.html:10
msgid "Author"
msgstr ""
#: cps/templates/book_edit.html:24
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
msgid "Description"
msgstr ""
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
msgid "Tags"
msgstr ""
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
#: cps/templates/search_form.html:33
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
#: cps/templates/search_form.html:37
msgid "Series"
msgstr ""
@ -561,69 +579,142 @@ msgstr ""
msgid "view book after edit"
msgstr ""
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
#: cps/templates/login.html:19 cps/templates/search_form.html:75
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
msgid "Get metadata"
msgstr ""
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
#: cps/templates/login.html:19 cps/templates/search_form.html:79
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
msgid "Submit"
msgstr ""
#: cps/templates/book_edit.html:121
msgid "Keyword"
msgstr ""
#: cps/templates/book_edit.html:122
msgid " Search keyword "
msgstr ""
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
msgid "Go!"
msgstr ""
#: cps/templates/book_edit.html:125
msgid "Click the cover to load metadata to the form"
msgstr ""
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
msgid "Loading..."
msgstr ""
#: cps/templates/book_edit.html:132
msgid "Close"
msgstr ""
#: cps/templates/book_edit.html:143
msgid "Search error!"
msgstr ""
#: cps/templates/book_edit.html:144
msgid "No Result! Please try anonther keyword."
msgstr ""
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
#: cps/templates/search_form.html:14
msgid "Publisher"
msgstr ""
#: cps/templates/book_edit.html:148
msgid "Source"
msgstr ""
#: cps/templates/config_edit.html:7
msgid "Location of Calibre database"
msgstr ""
#: cps/templates/config_edit.html:11
msgid "Server Port"
#: cps/templates/config_edit.html:13
msgid "Use google drive?"
msgstr ""
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
msgid "Title"
#: cps/templates/config_edit.html:17
msgid "Client id"
msgstr ""
#: cps/templates/config_edit.html:23
msgid "No. of random books to show"
#: cps/templates/config_edit.html:21
msgid "Client secret"
msgstr ""
#: cps/templates/config_edit.html:28
msgid "Regular expression for title sorting"
#: cps/templates/config_edit.html:25
msgid "Calibre Base URL"
msgstr ""
#: cps/templates/config_edit.html:42
msgid "Enable uploading"
#: cps/templates/config_edit.html:29
msgid "Google drive Calibre folder"
msgstr ""
#: cps/templates/config_edit.html:46
msgid "Enable anonymous browsing"
msgstr ""
#: cps/templates/config_edit.html:50
msgid "Enable public registration"
#: cps/templates/config_edit.html:38
msgid "Metadata Watch Channel ID"
msgstr ""
#: cps/templates/config_edit.html:52
msgid "Server Port"
msgstr ""
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
msgid "Title"
msgstr ""
#: cps/templates/config_edit.html:64
msgid "No. of random books to show"
msgstr ""
#: cps/templates/config_edit.html:68
msgid "Regular expression for ignoring columns"
msgstr ""
#: cps/templates/config_edit.html:72
msgid "Regular expression for title sorting"
msgstr ""
#: cps/templates/config_edit.html:86
msgid "Enable uploading"
msgstr ""
#: cps/templates/config_edit.html:90
msgid "Enable anonymous browsing"
msgstr ""
#: cps/templates/config_edit.html:94
msgid "Enable public registration"
msgstr ""
#: cps/templates/config_edit.html:96
msgid "Default Settings for new users"
msgstr ""
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
msgid "Admin user"
msgstr ""
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
msgid "Allow Downloads"
msgstr ""
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
msgid "Allow Uploads"
msgstr ""
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
msgid "Allow Edit"
msgstr ""
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
msgid "Allow Changing Password"
msgstr ""
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
#: cps/templates/login.html:4
msgid "Login"
msgstr ""
@ -640,23 +731,27 @@ msgstr ""
msgid "language"
msgstr ""
#: cps/templates/detail.html:74
#: cps/templates/detail.html:81
msgid "Publishing date"
msgstr ""
#: cps/templates/detail.html:106
#: cps/templates/detail.html:115
msgid "Read"
msgstr ""
#: cps/templates/detail.html:123
msgid "Description:"
msgstr ""
#: cps/templates/detail.html:134
#: cps/templates/detail.html:151
msgid "Read in browser"
msgstr ""
#: cps/templates/detail.html:154
#: cps/templates/detail.html:171
msgid "Add to shelf"
msgstr ""
#: cps/templates/detail.html:194
#: cps/templates/detail.html:211
msgid "Edit metadata"
msgstr ""
@ -736,19 +831,29 @@ msgstr ""
msgid "Show Random Books"
msgstr ""
#: cps/templates/index.xml:43 cps/templates/layout.html:140
#: cps/templates/index.xml:43 cps/templates/index.xml:47
#: cps/templates/layout.html:132
msgid "Read Books"
msgstr ""
#: cps/templates/index.xml:50 cps/templates/index.xml:54
#: cps/templates/layout.html:133
msgid "Unread Books"
msgstr ""
#: cps/templates/index.xml:57 cps/templates/layout.html:144
msgid "Authors"
msgstr ""
#: cps/templates/index.xml:47
#: cps/templates/index.xml:61
msgid "Books ordered by Author"
msgstr ""
#: cps/templates/index.xml:54
#: cps/templates/index.xml:68
msgid "Books ordered by category"
msgstr ""
#: cps/templates/index.xml:61
#: cps/templates/index.xml:75
msgid "Books ordered by series"
msgstr ""
@ -756,10 +861,6 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
#: cps/templates/layout.html:60
msgid "Go!"
msgstr ""
#: cps/templates/layout.html:68
msgid "Advanced Search"
msgstr ""
@ -776,31 +877,31 @@ msgstr ""
msgid "Browse"
msgstr ""
#: cps/templates/layout.html:132
#: cps/templates/layout.html:136
msgid "Discover"
msgstr ""
#: cps/templates/layout.html:135
#: cps/templates/layout.html:139
msgid "Categories"
msgstr ""
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
msgid "Languages"
msgstr ""
#: cps/templates/layout.html:145
#: cps/templates/layout.html:149
msgid "Public Shelves"
msgstr ""
#: cps/templates/layout.html:149
#: cps/templates/layout.html:153
msgid "Your Shelves"
msgstr ""
#: cps/templates/layout.html:154
#: cps/templates/layout.html:158
msgid "Create a Shelf"
msgstr ""
#: cps/templates/layout.html:155
#: cps/templates/layout.html:159
msgid "About"
msgstr ""
@ -866,15 +967,15 @@ msgstr ""
msgid "Results for:"
msgstr ""
#: cps/templates/search_form.html:23
#: cps/templates/search_form.html:27
msgid "Exclude Tags"
msgstr ""
#: cps/templates/search_form.html:43
#: cps/templates/search_form.html:47
msgid "Exclude Series"
msgstr ""
#: cps/templates/search_form.html:64
#: cps/templates/search_form.html:68
msgid "Exclude Languages"
msgstr ""
@ -899,37 +1000,37 @@ msgid "Drag 'n drop to rearrange order"
msgstr ""
#: cps/templates/stats.html:3
msgid "Linked libraries"
msgstr ""
#: cps/templates/stats.html:8
msgid "Program library"
msgstr ""
#: cps/templates/stats.html:9
msgid "Installed Version"
msgstr ""
#: cps/templates/stats.html:32
msgid "Calibre library statistics"
msgstr ""
#: cps/templates/stats.html:37
#: cps/templates/stats.html:8
msgid "Books in this Library"
msgstr ""
#: cps/templates/stats.html:41
#: cps/templates/stats.html:12
msgid "Authors in this Library"
msgstr ""
#: cps/templates/stats.html:45
#: cps/templates/stats.html:16
msgid "Categories in this Library"
msgstr ""
#: cps/templates/stats.html:49
#: cps/templates/stats.html:20
msgid "Series in this Library"
msgstr ""
#: cps/templates/stats.html:24
msgid "Linked libraries"
msgstr ""
#: cps/templates/stats.html:28
msgid "Program library"
msgstr ""
#: cps/templates/stats.html:29
msgid "Installed Version"
msgstr ""
#: cps/templates/user_edit.html:23
msgid "Kindle E-Mail"
msgstr ""
@ -942,43 +1043,47 @@ msgstr ""
msgid "Show all"
msgstr ""
#: cps/templates/user_edit.html:45
#: cps/templates/user_edit.html:47
msgid "Show random books"
msgstr ""
#: cps/templates/user_edit.html:49
#: cps/templates/user_edit.html:51
msgid "Show hot books"
msgstr ""
#: cps/templates/user_edit.html:53
#: cps/templates/user_edit.html:55
msgid "Show best rated books"
msgstr ""
#: cps/templates/user_edit.html:57
#: cps/templates/user_edit.html:59
msgid "Show language selection"
msgstr ""
#: cps/templates/user_edit.html:61
#: cps/templates/user_edit.html:63
msgid "Show series selection"
msgstr ""
#: cps/templates/user_edit.html:65
#: cps/templates/user_edit.html:67
msgid "Show category selection"
msgstr ""
#: cps/templates/user_edit.html:69
#: cps/templates/user_edit.html:71
msgid "Show author selection"
msgstr ""
#: cps/templates/user_edit.html:73
#: cps/templates/user_edit.html:75
msgid "Show read and unread"
msgstr ""
#: cps/templates/user_edit.html:79
msgid "Show random books in detail view"
msgstr ""
#: cps/templates/user_edit.html:105
#: cps/templates/user_edit.html:112
msgid "Delete this user"
msgstr ""
#: cps/templates/user_edit.html:116
#: cps/templates/user_edit.html:127
msgid "Recent Downloads"
msgstr ""

13
optional-requirements.txt Normal file
View File

@ -0,0 +1,13 @@
gevent==1.2.1
google-api-python-client==1.6.1
greenlet==0.4.12
httplib2==0.9.2
lxml==3.7.2
oauth2client==4.0.0
pyasn1-modules==0.0.8
pyasn1==0.1.9
PyDrive==1.3.1
PyYAML==3.12
rsa==3.4.2
six==1.10.0
uritemplate==3.0.0

View File

@ -1,4 +1,4 @@
##About
# About
Calibre Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
@ -6,7 +6,8 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png)
##Features
## Features
- Bootstrap 3 HTML5 interface
- full graphical setup
- User management
@ -28,10 +29,11 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
## Quick start
1. Execute the command: `python cps.py`
2. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
3. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
4. Go to Login page
1. Install required dependencies by executing `pip install -r requirements.txt`
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
5. Go to Login page
**Default admin login:**
*Username:* admin
@ -61,6 +63,40 @@ Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the sen
[Download](http://www.amazon.com/gp/feature.html?docId=1000765211) Amazon's KindleGen tool for your platform and place the binary named as `kindlegen` in the `vendor` folder.
## Using Google Drive integration
Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install -r optional-requirements.txt`
To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com
Once a project has been created, we need to create a client ID and a client secret that will be used to enable the OAuth request with google, and enable the Drive API. To do this, follow the steps below: -
1. Open project in developer console
2. Click Enable API, and enable google drive
3. Now on the sidebar, click Credentials
4. Click Create Credentials and OAuth Client ID
5. Select Web Application and then next
6. Give the Credentials a name and enter your callback, which will be CALIBRE_WEB_URL/gdrive/callback
7. Finally click save
The Drive API should now be setup and ready to use, so we need to integrate it into Calibre Web. This is done as below: -
1. Open config page
2. Enter the location that will be used to store the metadata.db file, and to temporary store uploaded books and other temporary files for upload
2. Tick Use Google Drive
3. Enter Client Secret and Client Key as provided via previous steps
4. Enter the folder that is the root of your calibre library
5. Enter base URL for calibre (used for google callbacks)
6 Now select Authenticate Google Drive
7. This should redirect you to google to allow it top use your Drive, and then redirect you back to the config page
8. Google Drive should now be connected and be used to get images and download Epubs. The metadata.db is stored in the calibre library location
### Optional
If your calibre web is using https, it is possible to add a "watch" to the drive. This will inform us if the metadata.db file is updated and allow us to update our calibre library accordingly.
9. Click enable watch of metadata.db
9. Note that this expires after a week, so will need to be manually refresh
## Docker image
Calibre Web can be run as Docker container. The latest image is available on [Docker Hub](https://registry.hub.docker.com/u/janeczku/calibre-web/).

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
Babel>=1.3
Flask-Babel==0.11.1
Flask-Login>=0.3.2
Flask-Principal>=0.3.2
Flask>=0.11
iso-639>=0.4.5
PyPDF2==1.26.0
pytz>=2016.10
requests>=2.11.1
SQLAlchemy>=0.8.4
tornado>=4.1
Wand>=0.4.4

View File

@ -1,22 +0,0 @@
Copyright (c) 2011 Matthew Frazier
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,22 +0,0 @@
Copyright (c) 2012 Ali Afshar
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,31 +0,0 @@
Copyright (c) 2011 by Armin Ronacher and the Django Software Foundation.
Some rights reserved.
Redistribution and use in source and binary forms, 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.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE 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
OWNER 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, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,5 +0,0 @@
from .pdf import PdfFileReader, PdfFileWriter
from .merger import PdfFileMerger
from .pagerange import PageRange, parse_filename_page_ranges
from ._version import __version__
__all__ = ["pdf", "PdfFileMerger"]

View File

@ -1 +0,0 @@
__version__ = '1.26.0'

View File

@ -1,362 +0,0 @@
# vim: sw=4:expandtab:foldmethod=marker
#
# Copyright (c) 2006, Mathieu Fenniak
# All rights reserved.
#
# Redistribution and use in source and binary forms, 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.
# * The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE 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 OWNER 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, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Implementation of stream filters for PDF.
"""
__author__ = "Mathieu Fenniak"
__author_email__ = "biziqe@mathieu.fenniak.net"
from .utils import PdfReadError, ord_, chr_
from sys import version_info
if version_info < ( 3, 0 ):
from cStringIO import StringIO
else:
from io import StringIO
import struct
try:
import zlib
def decompress(data):
return zlib.decompress(data)
def compress(data):
return zlib.compress(data)
except ImportError:
# Unable to import zlib. Attempt to use the System.IO.Compression
# library from the .NET framework. (IronPython only)
import System
from System import IO, Collections, Array
def _string_to_bytearr(buf):
retval = Array.CreateInstance(System.Byte, len(buf))
for i in range(len(buf)):
retval[i] = ord(buf[i])
return retval
def _bytearr_to_string(bytes):
retval = ""
for i in range(bytes.Length):
retval += chr(bytes[i])
return retval
def _read_bytes(stream):
ms = IO.MemoryStream()
buf = Array.CreateInstance(System.Byte, 2048)
while True:
bytes = stream.Read(buf, 0, buf.Length)
if bytes == 0:
break
else:
ms.Write(buf, 0, bytes)
retval = ms.ToArray()
ms.Close()
return retval
def decompress(data):
bytes = _string_to_bytearr(data)
ms = IO.MemoryStream()
ms.Write(bytes, 0, bytes.Length)
ms.Position = 0 # fseek 0
gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Decompress)
bytes = _read_bytes(gz)
retval = _bytearr_to_string(bytes)
gz.Close()
return retval
def compress(data):
bytes = _string_to_bytearr(data)
ms = IO.MemoryStream()
gz = IO.Compression.DeflateStream(ms, IO.Compression.CompressionMode.Compress, True)
gz.Write(bytes, 0, bytes.Length)
gz.Close()
ms.Position = 0 # fseek 0
bytes = ms.ToArray()
retval = _bytearr_to_string(bytes)
ms.Close()
return retval
class FlateDecode(object):
def decode(data, decodeParms):
data = decompress(data)
predictor = 1
if decodeParms:
try:
predictor = decodeParms.get("/Predictor", 1)
except AttributeError:
pass # usually an array with a null object was read
# predictor 1 == no predictor
if predictor != 1:
columns = decodeParms["/Columns"]
# PNG prediction:
if predictor >= 10 and predictor <= 15:
output = StringIO()
# PNG prediction can vary from row to row
rowlength = columns + 1
assert len(data) % rowlength == 0
prev_rowdata = (0,) * rowlength
for row in range(len(data) // rowlength):
rowdata = [ord_(x) for x in data[(row*rowlength):((row+1)*rowlength)]]
filterByte = rowdata[0]
if filterByte == 0:
pass
elif filterByte == 1:
for i in range(2, rowlength):
rowdata[i] = (rowdata[i] + rowdata[i-1]) % 256
elif filterByte == 2:
for i in range(1, rowlength):
rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256
else:
# unsupported PNG filter
raise PdfReadError("Unsupported PNG filter %r" % filterByte)
prev_rowdata = rowdata
output.write(''.join([chr(x) for x in rowdata[1:]]))
data = output.getvalue()
else:
# unsupported predictor
raise PdfReadError("Unsupported flatedecode predictor %r" % predictor)
return data
decode = staticmethod(decode)
def encode(data):
return compress(data)
encode = staticmethod(encode)
class ASCIIHexDecode(object):
def decode(data, decodeParms=None):
retval = ""
char = ""
x = 0
while True:
c = data[x]
if c == ">":
break
elif c.isspace():
x += 1
continue
char += c
if len(char) == 2:
retval += chr(int(char, base=16))
char = ""
x += 1
assert char == ""
return retval
decode = staticmethod(decode)
class LZWDecode(object):
"""Taken from:
http://www.java2s.com/Open-Source/Java-Document/PDF/PDF-Renderer/com/sun/pdfview/decode/LZWDecode.java.htm
"""
class decoder(object):
def __init__(self, data):
self.STOP=257
self.CLEARDICT=256
self.data=data
self.bytepos=0
self.bitpos=0
self.dict=[""]*4096
for i in range(256):
self.dict[i]=chr(i)
self.resetDict()
def resetDict(self):
self.dictlen=258
self.bitspercode=9
def nextCode(self):
fillbits=self.bitspercode
value=0
while fillbits>0 :
if self.bytepos >= len(self.data):
return -1
nextbits=ord(self.data[self.bytepos])
bitsfromhere=8-self.bitpos
if bitsfromhere>fillbits:
bitsfromhere=fillbits
value |= (((nextbits >> (8-self.bitpos-bitsfromhere)) &
(0xff >> (8-bitsfromhere))) <<
(fillbits-bitsfromhere))
fillbits -= bitsfromhere
self.bitpos += bitsfromhere
if self.bitpos >=8:
self.bitpos=0
self.bytepos = self.bytepos+1
return value
def decode(self):
""" algorithm derived from:
http://www.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html
and the PDFReference
"""
cW = self.CLEARDICT;
baos=""
while True:
pW = cW;
cW = self.nextCode();
if cW == -1:
raise PdfReadError("Missed the stop code in LZWDecode!")
if cW == self.STOP:
break;
elif cW == self.CLEARDICT:
self.resetDict();
elif pW == self.CLEARDICT:
baos+=self.dict[cW]
else:
if cW < self.dictlen:
baos += self.dict[cW]
p=self.dict[pW]+self.dict[cW][0]
self.dict[self.dictlen]=p
self.dictlen+=1
else:
p=self.dict[pW]+self.dict[pW][0]
baos+=p
self.dict[self.dictlen] = p;
self.dictlen+=1
if (self.dictlen >= (1 << self.bitspercode) - 1 and
self.bitspercode < 12):
self.bitspercode+=1
return baos
@staticmethod
def decode(data,decodeParams=None):
return LZWDecode.decoder(data).decode()
class ASCII85Decode(object):
def decode(data, decodeParms=None):
if version_info < ( 3, 0 ):
retval = ""
group = []
x = 0
hitEod = False
# remove all whitespace from data
data = [y for y in data if not (y in ' \n\r\t')]
while not hitEod:
c = data[x]
if len(retval) == 0 and c == "<" and data[x+1] == "~":
x += 2
continue
#elif c.isspace():
# x += 1
# continue
elif c == 'z':
assert len(group) == 0
retval += '\x00\x00\x00\x00'
x += 1
continue
elif c == "~" and data[x+1] == ">":
if len(group) != 0:
# cannot have a final group of just 1 char
assert len(group) > 1
cnt = len(group) - 1
group += [ 85, 85, 85 ]
hitEod = cnt
else:
break
else:
c = ord(c) - 33
assert c >= 0 and c < 85
group += [ c ]
if len(group) >= 5:
b = group[0] * (85**4) + \
group[1] * (85**3) + \
group[2] * (85**2) + \
group[3] * 85 + \
group[4]
assert b < (2**32 - 1)
c4 = chr((b >> 0) % 256)
c3 = chr((b >> 8) % 256)
c2 = chr((b >> 16) % 256)
c1 = chr(b >> 24)
retval += (c1 + c2 + c3 + c4)
if hitEod:
retval = retval[:-4+hitEod]
group = []
x += 1
return retval
else:
if isinstance(data, str):
data = data.encode('ascii')
n = b = 0
out = bytearray()
for c in data:
if ord('!') <= c and c <= ord('u'):
n += 1
b = b*85+(c-33)
if n == 5:
out += struct.pack(b'>L',b)
n = b = 0
elif c == ord('z'):
assert n == 0
out += b'\0\0\0\0'
elif c == ord('~'):
if n:
for _ in range(5-n):
b = b*85+84
out += struct.pack(b'>L',b)[:n-1]
break
return bytes(out)
decode = staticmethod(decode)
def decodeStreamData(stream):
from .generic import NameObject
filters = stream.get("/Filter", ())
if len(filters) and not isinstance(filters[0], NameObject):
# we have a single filter instance
filters = (filters,)
data = stream._data
# If there is not data to decode we should not try to decode the data.
if data:
for filterType in filters:
if filterType == "/FlateDecode" or filterType == "/Fl":
data = FlateDecode.decode(data, stream.get("/DecodeParms"))
elif filterType == "/ASCIIHexDecode" or filterType == "/AHx":
data = ASCIIHexDecode.decode(data)
elif filterType == "/LZWDecode" or filterType == "/LZW":
data = LZWDecode.decode(data, stream.get("/DecodeParms"))
elif filterType == "/ASCII85Decode" or filterType == "/A85":
data = ASCII85Decode.decode(data)
elif filterType == "/Crypt":
decodeParams = stream.get("/DecodeParams", {})
if "/Name" not in decodeParams and "/Type" not in decodeParams:
pass
else:
raise NotImplementedError("/Crypt filter with /Name or /Type not supported yet")
else:
# unsupported filter
raise NotImplementedError("unsupported filter %s" % filterType)
return data

1226
vendor/PyPDF2/generic.py vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,553 +0,0 @@
# vim: sw=4:expandtab:foldmethod=marker
#
# Copyright (c) 2006, Mathieu Fenniak
# All rights reserved.
#
# Redistribution and use in source and binary forms, 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.
# * The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE 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 OWNER 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, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .generic import *
from .utils import isString, str_
from .pdf import PdfFileReader, PdfFileWriter
from .pagerange import PageRange
from sys import version_info
if version_info < ( 3, 0 ):
from cStringIO import StringIO
StreamIO = StringIO
else:
from io import BytesIO
from io import FileIO as file
StreamIO = BytesIO
class _MergedPage(object):
"""
_MergedPage is used internally by PdfFileMerger to collect necessary
information on each page that is being merged.
"""
def __init__(self, pagedata, src, id):
self.src = src
self.pagedata = pagedata
self.out_pagedata = None
self.id = id
class PdfFileMerger(object):
"""
Initializes a PdfFileMerger object. PdfFileMerger merges multiple PDFs
into a single PDF. It can concatenate, slice, insert, or any combination
of the above.
See the functions :meth:`merge()<merge>` (or :meth:`append()<append>`)
and :meth:`write()<write>` for usage information.
:param bool strict: Determines whether user should be warned of all
problems and also causes some correctable problems to be fatal.
Defaults to ``True``.
"""
def __init__(self, strict=True):
self.inputs = []
self.pages = []
self.output = PdfFileWriter()
self.bookmarks = []
self.named_dests = []
self.id_count = 0
self.strict = strict
def merge(self, position, fileobj, bookmark=None, pages=None, import_bookmarks=True):
"""
Merges the pages from the given file into the output file at the
specified page number.
:param int position: The *page number* to insert this file. File will
be inserted after the given number.
:param fileobj: A File Object or an object that supports the standard read
and seek methods similar to a File Object. Could also be a
string representing a path to a PDF file.
:param str bookmark: Optionally, you may specify a bookmark to be applied at
the beginning of the included file by supplying the text of the bookmark.
:param pages: can be a :ref:`Page Range <page-range>` or a ``(start, stop[, step])`` tuple
to merge only the specified range of pages from the source
document into the output document.
:param bool import_bookmarks: You may prevent the source document's bookmarks
from being imported by specifying this as ``False``.
"""
# This parameter is passed to self.inputs.append and means
# that the stream used was created in this method.
my_file = False
# If the fileobj parameter is a string, assume it is a path
# and create a file object at that location. If it is a file,
# copy the file's contents into a BytesIO (or StreamIO) stream object; if
# it is a PdfFileReader, copy that reader's stream into a
# BytesIO (or StreamIO) stream.
# If fileobj is none of the above types, it is not modified
decryption_key = None
if isString(fileobj):
fileobj = file(fileobj, 'rb')
my_file = True
elif isinstance(fileobj, file):
fileobj.seek(0)
filecontent = fileobj.read()
fileobj = StreamIO(filecontent)
my_file = True
elif isinstance(fileobj, PdfFileReader):
orig_tell = fileobj.stream.tell()
fileobj.stream.seek(0)
filecontent = StreamIO(fileobj.stream.read())
fileobj.stream.seek(orig_tell) # reset the stream to its original location
fileobj = filecontent
if hasattr(fileobj, '_decryption_key'):
decryption_key = fileobj._decryption_key
my_file = True
# Create a new PdfFileReader instance using the stream
# (either file or BytesIO or StringIO) created above
pdfr = PdfFileReader(fileobj, strict=self.strict)
if decryption_key is not None:
pdfr._decryption_key = decryption_key
# Find the range of pages to merge.
if pages == None:
pages = (0, pdfr.getNumPages())
elif isinstance(pages, PageRange):
pages = pages.indices(pdfr.getNumPages())
elif not isinstance(pages, tuple):
raise TypeError('"pages" must be a tuple of (start, stop[, step])')
srcpages = []
if bookmark:
bookmark = Bookmark(TextStringObject(bookmark), NumberObject(self.id_count), NameObject('/Fit'))
outline = []
if import_bookmarks:
outline = pdfr.getOutlines()
outline = self._trim_outline(pdfr, outline, pages)
if bookmark:
self.bookmarks += [bookmark, outline]
else:
self.bookmarks += outline
dests = pdfr.namedDestinations
dests = self._trim_dests(pdfr, dests, pages)
self.named_dests += dests
# Gather all the pages that are going to be merged
for i in range(*pages):
pg = pdfr.getPage(i)
id = self.id_count
self.id_count += 1
mp = _MergedPage(pg, pdfr, id)
srcpages.append(mp)
self._associate_dests_to_pages(srcpages)
self._associate_bookmarks_to_pages(srcpages)
# Slice to insert the pages at the specified position
self.pages[position:position] = srcpages
# Keep track of our input files so we can close them later
self.inputs.append((fileobj, pdfr, my_file))
def append(self, fileobj, bookmark=None, pages=None, import_bookmarks=True):
"""
Identical to the :meth:`merge()<merge>` method, but assumes you want to concatenate
all pages onto the end of the file instead of specifying a position.
:param fileobj: A File Object or an object that supports the standard read
and seek methods similar to a File Object. Could also be a
string representing a path to a PDF file.
:param str bookmark: Optionally, you may specify a bookmark to be applied at
the beginning of the included file by supplying the text of the bookmark.
:param pages: can be a :ref:`Page Range <page-range>` or a ``(start, stop[, step])`` tuple
to merge only the specified range of pages from the source
document into the output document.
:param bool import_bookmarks: You may prevent the source document's bookmarks
from being imported by specifying this as ``False``.
"""
self.merge(len(self.pages), fileobj, bookmark, pages, import_bookmarks)
def write(self, fileobj):
"""
Writes all data that has been merged to the given output file.
:param fileobj: Output file. Can be a filename or any kind of
file-like object.
"""
my_file = False
if isString(fileobj):
fileobj = file(fileobj, 'wb')
my_file = True
# Add pages to the PdfFileWriter
# The commented out line below was replaced with the two lines below it to allow PdfFileMerger to work with PyPdf 1.13
for page in self.pages:
self.output.addPage(page.pagedata)
page.out_pagedata = self.output.getReference(self.output._pages.getObject()["/Kids"][-1].getObject())
#idnum = self.output._objects.index(self.output._pages.getObject()["/Kids"][-1].getObject()) + 1
#page.out_pagedata = IndirectObject(idnum, 0, self.output)
# Once all pages are added, create bookmarks to point at those pages
self._write_dests()
self._write_bookmarks()
# Write the output to the file
self.output.write(fileobj)
if my_file:
fileobj.close()
def close(self):
"""
Shuts all file descriptors (input and output) and clears all memory
usage.
"""
self.pages = []
for fo, pdfr, mine in self.inputs:
if mine:
fo.close()
self.inputs = []
self.output = None
def addMetadata(self, infos):
"""
Add custom metadata to the output.
:param dict infos: a Python dictionary where each key is a field
and each value is your new metadata.
Example: ``{u'/Title': u'My title'}``
"""
self.output.addMetadata(infos)
def setPageLayout(self, layout):
"""
Set the page layout
:param str layout: The page layout to be used
Valid layouts are:
/NoLayout Layout explicitly not specified
/SinglePage Show one page at a time
/OneColumn Show one column at a time
/TwoColumnLeft Show pages in two columns, odd-numbered pages on the left
/TwoColumnRight Show pages in two columns, odd-numbered pages on the right
/TwoPageLeft Show two pages at a time, odd-numbered pages on the left
/TwoPageRight Show two pages at a time, odd-numbered pages on the right
"""
self.output.setPageLayout(layout)
def setPageMode(self, mode):
"""
Set the page mode.
:param str mode: The page mode to use.
Valid modes are:
/UseNone Do not show outlines or thumbnails panels
/UseOutlines Show outlines (aka bookmarks) panel
/UseThumbs Show page thumbnails panel
/FullScreen Fullscreen view
/UseOC Show Optional Content Group (OCG) panel
/UseAttachments Show attachments panel
"""
self.output.setPageMode(mode)
def _trim_dests(self, pdf, dests, pages):
"""
Removes any named destinations that are not a part of the specified
page set.
"""
new_dests = []
prev_header_added = True
for k, o in list(dests.items()):
for j in range(*pages):
if pdf.getPage(j).getObject() == o['/Page'].getObject():
o[NameObject('/Page')] = o['/Page'].getObject()
assert str_(k) == str_(o['/Title'])
new_dests.append(o)
break
return new_dests
def _trim_outline(self, pdf, outline, pages):
"""
Removes any outline/bookmark entries that are not a part of the
specified page set.
"""
new_outline = []
prev_header_added = True
for i, o in enumerate(outline):
if isinstance(o, list):
sub = self._trim_outline(pdf, o, pages)
if sub:
if not prev_header_added:
new_outline.append(outline[i-1])
new_outline.append(sub)
else:
prev_header_added = False
for j in range(*pages):
if pdf.getPage(j).getObject() == o['/Page'].getObject():
o[NameObject('/Page')] = o['/Page'].getObject()
new_outline.append(o)
prev_header_added = True
break
return new_outline
def _write_dests(self):
dests = self.named_dests
for v in dests:
pageno = None
pdf = None
if '/Page' in v:
for i, p in enumerate(self.pages):
if p.id == v['/Page']:
v[NameObject('/Page')] = p.out_pagedata
pageno = i
pdf = p.src
break
if pageno != None:
self.output.addNamedDestinationObject(v)
def _write_bookmarks(self, bookmarks=None, parent=None):
if bookmarks == None:
bookmarks = self.bookmarks
last_added = None
for b in bookmarks:
if isinstance(b, list):
self._write_bookmarks(b, last_added)
continue
pageno = None
pdf = None
if '/Page' in b:
for i, p in enumerate(self.pages):
if p.id == b['/Page']:
#b[NameObject('/Page')] = p.out_pagedata
args = [NumberObject(p.id), NameObject(b['/Type'])]
#nothing more to add
#if b['/Type'] == '/Fit' or b['/Type'] == '/FitB'
if b['/Type'] == '/FitH' or b['/Type'] == '/FitBH':
if '/Top' in b and not isinstance(b['/Top'], NullObject):
args.append(FloatObject(b['/Top']))
else:
args.append(FloatObject(0))
del b['/Top']
elif b['/Type'] == '/FitV' or b['/Type'] == '/FitBV':
if '/Left' in b and not isinstance(b['/Left'], NullObject):
args.append(FloatObject(b['/Left']))
else:
args.append(FloatObject(0))
del b['/Left']
elif b['/Type'] == '/XYZ':
if '/Left' in b and not isinstance(b['/Left'], NullObject):
args.append(FloatObject(b['/Left']))
else:
args.append(FloatObject(0))
if '/Top' in b and not isinstance(b['/Top'], NullObject):
args.append(FloatObject(b['/Top']))
else:
args.append(FloatObject(0))
if '/Zoom' in b and not isinstance(b['/Zoom'], NullObject):
args.append(FloatObject(b['/Zoom']))
else:
args.append(FloatObject(0))
del b['/Top'], b['/Zoom'], b['/Left']
elif b['/Type'] == '/FitR':
if '/Left' in b and not isinstance(b['/Left'], NullObject):
args.append(FloatObject(b['/Left']))
else:
args.append(FloatObject(0))
if '/Bottom' in b and not isinstance(b['/Bottom'], NullObject):
args.append(FloatObject(b['/Bottom']))
else:
args.append(FloatObject(0))
if '/Right' in b and not isinstance(b['/Right'], NullObject):
args.append(FloatObject(b['/Right']))
else:
args.append(FloatObject(0))
if '/Top' in b and not isinstance(b['/Top'], NullObject):
args.append(FloatObject(b['/Top']))
else:
args.append(FloatObject(0))
del b['/Left'], b['/Right'], b['/Bottom'], b['/Top']
b[NameObject('/A')] = DictionaryObject({NameObject('/S'): NameObject('/GoTo'), NameObject('/D'): ArrayObject(args)})
pageno = i
pdf = p.src
break
if pageno != None:
del b['/Page'], b['/Type']
last_added = self.output.addBookmarkDict(b, parent)
def _associate_dests_to_pages(self, pages):
for nd in self.named_dests:
pageno = None
np = nd['/Page']
if isinstance(np, NumberObject):
continue
for p in pages:
if np.getObject() == p.pagedata.getObject():
pageno = p.id
if pageno != None:
nd[NameObject('/Page')] = NumberObject(pageno)
else:
raise ValueError("Unresolved named destination '%s'" % (nd['/Title'],))
def _associate_bookmarks_to_pages(self, pages, bookmarks=None):
if bookmarks == None:
bookmarks = self.bookmarks
for b in bookmarks:
if isinstance(b, list):
self._associate_bookmarks_to_pages(pages, b)
continue
pageno = None
bp = b['/Page']
if isinstance(bp, NumberObject):
continue
for p in pages:
if bp.getObject() == p.pagedata.getObject():
pageno = p.id
if pageno != None:
b[NameObject('/Page')] = NumberObject(pageno)
else:
raise ValueError("Unresolved bookmark '%s'" % (b['/Title'],))
def findBookmark(self, bookmark, root=None):
if root == None:
root = self.bookmarks
for i, b in enumerate(root):
if isinstance(b, list):
res = self.findBookmark(bookmark, b)
if res:
return [i] + res
elif b == bookmark or b['/Title'] == bookmark:
return [i]
return None
def addBookmark(self, title, pagenum, parent=None):
"""
Add a bookmark to this PDF file.
:param str title: Title to use for this bookmark.
:param int pagenum: Page number this bookmark will point to.
:param parent: A reference to a parent bookmark to create nested
bookmarks.
"""
if parent == None:
iloc = [len(self.bookmarks)-1]
elif isinstance(parent, list):
iloc = parent
else:
iloc = self.findBookmark(parent)
dest = Bookmark(TextStringObject(title), NumberObject(pagenum), NameObject('/FitH'), NumberObject(826))
if parent == None:
self.bookmarks.append(dest)
else:
bmparent = self.bookmarks
for i in iloc[:-1]:
bmparent = bmparent[i]
npos = iloc[-1]+1
if npos < len(bmparent) and isinstance(bmparent[npos], list):
bmparent[npos].append(dest)
else:
bmparent.insert(npos, [dest])
return dest
def addNamedDestination(self, title, pagenum):
"""
Add a destination to the output.
:param str title: Title to use
:param int pagenum: Page number this destination points at.
"""
dest = Destination(TextStringObject(title), NumberObject(pagenum), NameObject('/FitH'), NumberObject(826))
self.named_dests.append(dest)
class OutlinesObject(list):
def __init__(self, pdf, tree, parent=None):
list.__init__(self)
self.tree = tree
self.pdf = pdf
self.parent = parent
def remove(self, index):
obj = self[index]
del self[index]
self.tree.removeChild(obj)
def add(self, title, pagenum):
pageRef = self.pdf.getObject(self.pdf._pages)['/Kids'][pagenum]
action = DictionaryObject()
action.update({
NameObject('/D') : ArrayObject([pageRef, NameObject('/FitH'), NumberObject(826)]),
NameObject('/S') : NameObject('/GoTo')
})
actionRef = self.pdf._addObject(action)
bookmark = TreeObject()
bookmark.update({
NameObject('/A'): actionRef,
NameObject('/Title'): createStringObject(title),
})
self.pdf._addObject(bookmark)
self.tree.addChild(bookmark)
def removeAll(self):
for child in [x for x in self.tree.children()]:
self.tree.removeChild(child)
self.pop()

View File

@ -1,152 +0,0 @@
#!/usr/bin/env python
"""
Representation and utils for ranges of PDF file pages.
Copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.
All rights reserved. This software is available under a BSD license;
see https://github.com/mstamy2/PyPDF2/blob/master/LICENSE
"""
import re
from .utils import isString
_INT_RE = r"(0|-?[1-9]\d*)" # A decimal int, don't allow "-0".
PAGE_RANGE_RE = "^({int}|({int}?(:{int}?(:{int}?)?)))$".format(int=_INT_RE)
# groups: 12 34 5 6 7 8
class ParseError(Exception):
pass
PAGE_RANGE_HELP = """Remember, page indices start with zero.
Page range expression examples:
: all pages. -1 last page.
22 just the 23rd page. :-1 all but the last page.
0:3 the first three pages. -2 second-to-last page.
:3 the first three pages. -2: last two pages.
5: from the sixth page onward. -3:-1 third & second to last.
The third, "stride" or "step" number is also recognized.
::2 0 2 4 ... to the end. 3:0:-1 3 2 1 but not 0.
1:10:2 1 3 5 7 9 2::-1 2 1 0.
::-1 all pages in reverse order.
"""
class PageRange(object):
"""
A slice-like representation of a range of page indices,
i.e. page numbers, only starting at zero.
The syntax is like what you would put between brackets [ ].
The slice is one of the few Python types that can't be subclassed,
but this class converts to and from slices, and allows similar use.
o PageRange(str) parses a string representing a page range.
o PageRange(slice) directly "imports" a slice.
o to_slice() gives the equivalent slice.
o str() and repr() allow printing.
o indices(n) is like slice.indices(n).
"""
def __init__(self, arg):
"""
Initialize with either a slice -- giving the equivalent page range,
or a PageRange object -- making a copy,
or a string like
"int", "[int]:[int]" or "[int]:[int]:[int]",
where the brackets indicate optional ints.
{page_range_help}
Note the difference between this notation and arguments to slice():
slice(3) means the first three pages;
PageRange("3") means the range of only the fourth page.
However PageRange(slice(3)) means the first three pages.
"""
if isinstance(arg, slice):
self._slice = arg
return
if isinstance(arg, PageRange):
self._slice = arg.to_slice()
return
m = isString(arg) and re.match(PAGE_RANGE_RE, arg)
if not m:
raise ParseError(arg)
elif m.group(2):
# Special case: just an int means a range of one page.
start = int(m.group(2))
stop = start + 1 if start != -1 else None
self._slice = slice(start, stop)
else:
self._slice = slice(*[int(g) if g else None
for g in m.group(4, 6, 8)])
# Just formatting this when there is __doc__ for __init__
if __init__.__doc__:
__init__.__doc__ = __init__.__doc__.format(page_range_help=PAGE_RANGE_HELP)
@staticmethod
def valid(input):
""" True if input is a valid initializer for a PageRange. """
return isinstance(input, slice) or \
isinstance(input, PageRange) or \
(isString(input)
and bool(re.match(PAGE_RANGE_RE, input)))
def to_slice(self):
""" Return the slice equivalent of this page range. """
return self._slice
def __str__(self):
""" A string like "1:2:3". """
s = self._slice
if s.step == None:
if s.start != None and s.stop == s.start + 1:
return str(s.start)
indices = s.start, s.stop
else:
indices = s.start, s.stop, s.step
return ':'.join("" if i == None else str(i) for i in indices)
def __repr__(self):
""" A string like "PageRange('1:2:3')". """
return "PageRange(" + repr(str(self)) + ")"
def indices(self, n):
"""
n is the length of the list of pages to choose from.
Returns arguments for range(). See help(slice.indices).
"""
return self._slice.indices(n)
PAGE_RANGE_ALL = PageRange(":") # The range of all pages.
def parse_filename_page_ranges(args):
"""
Given a list of filenames and page ranges, return a list of
(filename, page_range) pairs.
First arg must be a filename; other ags are filenames, page-range
expressions, slice objects, or PageRange objects.
A filename not followed by a page range indicates all pages of the file.
"""
pairs = []
pdf_filename = None
did_page_range = False
for arg in args + [None]:
if PageRange.valid(arg):
if not pdf_filename:
raise ValueError("The first argument must be a filename, " \
"not a page range.")
pairs.append( (pdf_filename, PageRange(arg)) )
did_page_range = True
else:
# New filename or end of list--do all of the previous file?
if pdf_filename and not did_page_range:
pairs.append( (pdf_filename, PAGE_RANGE_ALL) )
pdf_filename = arg
did_page_range = False
return pairs

3004
vendor/PyPDF2/pdf.py vendored

File diff suppressed because it is too large Load Diff

295
vendor/PyPDF2/utils.py vendored
View File

@ -1,295 +0,0 @@
# Copyright (c) 2006, Mathieu Fenniak
# All rights reserved.
#
# Redistribution and use in source and binary forms, 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.
# * The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE 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 OWNER 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, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Utility functions for PDF library.
"""
__author__ = "Mathieu Fenniak"
__author_email__ = "biziqe@mathieu.fenniak.net"
import sys
try:
import __builtin__ as builtins
except ImportError: # Py3
import builtins
xrange_fn = getattr(builtins, "xrange", range)
_basestring = getattr(builtins, "basestring", str)
bytes_type = type(bytes()) # Works the same in Python 2.X and 3.X
string_type = getattr(builtins, "unicode", str)
int_types = (int, long) if sys.version_info[0] < 3 else (int,)
# Make basic type tests more consistent
def isString(s):
"""Test if arg is a string. Compatible with Python 2 and 3."""
return isinstance(s, _basestring)
def isInt(n):
"""Test if arg is an int. Compatible with Python 2 and 3."""
return isinstance(n, int_types)
def isBytes(b):
"""Test if arg is a bytes instance. Compatible with Python 2 and 3."""
return isinstance(b, bytes_type)
#custom implementation of warnings.formatwarning
def formatWarning(message, category, filename, lineno, line=None):
file = filename.replace("/", "\\").rsplit("\\", 1)[1] # find the file name
return "%s: %s [%s:%s]\n" % (category.__name__, message, file, lineno)
def readUntilWhitespace(stream, maxchars=None):
"""
Reads non-whitespace characters and returns them.
Stops upon encountering whitespace or when maxchars is reached.
"""
txt = b_("")
while True:
tok = stream.read(1)
if tok.isspace() or not tok:
break
txt += tok
if len(txt) == maxchars:
break
return txt
def readNonWhitespace(stream):
"""
Finds and reads the next non-whitespace character (ignores whitespace).
"""
tok = WHITESPACES[0]
while tok in WHITESPACES:
tok = stream.read(1)
return tok
def skipOverWhitespace(stream):
"""
Similar to readNonWhitespace, but returns a Boolean if more than
one whitespace character was read.
"""
tok = WHITESPACES[0]
cnt = 0;
while tok in WHITESPACES:
tok = stream.read(1)
cnt+=1
return (cnt > 1)
def skipOverComment(stream):
tok = stream.read(1)
stream.seek(-1, 1)
if tok == b_('%'):
while tok not in (b_('\n'), b_('\r')):
tok = stream.read(1)
def readUntilRegex(stream, regex, ignore_eof=False):
"""
Reads until the regular expression pattern matched (ignore the match)
Raise PdfStreamError on premature end-of-file.
:param bool ignore_eof: If true, ignore end-of-line and return immediately
"""
name = b_('')
while True:
tok = stream.read(16)
if not tok:
# stream has truncated prematurely
if ignore_eof == True:
return name
else:
raise PdfStreamError("Stream has ended unexpectedly")
m = regex.search(tok)
if m is not None:
name += tok[:m.start()]
stream.seek(m.start()-len(tok), 1)
break
name += tok
return name
class ConvertFunctionsToVirtualList(object):
def __init__(self, lengthFunction, getFunction):
self.lengthFunction = lengthFunction
self.getFunction = getFunction
def __len__(self):
return self.lengthFunction()
def __getitem__(self, index):
if isinstance(index, slice):
indices = xrange_fn(*index.indices(len(self)))
cls = type(self)
return cls(indices.__len__, lambda idx: self[indices[idx]])
if not isInt(index):
raise TypeError("sequence indices must be integers")
len_self = len(self)
if index < 0:
# support negative indexes
index = len_self + index
if index < 0 or index >= len_self:
raise IndexError("sequence index out of range")
return self.getFunction(index)
def RC4_encrypt(key, plaintext):
S = [i for i in range(256)]
j = 0
for i in range(256):
j = (j + S[i] + ord_(key[i % len(key)])) % 256
S[i], S[j] = S[j], S[i]
i, j = 0, 0
retval = b_("")
for x in range(len(plaintext)):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
t = S[(S[i] + S[j]) % 256]
retval += b_(chr(ord_(plaintext[x]) ^ t))
return retval
def matrixMultiply(a, b):
return [[sum([float(i)*float(j)
for i, j in zip(row, col)]
) for col in zip(*b)]
for row in a]
def markLocation(stream):
"""Creates text file showing current location in context."""
# Mainly for debugging
RADIUS = 5000
stream.seek(-RADIUS, 1)
outputDoc = open('PyPDF2_pdfLocation.txt', 'w')
outputDoc.write(stream.read(RADIUS))
outputDoc.write('HERE')
outputDoc.write(stream.read(RADIUS))
outputDoc.close()
stream.seek(-RADIUS, 1)
class PyPdfError(Exception):
pass
class PdfReadError(PyPdfError):
pass
class PageSizeNotDefinedError(PyPdfError):
pass
class PdfReadWarning(UserWarning):
pass
class PdfStreamError(PdfReadError):
pass
if sys.version_info[0] < 3:
def b_(s):
return s
else:
B_CACHE = {}
def b_(s):
bc = B_CACHE
if s in bc:
return bc[s]
if type(s) == bytes:
return s
else:
r = s.encode('latin-1')
if len(s) < 2:
bc[s] = r
return r
def u_(s):
if sys.version_info[0] < 3:
return unicode(s, 'unicode_escape')
else:
return s
def str_(b):
if sys.version_info[0] < 3:
return b
else:
if type(b) == bytes:
return b.decode('latin-1')
else:
return b
def ord_(b):
if sys.version_info[0] < 3 or type(b) == str:
return ord(b)
else:
return b
def chr_(c):
if sys.version_info[0] < 3:
return c
else:
return chr(c)
def barray(b):
if sys.version_info[0] < 3:
return b
else:
return bytearray(b)
def hexencode(b):
if sys.version_info[0] < 3:
return b.encode('hex')
else:
import codecs
coder = codecs.getencoder('hex_codec')
return coder(b)[0]
def hexStr(num):
return hex(num).replace('L', '')
WHITESPACES = [b_(x) for x in [' ', '\n', '\r', '\t', '\x00']]

358
vendor/PyPDF2/xmp.py vendored
View File

@ -1,358 +0,0 @@
import re
import datetime
import decimal
from .generic import PdfObject
from xml.dom import getDOMImplementation
from xml.dom.minidom import parseString
from .utils import u_
RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
DC_NAMESPACE = "http://purl.org/dc/elements/1.1/"
XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/"
PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/"
XMPMM_NAMESPACE = "http://ns.adobe.com/xap/1.0/mm/"
# What is the PDFX namespace, you might ask? I might ask that too. It's
# a completely undocumented namespace used to place "custom metadata"
# properties, which are arbitrary metadata properties with no semantic or
# documented meaning. Elements in the namespace are key/value-style storage,
# where the element name is the key and the content is the value. The keys
# are transformed into valid XML identifiers by substituting an invalid
# identifier character with \u2182 followed by the unicode hex ID of the
# original character. A key like "my car" is therefore "my\u21820020car".
#
# \u2182, in case you're wondering, is the unicode character
# \u{ROMAN NUMERAL TEN THOUSAND}, a straightforward and obvious choice for
# escaping characters.
#
# Intentional users of the pdfx namespace should be shot on sight. A
# custom data schema and sensical XML elements could be used instead, as is
# suggested by Adobe's own documentation on XMP (under "Extensibility of
# Schemas").
#
# Information presented here on the /pdfx/ schema is a result of limited
# reverse engineering, and does not constitute a full specification.
PDFX_NAMESPACE = "http://ns.adobe.com/pdfx/1.3/"
iso8601 = re.compile("""
(?P<year>[0-9]{4})
(-
(?P<month>[0-9]{2})
(-
(?P<day>[0-9]+)
(T
(?P<hour>[0-9]{2}):
(?P<minute>[0-9]{2})
(:(?P<second>[0-9]{2}(.[0-9]+)?))?
(?P<tzd>Z|[-+][0-9]{2}:[0-9]{2})
)?
)?
)?
""", re.VERBOSE)
class XmpInformation(PdfObject):
"""
An object that represents Adobe XMP metadata.
Usually accessed by :meth:`getXmpMetadata()<PyPDF2.PdfFileReader.getXmpMetadata>`
"""
def __init__(self, stream):
self.stream = stream
docRoot = parseString(self.stream.getData())
self.rdfRoot = docRoot.getElementsByTagNameNS(RDF_NAMESPACE, "RDF")[0]
self.cache = {}
def writeToStream(self, stream, encryption_key):
self.stream.writeToStream(stream, encryption_key)
def getElement(self, aboutUri, namespace, name):
for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"):
if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri:
attr = desc.getAttributeNodeNS(namespace, name)
if attr != None:
yield attr
for element in desc.getElementsByTagNameNS(namespace, name):
yield element
def getNodesInNamespace(self, aboutUri, namespace):
for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"):
if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri:
for i in range(desc.attributes.length):
attr = desc.attributes.item(i)
if attr.namespaceURI == namespace:
yield attr
for child in desc.childNodes:
if child.namespaceURI == namespace:
yield child
def _getText(self, element):
text = ""
for child in element.childNodes:
if child.nodeType == child.TEXT_NODE:
text += child.data
return text
def _converter_string(value):
return value
def _converter_date(value):
m = iso8601.match(value)
year = int(m.group("year"))
month = int(m.group("month") or "1")
day = int(m.group("day") or "1")
hour = int(m.group("hour") or "0")
minute = int(m.group("minute") or "0")
second = decimal.Decimal(m.group("second") or "0")
seconds = second.to_integral(decimal.ROUND_FLOOR)
milliseconds = (second - seconds) * 1000000
tzd = m.group("tzd") or "Z"
dt = datetime.datetime(year, month, day, hour, minute, seconds, milliseconds)
if tzd != "Z":
tzd_hours, tzd_minutes = [int(x) for x in tzd.split(":")]
tzd_hours *= -1
if tzd_hours < 0:
tzd_minutes *= -1
dt = dt + datetime.timedelta(hours=tzd_hours, minutes=tzd_minutes)
return dt
_test_converter_date = staticmethod(_converter_date)
def _getter_bag(namespace, name, converter):
def get(self):
cached = self.cache.get(namespace, {}).get(name)
if cached:
return cached
retval = []
for element in self.getElement("", namespace, name):
bags = element.getElementsByTagNameNS(RDF_NAMESPACE, "Bag")
if len(bags):
for bag in bags:
for item in bag.getElementsByTagNameNS(RDF_NAMESPACE, "li"):
value = self._getText(item)
value = converter(value)
retval.append(value)
ns_cache = self.cache.setdefault(namespace, {})
ns_cache[name] = retval
return retval
return get
def _getter_seq(namespace, name, converter):
def get(self):
cached = self.cache.get(namespace, {}).get(name)
if cached:
return cached
retval = []
for element in self.getElement("", namespace, name):
seqs = element.getElementsByTagNameNS(RDF_NAMESPACE, "Seq")
if len(seqs):
for seq in seqs:
for item in seq.getElementsByTagNameNS(RDF_NAMESPACE, "li"):
value = self._getText(item)
value = converter(value)
retval.append(value)
else:
value = converter(self._getText(element))
retval.append(value)
ns_cache = self.cache.setdefault(namespace, {})
ns_cache[name] = retval
return retval
return get
def _getter_langalt(namespace, name, converter):
def get(self):
cached = self.cache.get(namespace, {}).get(name)
if cached:
return cached
retval = {}
for element in self.getElement("", namespace, name):
alts = element.getElementsByTagNameNS(RDF_NAMESPACE, "Alt")
if len(alts):
for alt in alts:
for item in alt.getElementsByTagNameNS(RDF_NAMESPACE, "li"):
value = self._getText(item)
value = converter(value)
retval[item.getAttribute("xml:lang")] = value
else:
retval["x-default"] = converter(self._getText(element))
ns_cache = self.cache.setdefault(namespace, {})
ns_cache[name] = retval
return retval
return get
def _getter_single(namespace, name, converter):
def get(self):
cached = self.cache.get(namespace, {}).get(name)
if cached:
return cached
value = None
for element in self.getElement("", namespace, name):
if element.nodeType == element.ATTRIBUTE_NODE:
value = element.nodeValue
else:
value = self._getText(element)
break
if value != None:
value = converter(value)
ns_cache = self.cache.setdefault(namespace, {})
ns_cache[name] = value
return value
return get
dc_contributor = property(_getter_bag(DC_NAMESPACE, "contributor", _converter_string))
"""
Contributors to the resource (other than the authors). An unsorted
array of names.
"""
dc_coverage = property(_getter_single(DC_NAMESPACE, "coverage", _converter_string))
"""
Text describing the extent or scope of the resource.
"""
dc_creator = property(_getter_seq(DC_NAMESPACE, "creator", _converter_string))
"""
A sorted array of names of the authors of the resource, listed in order
of precedence.
"""
dc_date = property(_getter_seq(DC_NAMESPACE, "date", _converter_date))
"""
A sorted array of dates (datetime.datetime instances) of signifigance to
the resource. The dates and times are in UTC.
"""
dc_description = property(_getter_langalt(DC_NAMESPACE, "description", _converter_string))
"""
A language-keyed dictionary of textual descriptions of the content of the
resource.
"""
dc_format = property(_getter_single(DC_NAMESPACE, "format", _converter_string))
"""
The mime-type of the resource.
"""
dc_identifier = property(_getter_single(DC_NAMESPACE, "identifier", _converter_string))
"""
Unique identifier of the resource.
"""
dc_language = property(_getter_bag(DC_NAMESPACE, "language", _converter_string))
"""
An unordered array specifying the languages used in the resource.
"""
dc_publisher = property(_getter_bag(DC_NAMESPACE, "publisher", _converter_string))
"""
An unordered array of publisher names.
"""
dc_relation = property(_getter_bag(DC_NAMESPACE, "relation", _converter_string))
"""
An unordered array of text descriptions of relationships to other
documents.
"""
dc_rights = property(_getter_langalt(DC_NAMESPACE, "rights", _converter_string))
"""
A language-keyed dictionary of textual descriptions of the rights the
user has to this resource.
"""
dc_source = property(_getter_single(DC_NAMESPACE, "source", _converter_string))
"""
Unique identifier of the work from which this resource was derived.
"""
dc_subject = property(_getter_bag(DC_NAMESPACE, "subject", _converter_string))
"""
An unordered array of descriptive phrases or keywrods that specify the
topic of the content of the resource.
"""
dc_title = property(_getter_langalt(DC_NAMESPACE, "title", _converter_string))
"""
A language-keyed dictionary of the title of the resource.
"""
dc_type = property(_getter_bag(DC_NAMESPACE, "type", _converter_string))
"""
An unordered array of textual descriptions of the document type.
"""
pdf_keywords = property(_getter_single(PDF_NAMESPACE, "Keywords", _converter_string))
"""
An unformatted text string representing document keywords.
"""
pdf_pdfversion = property(_getter_single(PDF_NAMESPACE, "PDFVersion", _converter_string))
"""
The PDF file version, for example 1.0, 1.3.
"""
pdf_producer = property(_getter_single(PDF_NAMESPACE, "Producer", _converter_string))
"""
The name of the tool that created the PDF document.
"""
xmp_createDate = property(_getter_single(XMP_NAMESPACE, "CreateDate", _converter_date))
"""
The date and time the resource was originally created. The date and
time are returned as a UTC datetime.datetime object.
"""
xmp_modifyDate = property(_getter_single(XMP_NAMESPACE, "ModifyDate", _converter_date))
"""
The date and time the resource was last modified. The date and time
are returned as a UTC datetime.datetime object.
"""
xmp_metadataDate = property(_getter_single(XMP_NAMESPACE, "MetadataDate", _converter_date))
"""
The date and time that any metadata for this resource was last
changed. The date and time are returned as a UTC datetime.datetime
object.
"""
xmp_creatorTool = property(_getter_single(XMP_NAMESPACE, "CreatorTool", _converter_string))
"""
The name of the first known tool used to create the resource.
"""
xmpmm_documentId = property(_getter_single(XMPMM_NAMESPACE, "DocumentID", _converter_string))
"""
The common identifier for all versions and renditions of this resource.
"""
xmpmm_instanceId = property(_getter_single(XMPMM_NAMESPACE, "InstanceID", _converter_string))
"""
An identifier for a specific incarnation of a document, updated each
time a file is saved.
"""
def custom_properties(self):
if not hasattr(self, "_custom_properties"):
self._custom_properties = {}
for node in self.getNodesInNamespace("", PDFX_NAMESPACE):
key = node.localName
while True:
# see documentation about PDFX_NAMESPACE earlier in file
idx = key.find(u_("\u2182"))
if idx == -1:
break
key = key[:idx] + chr(int(key[idx+1:idx+5], base=16)) + key[idx+5:]
if node.nodeType == node.ATTRIBUTE_NODE:
value = node.nodeValue
else:
value = self._getText(node)
self._custom_properties[key] = value
return self._custom_properties
custom_properties = property(custom_properties)
"""
Retrieves custom metadata properties defined in the undocumented pdfx
metadata schema.
:return: a dictionary of key/value items for custom metadata properties.
:rtype: dict
"""

1
vendor/_version.py vendored
View File

@ -1 +0,0 @@
__version__ = '5.0.6'

28
vendor/babel/AUTHORS vendored
View File

@ -1,28 +0,0 @@
Babel is written and maintained by the Babel team and various contributors:
Maintainer and Current Project Lead:
- Armin Ronacher <armin.ronacher@active-4.com>
Contributors:
- Christopher Lenz <cmlenz@gmail.com>
- Alex Morega <alex@grep.ro>
- Felix Schwarz <felix.schwarz@oss.schwarz.eu>
- Pedro Algarvio <pedro@algarvio.me>
- Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>
- Philip Jenvey <pjenvey@underboss.org>
- Tobias Bieniek <Tobias.Bieniek@gmx.de>
- Jonas Borgström <jonas@edgewall.org>
- Daniel Neuhäuser <dasdasich@gmail.com>
- Nick Retallack <nick@bitcasa.com>
- Thomas Waldmann <tw@waldmann-edv.de>
- Lennart Regebro <regebro@gmail.com>
Babel was previously developed under the Copyright of Edgewall Software. The
following copyright notice holds true for releases before 2013: "Copyright (c)
2007 - 2011 by Edgewall Software"
In addition to the regular contributions Babel includes a fork of Lennart
Regebro's tzlocal that originally was licensed under the CC0 license. The
original copyright of that project is "Copyright 2013 by Lennart Regebro".

29
vendor/babel/LICENSE vendored
View File

@ -1,29 +0,0 @@
Copyright (C) 2013 by the Babel Team, see AUTHORS for more information.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. 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.
3. The name of the author may not be used to endorse or promote
products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR "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 AUTHOR 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, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
"""
babel
~~~~~
Integrated collection of utilities that assist in internationalizing and
localizing applications.
This package is basically composed of two major parts:
* tools to build and work with ``gettext`` message catalogs
* a Python interface to the CLDR (Common Locale Data Repository), providing
access to various locale display names, localized number and date
formatting, etc.
:copyright: (c) 2013 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
from babel.core import UnknownLocaleError, Locale, default_locale, \
negotiate_locale, parse_locale, get_locale_identifier
__version__ = '1.3'

View File

@ -1,51 +0,0 @@
import sys
PY2 = sys.version_info[0] == 2
_identity = lambda x: x
if not PY2:
text_type = str
string_types = (str,)
integer_types = (int, )
unichr = chr
text_to_native = lambda s, enc: s
iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items())
from io import StringIO, BytesIO
import pickle
izip = zip
imap = map
range_type = range
cmp = lambda a, b: (a > b) - (a < b)
else:
text_type = unicode
string_types = (str, unicode)
integer_types = (int, long)
text_to_native = lambda s, enc: s.encode(enc)
unichr = unichr
iterkeys = lambda d: d.iterkeys()
itervalues = lambda d: d.itervalues()
iteritems = lambda d: d.iteritems()
from cStringIO import StringIO as BytesIO
from StringIO import StringIO
import cPickle as pickle
from itertools import izip, imap
range_type = xrange
cmp = cmp
number_types = integer_types + (float,)

941
vendor/babel/core.py vendored
View File

@ -1,941 +0,0 @@
# -*- coding: utf-8 -*-
"""
babel.core
~~~~~~~~~~
Core locale representation and locale data access.
:copyright: (c) 2013 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
import os
from babel import localedata
from babel._compat import pickle, string_types
__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale',
'parse_locale']
_global_data = None
def _raise_no_data_error():
raise RuntimeError('The babel data files are not available. '
'This usually happens because you are using '
'a source checkout from Babel and you did '
'not build the data files. Just make sure '
'to run "python setup.py import_cldr" before '
'installing the library.')
def get_global(key):
"""Return the dictionary for the given key in the global data.
The global data is stored in the ``babel/global.dat`` file and contains
information independent of individual locales.
>>> get_global('zone_aliases')['UTC']
u'Etc/GMT'
>>> get_global('zone_territories')['Europe/Berlin']
u'DE'
.. versionadded:: 0.9
:param key: the data key
"""
global _global_data
if _global_data is None:
dirname = os.path.join(os.path.dirname(__file__))
filename = os.path.join(dirname, 'global.dat')
if not os.path.isfile(filename):
_raise_no_data_error()
fileobj = open(filename, 'rb')
try:
_global_data = pickle.load(fileobj)
finally:
fileobj.close()
return _global_data.get(key, {})
LOCALE_ALIASES = {
'ar': 'ar_SY', 'bg': 'bg_BG', 'bs': 'bs_BA', 'ca': 'ca_ES', 'cs': 'cs_CZ',
'da': 'da_DK', 'de': 'de_DE', 'el': 'el_GR', 'en': 'en_US', 'es': 'es_ES',
'et': 'et_EE', 'fa': 'fa_IR', 'fi': 'fi_FI', 'fr': 'fr_FR', 'gl': 'gl_ES',
'he': 'he_IL', 'hu': 'hu_HU', 'id': 'id_ID', 'is': 'is_IS', 'it': 'it_IT',
'ja': 'ja_JP', 'km': 'km_KH', 'ko': 'ko_KR', 'lt': 'lt_LT', 'lv': 'lv_LV',
'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL',
'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI',
'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA'
}
class UnknownLocaleError(Exception):
"""Exception thrown when a locale is requested for which no locale data
is available.
"""
def __init__(self, identifier):
"""Create the exception.
:param identifier: the identifier string of the unsupported locale
"""
Exception.__init__(self, 'unknown locale %r' % identifier)
#: The identifier of the locale that could not be found.
self.identifier = identifier
class Locale(object):
"""Representation of a specific locale.
>>> locale = Locale('en', 'US')
>>> repr(locale)
"Locale('en', territory='US')"
>>> locale.display_name
u'English (United States)'
A `Locale` object can also be instantiated from a raw locale string:
>>> locale = Locale.parse('en-US', sep='-')
>>> repr(locale)
"Locale('en', territory='US')"
`Locale` objects provide access to a collection of locale data, such as
territory and language names, number and date format patterns, and more:
>>> locale.number_symbols['decimal']
u'.'
If a locale is requested for which no locale data is available, an
`UnknownLocaleError` is raised:
>>> Locale.parse('en_DE')
Traceback (most recent call last):
...
UnknownLocaleError: unknown locale 'en_DE'
For more information see :rfc:`3066`.
"""
def __init__(self, language, territory=None, script=None, variant=None):
"""Initialize the locale object from the given identifier components.
>>> locale = Locale('en', 'US')
>>> locale.language
'en'
>>> locale.territory
'US'
:param language: the language code
:param territory: the territory (country or region) code
:param script: the script code
:param variant: the variant code
:raise `UnknownLocaleError`: if no locale data is available for the
requested locale
"""
#: the language code
self.language = language
#: the territory (country or region) code
self.territory = territory
#: the script code
self.script = script
#: the variant code
self.variant = variant
self.__data = None
identifier = str(self)
if not localedata.exists(identifier):
raise UnknownLocaleError(identifier)
@classmethod
def default(cls, category=None, aliases=LOCALE_ALIASES):
"""Return the system default locale for the specified category.
>>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:
... os.environ[name] = ''
>>> os.environ['LANG'] = 'fr_FR.UTF-8'
>>> Locale.default('LC_MESSAGES')
Locale('fr', territory='FR')
The following fallbacks to the variable are always considered:
- ``LANGUAGE``
- ``LC_ALL``
- ``LC_CTYPE``
- ``LANG``
:param category: one of the ``LC_XXX`` environment variable names
:param aliases: a dictionary of aliases for locale identifiers
"""
# XXX: use likely subtag expansion here instead of the
# aliases dictionary.
locale_string = default_locale(category, aliases=aliases)
return cls.parse(locale_string)
@classmethod
def negotiate(cls, preferred, available, sep='_', aliases=LOCALE_ALIASES):
"""Find the best match between available and requested locale strings.
>>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
Locale('de', territory='DE')
>>> Locale.negotiate(['de_DE', 'en_US'], ['en', 'de'])
Locale('de')
>>> Locale.negotiate(['de_DE', 'de'], ['en_US'])
You can specify the character used in the locale identifiers to separate
the differnet components. This separator is applied to both lists. Also,
case is ignored in the comparison:
>>> Locale.negotiate(['de-DE', 'de'], ['en-us', 'de-de'], sep='-')
Locale('de', territory='DE')
:param preferred: the list of locale identifers preferred by the user
:param available: the list of locale identifiers available
:param aliases: a dictionary of aliases for locale identifiers
"""
identifier = negotiate_locale(preferred, available, sep=sep,
aliases=aliases)
if identifier:
return Locale.parse(identifier, sep=sep)
@classmethod
def parse(cls, identifier, sep='_', resolve_likely_subtags=True):
"""Create a `Locale` instance for the given locale identifier.
>>> l = Locale.parse('de-DE', sep='-')
>>> l.display_name
u'Deutsch (Deutschland)'
If the `identifier` parameter is not a string, but actually a `Locale`
object, that object is returned:
>>> Locale.parse(l)
Locale('de', territory='DE')
This also can perform resolving of likely subtags which it does
by default. This is for instance useful to figure out the most
likely locale for a territory you can use ``'und'`` as the
language tag:
>>> Locale.parse('und_AT')
Locale('de', territory='AT')
:param identifier: the locale identifier string
:param sep: optional component separator
:param resolve_likely_subtags: if this is specified then a locale will
have its likely subtag resolved if the
locale otherwise does not exist. For
instance ``zh_TW`` by itself is not a
locale that exists but Babel can
automatically expand it to the full
form of ``zh_hant_TW``. Note that this
expansion is only taking place if no
locale exists otherwise. For instance
there is a locale ``en`` that can exist
by itself.
:raise `ValueError`: if the string does not appear to be a valid locale
identifier
:raise `UnknownLocaleError`: if no locale data is available for the
requested locale
"""
if identifier is None:
return None
elif isinstance(identifier, Locale):
return identifier
elif not isinstance(identifier, string_types):
raise TypeError('Unxpected value for identifier: %r' % (identifier,))
parts = parse_locale(identifier, sep=sep)
input_id = get_locale_identifier(parts)
def _try_load(parts):
try:
return cls(*parts)
except UnknownLocaleError:
return None
def _try_load_reducing(parts):
# Success on first hit, return it.
locale = _try_load(parts)
if locale is not None:
return locale
# Now try without script and variant
locale = _try_load(parts[:2])
if locale is not None:
return locale
locale = _try_load(parts)
if locale is not None:
return locale
if not resolve_likely_subtags:
raise UnknownLocaleError(input_id)
# From here onwards is some very bad likely subtag resolving. This
# whole logic is not entirely correct but good enough (tm) for the
# time being. This has been added so that zh_TW does not cause
# errors for people when they upgrade. Later we should properly
# implement ICU like fuzzy locale objects and provide a way to
# maximize and minimize locale tags.
language, territory, script, variant = parts
language = get_global('language_aliases').get(language, language)
territory = get_global('territory_aliases').get(territory, territory)
script = get_global('script_aliases').get(script, script)
variant = get_global('variant_aliases').get(variant, variant)
if territory == 'ZZ':
territory = None
if script == 'Zzzz':
script = None
parts = language, territory, script, variant
# First match: try the whole identifier
new_id = get_locale_identifier(parts)
likely_subtag = get_global('likely_subtags').get(new_id)
if likely_subtag is not None:
locale = _try_load_reducing(parse_locale(likely_subtag))
if locale is not None:
return locale
# If we did not find anything so far, try again with a
# simplified identifier that is just the language
likely_subtag = get_global('likely_subtags').get(language)
if likely_subtag is not None:
language2, _, script2, variant2 = parse_locale(likely_subtag)
locale = _try_load_reducing((language2, territory, script2, variant2))
if locale is not None:
return locale
raise UnknownLocaleError(input_id)
def __eq__(self, other):
for key in ('language', 'territory', 'script', 'variant'):
if not hasattr(other, key):
return False
return (self.language == other.language) and \
(self.territory == other.territory) and \
(self.script == other.script) and \
(self.variant == other.variant)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
parameters = ['']
for key in ('territory', 'script', 'variant'):
value = getattr(self, key)
if value is not None:
parameters.append('%s=%r' % (key, value))
parameter_string = '%r' % self.language + ', '.join(parameters)
return 'Locale(%s)' % parameter_string
def __str__(self):
return get_locale_identifier((self.language, self.territory,
self.script, self.variant))
@property
def _data(self):
if self.__data is None:
self.__data = localedata.LocaleDataDict(localedata.load(str(self)))
return self.__data
def get_display_name(self, locale=None):
"""Return the display name of the locale using the given locale.
The display name will include the language, territory, script, and
variant, if those are specified.
>>> Locale('zh', 'CN', script='Hans').get_display_name('en')
u'Chinese (Simplified, China)'
:param locale: the locale to use
"""
if locale is None:
locale = self
locale = Locale.parse(locale)
retval = locale.languages.get(self.language)
if self.territory or self.script or self.variant:
details = []
if self.script:
details.append(locale.scripts.get(self.script))
if self.territory:
details.append(locale.territories.get(self.territory))
if self.variant:
details.append(locale.variants.get(self.variant))
details = filter(None, details)
if details:
retval += ' (%s)' % u', '.join(details)
return retval
display_name = property(get_display_name, doc="""\
The localized display name of the locale.
>>> Locale('en').display_name
u'English'
>>> Locale('en', 'US').display_name
u'English (United States)'
>>> Locale('sv').display_name
u'svenska'
:type: `unicode`
""")
def get_language_name(self, locale=None):
"""Return the language of this locale in the given locale.
>>> Locale('zh', 'CN', script='Hans').get_language_name('de')
u'Chinesisch'
.. versionadded:: 1.0
:param locale: the locale to use
"""
if locale is None:
locale = self
locale = Locale.parse(locale)
return locale.languages.get(self.language)
language_name = property(get_language_name, doc="""\
The localized language name of the locale.
>>> Locale('en', 'US').language_name
u'English'
""")
def get_territory_name(self, locale=None):
"""Return the territory name in the given locale."""
if locale is None:
locale = self
locale = Locale.parse(locale)
return locale.territories.get(self.territory)
territory_name = property(get_territory_name, doc="""\
The localized territory name of the locale if available.
>>> Locale('de', 'DE').territory_name
u'Deutschland'
""")
def get_script_name(self, locale=None):
"""Return the script name in the given locale."""
if locale is None:
locale = self
locale = Locale.parse(locale)
return locale.scripts.get(self.script)
script_name = property(get_script_name, doc="""\
The localized script name of the locale if available.
>>> Locale('ms', 'SG', script='Latn').script_name
u'Latin'
""")
@property
def english_name(self):
"""The english display name of the locale.
>>> Locale('de').english_name
u'German'
>>> Locale('de', 'DE').english_name
u'German (Germany)'
:type: `unicode`"""
return self.get_display_name(Locale('en'))
#{ General Locale Display Names
@property
def languages(self):
"""Mapping of language codes to translated language names.
>>> Locale('de', 'DE').languages['ja']
u'Japanisch'
See `ISO 639 <http://www.loc.gov/standards/iso639-2/>`_ for
more information.
"""
return self._data['languages']
@property
def scripts(self):
"""Mapping of script codes to translated script names.
>>> Locale('en', 'US').scripts['Hira']
u'Hiragana'
See `ISO 15924 <http://www.evertype.com/standards/iso15924/>`_
for more information.
"""
return self._data['scripts']
@property
def territories(self):
"""Mapping of script codes to translated script names.
>>> Locale('es', 'CO').territories['DE']
u'Alemania'
See `ISO 3166 <http://www.iso.org/iso/en/prods-services/iso3166ma/>`_
for more information.
"""
return self._data['territories']
@property
def variants(self):
"""Mapping of script codes to translated script names.
>>> Locale('de', 'DE').variants['1901']
u'Alte deutsche Rechtschreibung'
"""
return self._data['variants']
#{ Number Formatting
@property
def currencies(self):
"""Mapping of currency codes to translated currency names. This
only returns the generic form of the currency name, not the count
specific one. If an actual number is requested use the
:func:`babel.numbers.get_currency_name` function.
>>> Locale('en').currencies['COP']
u'Colombian Peso'
>>> Locale('de', 'DE').currencies['COP']
u'Kolumbianischer Peso'
"""
return self._data['currency_names']
@property
def currency_symbols(self):
"""Mapping of currency codes to symbols.
>>> Locale('en', 'US').currency_symbols['USD']
u'$'
>>> Locale('es', 'CO').currency_symbols['USD']
u'US$'
"""
return self._data['currency_symbols']
@property
def number_symbols(self):
"""Symbols used in number formatting.
>>> Locale('fr', 'FR').number_symbols['decimal']
u','
"""
return self._data['number_symbols']
@property
def decimal_formats(self):
"""Locale patterns for decimal number formatting.
>>> Locale('en', 'US').decimal_formats[None]
<NumberPattern u'#,##0.###'>
"""
return self._data['decimal_formats']
@property
def currency_formats(self):
"""Locale patterns for currency number formatting.
>>> print Locale('en', 'US').currency_formats[None]
<NumberPattern u'\\xa4#,##0.00'>
"""
return self._data['currency_formats']
@property
def percent_formats(self):
"""Locale patterns for percent number formatting.
>>> Locale('en', 'US').percent_formats[None]
<NumberPattern u'#,##0%'>
"""
return self._data['percent_formats']
@property
def scientific_formats(self):
"""Locale patterns for scientific number formatting.
>>> Locale('en', 'US').scientific_formats[None]
<NumberPattern u'#E0'>
"""
return self._data['scientific_formats']
#{ Calendar Information and Date Formatting
@property
def periods(self):
"""Locale display names for day periods (AM/PM).
>>> Locale('en', 'US').periods['am']
u'AM'
"""
return self._data['periods']
@property
def days(self):
"""Locale display names for weekdays.
>>> Locale('de', 'DE').days['format']['wide'][3]
u'Donnerstag'
"""
return self._data['days']
@property
def months(self):
"""Locale display names for months.
>>> Locale('de', 'DE').months['format']['wide'][10]
u'Oktober'
"""
return self._data['months']
@property
def quarters(self):
"""Locale display names for quarters.
>>> Locale('de', 'DE').quarters['format']['wide'][1]
u'1. Quartal'
"""
return self._data['quarters']
@property
def eras(self):
"""Locale display names for eras.
>>> Locale('en', 'US').eras['wide'][1]
u'Anno Domini'
>>> Locale('en', 'US').eras['abbreviated'][0]
u'BC'
"""
return self._data['eras']
@property
def time_zones(self):
"""Locale display names for time zones.
>>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight']
u'British Summer Time'
>>> Locale('en', 'US').time_zones['America/St_Johns']['city']
u'St. John\u2019s'
"""
return self._data['time_zones']
@property
def meta_zones(self):
"""Locale display names for meta time zones.
Meta time zones are basically groups of different Olson time zones that
have the same GMT offset and daylight savings time.
>>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight']
u'Central European Summer Time'
.. versionadded:: 0.9
"""
return self._data['meta_zones']
@property
def zone_formats(self):
"""Patterns related to the formatting of time zones.
>>> Locale('en', 'US').zone_formats['fallback']
u'%(1)s (%(0)s)'
>>> Locale('pt', 'BR').zone_formats['region']
u'Hor\\xe1rio %s'
.. versionadded:: 0.9
"""
return self._data['zone_formats']
@property
def first_week_day(self):
"""The first day of a week, with 0 being Monday.
>>> Locale('de', 'DE').first_week_day
0
>>> Locale('en', 'US').first_week_day
6
"""
return self._data['week_data']['first_day']
@property
def weekend_start(self):
"""The day the weekend starts, with 0 being Monday.
>>> Locale('de', 'DE').weekend_start
5
"""
return self._data['week_data']['weekend_start']
@property
def weekend_end(self):
"""The day the weekend ends, with 0 being Monday.
>>> Locale('de', 'DE').weekend_end
6
"""
return self._data['week_data']['weekend_end']
@property
def min_week_days(self):
"""The minimum number of days in a week so that the week is counted as
the first week of a year or month.
>>> Locale('de', 'DE').min_week_days
4
"""
return self._data['week_data']['min_days']
@property
def date_formats(self):
"""Locale patterns for date formatting.
>>> Locale('en', 'US').date_formats['short']
<DateTimePattern u'M/d/yy'>
>>> Locale('fr', 'FR').date_formats['long']
<DateTimePattern u'd MMMM y'>
"""
return self._data['date_formats']
@property
def time_formats(self):
"""Locale patterns for time formatting.
>>> Locale('en', 'US').time_formats['short']
<DateTimePattern u'h:mm a'>
>>> Locale('fr', 'FR').time_formats['long']
<DateTimePattern u'HH:mm:ss z'>
"""
return self._data['time_formats']
@property
def datetime_formats(self):
"""Locale patterns for datetime formatting.
>>> Locale('en').datetime_formats['full']
u"{1} 'at' {0}"
>>> Locale('th').datetime_formats['medium']
u'{1}, {0}'
"""
return self._data['datetime_formats']
@property
def plural_form(self):
"""Plural rules for the locale.
>>> Locale('en').plural_form(1)
'one'
>>> Locale('en').plural_form(0)
'other'
>>> Locale('fr').plural_form(0)
'one'
>>> Locale('ru').plural_form(100)
'many'
"""
return self._data['plural_form']
def default_locale(category=None, aliases=LOCALE_ALIASES):
"""Returns the system default locale for a given category, based on
environment variables.
>>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE']:
... os.environ[name] = ''
>>> os.environ['LANG'] = 'fr_FR.UTF-8'
>>> default_locale('LC_MESSAGES')
'fr_FR'
The "C" or "POSIX" pseudo-locales are treated as aliases for the
"en_US_POSIX" locale:
>>> os.environ['LC_MESSAGES'] = 'POSIX'
>>> default_locale('LC_MESSAGES')
'en_US_POSIX'
The following fallbacks to the variable are always considered:
- ``LANGUAGE``
- ``LC_ALL``
- ``LC_CTYPE``
- ``LANG``
:param category: one of the ``LC_XXX`` environment variable names
:param aliases: a dictionary of aliases for locale identifiers
"""
varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG')
for name in filter(None, varnames):
locale = os.getenv(name)
if locale:
if name == 'LANGUAGE' and ':' in locale:
# the LANGUAGE variable may contain a colon-separated list of
# language codes; we just pick the language on the list
locale = locale.split(':')[0]
if locale in ('C', 'POSIX'):
locale = 'en_US_POSIX'
elif aliases and locale in aliases:
locale = aliases[locale]
try:
return get_locale_identifier(parse_locale(locale))
except ValueError:
pass
def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES):
"""Find the best match between available and requested locale strings.
>>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
'de_DE'
>>> negotiate_locale(['de_DE', 'en_US'], ['en', 'de'])
'de'
Case is ignored by the algorithm, the result uses the case of the preferred
locale identifier:
>>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at'])
'de_DE'
>>> negotiate_locale(['de_DE', 'en_US'], ['de_de', 'de_at'])
'de_DE'
By default, some web browsers unfortunately do not include the territory
in the locale identifier for many locales, and some don't even allow the
user to easily add the territory. So while you may prefer using qualified
locale identifiers in your web-application, they would not normally match
the language-only locale sent by such browsers. To workaround that, this
function uses a default mapping of commonly used langauge-only locale
identifiers to identifiers including the territory:
>>> negotiate_locale(['ja', 'en_US'], ['ja_JP', 'en_US'])
'ja_JP'
Some browsers even use an incorrect or outdated language code, such as "no"
for Norwegian, where the correct locale identifier would actually be "nb_NO"
(Bokmål) or "nn_NO" (Nynorsk). The aliases are intended to take care of
such cases, too:
>>> negotiate_locale(['no', 'sv'], ['nb_NO', 'sv_SE'])
'nb_NO'
You can override this default mapping by passing a different `aliases`
dictionary to this function, or you can bypass the behavior althogher by
setting the `aliases` parameter to `None`.
:param preferred: the list of locale strings preferred by the user
:param available: the list of locale strings available
:param sep: character that separates the different parts of the locale
strings
:param aliases: a dictionary of aliases for locale identifiers
"""
available = [a.lower() for a in available if a]
for locale in preferred:
ll = locale.lower()
if ll in available:
return locale
if aliases:
alias = aliases.get(ll)
if alias:
alias = alias.replace('_', sep)
if alias.lower() in available:
return alias
parts = locale.split(sep)
if len(parts) > 1 and parts[0].lower() in available:
return parts[0]
return None
def parse_locale(identifier, sep='_'):
"""Parse a locale identifier into a tuple of the form ``(language,
territory, script, variant)``.
>>> parse_locale('zh_CN')
('zh', 'CN', None, None)
>>> parse_locale('zh_Hans_CN')
('zh', 'CN', 'Hans', None)
The default component separator is "_", but a different separator can be
specified using the `sep` parameter:
>>> parse_locale('zh-CN', sep='-')
('zh', 'CN', None, None)
If the identifier cannot be parsed into a locale, a `ValueError` exception
is raised:
>>> parse_locale('not_a_LOCALE_String')
Traceback (most recent call last):
...
ValueError: 'not_a_LOCALE_String' is not a valid locale identifier
Encoding information and locale modifiers are removed from the identifier:
>>> parse_locale('it_IT@euro')
('it', 'IT', None, None)
>>> parse_locale('en_US.UTF-8')
('en', 'US', None, None)
>>> parse_locale('de_DE.iso885915@euro')
('de', 'DE', None, None)
See :rfc:`4646` for more information.
:param identifier: the locale identifier string
:param sep: character that separates the different components of the locale
identifier
:raise `ValueError`: if the string does not appear to be a valid locale
identifier
"""
if '.' in identifier:
# this is probably the charset/encoding, which we don't care about
identifier = identifier.split('.', 1)[0]
if '@' in identifier:
# this is a locale modifier such as @euro, which we don't care about
# either
identifier = identifier.split('@', 1)[0]
parts = identifier.split(sep)
lang = parts.pop(0).lower()
if not lang.isalpha():
raise ValueError('expected only letters, got %r' % lang)
script = territory = variant = None
if parts:
if len(parts[0]) == 4 and parts[0].isalpha():
script = parts.pop(0).title()
if parts:
if len(parts[0]) == 2 and parts[0].isalpha():
territory = parts.pop(0).upper()
elif len(parts[0]) == 3 and parts[0].isdigit():
territory = parts.pop(0)
if parts:
if len(parts[0]) == 4 and parts[0][0].isdigit() or \
len(parts[0]) >= 5 and parts[0][0].isalpha():
variant = parts.pop()
if parts:
raise ValueError('%r is not a valid locale identifier' % identifier)
return lang, territory, script, variant
def get_locale_identifier(tup, sep='_'):
"""The reverse of :func:`parse_locale`. It creates a locale identifier out
of a ``(language, territory, script, variant)`` tuple. Items can be set to
``None`` and trailing ``None``\s can also be left out of the tuple.
>>> get_locale_identifier(('de', 'DE', None, '1999'))
'de_DE_1999'
.. versionadded:: 1.0
:param tup: the tuple as returned by :func:`parse_locale`.
:param sep: the separator for the identifier.
"""
tup = tuple(tup[:4])
lang, territory, script, variant = tup + (None,) * (4 - len(tup))
return sep.join(filter(None, (lang, script, territory, variant)))

1181
vendor/babel/dates.py vendored

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,209 +0,0 @@
# -*- coding: utf-8 -*-
"""
babel.localedata
~~~~~~~~~~~~~~~~
Low-level locale data access.
:note: The `Locale` class, which uses this module under the hood, provides a
more convenient interface for accessing the locale data.
:copyright: (c) 2013 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
import os
import threading
from collections import MutableMapping
from babel._compat import pickle
_cache = {}
_cache_lock = threading.RLock()
_dirname = os.path.join(os.path.dirname(__file__), 'localedata')
def exists(name):
"""Check whether locale data is available for the given locale. Ther
return value is `True` if it exists, `False` otherwise.
:param name: the locale identifier string
"""
if name in _cache:
return True
return os.path.exists(os.path.join(_dirname, '%s.dat' % name))
def locale_identifiers():
"""Return a list of all locale identifiers for which locale data is
available.
.. versionadded:: 0.8.1
:return: a list of locale identifiers (strings)
"""
return [stem for stem, extension in [
os.path.splitext(filename) for filename in os.listdir(_dirname)
] if extension == '.dat' and stem != 'root']
def load(name, merge_inherited=True):
"""Load the locale data for the given locale.
The locale data is a dictionary that contains much of the data defined by
the Common Locale Data Repository (CLDR). This data is stored as a
collection of pickle files inside the ``babel`` package.
>>> d = load('en_US')
>>> d['languages']['sv']
u'Swedish'
Note that the results are cached, and subsequent requests for the same
locale return the same dictionary:
>>> d1 = load('en_US')
>>> d2 = load('en_US')
>>> d1 is d2
True
:param name: the locale identifier string (or "root")
:param merge_inherited: whether the inherited data should be merged into
the data of the requested locale
:raise `IOError`: if no locale data file is found for the given locale
identifer, or one of the locales it inherits from
"""
_cache_lock.acquire()
try:
data = _cache.get(name)
if not data:
# Load inherited data
if name == 'root' or not merge_inherited:
data = {}
else:
parts = name.split('_')
if len(parts) == 1:
parent = 'root'
else:
parent = '_'.join(parts[:-1])
data = load(parent).copy()
filename = os.path.join(_dirname, '%s.dat' % name)
fileobj = open(filename, 'rb')
try:
if name != 'root' and merge_inherited:
merge(data, pickle.load(fileobj))
else:
data = pickle.load(fileobj)
_cache[name] = data
finally:
fileobj.close()
return data
finally:
_cache_lock.release()
def merge(dict1, dict2):
"""Merge the data from `dict2` into the `dict1` dictionary, making copies
of nested dictionaries.
>>> d = {1: 'foo', 3: 'baz'}
>>> merge(d, {1: 'Foo', 2: 'Bar'})
>>> items = d.items(); items.sort(); items
[(1, 'Foo'), (2, 'Bar'), (3, 'baz')]
:param dict1: the dictionary to merge into
:param dict2: the dictionary containing the data that should be merged
"""
for key, val2 in dict2.items():
if val2 is not None:
val1 = dict1.get(key)
if isinstance(val2, dict):
if val1 is None:
val1 = {}
if isinstance(val1, Alias):
val1 = (val1, val2)
elif isinstance(val1, tuple):
alias, others = val1
others = others.copy()
merge(others, val2)
val1 = (alias, others)
else:
val1 = val1.copy()
merge(val1, val2)
else:
val1 = val2
dict1[key] = val1
class Alias(object):
"""Representation of an alias in the locale data.
An alias is a value that refers to some other part of the locale data,
as specified by the `keys`.
"""
def __init__(self, keys):
self.keys = tuple(keys)
def __repr__(self):
return '<%s %r>' % (type(self).__name__, self.keys)
def resolve(self, data):
"""Resolve the alias based on the given data.
This is done recursively, so if one alias resolves to a second alias,
that second alias will also be resolved.
:param data: the locale data
:type data: `dict`
"""
base = data
for key in self.keys:
data = data[key]
if isinstance(data, Alias):
data = data.resolve(base)
elif isinstance(data, tuple):
alias, others = data
data = alias.resolve(base)
return data
class LocaleDataDict(MutableMapping):
"""Dictionary wrapper that automatically resolves aliases to the actual
values.
"""
def __init__(self, data, base=None):
self._data = data
if base is None:
base = data
self.base = base
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def __getitem__(self, key):
orig = val = self._data[key]
if isinstance(val, Alias): # resolve an alias
val = val.resolve(self.base)
if isinstance(val, tuple): # Merge a partial dict with an alias
alias, others = val
val = alias.resolve(self.base).copy()
merge(val, others)
if type(val) is dict: # Return a nested alias-resolving dict
val = LocaleDataDict(val, base=self.base)
if val is not orig:
self._data[key] = val
return val
def __setitem__(self, key, value):
self._data[key] = value
def __delitem__(self, key):
del self._data[key]
def copy(self):
return LocaleDataDict(self._data.copy(), base=self.base)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (Umin_daysqKU weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U unit_patternsq&}q'u.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (Umin_daysqKU weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U unit_patternsq&}q'u.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (Umin_daysqKU weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U unit_patternsq&}q'u.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (U weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U unit_patternsq%}q&u.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (U weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U unit_patternsq%}q&u.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (Umin_daysqKU weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U unit_patternsq&}q'u.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (U weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U unit_patternsq%}q&u.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (U weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U unit_patternsq%}q&u.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (U weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U unit_patternsq%}q&u.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +0,0 @@
}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
}q U week_dataq }q (Umin_daysqKU weekend_startqKU first_dayqKU weekend_endqKuU zone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qU territoriesq}U
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U unit_patternsq&}q'u.

Binary file not shown.

Binary file not shown.

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