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

# Conflicts:
#	cps/translations/pl/LC_MESSAGES/messages.mo
#	cps/translations/pl/LC_MESSAGES/messages.po
pull/178/head
Radosław Kierznowski 7 years ago
commit 55ee323ca1

6
.gitignore vendored

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

@ -6,20 +6,33 @@ import sys
base_path = os.path.dirname(os.path.abspath(__file__)) base_path = os.path.dirname(os.path.abspath(__file__))
# Insert local directories into path # 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 cps import web
from tornado.wsgi import WSGIContainer try:
from tornado.httpserver import HTTPServer from gevent.wsgi import WSGIServer
from tornado.ioloop import IOLoop 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 __name__ == '__main__':
if web.ub.DEVELOPMENT: if web.ub.DEVELOPMENT:
web.app.run(host="0.0.0.0", port=web.ub.config.config_port, debug=True) web.app.run(host="0.0.0.0", port=web.ub.config.config_port, debug=True)
else: else:
http_server = HTTPServer(WSGIContainer(web.app)) if gevent_present:
http_server.listen(web.ub.config.config_port) web.app.logger.info('Attempting to start gevent')
IOLoop.instance().start() 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: if web.helper.global_task == 0:
web.app.logger.info("Performing restart of Calibre-web") web.app.logger.info("Performing restart of Calibre-web")

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

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

@ -5,7 +5,7 @@ import zipfile
from lxml import etree from lxml import etree
import os import os
import uploader import uploader
from iso639 import languages as isoLanguages
def extractCover(zip, coverFile, coverpath, tmp_file_name): def extractCover(zip, coverFile, coverpath, tmp_file_name):
if coverFile is None: if coverFile is None:
@ -41,23 +41,53 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
epub_metadata = {} epub_metadata = {}
for s in ['title', 'description', 'creator']:
for s in ['title', 'description', 'creator', 'language']:
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns) tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
if len(tmp) > 0: if len(tmp) > 0:
epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0] epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0]
else: else:
epub_metadata[s] = "Unknown" 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) coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
coverfile = None
if len(coversection) > 0: if len(coversection) > 0:
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path) coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
else: else:
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover']/@href", namespaces=ns) meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(coversection) > 0: if len(meta_cover) > 0:
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path) coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
else: if len(coversection) > 0:
coverfile = None 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: if epub_metadata['title'] is None:
title = original_file_name title = original_file_name
else: else:
@ -72,4 +102,5 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
description=epub_metadata['description'], description=epub_metadata['description'],
tags="", tags="",
series="", series="",
series_id="") series_id="",
languages=epub_metadata['language'])

@ -4,8 +4,10 @@
from lxml import etree from lxml import etree
import os import os
import uploader 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): 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'' first_name = u''
return first_name + ' ' + middle_name + ' ' + last_name 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) title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
if len(title): if len(title):
title = unicode(title[0]) title = str(title[0])
else: else:
title = u'' title = u''
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns) description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
if len(description): if len(description):
description = unicode(description[0]) description = str(description[0])
else: else:
description = u'' description = u''
@ -59,4 +61,5 @@ def get_fb2_info(tmp_file_path, original_file_extension):
description=description, description=description,
tags="", tags="",
series="", series="",
series_id="") series_id="",
languages="")

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

@ -13,11 +13,18 @@ import os
import traceback import traceback
import re import re
import unicodedata 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 import encoders
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.generator import Generator from email.generator import Generator
from email.utils import formatdate from email.utils import formatdate
from email.utils import make_msgid from email.utils import make_msgid
@ -28,11 +35,16 @@ import shutil
import requests import requests
import zipfile import zipfile
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
try:
import gdriveutils as gd
except ImportError:
pass
import web
try: try:
import unidecode import unidecode
use_unidecode=True use_unidecode=True
except: except Exception as e:
use_unidecode=False use_unidecode=False
# Global variables # Global variables
@ -147,7 +159,7 @@ def send_raw_email(kindle_mail, msg):
smtplib.stderr = org_stderr 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()) app.logger.error(traceback.print_exc())
return _("Failed to send mail: %s" % str(e)) 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=value.replace(u'ß',u'ss')
value = unicodedata.normalize('NFKD', value) value = unicodedata.normalize('NFKD', value)
re_slugify = re.compile('[\W\s-]', re.UNICODE) 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: if replace_whitespace:
#*+:\"/<>? werden durch _ ersetzt #*+:\"/<>? werden durch _ ersetzt
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U) 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] book.path = new_authordir + '/' + book.path.split('/')[1]
db.session.commit() 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): class Updater(threading.Thread):
def __init__(self): def __init__(self):
@ -305,9 +344,13 @@ class Updater(threading.Thread):
ub.session.close() ub.session.close()
ub.engine.dispose() ub.engine.dispose()
self.status=6 self.status=6
# stop tornado server
server = IOLoop.instance() if web.gevent_server:
server.add_callback(server.stop) web.gevent_server.stop()
else:
# stop tornado server
server = IOLoop.instance()
server.add_callback(server.stop)
self.status=7 self.status=7
def get_update_status(self): def get_update_status(self):
@ -379,7 +422,7 @@ class Updater(threading.Thread):
try: try:
os.chown(dst_file, permission.st_uid, permission.st_uid) os.chown(dst_file, permission.st_uid, permission.st_uid)
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.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() e = sys.exc_info()
logging.getLogger('cps.web').debug('Fail '+str(dst_file)+' error: '+str(e)) logging.getLogger('cps.web').debug('Fail '+str(dst_file)+' error: '+str(e))
return return
@ -421,7 +464,7 @@ class Updater(threading.Thread):
logging.getLogger('cps.web').debug("Delete file " + item_path) logging.getLogger('cps.web').debug("Delete file " + item_path)
log_from_thread("Delete file " + item_path) log_from_thread("Delete file " + item_path)
os.remove(item_path) os.remove(item_path)
except: except Exception as e:
logging.getLogger('cps.web').debug("Could not remove:" + item_path) logging.getLogger('cps.web').debug("Could not remove:" + item_path)
shutil.rmtree(source, ignore_errors=True) shutil.rmtree(source, ignore_errors=True)

@ -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);
}
});
});

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

File diff suppressed because it is too large Load Diff

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

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

@ -39,7 +39,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="rating">{{_('Rating')}}</label> <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>
<div class="form-group"> <div class="form-group">
<label for="cover_url">{{_('Cover URL (jpg)')}}</label> <label for="cover_url">{{_('Cover URL (jpg)')}}</label>
@ -104,16 +104,54 @@
<input name="detail_view" type="checkbox" checked> {{_('view book after edit')}} <input name="detail_view" type="checkbox" checked> {{_('view book after edit')}}
</label> </label>
</div> </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> <button type="submit" class="btn btn-default">{{_('Submit')}}</button>
<a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
</form> </form>
</div> </div>
{% endif %} {% 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 %} {% endblock %}
{% block js %} {% 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/libs/typeahead.bundle.js') }}"></script>
<script src="{{ url_for('static', filename='js/edit_books.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 %} {% endblock %}
{% block header %} {% block header %}
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen"> <link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">

@ -7,6 +7,47 @@
<label for="config_calibre_dir">{{_('Location of Calibre database')}}</label> <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"> <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> </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"> <div class="form-group">
<label for="config_port">{{_('Server Port')}}</label> <label for="config_port">{{_('Server Port')}}</label>
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required> <input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required>
@ -23,7 +64,10 @@
<label for="config_random_books">{{_('No. of random books to show')}}</label> <label for="config_random_books">{{_('No. of random books to show')}}</label>
<input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off"> <input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off">
</div> </div>
<div class="form-group">
<label for="config_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"> <div class="form-group">
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label> <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"> <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 %}> <input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Changing Password')}}</label> <label for="passwd_role">{{_('Allow Changing Password')}}</label>
</div> </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> <button type="submit" class="btn btn-default">{{_('Submit')}}</button>
{% if not origin %} {% if not origin %}
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a> <a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
@ -80,3 +128,22 @@
</form> </form>
</div> </div>
{% endblock %} {% 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 %}

@ -2,7 +2,7 @@
{% block body %} {% block body %}
<div class="single"> <div class="single">
<div class="row"> <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"> <div class="cover">
{% if entry.has_cover %} {% if entry.has_cover %}
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" /> <img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" />
@ -107,6 +107,16 @@
</div> </div>
</p> </p>
{% endif %} {% 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%} {% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
@ -126,7 +136,7 @@
</button> </button>
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1"> <ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
{% for format in entry.data %} {% 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%} {%endfor%}
</ul> </ul>
</div> </div>
@ -208,3 +218,34 @@
</div> </div>
{% endblock %} {% 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 %}

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

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

@ -39,6 +39,20 @@
<id>{{url_for('feed_discover')}}</id> <id>{{url_for('feed_discover')}}</id>
<content type="text">{{_('Show Random Books')}}</content> <content type="text">{{_('Show Random Books')}}</content>
</entry> </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> <entry>
<title>{{_('Authors')}}</title> <title>{{_('Authors')}}</title>
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_authorindex')}}"/> <link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_authorindex')}}"/>

@ -128,6 +128,10 @@
{% if g.user.show_best_rated_books() %} {% 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> <li><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span> {{_('Best rated Books')}}</a></li>
{%endif%} {%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() %} {% if g.user.show_random_books() %}
<li id="nav_rand"><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span> {{_('Discover')}}</a></li> <li id="nav_rand"><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span> {{_('Discover')}}</a></li>
{%endif%} {%endif%}

@ -3,10 +3,11 @@
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{ _('Delete this Shelf') }} </a> {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf name') }} </a> <div data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </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>
{% endif %}
{% endif %} {% endif %}
<div class="row"> <div class="row">
@ -39,4 +40,20 @@
{% endfor %} {% endfor %}
</div> </div>
</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 %} {% endblock %}

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

@ -70,6 +70,10 @@
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}> <input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
<label for="show_author">{{_('Show author selection')}}</label> <label for="show_author">{{_('Show author selection')}}</label>
</div> </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"> <div class="form-group">
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}> <input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
<label for="show_detail_random">{{_('Show random books in detail view')}}</label> <label for="show_detail_random">{{_('Show random books in detail view')}}</label>
@ -100,6 +104,10 @@
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}> <input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Changing Password')}}</label> <label for="passwd_role">{{_('Allow Changing Password')}}</label>
</div> </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 %}
{% endif %} {% endif %}
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %} {% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}

@ -21,7 +21,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Calibre-web\n" "Project-Id-Version: Calibre-web\n"
"Report-Msgid-Bugs-To: https://github.com/janeczku/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" "PO-Revision-Date: 2016-07-12 19:54+0200\n"
"Last-Translator: Ozzie Isaacs\n" "Last-Translator: Ozzie Isaacs\n"
"Language: de\n" "Language: de\n"
@ -32,347 +32,360 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\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" msgid "not installed"
msgstr "Nicht installiert" msgstr "Nicht installiert"
#: cps/helper.py:150 #: cps/helper.py:164
#, python-format #, python-format
msgid "Failed to send mail: %s" msgid "Failed to send mail: %s"
msgstr "E-Mail: %s konnte nicht gesendet werden" msgstr "E-Mail: %s konnte nicht gesendet werden"
#: cps/helper.py:157 #: cps/helper.py:171
msgid "Calibre-web test email" msgid "Calibre-web test email"
msgstr "Calibre-web Test E-Mail" 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." msgid "This email has been sent via calibre web."
msgstr "Die E-Mail wurde via calibre-web versendet" 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" msgid "Send to Kindle"
msgstr "An Kindle senden" 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" msgid "Could not find any formats suitable for sending by email"
msgstr "" msgstr ""
"Konnte keine Formate finden welche für das versenden per E-Mail geeignet " "Konnte keine Formate finden welche für das versenden per E-Mail geeignet "
"sind" "sind"
#: cps/helper.py:194 #: cps/helper.py:210
msgid "Could not convert epub to mobi" msgid "Could not convert epub to mobi"
msgstr "Konnte .epub nicht nach .mobi konvertieren" msgstr "Konnte .epub nicht nach .mobi konvertieren"
#: cps/ub.py:434 #: cps/ub.py:488
msgid "Guest" msgid "Guest"
msgstr "Gast" msgstr "Gast"
#: cps/web.py:734 #: cps/web.py:904
msgid "Requesting update package" msgid "Requesting update package"
msgstr "Frage Update Paket an" msgstr "Frage Update Paket an"
#: cps/web.py:735 #: cps/web.py:905
msgid "Downloading update package" msgid "Downloading update package"
msgstr "Lade Update Paket herunter" msgstr "Lade Update Paket herunter"
#: cps/web.py:736 #: cps/web.py:906
msgid "Unzipping update package" msgid "Unzipping update package"
msgstr "Entpacke Update Paket" msgstr "Entpacke Update Paket"
#: cps/web.py:737 #: cps/web.py:907
msgid "Files are replaced" msgid "Files are replaced"
msgstr "Ersetze Dateien" msgstr "Ersetze Dateien"
#: cps/web.py:738 #: cps/web.py:908
msgid "Database connections are closed" msgid "Database connections are closed"
msgstr "Schließe Datenbankverbindungen" msgstr "Schließe Datenbankverbindungen"
#: cps/web.py:739 #: cps/web.py:909
msgid "Server is stopped" msgid "Server is stopped"
msgstr "Stoppe Server" msgstr "Stoppe Server"
#: cps/web.py:740 #: cps/web.py:910
msgid "Update finished, please press okay and reload page" msgid "Update finished, please press okay and reload page"
msgstr "Update abgeschlossen, bitte okay drücken und Seite neu laden" msgstr "Update abgeschlossen, bitte okay drücken und Seite neu laden"
#: cps/web.py:810 #: cps/web.py:983
msgid "Latest Books" msgid "Latest Books"
msgstr "Letzte Bücher" msgstr "Letzte Bücher"
#: cps/web.py:835 #: cps/web.py:1014
msgid "Hot Books (most downloaded)" msgid "Hot Books (most downloaded)"
msgstr "Beliebte Bücher (die meisten Downloads)" msgstr "Beliebte Bücher (die meisten Downloads)"
#: cps/web.py:845 #: cps/web.py:1024
msgid "Best rated books" msgid "Best rated books"
msgstr "Best bewertete Bücher" 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" msgid "Random Books"
msgstr "Zufällige Bücher" msgstr "Zufällige Bücher"
#: cps/web.py:867 #: cps/web.py:1046
msgid "Author list" msgid "Author list"
msgstr "Autorenliste" msgstr "Autorenliste"
#: cps/web.py:878 #: cps/web.py:1057
#, python-format #, python-format
msgid "Author: %(name)s" msgid "Author: %(name)s"
msgstr "Autor: %(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:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
#: cps/web.py:2115 #: cps/web.py:2579
msgid "Error opening eBook. File does not exist or file is not accessible:" msgid "Error opening eBook. File does not exist or file is not accessible:"
msgstr "" msgstr ""
"Buch öffnen fehlgeschlagen. Datei existiert nicht, oder ist nicht " "Buch öffnen fehlgeschlagen. Datei existiert nicht, oder ist nicht "
"zugänglich." "zugänglich."
#: cps/templates/index.xml:57 cps/web.py:894 #: cps/templates/index.xml:71 cps/web.py:1073
msgid "Series list" msgid "Series list"
msgstr "Liste Serien" msgstr "Liste Serien"
#: cps/web.py:906 #: cps/web.py:1085
#, python-format #, python-format
msgid "Series: %(serie)s" msgid "Series: %(serie)s"
msgstr "Serie: %(serie)s" msgstr "Serie: %(serie)s"
#: cps/web.py:939 #: cps/web.py:1118
msgid "Available languages" msgid "Available languages"
msgstr "Verfügbare Sprachen" msgstr "Verfügbare Sprachen"
#: cps/web.py:954 #: cps/web.py:1133
#, python-format #, python-format
msgid "Language: %(name)s" msgid "Language: %(name)s"
msgstr "Sprache: %(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" msgid "Category list"
msgstr "Kategorieliste" msgstr "Kategorieliste"
#: cps/web.py:979 #: cps/web.py:1158
#, python-format #, python-format
msgid "Category: %(name)s" msgid "Category: %(name)s"
msgstr "Kategorie: %(name)s" msgstr "Kategorie: %(name)s"
#: cps/web.py:1040 #: cps/web.py:1267
msgid "Statistics" msgid "Statistics"
msgstr "Statistiken" msgstr "Statistiken"
#: cps/web.py:1061 #: cps/web.py:1375
msgid "Server restarted, please reload page" msgid "Server restarted, please reload page"
msgstr "Server neu gestartet,bitte Seite neu laden" msgstr "Server neu gestartet,bitte Seite neu laden"
#: cps/web.py:1063 #: cps/web.py:1377
msgid "Performing shutdown of server, please close window" msgid "Performing shutdown of server, please close window"
msgstr "Server wird runtergefahren, bitte Fenster schließen" msgstr "Server wird runtergefahren, bitte Fenster schließen"
#: cps/web.py:1073 #: cps/web.py:1392
msgid "Update done" msgid "Update done"
msgstr "Update durchgeführt" msgstr "Update durchgeführt"
#: cps/web.py:1147 cps/web.py:1160 #: cps/web.py:1470 cps/web.py:1483
msgid "search" msgid "search"
msgstr "Suche" 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" msgid "Read a Book"
msgstr "Lese ein Buch" 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!" msgid "Please fill out all fields!"
msgstr "Bitte alle Felder ausfüllen!" 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" msgid "register"
msgstr "Registieren" msgstr "Registieren"
#: cps/web.py:1292 #: cps/web.py:1692
msgid "An unknown error occured. Please try again later." msgid "An unknown error occured. Please try again later."
msgstr "Es ist ein unbekannter Fehler aufgetreten. Bitte später erneut versuchen." 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." msgid "This username or email address is already in use."
msgstr "Der Benutzername oder die E-Mailadresse ist in bereits in Benutzung." msgstr "Der Benutzername oder die E-Mailadresse ist in bereits in Benutzung."
#: cps/web.py:1315 #: cps/web.py:1715
#, python-format #, python-format
msgid "you are now logged in as: '%(nickname)s'" msgid "you are now logged in as: '%(nickname)s'"
msgstr "Du bist nun eingeloggt als '%(nickname)s'" msgstr "Du bist nun eingeloggt als '%(nickname)s'"
#: cps/web.py:1320 #: cps/web.py:1720
msgid "Wrong Username or Password" msgid "Wrong Username or Password"
msgstr "Falscher Benutzername oder Passwort" msgstr "Falscher Benutzername oder Passwort"
#: cps/web.py:1322 #: cps/web.py:1722
msgid "login" msgid "login"
msgstr "Login" msgstr "Login"
#: cps/web.py:1339 #: cps/web.py:1739
msgid "Please configure the SMTP mail settings first..." msgid "Please configure the SMTP mail settings first..."
msgstr "Bitte zuerst die SMTP Mail Einstellung konfigurieren ..." msgstr "Bitte zuerst die SMTP Mail Einstellung konfigurieren ..."
#: cps/web.py:1343 #: cps/web.py:1743
#, python-format #, python-format
msgid "Book successfully send to %(kindlemail)s" msgid "Book successfully send to %(kindlemail)s"
msgstr "Buch erfolgreich versandt an %(kindlemail)s" msgstr "Buch erfolgreich versandt an %(kindlemail)s"
#: cps/web.py:1347 #: cps/web.py:1747
#, python-format #, python-format
msgid "There was an error sending this book: %(res)s" msgid "There was an error sending this book: %(res)s"
msgstr "Beim Senden des Buchs trat ein Fehler auf: %(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..." msgid "Please configure your kindle email address first..."
msgstr "Bitte die Kindle E-Mail Adresse zuuerst konfigurieren..." msgstr "Bitte die Kindle E-Mail Adresse zuuerst konfigurieren..."
#: cps/web.py:1369 #: cps/web.py:1774
#, python-format #, python-format
msgid "Book has been added to shelf: %(sname)s" msgid "Book has been added to shelf: %(sname)s"
msgstr "Das Buch wurde dem Bücherregal: %(sname)s hinzugefügt" msgstr "Das Buch wurde dem Bücherregal: %(sname)s hinzugefügt"
#: cps/web.py:1390 #: cps/web.py:1793
#, python-format #, python-format
msgid "Book has been removed from shelf: %(sname)s" msgid "Book has been removed from shelf: %(sname)s"
msgstr "Das Buch wurde aus dem Bücherregal: %(sname)s entfernt" 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 #, python-format
msgid "A shelf with the name '%(title)s' already exists." msgid "A shelf with the name '%(title)s' already exists."
msgstr "Es existiert bereits ein Bücheregal mit dem Titel '%(title)s'" msgstr "Es existiert bereits ein Bücheregal mit dem Titel '%(title)s'"
#: cps/web.py:1414 #: cps/web.py:1817
#, python-format #, python-format
msgid "Shelf %(title)s created" msgid "Shelf %(title)s created"
msgstr "Bücherregal %(title)s erzeugt" 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" msgid "There was an error"
msgstr "Es trat ein Fehler auf" 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" msgid "create a shelf"
msgstr "Bücherregal erzeugen" msgstr "Bücherregal erzeugen"
#: cps/web.py:1442 #: cps/web.py:1845
#, python-format #, python-format
msgid "Shelf %(title)s changed" msgid "Shelf %(title)s changed"
msgstr "Bücherregal %(title)s verändert" 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" msgid "Edit a shelf"
msgstr "Bücherregal editieren" msgstr "Bücherregal editieren"
#: cps/web.py:1465 #: cps/web.py:1868
#, python-format #, python-format
msgid "successfully deleted shelf %(name)s" msgid "successfully deleted shelf %(name)s"
msgstr "Bücherregal %(name)s erfolgreich gelöscht" msgstr "Bücherregal %(name)s erfolgreich gelöscht"
#: cps/web.py:1487 #: cps/web.py:1890
#, python-format #, python-format
msgid "Shelf: '%(name)s'" msgid "Shelf: '%(name)s'"
msgstr "Bücherregal: '%(name)s'" msgstr "Bücherregal: '%(name)s'"
#: cps/web.py:1518 #: cps/web.py:1921
#, python-format #, python-format
msgid "Change order of Shelf: '%(name)s'" msgid "Change order of Shelf: '%(name)s'"
msgstr "Reihenfolge in Bücherregal '%(name)s' verändern" 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." msgid "Found an existing account for this email address."
msgstr "Es existiert ein Benutzerkonto für diese E-Mailadresse" 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 #, python-format
msgid "%(name)s's profile" msgid "%(name)s's profile"
msgstr "%(name)s's Profil" msgstr "%(name)s's Profil"
#: cps/web.py:1583 #: cps/web.py:1988
msgid "Profile updated" msgid "Profile updated"
msgstr "Profil aktualisiert" msgstr "Profil aktualisiert"
#: cps/web.py:1597 #: cps/web.py:2002
msgid "Admin page" msgid "Admin page"
msgstr "Admin Seite" msgstr "Admin Seite"
#: cps/web.py:1668 #: cps/web.py:2106
msgid "Calibre-web configuration updated" msgid "Calibre-web configuration updated"
msgstr "Calibre-web Konfiguration wurde aktualisiert" 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" msgid "Basic Configuration"
msgstr "Basis Konfiguration" msgstr "Basis Konfiguration"
#: cps/web.py:1679 #: cps/web.py:2117
msgid "DB location is not valid, please enter correct path" msgid "DB location is not valid, please enter correct path"
msgstr "DB Speicherort ist ungültig, bitte Pfad korrigieren" 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" msgid "Add new user"
msgstr "Neuen Benutzer hinzufügen" msgstr "Neuen Benutzer hinzufügen"
#: cps/web.py:1753 #: cps/web.py:2194
#, python-format #, python-format
msgid "User '%(user)s' created" msgid "User '%(user)s' created"
msgstr "Benutzer '%(user)s' angelegt" msgstr "Benutzer '%(user)s' angelegt"
#: cps/web.py:1757 #: cps/web.py:2198
msgid "Found an existing account for this email address or nickname." msgid "Found an existing account for this email address or nickname."
msgstr "" msgstr ""
"Es existiert ein Benutzerkonto für diese Emailadresse oder den " "Es existiert ein Benutzerkonto für diese Emailadresse oder den "
"Benutzernamen." "Benutzernamen."
#: cps/web.py:1779 #: cps/web.py:2220
msgid "Mail settings updated" msgid "Mail settings updated"
msgstr "E-Mail Einstellungen aktualisiert" msgstr "E-Mail Einstellungen aktualisiert"
#: cps/web.py:1785 #: cps/web.py:2227
#, python-format #, python-format
msgid "Test E-Mail successfully send to %(kindlemail)s" msgid "Test E-Mail successfully send to %(kindlemail)s"
msgstr "Test E-Mail erfolgreich an %(kindlemail)s versendet" msgstr "Test E-Mail erfolgreich an %(kindlemail)s versendet"
#: cps/web.py:1788 #: cps/web.py:2230
#, python-format #, python-format
msgid "There was an error sending the Test E-Mail: %(res)s" msgid "There was an error sending the Test E-Mail: %(res)s"
msgstr "Fehler beim versenden der 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" msgid "Edit mail settings"
msgstr "E-Mail Einstellungen editieren" msgstr "E-Mail Einstellungen editieren"
#: cps/web.py:1817 #: cps/web.py:2263
#, python-format #, python-format
msgid "User '%(nick)s' deleted" msgid "User '%(nick)s' deleted"
msgstr "Benutzer '%(nick)s' gelöscht" msgstr "Benutzer '%(nick)s' gelöscht"
#: cps/web.py:1898 #: cps/web.py:2349
#, python-format #, python-format
msgid "User '%(nick)s' updated" msgid "User '%(nick)s' updated"
msgstr "Benutzer '%(nick)s' aktualisiert" msgstr "Benutzer '%(nick)s' aktualisiert"
#: cps/web.py:1901 #: cps/web.py:2352
msgid "An unknown error occured." msgid "An unknown error occured."
msgstr "Es ist ein unbekanter Fehler aufgetreten" msgstr "Es ist ein unbekanter Fehler aufgetreten"
#: cps/web.py:1904 #: cps/web.py:2355
#, python-format #, python-format
msgid "Edit User %(nick)s" msgid "Edit User %(nick)s"
msgstr "Benutzer %(nick)s bearbeiten" 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" msgid "edit metadata"
msgstr "Metadaten editieren" 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 #, python-format
msgid "Failed to create path %s (Permission denied)." msgid "Failed to create path %s (Permission denied)."
msgstr "Fehler beim Erzeugen des Pfads %s (Zugriff verweigert)" msgstr "Fehler beim Erzeugen des Pfads %s (Zugriff verweigert)"
#: cps/web.py:2150 #: cps/web.py:2626
#, python-format #, python-format
msgid "Failed to store file %s (Permission denied)." msgid "Failed to store file %s (Permission denied)."
msgstr "Fehler beim speichern der Datei %s (Zugriff verweigert)" msgstr "Fehler beim speichern der Datei %s (Zugriff verweigert)"
#: cps/web.py:2155 #: cps/web.py:2631
#, python-format #, python-format
msgid "Failed to delete file %s (Permission denied)." msgid "Failed to delete file %s (Permission denied)."
msgstr "Fehler beim Löschen von Datei %s (Zugriff verweigert)" msgstr "Fehler beim Löschen von Datei %s (Zugriff verweigert)"
@ -401,7 +414,7 @@ msgstr "DLS"
msgid "Admin" msgid "Admin"
msgstr "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" msgid "Download"
msgstr "Download" msgstr "Download"
@ -457,7 +470,7 @@ msgstr "Konfiguration"
msgid "Calibre DB dir" msgid "Calibre DB dir"
msgstr "Calibre DB Pfad" 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" msgid "Log Level"
msgstr "Log Level" msgstr "Log Level"
@ -465,7 +478,7 @@ msgstr "Log Level"
msgid "Port" msgid "Port"
msgstr "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" msgid "Books per page"
msgstr "Bücher pro Seite" msgstr "Bücher pro Seite"
@ -494,42 +507,46 @@ msgid "Newest commit timestamp"
msgstr "Neuestes Commit Datum" msgstr "Neuestes Commit Datum"
#: cps/templates/admin.html:83 #: cps/templates/admin.html:83
msgid "Reconnect to Calibre DB"
msgstr "Calibre-DB neu verbinden"
#: cps/templates/admin.html:84
msgid "Restart Calibre-web" msgid "Restart Calibre-web"
msgstr "Calibre-web Neustarten" msgstr "Calibre-web Neustarten"
#: cps/templates/admin.html:84 #: cps/templates/admin.html:85
msgid "Stop Calibre-web" msgid "Stop Calibre-web"
msgstr "Stoppe Calibre-web" msgstr "Stoppe Calibre-web"
#: cps/templates/admin.html:85 #: cps/templates/admin.html:86
msgid "Check for update" msgid "Check for update"
msgstr "Suche nach Update" msgstr "Suche nach Update"
#: cps/templates/admin.html:86 #: cps/templates/admin.html:87
msgid "Perform Update" msgid "Perform Update"
msgstr "Update durchführen" msgstr "Update durchführen"
#: cps/templates/admin.html:96 #: cps/templates/admin.html:97
msgid "Do you really want to restart Calibre-web?" msgid "Do you really want to restart Calibre-web?"
msgstr "Calibre-web wirklich neustarten?" msgstr "Calibre-web wirklich neustarten?"
#: cps/templates/admin.html:101 cps/templates/admin.html:115 #: cps/templates/admin.html:102 cps/templates/admin.html:116
#: cps/templates/admin.html:136 #: cps/templates/admin.html:137
msgid "Ok" msgid "Ok"
msgstr "Ok" msgstr "Ok"
#: cps/templates/admin.html:102 cps/templates/admin.html:116 #: cps/templates/admin.html:103 cps/templates/admin.html:117
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75 #: 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/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" msgid "Back"
msgstr "Zurück" msgstr "Zurück"
#: cps/templates/admin.html:114 #: cps/templates/admin.html:115
msgid "Do you really want to stop Calibre-web?" msgid "Do you really want to stop Calibre-web?"
msgstr "Calibre-web wirklich stoppen" msgstr "Calibre-web wirklich stoppen"
#: cps/templates/admin.html:127 #: cps/templates/admin.html:128
msgid "Updating, please do not reload page" msgid "Updating, please do not reload page"
msgstr "Updatevorgang, bitte Seite nicht neu laden" msgstr "Updatevorgang, bitte Seite nicht neu laden"
@ -537,20 +554,21 @@ msgstr "Updatevorgang, bitte Seite nicht neu laden"
msgid "Book Title" msgid "Book Title"
msgstr "Buchtitel" 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" msgid "Author"
msgstr "Autor" msgstr "Autor"
#: cps/templates/book_edit.html:24 #: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
msgid "Description" msgid "Description"
msgstr "Beschreibung" 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" msgid "Tags"
msgstr "Tags" msgstr "Tags"
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138 #: cps/templates/book_edit.html:33 cps/templates/layout.html:142
#: cps/templates/search_form.html:33 #: cps/templates/search_form.html:37
msgid "Series" msgid "Series"
msgstr "Serien" msgstr "Serien"
@ -582,69 +600,142 @@ msgstr "Nein"
msgid "view book after edit" msgid "view book after edit"
msgstr "Buch nach Bearbeitung ansehen" msgstr "Buch nach Bearbeitung ansehen"
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73 #: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
#: cps/templates/login.html:19 cps/templates/search_form.html:75 msgid "Get metadata"
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109 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" msgid "Submit"
msgstr "Abschicken" 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 #: cps/templates/config_edit.html:7
msgid "Location of Calibre database" msgid "Location of Calibre database"
msgstr "Speicherort der Calibre Datenbank" 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" msgid "Server Port"
msgstr "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" msgid "Title"
msgstr "Titel" msgstr "Titel"
#: cps/templates/config_edit.html:23 #: cps/templates/config_edit.html:64
msgid "No. of random books to show" msgid "No. of random books to show"
msgstr "Anzahl Anzeige zufällige Bücher" 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" msgid "Regular expression for title sorting"
msgstr "Regulärer Ausdruck für Titelsortierung" msgstr "Regulärer Ausdruck für Titelsortierung"
#: cps/templates/config_edit.html:42 #: cps/templates/config_edit.html:86
msgid "Enable uploading" msgid "Enable uploading"
msgstr "Hochladen aktivieren" msgstr "Hochladen aktivieren"
#: cps/templates/config_edit.html:46 #: cps/templates/config_edit.html:90
msgid "Enable anonymous browsing" msgid "Enable anonymous browsing"
msgstr "Anonymes Browsen aktivieren" msgstr "Anonymes Browsen aktivieren"
#: cps/templates/config_edit.html:50 #: cps/templates/config_edit.html:94
msgid "Enable public registration" msgid "Enable public registration"
msgstr "Öffentliche Registrierung aktivieren" msgstr "Öffentliche Registrierung aktivieren"
#: cps/templates/config_edit.html:52 #: cps/templates/config_edit.html:96
msgid "Default Settings for new users" msgid "Default Settings for new users"
msgstr "Default Einstellungen für neue Benutzer" 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" msgid "Admin user"
msgstr "Admin Benutzer" 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" msgid "Allow Downloads"
msgstr "Downloads erlauben" 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" msgid "Allow Uploads"
msgstr "Uploads erlauben" 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" msgid "Allow Edit"
msgstr "Bearbeiten erlauben" 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" msgid "Allow Changing Password"
msgstr "Passwort ändern erlauben" 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 #: cps/templates/login.html:4
msgid "Login" msgid "Login"
msgstr "Login" msgstr "Login"
@ -661,23 +752,27 @@ msgstr "von"
msgid "language" msgid "language"
msgstr "Sprache" msgstr "Sprache"
#: cps/templates/detail.html:74 #: cps/templates/detail.html:81
msgid "Publishing date" msgid "Publishing date"
msgstr "Herausgabedatum" msgstr "Herausgabedatum"
#: cps/templates/detail.html:106 #: cps/templates/detail.html:115
msgid "Read"
msgstr "Gelesen"
#: cps/templates/detail.html:123
msgid "Description:" msgid "Description:"
msgstr "Beschreibung" msgstr "Beschreibung"
#: cps/templates/detail.html:134 #: cps/templates/detail.html:151
msgid "Read in browser" msgid "Read in browser"
msgstr "Im Browser lesen" msgstr "Im Browser lesen"
#: cps/templates/detail.html:154 #: cps/templates/detail.html:171
msgid "Add to shelf" msgid "Add to shelf"
msgstr "Zu Bücherregal hinzufügen" msgstr "Zu Bücherregal hinzufügen"
#: cps/templates/detail.html:194 #: cps/templates/detail.html:211
msgid "Edit metadata" msgid "Edit metadata"
msgstr "Metadaten bearbeiten" msgstr "Metadaten bearbeiten"
@ -759,19 +854,29 @@ msgstr "Die neuesten Bücher"
msgid "Show Random Books" msgid "Show Random Books"
msgstr "Zeige zufällige Bücher" 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" msgid "Authors"
msgstr "Autoren" msgstr "Autoren"
#: cps/templates/index.xml:47 #: cps/templates/index.xml:61
msgid "Books ordered by Author" msgid "Books ordered by Author"
msgstr "Bücher nach Autoren sortiert" msgstr "Bücher nach Autoren sortiert"
#: cps/templates/index.xml:54 #: cps/templates/index.xml:68
msgid "Books ordered by category" msgid "Books ordered by category"
msgstr "Bücher nach Kategorien sortiert" msgstr "Bücher nach Kategorien sortiert"
#: cps/templates/index.xml:61 #: cps/templates/index.xml:75
msgid "Books ordered by series" msgid "Books ordered by series"
msgstr "Bücher nach Reihen geordnet" msgstr "Bücher nach Reihen geordnet"
@ -779,10 +884,6 @@ msgstr "Bücher nach Reihen geordnet"
msgid "Toggle navigation" msgid "Toggle navigation"
msgstr "Nagivation umschalten" msgstr "Nagivation umschalten"
#: cps/templates/layout.html:60
msgid "Go!"
msgstr "Los!"
#: cps/templates/layout.html:68 #: cps/templates/layout.html:68
msgid "Advanced Search" msgid "Advanced Search"
msgstr "Erweiterte Suche" msgstr "Erweiterte Suche"
@ -799,31 +900,31 @@ msgstr "Registrieren"
msgid "Browse" msgid "Browse"
msgstr "Browsen" msgstr "Browsen"
#: cps/templates/layout.html:132 #: cps/templates/layout.html:136
msgid "Discover" msgid "Discover"
msgstr "Entdecke" msgstr "Entdecke"
#: cps/templates/layout.html:135 #: cps/templates/layout.html:139
msgid "Categories" msgid "Categories"
msgstr "Kategorien" 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" msgid "Languages"
msgstr "Sprachen" msgstr "Sprachen"
#: cps/templates/layout.html:145 #: cps/templates/layout.html:149
msgid "Public Shelves" msgid "Public Shelves"
msgstr "Öffentiche Bücherregale" msgstr "Öffentiche Bücherregale"
#: cps/templates/layout.html:149 #: cps/templates/layout.html:153
msgid "Your Shelves" msgid "Your Shelves"
msgstr "Deine Bücherregale" msgstr "Deine Bücherregale"
#: cps/templates/layout.html:154 #: cps/templates/layout.html:158
msgid "Create a Shelf" msgid "Create a Shelf"
msgstr "Bücherregal erzeugen" msgstr "Bücherregal erzeugen"
#: cps/templates/layout.html:155 #: cps/templates/layout.html:159
msgid "About" msgid "About"
msgstr "Über" msgstr "Über"
@ -889,15 +990,15 @@ msgstr "Versuche eine andere Suche"
msgid "Results for:" msgid "Results for:"
msgstr "Ergebnisse für:" msgstr "Ergebnisse für:"
#: cps/templates/search_form.html:23 #: cps/templates/search_form.html:27
msgid "Exclude Tags" msgid "Exclude Tags"
msgstr "Tags ausschließen" msgstr "Tags ausschließen"
#: cps/templates/search_form.html:43 #: cps/templates/search_form.html:47
msgid "Exclude Series" msgid "Exclude Series"
msgstr "Serie ausschließen" msgstr "Serie ausschließen"
#: cps/templates/search_form.html:64 #: cps/templates/search_form.html:68
msgid "Exclude Languages" msgid "Exclude Languages"
msgstr "Sprache ausschließen" msgstr "Sprache ausschließen"
@ -922,37 +1023,37 @@ msgid "Drag 'n drop to rearrange order"
msgstr "Drag 'n drop um Reihenfolge zu ändern" msgstr "Drag 'n drop um Reihenfolge zu ändern"
#: cps/templates/stats.html:3 #: 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" msgid "Calibre library statistics"
msgstr "Calibre Bibliothek Statistiken" msgstr "Calibre Bibliothek Statistiken"
#: cps/templates/stats.html:37 #: cps/templates/stats.html:8
msgid "Books in this Library" msgid "Books in this Library"
msgstr "Bücher in dieser Bibliothek" msgstr "Bücher in dieser Bibliothek"
#: cps/templates/stats.html:41 #: cps/templates/stats.html:12
msgid "Authors in this Library" msgid "Authors in this Library"
msgstr "Autoren in dieser Bibliothek" msgstr "Autoren in dieser Bibliothek"
#: cps/templates/stats.html:45 #: cps/templates/stats.html:16
msgid "Categories in this Library" msgid "Categories in this Library"
msgstr "Kategorien in dieser Bibliothek" msgstr "Kategorien in dieser Bibliothek"
#: cps/templates/stats.html:49 #: cps/templates/stats.html:20
msgid "Series in this Library" msgid "Series in this Library"
msgstr "Serien in dieser Bibliothek" 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 #: cps/templates/user_edit.html:23
msgid "Kindle E-Mail" msgid "Kindle E-Mail"
msgstr "Kindle E-Mail" msgstr "Kindle E-Mail"
@ -965,43 +1066,47 @@ msgstr "Zeige nur Bücher mit dieser Sprache"
msgid "Show all" msgid "Show all"
msgstr "Zeige alle" msgstr "Zeige alle"
#: cps/templates/user_edit.html:45 #: cps/templates/user_edit.html:47
msgid "Show random books" msgid "Show random books"
msgstr "Zeige Zufällige Bücher" msgstr "Zeige Zufällige Bücher"
#: cps/templates/user_edit.html:49 #: cps/templates/user_edit.html:51
msgid "Show hot books" msgid "Show hot books"
msgstr "Zeige Auswahl Beliebte Bücher" msgstr "Zeige Auswahl Beliebte Bücher"
#: cps/templates/user_edit.html:53 #: cps/templates/user_edit.html:55
msgid "Show best rated books" msgid "Show best rated books"
msgstr "Zeige am besten bewertete Bücher" msgstr "Zeige am besten bewertete Bücher"
#: cps/templates/user_edit.html:57 #: cps/templates/user_edit.html:59
msgid "Show language selection" msgid "Show language selection"
msgstr "Zeige Sprachauswahl" msgstr "Zeige Sprachauswahl"
#: cps/templates/user_edit.html:61 #: cps/templates/user_edit.html:63
msgid "Show series selection" msgid "Show series selection"
msgstr "Zeige Serienauswahl" msgstr "Zeige Serienauswahl"
#: cps/templates/user_edit.html:65 #: cps/templates/user_edit.html:67
msgid "Show category selection" msgid "Show category selection"
msgstr "Zeige Kategorienauswahl" msgstr "Zeige Kategorienauswahl"
#: cps/templates/user_edit.html:69 #: cps/templates/user_edit.html:71
msgid "Show author selection" msgid "Show author selection"
msgstr "Zeige Autorenauswahl" 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" msgid "Show random books in detail view"
msgstr "Zeige zufällige Bücher in der Detailansicht" 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" msgid "Delete this user"
msgstr "Benutzer löschen" msgstr "Benutzer löschen"
#: cps/templates/user_edit.html:116 #: cps/templates/user_edit.html:127
msgid "Recent Downloads" msgid "Recent Downloads"
msgstr "Letzte Downloads" msgstr "Letzte Downloads"

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

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

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

@ -10,6 +10,8 @@ import os
import logging import logging
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from flask_babel import gettext as _ 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") 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) engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False)
@ -22,6 +24,7 @@ ROLE_UPLOAD = 4
ROLE_EDIT = 8 ROLE_EDIT = 8
ROLE_PASSWD = 16 ROLE_PASSWD = 16
ROLE_ANONYMOUS = 32 ROLE_ANONYMOUS = 32
ROLE_EDIT_SHELFS = 64
DETAIL_RANDOM = 1 DETAIL_RANDOM = 1
SIDEBAR_LANGUAGE = 2 SIDEBAR_LANGUAGE = 2
@ -31,6 +34,7 @@ SIDEBAR_HOT = 16
SIDEBAR_RANDOM = 32 SIDEBAR_RANDOM = 32
SIDEBAR_AUTHOR = 64 SIDEBAR_AUTHOR = 64
SIDEBAR_BEST_RATED = 128 SIDEBAR_BEST_RATED = 128
SIDEBAR_READ_AND_UNREAD = 256
DEFAULT_PASS = "admin123" DEFAULT_PASS = "admin123"
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083)) DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
@ -83,6 +87,12 @@ class UserBase:
else: else:
return False 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): def is_active(self):
return True return True
@ -90,7 +100,7 @@ class UserBase:
return False return False
def get_id(self): def get_id(self):
return unicode(self.id) return str(self.id)
def filter_language(self): def filter_language(self):
return self.default_language return self.default_language
@ -137,6 +147,12 @@ class UserBase:
else: else:
return False 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): def show_detail_random(self):
if self.sidebar_view is not None: if self.sidebar_view is not None:
return True if self.sidebar_view & DETAIL_RANDOM == DETAIL_RANDOM else False 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.role = data.role
self.sidebar_view = data.sidebar_view self.sidebar_view = data.sidebar_view
self.default_language = data.default_language self.default_language = data.default_language
self.default_language = data.default_language
self.locale = data.locale self.locale = data.locale
self.anon_browse = settings.config_anonbrowse self.anon_browse = settings.config_anonbrowse
@ -217,6 +232,14 @@ class BookShelf(Base):
def __repr__(self): def __repr__(self):
return '<Book %r>' % self.id 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 # Baseclass representing Downloads from calibre-web in app.db
class Downloads(Base): class Downloads(Base):
@ -253,6 +276,14 @@ class Settings(Base):
config_anonbrowse = Column(SmallInteger, default=0) config_anonbrowse = Column(SmallInteger, default=0)
config_public_reg = Column(SmallInteger, default=0) config_public_reg = Column(SmallInteger, default=0)
config_default_role = 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): def __repr__(self):
pass pass
@ -279,7 +310,18 @@ class Config:
self.config_anonbrowse = data.config_anonbrowse self.config_anonbrowse = data.config_anonbrowse
self.config_public_reg = data.config_public_reg self.config_public_reg = data.config_public_reg
self.config_default_role = data.config_default_role 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 self.db_configured = True
else: else:
self.db_configured = False self.db_configured = False
@ -318,6 +360,12 @@ class Config:
else: else:
return False 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): def get_Log_Level(self):
ret_value="" ret_value=""
if self.config_log_level == logging.INFO: 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 # everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
# rows with SQL commands # rows with SQL commands
def migrate_Database(): def migrate_Database():
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
ReadBook.__table__.create(bind = engine)
try: try:
session.query(exists().where(User.locale)).scalar() session.query(exists().where(User.locale)).scalar()
session.commit() 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_anonbrowse` SmallInteger DEFAULT 0")
conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0") conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0")
session.commit() 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: try:
session.query(exists().where(Settings.config_default_role)).scalar() session.query(exists().where(Settings.config_default_role)).scalar()
session.commit() session.commit()
@ -438,7 +506,7 @@ def create_anonymous_user():
session.add(user) session.add(user)
try: try:
session.commit() session.commit()
except: except Exception as e:
session.rollback() session.rollback()
pass pass
@ -449,14 +517,15 @@ def create_admin_user():
user.nickname = "admin" user.nickname = "admin"
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_PASSWD 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 + \ 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) user.password = generate_password_hash(DEFAULT_PASS)
session.add(user) session.add(user)
try: try:
session.commit() session.commit()
except: except Exception as e:
session.rollback() session.rollback()
pass pass

@ -7,7 +7,7 @@ import hashlib
from collections import namedtuple from collections import namedtuple
import book_formats 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 :rtype: BookMeta

File diff suppressed because it is too large Load Diff

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

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

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

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

@ -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. 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) ![screenshot](https://raw.githubusercontent.com/janeczku/docker-calibre-web/master/screenshot.png)
##Features ## Features
- Bootstrap 3 HTML5 interface - Bootstrap 3 HTML5 interface
- full graphical setup - full graphical setup
- User management - User management
@ -28,13 +29,14 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
## Quick start ## Quick start
1. Execute the command: `python cps.py` 1. Install required dependencies by executing `pip install -r requirements.txt`
2. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog 2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
3. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button 3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
4. Go to Login page 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:** **Default admin login:**
*Username:* admin *Username:* admin
*Password:* admin123 *Password:* admin123
## Runtime Configuration Options ## Runtime Configuration Options
@ -56,10 +58,44 @@ Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick librar
## Requirements ## Requirements
Python 2.7+ Python 2.7+
Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature:
[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. Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the send-to-kindle feature:
[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 ## Docker image
@ -131,4 +167,4 @@ Replace the user and ExecStart with your user and foldernames.
`sudo systemctl enable cps.service` `sudo systemctl enable cps.service`
enables the service. enables the service.

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

@ -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.

@ -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.

@ -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.

@ -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"]

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

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

File diff suppressed because it is too large Load Diff

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

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

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

@ -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 +0,0 @@
__version__ = '5.0.6'

@ -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".

@ -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.

@ -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'

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

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

@ -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.

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

Loading…
Cancel
Save