mirror of
https://github.com/janeczku/calibre-web
synced 2026-05-21 04:42:11 +00:00
Merge remote-tracking branch 'refs/remotes/janeczku/master'
# Conflicts: # cps/translations/pl/LC_MESSAGES/messages.mo # cps/translations/pl/LC_MESSAGES/messages.po
This commit is contained in:
@@ -23,3 +23,9 @@ cps/static/[0-9]*
|
||||
*.bak
|
||||
*.log.*
|
||||
tags
|
||||
|
||||
settings.yaml
|
||||
gdrive_credentials
|
||||
|
||||
#kindlegen
|
||||
vendor/kindlegen
|
||||
|
||||
@@ -6,20 +6,33 @@ import sys
|
||||
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
# Insert local directories into path
|
||||
sys.path.insert(0, os.path.join(base_path, 'vendor'))
|
||||
sys.path.append(base_path)
|
||||
sys.path.append(os.path.join(base_path, 'cps'))
|
||||
sys.path.append(os.path.join(base_path, 'vendor'))
|
||||
|
||||
from cps import web
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
try:
|
||||
from gevent.wsgi import WSGIServer
|
||||
gevent_present = True
|
||||
except ImportError:
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
gevent_present = False
|
||||
|
||||
if __name__ == '__main__':
|
||||
if web.ub.DEVELOPMENT:
|
||||
web.app.run(host="0.0.0.0", port=web.ub.config.config_port, debug=True)
|
||||
else:
|
||||
http_server = HTTPServer(WSGIContainer(web.app))
|
||||
http_server.listen(web.ub.config.config_port)
|
||||
IOLoop.instance().start()
|
||||
if gevent_present:
|
||||
web.app.logger.info('Attempting to start gevent')
|
||||
web.start_gevent()
|
||||
else:
|
||||
web.app.logger.info('Falling back to Tornado')
|
||||
http_server = HTTPServer(WSGIContainer(web.app))
|
||||
http_server.listen(web.ub.config.config_port)
|
||||
IOLoop.instance().start()
|
||||
IOLoop.instance().close(True)
|
||||
|
||||
if web.helper.global_task == 0:
|
||||
web.app.logger.info("Performing restart of Calibre-web")
|
||||
|
||||
+9
-7
@@ -14,28 +14,28 @@ try:
|
||||
from wand.image import Image
|
||||
from wand import version as ImageVersion
|
||||
use_generic_pdf_cover = False
|
||||
except ImportError, e:
|
||||
except ImportError as e:
|
||||
logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e)
|
||||
use_generic_pdf_cover = True
|
||||
try:
|
||||
from PyPDF2 import PdfFileReader
|
||||
from PyPDF2 import __version__ as PyPdfVersion
|
||||
use_pdf_meta = True
|
||||
except ImportError, e:
|
||||
except ImportError as e:
|
||||
logger.warning('cannot import PyPDF2, extracting pdf metadata will not work: %s', e)
|
||||
use_pdf_meta = False
|
||||
|
||||
try:
|
||||
import epub
|
||||
use_epub_meta = True
|
||||
except ImportError, e:
|
||||
except ImportError as e:
|
||||
logger.warning('cannot import epub, extracting epub metadata will not work: %s', e)
|
||||
use_epub_meta = False
|
||||
|
||||
try:
|
||||
import fb2
|
||||
use_fb2_meta = True
|
||||
except ImportError, e:
|
||||
except ImportError as e:
|
||||
logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e)
|
||||
use_fb2_meta = False
|
||||
|
||||
@@ -48,7 +48,7 @@ def process(tmp_file_path, original_file_name, original_file_extension):
|
||||
return epub.get_epub_info(tmp_file_path, original_file_name, original_file_extension)
|
||||
if ".FB2" == original_file_extension.upper() and use_fb2_meta is True:
|
||||
return fb2.get_fb2_info(tmp_file_path, original_file_extension)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warning('cannot parse metadata, using default: %s', e)
|
||||
return default_meta(tmp_file_path, original_file_name, original_file_extension)
|
||||
|
||||
@@ -63,7 +63,8 @@ def default_meta(tmp_file_path, original_file_name, original_file_extension):
|
||||
description="",
|
||||
tags="",
|
||||
series="",
|
||||
series_id="")
|
||||
series_id="",
|
||||
languages="")
|
||||
|
||||
|
||||
def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
|
||||
@@ -91,7 +92,8 @@ def pdf_meta(tmp_file_path, original_file_name, original_file_extension):
|
||||
description=subject,
|
||||
tags="",
|
||||
series="",
|
||||
series_id="")
|
||||
series_id="",
|
||||
languages="")
|
||||
|
||||
|
||||
def pdf_preview(tmp_file_path, tmp_dir):
|
||||
|
||||
@@ -11,10 +11,8 @@ from ub import config
|
||||
import ub
|
||||
|
||||
session = None
|
||||
cc_exceptions = None
|
||||
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
|
||||
cc_classes = None
|
||||
cc_ids = None
|
||||
books_custom_column_links = None
|
||||
engine = None
|
||||
|
||||
|
||||
@@ -247,7 +245,7 @@ class Books(Base):
|
||||
identifiers = relationship('Identifiers', backref='books')
|
||||
|
||||
def __init__(self, title, sort, author_sort, timestamp, pubdate, series_index, last_modified, path, has_cover,
|
||||
authors, tags):
|
||||
authors, tags, languages = None):
|
||||
self.title = title
|
||||
self.sort = sort
|
||||
self.author_sort = author_sort
|
||||
@@ -283,22 +281,19 @@ class Custom_Columns(Base):
|
||||
|
||||
|
||||
def setup_db():
|
||||
global session
|
||||
global cc_exceptions
|
||||
global cc_classes
|
||||
global cc_ids
|
||||
global books_custom_column_links
|
||||
global engine
|
||||
global session
|
||||
global cc_classes
|
||||
|
||||
if config.config_calibre_dir is None or config.config_calibre_dir == u'':
|
||||
return False
|
||||
|
||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||
engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False, isolation_level="SERIALIZABLE")
|
||||
#engine = create_engine('sqlite:///{0}'.format(dbpath.encode('utf-8')), echo=False, isolation_level="SERIALIZABLE")
|
||||
engine = create_engine('sqlite:///'+ dbpath, echo=False, isolation_level="SERIALIZABLE")
|
||||
try:
|
||||
conn = engine.connect()
|
||||
|
||||
except:
|
||||
except Exception as e:
|
||||
content = ub.session.query(ub.Settings).first()
|
||||
content.config_calibre_dir = None
|
||||
content.db_configured = False
|
||||
@@ -311,43 +306,43 @@ def setup_db():
|
||||
config.loadSettings()
|
||||
conn.connection.create_function('title_sort', 1, title_sort)
|
||||
|
||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
||||
if not cc_classes:
|
||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
||||
|
||||
cc_ids = []
|
||||
cc_exceptions = ['datetime', 'int', 'comments', 'float', 'composite', 'series']
|
||||
books_custom_column_links = {}
|
||||
cc_classes = {}
|
||||
for row in cc:
|
||||
if row.datatype not in cc_exceptions:
|
||||
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'),
|
||||
primary_key=True),
|
||||
Column('value', Integer,
|
||||
ForeignKey('custom_column_' + str(row.id) + '.id'),
|
||||
primary_key=True)
|
||||
)
|
||||
cc_ids.append([row.id, row.datatype])
|
||||
if row.datatype == 'bool':
|
||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||
'id': Column(Integer, primary_key=True),
|
||||
'book': Column(Integer, ForeignKey('books.id')),
|
||||
'value': Column(Boolean)}
|
||||
cc_ids = []
|
||||
books_custom_column_links = {}
|
||||
cc_classes = {}
|
||||
for row in cc:
|
||||
if row.datatype not in cc_exceptions:
|
||||
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', Base.metadata,
|
||||
Column('book', Integer, ForeignKey('books.id'),
|
||||
primary_key=True),
|
||||
Column('value', Integer,
|
||||
ForeignKey('custom_column_' + str(row.id) + '.id'),
|
||||
primary_key=True)
|
||||
)
|
||||
cc_ids.append([row.id, row.datatype])
|
||||
if row.datatype == 'bool':
|
||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||
'id': Column(Integer, primary_key=True),
|
||||
'book': Column(Integer, ForeignKey('books.id')),
|
||||
'value': Column(Boolean)}
|
||||
else:
|
||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||
'id': Column(Integer, primary_key=True),
|
||||
'value': Column(String)}
|
||||
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
|
||||
|
||||
for id in cc_ids:
|
||||
if id[1] == 'bool':
|
||||
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
|
||||
primaryjoin=(
|
||||
Books.id == cc_classes[id[0]].book),
|
||||
backref='books'))
|
||||
else:
|
||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||
'id': Column(Integer, primary_key=True),
|
||||
'value': Column(String)}
|
||||
cc_classes[row.id] = type('Custom_Column_' + str(row.id), (Base,), ccdict)
|
||||
|
||||
for id in cc_ids:
|
||||
if id[1] == 'bool':
|
||||
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
|
||||
primaryjoin=(
|
||||
Books.id == cc_classes[id[0]].book),
|
||||
backref='books'))
|
||||
else:
|
||||
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
|
||||
secondary=books_custom_column_links[id[0]],
|
||||
backref='books'))
|
||||
setattr(Books, 'custom_column_' + str(id[0]), relationship(cc_classes[id[0]],
|
||||
secondary=books_custom_column_links[id[0]],
|
||||
backref='books'))
|
||||
|
||||
# Base.metadata.create_all(engine)
|
||||
Session = sessionmaker()
|
||||
|
||||
+40
-9
@@ -5,7 +5,7 @@ import zipfile
|
||||
from lxml import etree
|
||||
import os
|
||||
import uploader
|
||||
|
||||
from iso639 import languages as isoLanguages
|
||||
|
||||
def extractCover(zip, coverFile, coverpath, tmp_file_name):
|
||||
if coverFile is None:
|
||||
@@ -41,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]
|
||||
|
||||
epub_metadata = {}
|
||||
for s in ['title', 'description', 'creator']:
|
||||
|
||||
for s in ['title', 'description', 'creator', 'language']:
|
||||
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
||||
if len(tmp) > 0:
|
||||
epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0]
|
||||
else:
|
||||
epub_metadata[s] = "Unknown"
|
||||
|
||||
if epub_metadata['description'] == "Unknown":
|
||||
description = tree.xpath("//*[local-name() = 'description']/text()")
|
||||
if len(description) > 0:
|
||||
epub_metadata['description'] = description
|
||||
else:
|
||||
epub_metadata['description'] = ""
|
||||
|
||||
if epub_metadata['language'] == "Unknown":
|
||||
epub_metadata['language'] == ""
|
||||
else:
|
||||
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
||||
if len(lang) == 2:
|
||||
epub_metadata['language'] = isoLanguages.get(part1=lang).name
|
||||
elif len(lang) == 3:
|
||||
epub_metadata['language'] = isoLanguages.get(part3=lang).name
|
||||
else:
|
||||
epub_metadata['language'] = ""
|
||||
|
||||
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
||||
coverfile = None
|
||||
if len(coversection) > 0:
|
||||
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
|
||||
else:
|
||||
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover']/@href", namespaces=ns)
|
||||
if len(coversection) > 0:
|
||||
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
|
||||
else:
|
||||
coverfile = None
|
||||
|
||||
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
|
||||
if len(meta_cover) > 0:
|
||||
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
|
||||
if len(coversection) > 0:
|
||||
filetype = coversection[0].rsplit('.',1)[-1]
|
||||
if filetype == "xhtml" or filetype == "html": #if cover is (x)html format
|
||||
markup = zip.read(os.path.join(coverpath,coversection[0]))
|
||||
markupTree = etree.fromstring(markup)
|
||||
#no matter xhtml or html with no namespace
|
||||
imgsrc = markupTree.xpath("//*[local-name() = 'img']/@src")
|
||||
#imgsrc maybe startwith "../"" so fullpath join then relpath to cwd
|
||||
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(coverpath, coversection[0])), imgsrc[0]))
|
||||
coverfile = extractCover(zip, filename, "", tmp_file_path)
|
||||
else:
|
||||
coverfile = extractCover(zip, coversection[0], coverpath, tmp_file_path)
|
||||
|
||||
if epub_metadata['title'] is None:
|
||||
title = original_file_name
|
||||
else:
|
||||
@@ -72,4 +102,5 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
||||
description=epub_metadata['description'],
|
||||
tags="",
|
||||
series="",
|
||||
series_id="")
|
||||
series_id="",
|
||||
languages=epub_metadata['language'])
|
||||
|
||||
+9
-6
@@ -4,8 +4,10 @@
|
||||
from lxml import etree
|
||||
import os
|
||||
import uploader
|
||||
import StringIO
|
||||
|
||||
try:
|
||||
from io import StringIO
|
||||
except ImportError as e:
|
||||
import StringIO
|
||||
|
||||
def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
|
||||
@@ -37,16 +39,16 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
first_name = u''
|
||||
return first_name + ' ' + middle_name + ' ' + last_name
|
||||
|
||||
author = unicode(", ".join(map(get_author, authors)))
|
||||
author = str(", ".join(map(get_author, authors)))
|
||||
|
||||
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
|
||||
if len(title):
|
||||
title = unicode(title[0])
|
||||
title = str(title[0])
|
||||
else:
|
||||
title = u''
|
||||
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
||||
if len(description):
|
||||
description = unicode(description[0])
|
||||
description = str(description[0])
|
||||
else:
|
||||
description = u''
|
||||
|
||||
@@ -59,4 +61,5 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
||||
description=description,
|
||||
tags="",
|
||||
series="",
|
||||
series_id="")
|
||||
series_id="",
|
||||
languages="")
|
||||
|
||||
@@ -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
|
||||
+55
-12
@@ -13,11 +13,18 @@ import os
|
||||
import traceback
|
||||
import re
|
||||
import unicodedata
|
||||
from StringIO import StringIO
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
from email.MIMEBase import MIMEBase
|
||||
from email.MIMEMultipart import MIMEMultipart
|
||||
from email.MIMEText import MIMEText
|
||||
except ImportError as e:
|
||||
from io import StringIO
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from email import encoders
|
||||
from email.MIMEBase import MIMEBase
|
||||
from email.MIMEMultipart import MIMEMultipart
|
||||
from email.MIMEText import MIMEText
|
||||
from email.generator import Generator
|
||||
from email.utils import formatdate
|
||||
from email.utils import make_msgid
|
||||
@@ -28,11 +35,16 @@ import shutil
|
||||
import requests
|
||||
import zipfile
|
||||
from tornado.ioloop import IOLoop
|
||||
try:
|
||||
import gdriveutils as gd
|
||||
except ImportError:
|
||||
pass
|
||||
import web
|
||||
|
||||
try:
|
||||
import unidecode
|
||||
use_unidecode=True
|
||||
except:
|
||||
except Exception as e:
|
||||
use_unidecode=False
|
||||
|
||||
# Global variables
|
||||
@@ -147,7 +159,7 @@ def send_raw_email(kindle_mail, msg):
|
||||
|
||||
smtplib.stderr = org_stderr
|
||||
|
||||
except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException), e:
|
||||
except (socket.error, smtplib.SMTPRecipientsRefused, smtplib.SMTPException) as e:
|
||||
app.logger.error(traceback.print_exc())
|
||||
return _("Failed to send mail: %s" % str(e))
|
||||
|
||||
@@ -239,7 +251,10 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
value=value.replace(u'ß',u'ss')
|
||||
value = unicodedata.normalize('NFKD', value)
|
||||
re_slugify = re.compile('[\W\s-]', re.UNICODE)
|
||||
value = unicode(re_slugify.sub('', value).strip())
|
||||
if type(value) is str: #Python3 str, Python2 unicode
|
||||
value = re_slugify.sub('', value).strip()
|
||||
else:
|
||||
value = unicode(re_slugify.sub('', value).strip())
|
||||
if replace_whitespace:
|
||||
#*+:\"/<>? werden durch _ ersetzt
|
||||
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U)
|
||||
@@ -280,6 +295,30 @@ def update_dir_stucture(book_id, calibrepath):
|
||||
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||
db.session.commit()
|
||||
|
||||
def update_dir_structure_gdrive(book_id):
|
||||
db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
|
||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
|
||||
|
||||
authordir = book.path.split('/')[0]
|
||||
new_authordir = get_valid_filename(book.authors[0].name)
|
||||
titledir = book.path.split('/')[1]
|
||||
new_titledir = get_valid_filename(book.title) + " (" + str(book_id) + ")"
|
||||
|
||||
if titledir != new_titledir:
|
||||
print (titledir)
|
||||
gFile=gd.getFileFromEbooksFolder(web.Gdrive.Instance().drive,os.path.dirname(book.path),titledir)
|
||||
gFile['title']= new_titledir
|
||||
gFile.Upload()
|
||||
book.path = book.path.split('/')[0] + '/' + new_titledir
|
||||
|
||||
if authordir != new_authordir:
|
||||
gFile=gd.getFileFromEbooksFolder(web.Gdrive.Instance().drive,None,authordir)
|
||||
gFile['title']= new_authordir
|
||||
gFile.Upload()
|
||||
book.path = new_authordir + '/' + book.path.split('/')[1]
|
||||
|
||||
db.session.commit()
|
||||
|
||||
class Updater(threading.Thread):
|
||||
|
||||
def __init__(self):
|
||||
@@ -305,9 +344,13 @@ class Updater(threading.Thread):
|
||||
ub.session.close()
|
||||
ub.engine.dispose()
|
||||
self.status=6
|
||||
# stop tornado server
|
||||
server = IOLoop.instance()
|
||||
server.add_callback(server.stop)
|
||||
|
||||
if web.gevent_server:
|
||||
web.gevent_server.stop()
|
||||
else:
|
||||
# stop tornado server
|
||||
server = IOLoop.instance()
|
||||
server.add_callback(server.stop)
|
||||
self.status=7
|
||||
|
||||
def get_update_status(self):
|
||||
@@ -379,7 +422,7 @@ class Updater(threading.Thread):
|
||||
try:
|
||||
os.chown(dst_file, permission.st_uid, permission.st_uid)
|
||||
# print('Permissions: User '+str(new_permissions.st_uid)+' Group '+str(new_permissions.st_uid))
|
||||
except:
|
||||
except Exception as e:
|
||||
e = sys.exc_info()
|
||||
logging.getLogger('cps.web').debug('Fail '+str(dst_file)+' error: '+str(e))
|
||||
return
|
||||
@@ -421,7 +464,7 @@ class Updater(threading.Thread):
|
||||
logging.getLogger('cps.web').debug("Delete file " + item_path)
|
||||
log_from_thread("Delete file " + item_path)
|
||||
os.remove(item_path)
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.getLogger('cps.web').debug("Could not remove:" + item_path)
|
||||
shutil.rmtree(source, ignore_errors=True)
|
||||
|
||||
|
||||
@@ -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(" ").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);
|
||||
Vendored
+1277
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() {
|
||||
$('#spinner2').show();
|
||||
$.ajax({
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
<div>{{_('Current commit timestamp')}}: <span>{{commit}} </span></div>
|
||||
<div class="hidden" id="update_info">{{_('Newest commit timestamp')}}: <span></span></div>
|
||||
<p></p>
|
||||
<div class="btn btn-default" id="restart_database">{{_('Reconnect to Calibre DB')}}</div>
|
||||
<div class="btn btn-default" data-toggle="modal" data-target="#RestartDialog">{{_('Restart Calibre-web')}}</div>
|
||||
<div class="btn btn-default" data-toggle="modal" data-target="#ShutdownDialog">{{_('Stop Calibre-web')}}</div>
|
||||
<div class="btn btn-default" id="check_for_update">{{_('Check for update')}}</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rating">{{_('Rating')}}</label>
|
||||
<input type="number" min="0" max="5" step="1" class="form-control" name="rating" id="rating" value="{% if book.ratings %}{{book.ratings[0].rating / 2}}{% endif %}">
|
||||
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cover_url">{{_('Cover URL (jpg)')}}</label>
|
||||
@@ -104,16 +104,54 @@
|
||||
<input name="detail_view" type="checkbox" checked> {{_('view book after edit')}}
|
||||
</label>
|
||||
</div>
|
||||
<a href="#" id="get_meta" class="btn btn-default" data-toggle="modal" data-target="#metaModal">{{_('Get metadata')}}</a>
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
<a href="{{ url_for('show_book',id=book.id) }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="metaModalLabel">{{_('Get metadata')}}</h4>
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
|
||||
<input type="text" class="form-control" id="keyword" placeholder="{{_(" Search keyword ")}}">
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" id="do-search">{{_("Go!")}}</button>
|
||||
<span>{{_('Click the cover to load metadata to the form')}}</span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-body" id="meta-info">
|
||||
{{_("Loading...")}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
var i18n_msg = {
|
||||
'loading': {{_('Loading...')|safe|tojson}},
|
||||
'search_error': {{_('Search error!')|safe|tojson}},
|
||||
'no_result': {{_('No Result! Please try anonther keyword.')|safe|tojson}},
|
||||
'author': {{_('Author')|safe|tojson}},
|
||||
'publisher': {{_('Publisher')|safe|tojson}},
|
||||
'description': {{_('Description')|safe|tojson}},
|
||||
'source': {{_('Source')|safe|tojson}},
|
||||
};
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/libs/typeahead.bundle.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/edit_books.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/libs/bootstrap-rating-input.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/get_meta.js') }}"></script>
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<link href="{{ url_for('static', filename='css/libs/typeahead.css') }}" rel="stylesheet" media="screen">
|
||||
|
||||
@@ -7,6 +7,47 @@
|
||||
<label for="config_calibre_dir">{{_('Location of Calibre database')}}</label>
|
||||
<input type="text" class="form-control" name="config_calibre_dir" id="config_calibre_dir" value="{% if content.config_calibre_dir != None %}{{ content.config_calibre_dir }}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
{% if gdrive %}
|
||||
<div class="form-group required">
|
||||
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" {% if content.config_use_google_drive %}checked{% endif %} >
|
||||
<label for="config_use_google_drive">{{_('Use google drive?')}}</label>
|
||||
</div>
|
||||
<div id="gdrive_settings">
|
||||
<div class="form-group required">
|
||||
<label for="config_google_drive_client_id">{{_('Client id')}}</label>
|
||||
<input type="text" class="form-control" name="config_google_drive_client_id" id="config_google_drive_client_id" value="{% if content.config_google_drive_client_id %}{{content.config_google_drive_client_id}}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group required">
|
||||
<label for="config_google_drive_client_secret">{{_('Client secret')}}</label>
|
||||
<input type="text" class="form-control" name="config_google_drive_client_secret" id="config_google_drive_client_secret" value="{% if content.config_google_drive_client_secret %}{{content.config_google_drive_client_secret}}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group required">
|
||||
<label for="config_google_drive_calibre_url_base">{{_('Calibre Base URL')}}</label>
|
||||
<input type="text" class="form-control" name="config_google_drive_calibre_url_base" id="config_google_drive_calibre_url_base" value="{% if content.config_google_drive_calibre_url_base %}{{content.config_google_drive_calibre_url_base}}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group required">
|
||||
<label for="config_google_drive_folder">{{_('Google drive Calibre folder')}}</label>
|
||||
<input type="text" class="form-control" name="config_google_drive_folder" id="config_google_drive_folder" value="{% if content.config_google_drive_folder %}{{content.config_google_drive_folder}}{% endif %}" autocomplete="off" required>
|
||||
</div>
|
||||
{% if show_authenticate_google_drive %}
|
||||
<div class="form-group required">
|
||||
<a href="{{ url_for('authenticate_google_drive') }}" class="btn btn-primary">Authenticate Google Drive</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if content.config_google_drive_watch_changes_response %}
|
||||
<label for="config_google_drive_watch_changes_response">{{_('Metadata Watch Channel ID')}}</label>
|
||||
<div class="form-group input-group required">
|
||||
<input type="text" class="form-control" name="config_google_drive_watch_changes_response" id="config_google_drive_watch_changes_response" value="{{ content.config_google_drive_watch_changes_response['id'] }} expires on {{ content.config_google_drive_watch_changes_response['expiration'] | strftime }}" autocomplete="off" disabled="">
|
||||
<span class="input-group-btn">
|
||||
<a href="{{ url_for('revoke_watch_gdrive') }}" class="btn btn-primary">Revoke</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<a href="{{ url_for('watch_gdrive') }}" class="btn btn-primary">Enable watch of metadata.db</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="config_port">{{_('Server Port')}}</label>
|
||||
<input type="number" min="1" max="65535" class="form-control" name="config_port" id="config_port" value="{% if content.config_port != None %}{{ content.config_port }}{% endif %}" autocomplete="off" required>
|
||||
@@ -23,7 +64,10 @@
|
||||
<label for="config_random_books">{{_('No. of random books to show')}}</label>
|
||||
<input type="number" min="1" max="30" class="form-control" name="config_random_books" id="config_random_books" value="{% if content.config_random_books != None %}{{ content.config_random_books }}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="config_columns_to_ignore">{{_('Regular expression for ignoring columns')}}</label>
|
||||
<input type="text" class="form-control" name="config_columns_to_ignore" id="config_columns_to_ignore" value="{% if content.config_columns_to_ignore != None %}{{ content.config_columns_to_ignore }}{% endif %}" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
|
||||
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if content.config_title_regex != None %}{{ content.config_title_regex }}{% endif %}" autocomplete="off">
|
||||
@@ -70,6 +114,10 @@
|
||||
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
|
||||
<label for="passwd_role">{{_('Allow Changing Password')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="edit_shelf_role" id="edit_shelf_role" {% if content.role_edit_shelfs() %}checked{% endif %}>
|
||||
<label for="passwd_role">{{_('Allow Editing Public Shelfs')}}</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
{% if not origin %}
|
||||
<a href="{{ url_for('admin') }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
@@ -80,3 +128,22 @@
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#config_use_google_drive').trigger("change");
|
||||
});
|
||||
$('#config_use_google_drive').change(function(){
|
||||
formInputs=$("#gdrive_settings :input");
|
||||
isChecked=this.checked;
|
||||
formInputs.each(function(formInput) {
|
||||
$(this).prop('required',isChecked);
|
||||
});
|
||||
if (this.checked) {
|
||||
$('#gdrive_settings').show();
|
||||
} else {
|
||||
$('#gdrive_settings').hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% block body %}
|
||||
<div class="single">
|
||||
<div class="row">
|
||||
<div class="col-sm-3 col-lg-3 col-xs-12">
|
||||
<div class="col-sm-3 col-lg-3 col-xs-5">
|
||||
<div class="cover">
|
||||
{% if entry.has_cover %}
|
||||
<img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" />
|
||||
@@ -107,6 +107,16 @@
|
||||
</div>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if not g.user.is_anonymous() %}
|
||||
<p>
|
||||
<div class="custom_columns" id="have_read_container">
|
||||
<form id="have_read_form" action="{{ url_for('toggle_read', id=entry.id)}}" method="POST") >
|
||||
<input id="have_read_cb" type="checkbox" {% if have_read %}checked{% endif %} >
|
||||
<label for="have_read_cb">{{_('Read')}}</label>
|
||||
</form>
|
||||
</div>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if entry.comments|length > 0 and entry.comments[0].text|length > 0%}
|
||||
@@ -126,7 +136,7 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
{% for format in entry.data %}
|
||||
<li><a href="{{ url_for('get_download_link', book_id=entry.id, format=format.format|lower) }}">{{format.format}}</a></li>
|
||||
<li><a href="{{ url_for('get_download_link_ext', book_id=entry.id, format=format.format|lower, anyname=entry.id|string+'.'+format.format) }}">{{format.format}}</a></li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -208,3 +218,34 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ url_for('static', filename='js/libs/jquery.form.js') }}"></script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var haveReadForm = $('#have_read_form');
|
||||
haveReadForm.ajaxForm();
|
||||
});
|
||||
$("#have_read_container").attr('unselectable','on')
|
||||
.css({'-moz-user-select':'-moz-none',
|
||||
'-moz-user-select':'none',
|
||||
'-o-user-select':'none',
|
||||
'-khtml-user-select':'none', /* you could also put this in a class */
|
||||
'-webkit-user-select':'none',/* and add the CSS class here instead */
|
||||
'-ms-user-select':'none',
|
||||
'user-select':'none'
|
||||
}).bind('selectstart', function(){ return false; });
|
||||
$("#have_read_container").click(function() {
|
||||
var haveReadForm = $('#have_read_form');
|
||||
if ($("#have_read").find('span').hasClass('glyphicon-ok')) {
|
||||
$("#have_read").find('span').removeClass('glyphicon-ok');
|
||||
$("#have_read").find('span').addClass('glyphicon-remove');
|
||||
} else {
|
||||
$("#have_read").find('span').removeClass('glyphicon-remove');
|
||||
$("#have_read").find('span').addClass('glyphicon-ok');
|
||||
}
|
||||
haveReadForm.submit()
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<uri>https://github.com/janeczku/calibre-web</uri>
|
||||
</author>
|
||||
|
||||
{% if entries and entries[0] %}
|
||||
{% for entry in entries %}
|
||||
<entry>
|
||||
<title>{{entry.title}}</title>
|
||||
@@ -60,6 +61,7 @@
|
||||
{% endfor %}
|
||||
</entry>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for entry in listelements %}
|
||||
<entry>
|
||||
<title>{{entry.name}}</title>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<div class="discover load-more">
|
||||
<h2>{{title}}</h2>
|
||||
<div class="row">
|
||||
{% if entries[0] %}
|
||||
{% for entry in entries %}
|
||||
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
@@ -76,6 +77,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -39,6 +39,20 @@
|
||||
<id>{{url_for('feed_discover')}}</id>
|
||||
<content type="text">{{_('Show Random Books')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Read Books')}}</title>
|
||||
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_read_books')}}" />
|
||||
<link rel="subsection" href="{{url_for('feed_read_books')}}" type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition"/>
|
||||
<id>{{url_for('feed_read_books')}}</id>
|
||||
<content type="text">{{_('Read Books')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Unread Books')}}</title>
|
||||
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_unread_books')}}" />
|
||||
<link rel="subsection" href="{{url_for('feed_unread_books')}}" type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition"/>
|
||||
<id>{{url_for('feed_unread_books')}}</id>
|
||||
<content type="text">{{_('Unread Books')}}</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>{{_('Authors')}}</title>
|
||||
<link type="application/atom+xml;profile=opds-catalog;type=feed;kind=acquisition" href="{{url_for('feed_authorindex')}}"/>
|
||||
|
||||
@@ -128,6 +128,10 @@
|
||||
{% if g.user.show_best_rated_books() %}
|
||||
<li><a href="{{url_for('best_rated_books')}}"><span class="glyphicon glyphicon-star"></span> {{_('Best rated Books')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_read_and_unread() %}
|
||||
<li><a href="{{url_for('read_books')}}"><span class="glyphicon glyphicon-eye-open"></span> {{_('Read Books')}}</a></li>
|
||||
<li><a href="{{url_for('unread_books')}}"><span class="glyphicon glyphicon-eye-close"></span> {{_('Unread Books')}}</a></li>
|
||||
{%endif%}
|
||||
{% if g.user.show_random_books() %}
|
||||
<li id="nav_rand"><a href="{{url_for('discover')}}"><span class="glyphicon glyphicon-random"></span> {{_('Discover')}}</a></li>
|
||||
{%endif%}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<div class="discover">
|
||||
<h2>{{title}}</h2>
|
||||
{% if g.user.is_authenticated %}
|
||||
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{ _('Delete this Shelf') }} </a>
|
||||
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf name') }} </a>
|
||||
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
|
||||
|
||||
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
|
||||
<div data-toggle="modal" data-target="#DeleteShelfDialog" class="btn btn-danger">{{ _('Delete this Shelf') }} </div>
|
||||
<a href="{{ url_for('edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf name') }} </a>
|
||||
<a href="{{ url_for('order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Change order') }} </a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
|
||||
@@ -39,4 +40,20 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="DeleteShelfDialog" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-center">
|
||||
<span>{{_('Do you really want to delete the shelf?')}}</span>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<span>{{_('Shelf will be lost for everybody and forever!')}}</span>
|
||||
<p></p>
|
||||
<a href="{{ url_for('delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('Ok')}}</a>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Back')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
<label for="title">{{_('Title')}}</label>
|
||||
<input type="text" class="form-control" name="title" id="title" value="{{ shelf.name if shelf.name != None }}">
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="is_public" {% if shelf.is_public == 1 %}checked{% endif %}> {{_('should the shelf be public?')}}
|
||||
</label>
|
||||
</div>
|
||||
{% if g.user.role_edit_shelfs() %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="is_public" {% if shelf.is_public == 1 %}checked{% endif %}> {{_('should the shelf be public?')}}
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-default">{{_('Submit')}}</button>
|
||||
{% if shelf.id != None %}
|
||||
<a href="{{ url_for('show_shelf', shelf_id=shelf.id) }}" class="btn btn-default">{{_('Back')}}</a>
|
||||
|
||||
@@ -70,6 +70,10 @@
|
||||
<input type="checkbox" name="show_author" id="show_author" {% if content.show_author() %}checked{% endif %}>
|
||||
<label for="show_author">{{_('Show author selection')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_read_and_unread" id="show_read_and_unread" {% if content.show_read_and_unread() %}checked{% endif %}>
|
||||
<label for="show_read_and_unread">{{_('Show read and unread')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
|
||||
<label for="show_detail_random">{{_('Show random books in detail view')}}</label>
|
||||
@@ -100,6 +104,10 @@
|
||||
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
|
||||
<label for="passwd_role">{{_('Allow Changing Password')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="edit_shelf_role" id="edit_shelf_role" {% if content.role_edit_shelfs() %}checked{% endif %}>
|
||||
<label for="passwd_role">{{_('Allow Editing Public Shelfs')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if g.user and g.user.role_admin() and not profile and not new_user and not content.role_anonymous() %}
|
||||
|
||||
Binary file not shown.
@@ -21,7 +21,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Calibre-web\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
|
||||
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
|
||||
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
|
||||
"PO-Revision-Date: 2016-07-12 19:54+0200\n"
|
||||
"Last-Translator: Ozzie Isaacs\n"
|
||||
"Language: de\n"
|
||||
@@ -32,347 +32,360 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
|
||||
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
|
||||
msgid "not installed"
|
||||
msgstr "Nicht installiert"
|
||||
|
||||
#: cps/helper.py:150
|
||||
#: cps/helper.py:164
|
||||
#, python-format
|
||||
msgid "Failed to send mail: %s"
|
||||
msgstr "E-Mail: %s konnte nicht gesendet werden"
|
||||
|
||||
#: cps/helper.py:157
|
||||
#: cps/helper.py:171
|
||||
msgid "Calibre-web test email"
|
||||
msgstr "Calibre-web Test E-Mail"
|
||||
|
||||
#: cps/helper.py:158 cps/helper.py:168
|
||||
#: cps/helper.py:172 cps/helper.py:184
|
||||
msgid "This email has been sent via calibre web."
|
||||
msgstr "Die E-Mail wurde via calibre-web versendet"
|
||||
|
||||
#: cps/helper.py:167 cps/templates/detail.html:130
|
||||
#: cps/helper.py:181 cps/templates/detail.html:146
|
||||
msgid "Send to Kindle"
|
||||
msgstr "An Kindle senden"
|
||||
|
||||
#: cps/helper.py:185 cps/helper.py:200
|
||||
#: cps/helper.py:201 cps/helper.py:216
|
||||
msgid "Could not find any formats suitable for sending by email"
|
||||
msgstr ""
|
||||
"Konnte keine Formate finden welche für das versenden per E-Mail geeignet "
|
||||
"sind"
|
||||
|
||||
#: cps/helper.py:194
|
||||
#: cps/helper.py:210
|
||||
msgid "Could not convert epub to mobi"
|
||||
msgstr "Konnte .epub nicht nach .mobi konvertieren"
|
||||
|
||||
#: cps/ub.py:434
|
||||
#: cps/ub.py:488
|
||||
msgid "Guest"
|
||||
msgstr "Gast"
|
||||
|
||||
#: cps/web.py:734
|
||||
#: cps/web.py:904
|
||||
msgid "Requesting update package"
|
||||
msgstr "Frage Update Paket an"
|
||||
|
||||
#: cps/web.py:735
|
||||
#: cps/web.py:905
|
||||
msgid "Downloading update package"
|
||||
msgstr "Lade Update Paket herunter"
|
||||
|
||||
#: cps/web.py:736
|
||||
#: cps/web.py:906
|
||||
msgid "Unzipping update package"
|
||||
msgstr "Entpacke Update Paket"
|
||||
|
||||
#: cps/web.py:737
|
||||
#: cps/web.py:907
|
||||
msgid "Files are replaced"
|
||||
msgstr "Ersetze Dateien"
|
||||
|
||||
#: cps/web.py:738
|
||||
#: cps/web.py:908
|
||||
msgid "Database connections are closed"
|
||||
msgstr "Schließe Datenbankverbindungen"
|
||||
|
||||
#: cps/web.py:739
|
||||
#: cps/web.py:909
|
||||
msgid "Server is stopped"
|
||||
msgstr "Stoppe Server"
|
||||
|
||||
#: cps/web.py:740
|
||||
#: cps/web.py:910
|
||||
msgid "Update finished, please press okay and reload page"
|
||||
msgstr "Update abgeschlossen, bitte okay drücken und Seite neu laden"
|
||||
|
||||
#: cps/web.py:810
|
||||
#: cps/web.py:983
|
||||
msgid "Latest Books"
|
||||
msgstr "Letzte Bücher"
|
||||
|
||||
#: cps/web.py:835
|
||||
#: cps/web.py:1014
|
||||
msgid "Hot Books (most downloaded)"
|
||||
msgstr "Beliebte Bücher (die meisten Downloads)"
|
||||
|
||||
#: cps/web.py:845
|
||||
#: cps/web.py:1024
|
||||
msgid "Best rated books"
|
||||
msgstr "Best bewertete Bücher"
|
||||
|
||||
#: cps/templates/index.xml:36 cps/web.py:854
|
||||
#: cps/templates/index.xml:36 cps/web.py:1033
|
||||
msgid "Random Books"
|
||||
msgstr "Zufällige Bücher"
|
||||
|
||||
#: cps/web.py:867
|
||||
#: cps/web.py:1046
|
||||
msgid "Author list"
|
||||
msgstr "Autorenliste"
|
||||
|
||||
#: cps/web.py:878
|
||||
#: cps/web.py:1057
|
||||
#, python-format
|
||||
msgid "Author: %(name)s"
|
||||
msgstr "Autor: %(name)s"
|
||||
|
||||
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
|
||||
#: cps/web.py:2115
|
||||
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
|
||||
#: cps/web.py:2579
|
||||
msgid "Error opening eBook. File does not exist or file is not accessible:"
|
||||
msgstr ""
|
||||
"Buch öffnen fehlgeschlagen. Datei existiert nicht, oder ist nicht "
|
||||
"zugänglich."
|
||||
|
||||
#: cps/templates/index.xml:57 cps/web.py:894
|
||||
#: cps/templates/index.xml:71 cps/web.py:1073
|
||||
msgid "Series list"
|
||||
msgstr "Liste Serien"
|
||||
|
||||
#: cps/web.py:906
|
||||
#: cps/web.py:1085
|
||||
#, python-format
|
||||
msgid "Series: %(serie)s"
|
||||
msgstr "Serie: %(serie)s"
|
||||
|
||||
#: cps/web.py:939
|
||||
#: cps/web.py:1118
|
||||
msgid "Available languages"
|
||||
msgstr "Verfügbare Sprachen"
|
||||
|
||||
#: cps/web.py:954
|
||||
#: cps/web.py:1133
|
||||
#, python-format
|
||||
msgid "Language: %(name)s"
|
||||
msgstr "Sprache: %(name)s"
|
||||
|
||||
#: cps/templates/index.xml:50 cps/web.py:967
|
||||
#: cps/templates/index.xml:64 cps/web.py:1146
|
||||
msgid "Category list"
|
||||
msgstr "Kategorieliste"
|
||||
|
||||
#: cps/web.py:979
|
||||
#: cps/web.py:1158
|
||||
#, python-format
|
||||
msgid "Category: %(name)s"
|
||||
msgstr "Kategorie: %(name)s"
|
||||
|
||||
#: cps/web.py:1040
|
||||
#: cps/web.py:1267
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiken"
|
||||
|
||||
#: cps/web.py:1061
|
||||
#: cps/web.py:1375
|
||||
msgid "Server restarted, please reload page"
|
||||
msgstr "Server neu gestartet,bitte Seite neu laden"
|
||||
|
||||
#: cps/web.py:1063
|
||||
#: cps/web.py:1377
|
||||
msgid "Performing shutdown of server, please close window"
|
||||
msgstr "Server wird runtergefahren, bitte Fenster schließen"
|
||||
|
||||
#: cps/web.py:1073
|
||||
#: cps/web.py:1392
|
||||
msgid "Update done"
|
||||
msgstr "Update durchgeführt"
|
||||
|
||||
#: cps/web.py:1147 cps/web.py:1160
|
||||
#: cps/web.py:1470 cps/web.py:1483
|
||||
msgid "search"
|
||||
msgstr "Suche"
|
||||
|
||||
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
|
||||
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
|
||||
msgid "Read a Book"
|
||||
msgstr "Lese ein Buch"
|
||||
|
||||
#: cps/web.py:1276 cps/web.py:1713
|
||||
#: cps/web.py:1676 cps/web.py:2152
|
||||
msgid "Please fill out all fields!"
|
||||
msgstr "Bitte alle Felder ausfüllen!"
|
||||
|
||||
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
|
||||
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
|
||||
msgid "register"
|
||||
msgstr "Registieren"
|
||||
|
||||
#: cps/web.py:1292
|
||||
#: cps/web.py:1692
|
||||
msgid "An unknown error occured. Please try again later."
|
||||
msgstr "Es ist ein unbekannter Fehler aufgetreten. Bitte später erneut versuchen."
|
||||
|
||||
#: cps/web.py:1297
|
||||
#: cps/web.py:1697
|
||||
msgid "This username or email address is already in use."
|
||||
msgstr "Der Benutzername oder die E-Mailadresse ist in bereits in Benutzung."
|
||||
|
||||
#: cps/web.py:1315
|
||||
#: cps/web.py:1715
|
||||
#, python-format
|
||||
msgid "you are now logged in as: '%(nickname)s'"
|
||||
msgstr "Du bist nun eingeloggt als '%(nickname)s'"
|
||||
|
||||
#: cps/web.py:1320
|
||||
#: cps/web.py:1720
|
||||
msgid "Wrong Username or Password"
|
||||
msgstr "Falscher Benutzername oder Passwort"
|
||||
|
||||
#: cps/web.py:1322
|
||||
#: cps/web.py:1722
|
||||
msgid "login"
|
||||
msgstr "Login"
|
||||
|
||||
#: cps/web.py:1339
|
||||
#: cps/web.py:1739
|
||||
msgid "Please configure the SMTP mail settings first..."
|
||||
msgstr "Bitte zuerst die SMTP Mail Einstellung konfigurieren ..."
|
||||
|
||||
#: cps/web.py:1343
|
||||
#: cps/web.py:1743
|
||||
#, python-format
|
||||
msgid "Book successfully send to %(kindlemail)s"
|
||||
msgstr "Buch erfolgreich versandt an %(kindlemail)s"
|
||||
|
||||
#: cps/web.py:1347
|
||||
#: cps/web.py:1747
|
||||
#, python-format
|
||||
msgid "There was an error sending this book: %(res)s"
|
||||
msgstr "Beim Senden des Buchs trat ein Fehler auf: %(res)s"
|
||||
|
||||
#: cps/web.py:1349
|
||||
#: cps/web.py:1749 cps/web.py:2232
|
||||
msgid "Please configure your kindle email address first..."
|
||||
msgstr "Bitte die Kindle E-Mail Adresse zuuerst konfigurieren..."
|
||||
|
||||
#: cps/web.py:1369
|
||||
#: cps/web.py:1774
|
||||
#, python-format
|
||||
msgid "Book has been added to shelf: %(sname)s"
|
||||
msgstr "Das Buch wurde dem Bücherregal: %(sname)s hinzugefügt"
|
||||
|
||||
#: cps/web.py:1390
|
||||
#: cps/web.py:1793
|
||||
#, python-format
|
||||
msgid "Book has been removed from shelf: %(sname)s"
|
||||
msgstr "Das Buch wurde aus dem Bücherregal: %(sname)s entfernt"
|
||||
|
||||
#: cps/web.py:1409 cps/web.py:1433
|
||||
#: cps/web.py:1812 cps/web.py:1836
|
||||
#, python-format
|
||||
msgid "A shelf with the name '%(title)s' already exists."
|
||||
msgstr "Es existiert bereits ein Bücheregal mit dem Titel '%(title)s'"
|
||||
|
||||
#: cps/web.py:1414
|
||||
#: cps/web.py:1817
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s created"
|
||||
msgstr "Bücherregal %(title)s erzeugt"
|
||||
|
||||
#: cps/web.py:1416 cps/web.py:1444
|
||||
#: cps/web.py:1819 cps/web.py:1847
|
||||
msgid "There was an error"
|
||||
msgstr "Es trat ein Fehler auf"
|
||||
|
||||
#: cps/web.py:1417 cps/web.py:1419
|
||||
#: cps/web.py:1820 cps/web.py:1822
|
||||
msgid "create a shelf"
|
||||
msgstr "Bücherregal erzeugen"
|
||||
|
||||
#: cps/web.py:1442
|
||||
#: cps/web.py:1845
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s changed"
|
||||
msgstr "Bücherregal %(title)s verändert"
|
||||
|
||||
#: cps/web.py:1445 cps/web.py:1447
|
||||
#: cps/web.py:1848 cps/web.py:1850
|
||||
msgid "Edit a shelf"
|
||||
msgstr "Bücherregal editieren"
|
||||
|
||||
#: cps/web.py:1465
|
||||
#: cps/web.py:1868
|
||||
#, python-format
|
||||
msgid "successfully deleted shelf %(name)s"
|
||||
msgstr "Bücherregal %(name)s erfolgreich gelöscht"
|
||||
|
||||
#: cps/web.py:1487
|
||||
#: cps/web.py:1890
|
||||
#, python-format
|
||||
msgid "Shelf: '%(name)s'"
|
||||
msgstr "Bücherregal: '%(name)s'"
|
||||
|
||||
#: cps/web.py:1518
|
||||
#: cps/web.py:1921
|
||||
#, python-format
|
||||
msgid "Change order of Shelf: '%(name)s'"
|
||||
msgstr "Reihenfolge in Bücherregal '%(name)s' verändern"
|
||||
|
||||
#: cps/web.py:1580
|
||||
#: cps/web.py:1985
|
||||
msgid "Found an existing account for this email address."
|
||||
msgstr "Es existiert ein Benutzerkonto für diese E-Mailadresse"
|
||||
|
||||
#: cps/web.py:1582 cps/web.py:1586
|
||||
#: cps/web.py:1987 cps/web.py:1991
|
||||
#, python-format
|
||||
msgid "%(name)s's profile"
|
||||
msgstr "%(name)s's Profil"
|
||||
|
||||
#: cps/web.py:1583
|
||||
#: cps/web.py:1988
|
||||
msgid "Profile updated"
|
||||
msgstr "Profil aktualisiert"
|
||||
|
||||
#: cps/web.py:1597
|
||||
#: cps/web.py:2002
|
||||
msgid "Admin page"
|
||||
msgstr "Admin Seite"
|
||||
|
||||
#: cps/web.py:1668
|
||||
#: cps/web.py:2106
|
||||
msgid "Calibre-web configuration updated"
|
||||
msgstr "Calibre-web Konfiguration wurde aktualisiert"
|
||||
|
||||
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
|
||||
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
|
||||
msgid "Basic Configuration"
|
||||
msgstr "Basis Konfiguration"
|
||||
|
||||
#: cps/web.py:1679
|
||||
#: cps/web.py:2117
|
||||
msgid "DB location is not valid, please enter correct path"
|
||||
msgstr "DB Speicherort ist ungültig, bitte Pfad korrigieren"
|
||||
|
||||
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
|
||||
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
|
||||
msgid "Add new user"
|
||||
msgstr "Neuen Benutzer hinzufügen"
|
||||
|
||||
#: cps/web.py:1753
|
||||
#: cps/web.py:2194
|
||||
#, python-format
|
||||
msgid "User '%(user)s' created"
|
||||
msgstr "Benutzer '%(user)s' angelegt"
|
||||
|
||||
#: cps/web.py:1757
|
||||
#: cps/web.py:2198
|
||||
msgid "Found an existing account for this email address or nickname."
|
||||
msgstr ""
|
||||
"Es existiert ein Benutzerkonto für diese Emailadresse oder den "
|
||||
"Benutzernamen."
|
||||
|
||||
#: cps/web.py:1779
|
||||
#: cps/web.py:2220
|
||||
msgid "Mail settings updated"
|
||||
msgstr "E-Mail Einstellungen aktualisiert"
|
||||
|
||||
#: cps/web.py:1785
|
||||
#: cps/web.py:2227
|
||||
#, python-format
|
||||
msgid "Test E-Mail successfully send to %(kindlemail)s"
|
||||
msgstr "Test E-Mail erfolgreich an %(kindlemail)s versendet"
|
||||
|
||||
#: cps/web.py:1788
|
||||
#: cps/web.py:2230
|
||||
#, python-format
|
||||
msgid "There was an error sending the Test E-Mail: %(res)s"
|
||||
msgstr "Fehler beim versenden der Test E-Mail: %(res)s"
|
||||
|
||||
#: cps/web.py:1789
|
||||
#: cps/web.py:2234
|
||||
msgid "E-Mail settings updated"
|
||||
msgstr "E-Mail Einstellungen wurde aktualisiert"
|
||||
|
||||
#: cps/web.py:2235
|
||||
msgid "Edit mail settings"
|
||||
msgstr "E-Mail Einstellungen editieren"
|
||||
|
||||
#: cps/web.py:1817
|
||||
#: cps/web.py:2263
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' deleted"
|
||||
msgstr "Benutzer '%(nick)s' gelöscht"
|
||||
|
||||
#: cps/web.py:1898
|
||||
#: cps/web.py:2349
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' updated"
|
||||
msgstr "Benutzer '%(nick)s' aktualisiert"
|
||||
|
||||
#: cps/web.py:1901
|
||||
#: cps/web.py:2352
|
||||
msgid "An unknown error occured."
|
||||
msgstr "Es ist ein unbekanter Fehler aufgetreten"
|
||||
|
||||
#: cps/web.py:1904
|
||||
#: cps/web.py:2355
|
||||
#, python-format
|
||||
msgid "Edit User %(nick)s"
|
||||
msgstr "Benutzer %(nick)s bearbeiten"
|
||||
|
||||
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
|
||||
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
|
||||
msgid "edit metadata"
|
||||
msgstr "Metadaten editieren"
|
||||
|
||||
#: cps/web.py:2145
|
||||
#: cps/web.py:2598
|
||||
#, python-format
|
||||
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
|
||||
msgstr "Die Dateiendung \"%s\" kann nicht auf diesen Server hochgeladen werden"
|
||||
|
||||
#: cps/web.py:2604
|
||||
msgid "File to be uploaded must have an extension"
|
||||
msgstr "Datei müssen eine Erweiterung haben, um hochgeladen zu werden"
|
||||
|
||||
#: cps/web.py:2621
|
||||
#, python-format
|
||||
msgid "Failed to create path %s (Permission denied)."
|
||||
msgstr "Fehler beim Erzeugen des Pfads %s (Zugriff verweigert)"
|
||||
|
||||
#: cps/web.py:2150
|
||||
#: cps/web.py:2626
|
||||
#, python-format
|
||||
msgid "Failed to store file %s (Permission denied)."
|
||||
msgstr "Fehler beim speichern der Datei %s (Zugriff verweigert)"
|
||||
|
||||
#: cps/web.py:2155
|
||||
#: cps/web.py:2631
|
||||
#, python-format
|
||||
msgid "Failed to delete file %s (Permission denied)."
|
||||
msgstr "Fehler beim Löschen von Datei %s (Zugriff verweigert)"
|
||||
@@ -401,7 +414,7 @@ msgstr "DLS"
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:117
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:134
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
|
||||
@@ -457,7 +470,7 @@ msgstr "Konfiguration"
|
||||
msgid "Calibre DB dir"
|
||||
msgstr "Calibre DB Pfad"
|
||||
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
|
||||
msgid "Log Level"
|
||||
msgstr "Log Level"
|
||||
|
||||
@@ -465,7 +478,7 @@ msgstr "Log Level"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
|
||||
msgid "Books per page"
|
||||
msgstr "Bücher pro Seite"
|
||||
|
||||
@@ -494,42 +507,46 @@ msgid "Newest commit timestamp"
|
||||
msgstr "Neuestes Commit Datum"
|
||||
|
||||
#: cps/templates/admin.html:83
|
||||
msgid "Reconnect to Calibre DB"
|
||||
msgstr "Calibre-DB neu verbinden"
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
msgid "Restart Calibre-web"
|
||||
msgstr "Calibre-web Neustarten"
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
#: cps/templates/admin.html:85
|
||||
msgid "Stop Calibre-web"
|
||||
msgstr "Stoppe Calibre-web"
|
||||
|
||||
#: cps/templates/admin.html:85
|
||||
#: cps/templates/admin.html:86
|
||||
msgid "Check for update"
|
||||
msgstr "Suche nach Update"
|
||||
|
||||
#: cps/templates/admin.html:86
|
||||
#: cps/templates/admin.html:87
|
||||
msgid "Perform Update"
|
||||
msgstr "Update durchführen"
|
||||
|
||||
#: cps/templates/admin.html:96
|
||||
#: cps/templates/admin.html:97
|
||||
msgid "Do you really want to restart Calibre-web?"
|
||||
msgstr "Calibre-web wirklich neustarten?"
|
||||
|
||||
#: cps/templates/admin.html:101 cps/templates/admin.html:115
|
||||
#: cps/templates/admin.html:136
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/admin.html:137
|
||||
msgid "Ok"
|
||||
msgstr "Ok"
|
||||
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
|
||||
#: cps/templates/admin.html:103 cps/templates/admin.html:117
|
||||
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
|
||||
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: cps/templates/admin.html:114
|
||||
#: cps/templates/admin.html:115
|
||||
msgid "Do you really want to stop Calibre-web?"
|
||||
msgstr "Calibre-web wirklich stoppen"
|
||||
|
||||
#: cps/templates/admin.html:127
|
||||
#: cps/templates/admin.html:128
|
||||
msgid "Updating, please do not reload page"
|
||||
msgstr "Updatevorgang, bitte Seite nicht neu laden"
|
||||
|
||||
@@ -537,20 +554,21 @@ msgstr "Updatevorgang, bitte Seite nicht neu laden"
|
||||
msgid "Book Title"
|
||||
msgstr "Buchtitel"
|
||||
|
||||
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
|
||||
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
|
||||
#: cps/templates/search_form.html:10
|
||||
msgid "Author"
|
||||
msgstr "Autor"
|
||||
|
||||
#: cps/templates/book_edit.html:24
|
||||
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
|
||||
msgid "Description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
|
||||
#: cps/templates/search_form.html:33
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
|
||||
#: cps/templates/search_form.html:37
|
||||
msgid "Series"
|
||||
msgstr "Serien"
|
||||
|
||||
@@ -582,69 +600,142 @@ msgstr "Nein"
|
||||
msgid "view book after edit"
|
||||
msgstr "Buch nach Bearbeitung ansehen"
|
||||
|
||||
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:75
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
|
||||
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
|
||||
msgid "Get metadata"
|
||||
msgstr "Metadaten laden"
|
||||
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:79
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
|
||||
msgid "Submit"
|
||||
msgstr "Abschicken"
|
||||
|
||||
#: cps/templates/book_edit.html:121
|
||||
msgid "Keyword"
|
||||
msgstr "Suchbegriff"
|
||||
|
||||
#: cps/templates/book_edit.html:122
|
||||
msgid " Search keyword "
|
||||
msgstr "Suchbegriff"
|
||||
|
||||
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "Los!"
|
||||
|
||||
#: cps/templates/book_edit.html:125
|
||||
msgid "Click the cover to load metadata to the form"
|
||||
msgstr "Klicke auf das Bild um die Metadaten zu übertragen"
|
||||
|
||||
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
|
||||
msgid "Loading..."
|
||||
msgstr "Lade..."
|
||||
|
||||
#: cps/templates/book_edit.html:132
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: cps/templates/book_edit.html:143
|
||||
msgid "Search error!"
|
||||
msgstr "Fehler bei Suche!"
|
||||
|
||||
#: cps/templates/book_edit.html:144
|
||||
msgid "No Result! Please try anonther keyword."
|
||||
msgstr "Kein Ergebniss! Bitte anderen Begriff versuchen"
|
||||
|
||||
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
|
||||
#: cps/templates/search_form.html:14
|
||||
msgid "Publisher"
|
||||
msgstr "Herausgeber"
|
||||
|
||||
#: cps/templates/book_edit.html:148
|
||||
msgid "Source"
|
||||
msgstr "Quelle"
|
||||
|
||||
#: cps/templates/config_edit.html:7
|
||||
msgid "Location of Calibre database"
|
||||
msgstr "Speicherort der Calibre Datenbank"
|
||||
|
||||
#: cps/templates/config_edit.html:11
|
||||
#: cps/templates/config_edit.html:13
|
||||
msgid "Use google drive?"
|
||||
msgstr "Google Drive benutzen"
|
||||
|
||||
#: cps/templates/config_edit.html:17
|
||||
msgid "Client id"
|
||||
msgstr "Benutzer Id"
|
||||
|
||||
#: cps/templates/config_edit.html:21
|
||||
msgid "Client secret"
|
||||
msgstr "Benutzer Secret"
|
||||
|
||||
#: cps/templates/config_edit.html:25
|
||||
msgid "Calibre Base URL"
|
||||
msgstr "Calibnre Basis URL"
|
||||
|
||||
#: cps/templates/config_edit.html:29
|
||||
msgid "Google drive Calibre folder"
|
||||
msgstr "Google Drive Calibre Ordner"
|
||||
|
||||
#: cps/templates/config_edit.html:38
|
||||
msgid "Metadata Watch Channel ID"
|
||||
msgstr "Matadata Überwachungs-ID"
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
msgid "Server Port"
|
||||
msgstr "Server Port"
|
||||
|
||||
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
|
||||
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: cps/templates/config_edit.html:23
|
||||
#: cps/templates/config_edit.html:64
|
||||
msgid "No. of random books to show"
|
||||
msgstr "Anzahl Anzeige zufällige Bücher"
|
||||
|
||||
#: cps/templates/config_edit.html:28
|
||||
#: cps/templates/config_edit.html:68
|
||||
msgid "Regular expression for ignoring columns"
|
||||
msgstr "Regulärer Ausdruck um Spalten zu ignorien"
|
||||
|
||||
#: cps/templates/config_edit.html:72
|
||||
msgid "Regular expression for title sorting"
|
||||
msgstr "Regulärer Ausdruck für Titelsortierung"
|
||||
|
||||
#: cps/templates/config_edit.html:42
|
||||
#: cps/templates/config_edit.html:86
|
||||
msgid "Enable uploading"
|
||||
msgstr "Hochladen aktivieren"
|
||||
|
||||
#: cps/templates/config_edit.html:46
|
||||
#: cps/templates/config_edit.html:90
|
||||
msgid "Enable anonymous browsing"
|
||||
msgstr "Anonymes Browsen aktivieren"
|
||||
|
||||
#: cps/templates/config_edit.html:50
|
||||
#: cps/templates/config_edit.html:94
|
||||
msgid "Enable public registration"
|
||||
msgstr "Öffentliche Registrierung aktivieren"
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
#: cps/templates/config_edit.html:96
|
||||
msgid "Default Settings for new users"
|
||||
msgstr "Default Einstellungen für neue Benutzer"
|
||||
|
||||
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
|
||||
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
|
||||
msgid "Admin user"
|
||||
msgstr "Admin Benutzer"
|
||||
|
||||
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
|
||||
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
|
||||
msgid "Allow Downloads"
|
||||
msgstr "Downloads erlauben"
|
||||
|
||||
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
|
||||
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
|
||||
msgid "Allow Uploads"
|
||||
msgstr "Uploads erlauben"
|
||||
|
||||
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
|
||||
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
|
||||
msgid "Allow Edit"
|
||||
msgstr "Bearbeiten erlauben"
|
||||
|
||||
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
|
||||
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
|
||||
msgid "Allow Changing Password"
|
||||
msgstr "Passwort ändern erlauben"
|
||||
|
||||
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
|
||||
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
|
||||
#: cps/templates/login.html:4
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
@@ -661,23 +752,27 @@ msgstr "von"
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: cps/templates/detail.html:74
|
||||
#: cps/templates/detail.html:81
|
||||
msgid "Publishing date"
|
||||
msgstr "Herausgabedatum"
|
||||
|
||||
#: cps/templates/detail.html:106
|
||||
#: cps/templates/detail.html:115
|
||||
msgid "Read"
|
||||
msgstr "Gelesen"
|
||||
|
||||
#: cps/templates/detail.html:123
|
||||
msgid "Description:"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: cps/templates/detail.html:134
|
||||
#: cps/templates/detail.html:151
|
||||
msgid "Read in browser"
|
||||
msgstr "Im Browser lesen"
|
||||
|
||||
#: cps/templates/detail.html:154
|
||||
#: cps/templates/detail.html:171
|
||||
msgid "Add to shelf"
|
||||
msgstr "Zu Bücherregal hinzufügen"
|
||||
|
||||
#: cps/templates/detail.html:194
|
||||
#: cps/templates/detail.html:211
|
||||
msgid "Edit metadata"
|
||||
msgstr "Metadaten bearbeiten"
|
||||
|
||||
@@ -759,19 +854,29 @@ msgstr "Die neuesten Bücher"
|
||||
msgid "Show Random Books"
|
||||
msgstr "Zeige zufällige Bücher"
|
||||
|
||||
#: cps/templates/index.xml:43 cps/templates/layout.html:140
|
||||
#: cps/templates/index.xml:43 cps/templates/index.xml:47
|
||||
#: cps/templates/layout.html:132
|
||||
msgid "Read Books"
|
||||
msgstr "Gelesene Bücher"
|
||||
|
||||
#: cps/templates/index.xml:50 cps/templates/index.xml:54
|
||||
#: cps/templates/layout.html:133
|
||||
msgid "Unread Books"
|
||||
msgstr "Ungelesene Bücher"
|
||||
|
||||
#: cps/templates/index.xml:57 cps/templates/layout.html:144
|
||||
msgid "Authors"
|
||||
msgstr "Autoren"
|
||||
|
||||
#: cps/templates/index.xml:47
|
||||
#: cps/templates/index.xml:61
|
||||
msgid "Books ordered by Author"
|
||||
msgstr "Bücher nach Autoren sortiert"
|
||||
|
||||
#: cps/templates/index.xml:54
|
||||
#: cps/templates/index.xml:68
|
||||
msgid "Books ordered by category"
|
||||
msgstr "Bücher nach Kategorien sortiert"
|
||||
|
||||
#: cps/templates/index.xml:61
|
||||
#: cps/templates/index.xml:75
|
||||
msgid "Books ordered by series"
|
||||
msgstr "Bücher nach Reihen geordnet"
|
||||
|
||||
@@ -779,10 +884,6 @@ msgstr "Bücher nach Reihen geordnet"
|
||||
msgid "Toggle navigation"
|
||||
msgstr "Nagivation umschalten"
|
||||
|
||||
#: cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "Los!"
|
||||
|
||||
#: cps/templates/layout.html:68
|
||||
msgid "Advanced Search"
|
||||
msgstr "Erweiterte Suche"
|
||||
@@ -799,31 +900,31 @@ msgstr "Registrieren"
|
||||
msgid "Browse"
|
||||
msgstr "Browsen"
|
||||
|
||||
#: cps/templates/layout.html:132
|
||||
#: cps/templates/layout.html:136
|
||||
msgid "Discover"
|
||||
msgstr "Entdecke"
|
||||
|
||||
#: cps/templates/layout.html:135
|
||||
#: cps/templates/layout.html:139
|
||||
msgid "Categories"
|
||||
msgstr "Kategorien"
|
||||
|
||||
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
|
||||
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
|
||||
msgid "Languages"
|
||||
msgstr "Sprachen"
|
||||
|
||||
#: cps/templates/layout.html:145
|
||||
#: cps/templates/layout.html:149
|
||||
msgid "Public Shelves"
|
||||
msgstr "Öffentiche Bücherregale"
|
||||
|
||||
#: cps/templates/layout.html:149
|
||||
#: cps/templates/layout.html:153
|
||||
msgid "Your Shelves"
|
||||
msgstr "Deine Bücherregale"
|
||||
|
||||
#: cps/templates/layout.html:154
|
||||
#: cps/templates/layout.html:158
|
||||
msgid "Create a Shelf"
|
||||
msgstr "Bücherregal erzeugen"
|
||||
|
||||
#: cps/templates/layout.html:155
|
||||
#: cps/templates/layout.html:159
|
||||
msgid "About"
|
||||
msgstr "Über"
|
||||
|
||||
@@ -889,15 +990,15 @@ msgstr "Versuche eine andere Suche"
|
||||
msgid "Results for:"
|
||||
msgstr "Ergebnisse für:"
|
||||
|
||||
#: cps/templates/search_form.html:23
|
||||
#: cps/templates/search_form.html:27
|
||||
msgid "Exclude Tags"
|
||||
msgstr "Tags ausschließen"
|
||||
|
||||
#: cps/templates/search_form.html:43
|
||||
#: cps/templates/search_form.html:47
|
||||
msgid "Exclude Series"
|
||||
msgstr "Serie ausschließen"
|
||||
|
||||
#: cps/templates/search_form.html:64
|
||||
#: cps/templates/search_form.html:68
|
||||
msgid "Exclude Languages"
|
||||
msgstr "Sprache ausschließen"
|
||||
|
||||
@@ -922,37 +1023,37 @@ msgid "Drag 'n drop to rearrange order"
|
||||
msgstr "Drag 'n drop um Reihenfolge zu ändern"
|
||||
|
||||
#: cps/templates/stats.html:3
|
||||
msgid "Linked libraries"
|
||||
msgstr "Dynamische Bibliotheken"
|
||||
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Program library"
|
||||
msgstr "Programm Bibliotheken"
|
||||
|
||||
#: cps/templates/stats.html:9
|
||||
msgid "Installed Version"
|
||||
msgstr "Installierte Version"
|
||||
|
||||
#: cps/templates/stats.html:32
|
||||
msgid "Calibre library statistics"
|
||||
msgstr "Calibre Bibliothek Statistiken"
|
||||
|
||||
#: cps/templates/stats.html:37
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Books in this Library"
|
||||
msgstr "Bücher in dieser Bibliothek"
|
||||
|
||||
#: cps/templates/stats.html:41
|
||||
#: cps/templates/stats.html:12
|
||||
msgid "Authors in this Library"
|
||||
msgstr "Autoren in dieser Bibliothek"
|
||||
|
||||
#: cps/templates/stats.html:45
|
||||
#: cps/templates/stats.html:16
|
||||
msgid "Categories in this Library"
|
||||
msgstr "Kategorien in dieser Bibliothek"
|
||||
|
||||
#: cps/templates/stats.html:49
|
||||
#: cps/templates/stats.html:20
|
||||
msgid "Series in this Library"
|
||||
msgstr "Serien in dieser Bibliothek"
|
||||
|
||||
#: cps/templates/stats.html:24
|
||||
msgid "Linked libraries"
|
||||
msgstr "Dynamische Bibliotheken"
|
||||
|
||||
#: cps/templates/stats.html:28
|
||||
msgid "Program library"
|
||||
msgstr "Programm Bibliotheken"
|
||||
|
||||
#: cps/templates/stats.html:29
|
||||
msgid "Installed Version"
|
||||
msgstr "Installierte Version"
|
||||
|
||||
#: cps/templates/user_edit.html:23
|
||||
msgid "Kindle E-Mail"
|
||||
msgstr "Kindle E-Mail"
|
||||
@@ -965,43 +1066,47 @@ msgstr "Zeige nur Bücher mit dieser Sprache"
|
||||
msgid "Show all"
|
||||
msgstr "Zeige alle"
|
||||
|
||||
#: cps/templates/user_edit.html:45
|
||||
#: cps/templates/user_edit.html:47
|
||||
msgid "Show random books"
|
||||
msgstr "Zeige Zufällige Bücher"
|
||||
|
||||
#: cps/templates/user_edit.html:49
|
||||
#: cps/templates/user_edit.html:51
|
||||
msgid "Show hot books"
|
||||
msgstr "Zeige Auswahl Beliebte Bücher"
|
||||
|
||||
#: cps/templates/user_edit.html:53
|
||||
#: cps/templates/user_edit.html:55
|
||||
msgid "Show best rated books"
|
||||
msgstr "Zeige am besten bewertete Bücher"
|
||||
|
||||
#: cps/templates/user_edit.html:57
|
||||
#: cps/templates/user_edit.html:59
|
||||
msgid "Show language selection"
|
||||
msgstr "Zeige Sprachauswahl"
|
||||
|
||||
#: cps/templates/user_edit.html:61
|
||||
#: cps/templates/user_edit.html:63
|
||||
msgid "Show series selection"
|
||||
msgstr "Zeige Serienauswahl"
|
||||
|
||||
#: cps/templates/user_edit.html:65
|
||||
#: cps/templates/user_edit.html:67
|
||||
msgid "Show category selection"
|
||||
msgstr "Zeige Kategorienauswahl"
|
||||
|
||||
#: cps/templates/user_edit.html:69
|
||||
#: cps/templates/user_edit.html:71
|
||||
msgid "Show author selection"
|
||||
msgstr "Zeige Autorenauswahl"
|
||||
|
||||
#: cps/templates/user_edit.html:73
|
||||
#: cps/templates/user_edit.html:75
|
||||
msgid "Show read and unread"
|
||||
msgstr "Zeige Gelesen/Ungelesen Auswahl"
|
||||
|
||||
#: cps/templates/user_edit.html:79
|
||||
msgid "Show random books in detail view"
|
||||
msgstr "Zeige zufällige Bücher in der Detailansicht"
|
||||
|
||||
#: cps/templates/user_edit.html:105
|
||||
#: cps/templates/user_edit.html:112
|
||||
msgid "Delete this user"
|
||||
msgstr "Benutzer löschen"
|
||||
|
||||
#: cps/templates/user_edit.html:116
|
||||
#: cps/templates/user_edit.html:127
|
||||
msgid "Recent Downloads"
|
||||
msgstr "Letzte Downloads"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Calibre-web\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
|
||||
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
|
||||
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
|
||||
"PO-Revision-Date: 2016-11-13 18:35+0100\n"
|
||||
"Last-Translator: Juan F. Villa <juan.villa@paisdelconocimiento.org>\n"
|
||||
"Language: es\n"
|
||||
@@ -25,341 +25,354 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
|
||||
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
|
||||
msgid "not installed"
|
||||
msgstr "No instalado"
|
||||
|
||||
#: cps/helper.py:150
|
||||
#: cps/helper.py:164
|
||||
#, python-format
|
||||
msgid "Failed to send mail: %s"
|
||||
msgstr "Fallo al enviar el correo : %s"
|
||||
|
||||
#: cps/helper.py:157
|
||||
#: cps/helper.py:171
|
||||
msgid "Calibre-web test email"
|
||||
msgstr "Prueba de Correo Calibre-web"
|
||||
|
||||
#: cps/helper.py:158 cps/helper.py:168
|
||||
#: cps/helper.py:172 cps/helper.py:184
|
||||
msgid "This email has been sent via calibre web."
|
||||
msgstr "Este mensaje ha sido enviado via Calibre Web."
|
||||
|
||||
#: cps/helper.py:167 cps/templates/detail.html:130
|
||||
#: cps/helper.py:181 cps/templates/detail.html:146
|
||||
msgid "Send to Kindle"
|
||||
msgstr "Enviar a Kindle"
|
||||
|
||||
#: cps/helper.py:185 cps/helper.py:200
|
||||
#: cps/helper.py:201 cps/helper.py:216
|
||||
msgid "Could not find any formats suitable for sending by email"
|
||||
msgstr "Formato no compatible para enviar por correo electronico"
|
||||
|
||||
#: cps/helper.py:194
|
||||
#: cps/helper.py:210
|
||||
msgid "Could not convert epub to mobi"
|
||||
msgstr "No fue posible convertir de epub a mobi"
|
||||
|
||||
#: cps/ub.py:434
|
||||
#: cps/ub.py:488
|
||||
msgid "Guest"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:734
|
||||
#: cps/web.py:904
|
||||
msgid "Requesting update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:735
|
||||
#: cps/web.py:905
|
||||
msgid "Downloading update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:736
|
||||
#: cps/web.py:906
|
||||
msgid "Unzipping update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:737
|
||||
#: cps/web.py:907
|
||||
msgid "Files are replaced"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:738
|
||||
#: cps/web.py:908
|
||||
msgid "Database connections are closed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:739
|
||||
#: cps/web.py:909
|
||||
msgid "Server is stopped"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:740
|
||||
#: cps/web.py:910
|
||||
msgid "Update finished, please press okay and reload page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:810
|
||||
#: cps/web.py:983
|
||||
msgid "Latest Books"
|
||||
msgstr "Libros recientes"
|
||||
|
||||
#: cps/web.py:835
|
||||
#: cps/web.py:1014
|
||||
msgid "Hot Books (most downloaded)"
|
||||
msgstr "Libros Populares (los mas descargados)"
|
||||
|
||||
#: cps/web.py:845
|
||||
#: cps/web.py:1024
|
||||
msgid "Best rated books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:36 cps/web.py:854
|
||||
#: cps/templates/index.xml:36 cps/web.py:1033
|
||||
msgid "Random Books"
|
||||
msgstr "Libros al Azar"
|
||||
|
||||
#: cps/web.py:867
|
||||
#: cps/web.py:1046
|
||||
msgid "Author list"
|
||||
msgstr "Lista de Autores"
|
||||
|
||||
#: cps/web.py:878
|
||||
#: cps/web.py:1057
|
||||
#, python-format
|
||||
msgid "Author: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
|
||||
#: cps/web.py:2115
|
||||
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
|
||||
#: cps/web.py:2579
|
||||
msgid "Error opening eBook. File does not exist or file is not accessible:"
|
||||
msgstr "Error en apertura del Objeto. El archivo no existe o no es accesible"
|
||||
|
||||
#: cps/templates/index.xml:57 cps/web.py:894
|
||||
#: cps/templates/index.xml:71 cps/web.py:1073
|
||||
msgid "Series list"
|
||||
msgstr "lista de Series"
|
||||
|
||||
#: cps/web.py:906
|
||||
#: cps/web.py:1085
|
||||
#, python-format
|
||||
msgid "Series: %(serie)s"
|
||||
msgstr "Series : %(serie)s"
|
||||
|
||||
#: cps/web.py:939
|
||||
#: cps/web.py:1118
|
||||
msgid "Available languages"
|
||||
msgstr "Lenguajes disponibles"
|
||||
|
||||
#: cps/web.py:954
|
||||
#: cps/web.py:1133
|
||||
#, python-format
|
||||
msgid "Language: %(name)s"
|
||||
msgstr "Lenguaje: %(name)s"
|
||||
|
||||
#: cps/templates/index.xml:50 cps/web.py:967
|
||||
#: cps/templates/index.xml:64 cps/web.py:1146
|
||||
msgid "Category list"
|
||||
msgstr "Lista de Categorias"
|
||||
|
||||
#: cps/web.py:979
|
||||
#: cps/web.py:1158
|
||||
#, python-format
|
||||
msgid "Category: %(name)s"
|
||||
msgstr "Categoria : %(name)s"
|
||||
|
||||
#: cps/web.py:1040
|
||||
#: cps/web.py:1267
|
||||
msgid "Statistics"
|
||||
msgstr "Estadisticas"
|
||||
|
||||
#: cps/web.py:1061
|
||||
#: cps/web.py:1375
|
||||
msgid "Server restarted, please reload page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1063
|
||||
#: cps/web.py:1377
|
||||
msgid "Performing shutdown of server, please close window"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1073
|
||||
#: cps/web.py:1392
|
||||
msgid "Update done"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1147 cps/web.py:1160
|
||||
#: cps/web.py:1470 cps/web.py:1483
|
||||
msgid "search"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
|
||||
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
|
||||
msgid "Read a Book"
|
||||
msgstr "Leer un Libro"
|
||||
|
||||
#: cps/web.py:1276 cps/web.py:1713
|
||||
#: cps/web.py:1676 cps/web.py:2152
|
||||
msgid "Please fill out all fields!"
|
||||
msgstr "Por favor llenar todos los campos!"
|
||||
|
||||
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
|
||||
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
|
||||
msgid "register"
|
||||
msgstr "Registrarse"
|
||||
|
||||
#: cps/web.py:1292
|
||||
#: cps/web.py:1692
|
||||
msgid "An unknown error occured. Please try again later."
|
||||
msgstr "Ocurrio un error. Intentar de nuevo mas tarde."
|
||||
|
||||
#: cps/web.py:1297
|
||||
#: cps/web.py:1697
|
||||
msgid "This username or email address is already in use."
|
||||
msgstr "Usuario o direccion de correo en uso."
|
||||
|
||||
#: cps/web.py:1315
|
||||
#: cps/web.py:1715
|
||||
#, python-format
|
||||
msgid "you are now logged in as: '%(nickname)s'"
|
||||
msgstr "Sesion iniciada como : '%(nickname)s'"
|
||||
|
||||
#: cps/web.py:1320
|
||||
#: cps/web.py:1720
|
||||
msgid "Wrong Username or Password"
|
||||
msgstr "Usuario o contraseña invalido"
|
||||
|
||||
#: cps/web.py:1322
|
||||
#: cps/web.py:1722
|
||||
msgid "login"
|
||||
msgstr "Iniciar Sesion"
|
||||
|
||||
#: cps/web.py:1339
|
||||
#: cps/web.py:1739
|
||||
msgid "Please configure the SMTP mail settings first..."
|
||||
msgstr "Configurar primero los parametros SMTP por favor..."
|
||||
|
||||
#: cps/web.py:1343
|
||||
#: cps/web.py:1743
|
||||
#, python-format
|
||||
msgid "Book successfully send to %(kindlemail)s"
|
||||
msgstr "Envio de Libro a %(kindlemail)s correctamente"
|
||||
|
||||
#: cps/web.py:1347
|
||||
#: cps/web.py:1747
|
||||
#, python-format
|
||||
msgid "There was an error sending this book: %(res)s"
|
||||
msgstr "Ha sucedido un error en el envio del Libro: %(res)s"
|
||||
|
||||
#: cps/web.py:1349
|
||||
#: cps/web.py:1749 cps/web.py:2232
|
||||
msgid "Please configure your kindle email address first..."
|
||||
msgstr "Configurar primero la dirección de correo Kindle por favor..."
|
||||
|
||||
#: cps/web.py:1369
|
||||
#: cps/web.py:1774
|
||||
#, python-format
|
||||
msgid "Book has been added to shelf: %(sname)s"
|
||||
msgstr "El libro fue agregado a el estante: %(sname)s"
|
||||
|
||||
#: cps/web.py:1390
|
||||
#: cps/web.py:1793
|
||||
#, python-format
|
||||
msgid "Book has been removed from shelf: %(sname)s"
|
||||
msgstr "El libro fue removido del estante: %(sname)s"
|
||||
|
||||
#: cps/web.py:1409 cps/web.py:1433
|
||||
#: cps/web.py:1812 cps/web.py:1836
|
||||
#, python-format
|
||||
msgid "A shelf with the name '%(title)s' already exists."
|
||||
msgstr "Une étagère de ce nom '%(title)s' existe déjà."
|
||||
|
||||
#: cps/web.py:1414
|
||||
#: cps/web.py:1817
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s created"
|
||||
msgstr "Estante %(title)s creado"
|
||||
|
||||
#: cps/web.py:1416 cps/web.py:1444
|
||||
#: cps/web.py:1819 cps/web.py:1847
|
||||
msgid "There was an error"
|
||||
msgstr "Hemos tenido un error"
|
||||
|
||||
#: cps/web.py:1417 cps/web.py:1419
|
||||
#: cps/web.py:1820 cps/web.py:1822
|
||||
msgid "create a shelf"
|
||||
msgstr "Crear un Estante"
|
||||
|
||||
#: cps/web.py:1442
|
||||
#: cps/web.py:1845
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s changed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1445 cps/web.py:1447
|
||||
#: cps/web.py:1848 cps/web.py:1850
|
||||
msgid "Edit a shelf"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1465
|
||||
#: cps/web.py:1868
|
||||
#, python-format
|
||||
msgid "successfully deleted shelf %(name)s"
|
||||
msgstr "Estante %(name)s fue borrado correctamente"
|
||||
|
||||
#: cps/web.py:1487
|
||||
#: cps/web.py:1890
|
||||
#, python-format
|
||||
msgid "Shelf: '%(name)s'"
|
||||
msgstr "Estante: '%(name)s'"
|
||||
|
||||
#: cps/web.py:1518
|
||||
#: cps/web.py:1921
|
||||
#, python-format
|
||||
msgid "Change order of Shelf: '%(name)s'"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1580
|
||||
#: cps/web.py:1985
|
||||
msgid "Found an existing account for this email address."
|
||||
msgstr "Existe una cuenta vinculada a esta cuenta de correo."
|
||||
|
||||
#: cps/web.py:1582 cps/web.py:1586
|
||||
#: cps/web.py:1987 cps/web.py:1991
|
||||
#, python-format
|
||||
msgid "%(name)s's profile"
|
||||
msgstr "Perfil de %(name)s"
|
||||
|
||||
#: cps/web.py:1583
|
||||
#: cps/web.py:1988
|
||||
msgid "Profile updated"
|
||||
msgstr "Perfil actualizado"
|
||||
|
||||
#: cps/web.py:1597
|
||||
#: cps/web.py:2002
|
||||
msgid "Admin page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1668
|
||||
#: cps/web.py:2106
|
||||
msgid "Calibre-web configuration updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
|
||||
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
|
||||
msgid "Basic Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1679
|
||||
#: cps/web.py:2117
|
||||
msgid "DB location is not valid, please enter correct path"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
|
||||
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
|
||||
msgid "Add new user"
|
||||
msgstr "Agregar un nuevo usuario"
|
||||
|
||||
#: cps/web.py:1753
|
||||
#: cps/web.py:2194
|
||||
#, python-format
|
||||
msgid "User '%(user)s' created"
|
||||
msgstr "Usuario '%(user)s' creado"
|
||||
|
||||
#: cps/web.py:1757
|
||||
#: cps/web.py:2198
|
||||
msgid "Found an existing account for this email address or nickname."
|
||||
msgstr "Se ha encontrado una cuenta vinculada a esta cuenta de correo o usuario."
|
||||
|
||||
#: cps/web.py:1779
|
||||
#: cps/web.py:2220
|
||||
msgid "Mail settings updated"
|
||||
msgstr "Parametros de correo actualizados"
|
||||
|
||||
#: cps/web.py:1785
|
||||
#: cps/web.py:2227
|
||||
#, python-format
|
||||
msgid "Test E-Mail successfully send to %(kindlemail)s"
|
||||
msgstr "Exito al realizar envio de prueba a %(kindlemail)s"
|
||||
|
||||
#: cps/web.py:1788
|
||||
#: cps/web.py:2230
|
||||
#, python-format
|
||||
msgid "There was an error sending the Test E-Mail: %(res)s"
|
||||
msgstr "Error al realizar envio de prueba a E-Mail: %(res)s"
|
||||
|
||||
#: cps/web.py:1789
|
||||
#: cps/web.py:2234
|
||||
msgid "E-Mail settings updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2235
|
||||
msgid "Edit mail settings"
|
||||
msgstr "Editar parametros de correo"
|
||||
|
||||
#: cps/web.py:1817
|
||||
#: cps/web.py:2263
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' deleted"
|
||||
msgstr "Usuario '%(nick)s' borrado"
|
||||
|
||||
#: cps/web.py:1898
|
||||
#: cps/web.py:2349
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' updated"
|
||||
msgstr "Usuario '%(nick)s' Actualizado"
|
||||
|
||||
#: cps/web.py:1901
|
||||
#: cps/web.py:2352
|
||||
msgid "An unknown error occured."
|
||||
msgstr "Oups ! Error inesperado."
|
||||
|
||||
#: cps/web.py:1904
|
||||
#: cps/web.py:2355
|
||||
#, python-format
|
||||
msgid "Edit User %(nick)s"
|
||||
msgstr "Editar Usuario %(nick)s"
|
||||
|
||||
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
|
||||
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
|
||||
msgid "edit metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2145
|
||||
#: cps/web.py:2598
|
||||
#, python-format
|
||||
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2604
|
||||
msgid "File to be uploaded must have an extension"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2621
|
||||
#, python-format
|
||||
msgid "Failed to create path %s (Permission denied)."
|
||||
msgstr "Fallo al crear la ruta %s (permiso negado)"
|
||||
|
||||
#: cps/web.py:2150
|
||||
#: cps/web.py:2626
|
||||
#, python-format
|
||||
msgid "Failed to store file %s (Permission denied)."
|
||||
msgstr "Fallo al almacenar el archivo %s (permiso negado)"
|
||||
|
||||
#: cps/web.py:2155
|
||||
#: cps/web.py:2631
|
||||
#, python-format
|
||||
msgid "Failed to delete file %s (Permission denied)."
|
||||
msgstr "Fallo al borrar el archivo %s (permiso negado)"
|
||||
@@ -388,7 +401,7 @@ msgstr "DLS"
|
||||
msgid "Admin"
|
||||
msgstr "Administracion"
|
||||
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:117
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:134
|
||||
msgid "Download"
|
||||
msgstr "Descarga"
|
||||
|
||||
@@ -444,7 +457,7 @@ msgstr ""
|
||||
msgid "Calibre DB dir"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
|
||||
msgid "Log Level"
|
||||
msgstr ""
|
||||
|
||||
@@ -452,7 +465,7 @@ msgstr ""
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
|
||||
msgid "Books per page"
|
||||
msgstr ""
|
||||
|
||||
@@ -481,42 +494,46 @@ msgid "Newest commit timestamp"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:83
|
||||
msgid "Restart Calibre-web"
|
||||
msgid "Reconnect to Calibre DB"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
msgid "Stop Calibre-web"
|
||||
msgid "Restart Calibre-web"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:85
|
||||
msgid "Check for update"
|
||||
msgid "Stop Calibre-web"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:86
|
||||
msgid "Check for update"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:87
|
||||
msgid "Perform Update"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:96
|
||||
#: cps/templates/admin.html:97
|
||||
msgid "Do you really want to restart Calibre-web?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:101 cps/templates/admin.html:115
|
||||
#: cps/templates/admin.html:136
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/admin.html:137
|
||||
msgid "Ok"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
|
||||
#: cps/templates/admin.html:103 cps/templates/admin.html:117
|
||||
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
|
||||
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
|
||||
msgid "Back"
|
||||
msgstr "Regresar"
|
||||
|
||||
#: cps/templates/admin.html:114
|
||||
#: cps/templates/admin.html:115
|
||||
msgid "Do you really want to stop Calibre-web?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:127
|
||||
#: cps/templates/admin.html:128
|
||||
msgid "Updating, please do not reload page"
|
||||
msgstr ""
|
||||
|
||||
@@ -524,20 +541,21 @@ msgstr ""
|
||||
msgid "Book Title"
|
||||
msgstr "Titulo del Libro"
|
||||
|
||||
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
|
||||
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
|
||||
#: cps/templates/search_form.html:10
|
||||
msgid "Author"
|
||||
msgstr "Autor"
|
||||
|
||||
#: cps/templates/book_edit.html:24
|
||||
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
|
||||
msgid "Description"
|
||||
msgstr "Descripcion"
|
||||
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
|
||||
msgid "Tags"
|
||||
msgstr "Etiqueta"
|
||||
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
|
||||
#: cps/templates/search_form.html:33
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
|
||||
#: cps/templates/search_form.html:37
|
||||
msgid "Series"
|
||||
msgstr "Series"
|
||||
|
||||
@@ -569,69 +587,142 @@ msgstr "No"
|
||||
msgid "view book after edit"
|
||||
msgstr "Ver libro tras la edicion"
|
||||
|
||||
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:75
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
|
||||
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
|
||||
msgid "Get metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:79
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
|
||||
msgid "Submit"
|
||||
msgstr "Enviar"
|
||||
|
||||
#: cps/templates/book_edit.html:121
|
||||
msgid "Keyword"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:122
|
||||
msgid " Search keyword "
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "Vamos!"
|
||||
|
||||
#: cps/templates/book_edit.html:125
|
||||
msgid "Click the cover to load metadata to the form"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:132
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:143
|
||||
msgid "Search error!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:144
|
||||
msgid "No Result! Please try anonther keyword."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
|
||||
#: cps/templates/search_form.html:14
|
||||
msgid "Publisher"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:148
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:7
|
||||
msgid "Location of Calibre database"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:11
|
||||
msgid "Server Port"
|
||||
#: cps/templates/config_edit.html:13
|
||||
msgid "Use google drive?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr "Titulo"
|
||||
|
||||
#: cps/templates/config_edit.html:23
|
||||
msgid "No. of random books to show"
|
||||
#: cps/templates/config_edit.html:17
|
||||
msgid "Client id"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:28
|
||||
msgid "Regular expression for title sorting"
|
||||
#: cps/templates/config_edit.html:21
|
||||
msgid "Client secret"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:42
|
||||
msgid "Enable uploading"
|
||||
#: cps/templates/config_edit.html:25
|
||||
msgid "Calibre Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:46
|
||||
msgid "Enable anonymous browsing"
|
||||
#: cps/templates/config_edit.html:29
|
||||
msgid "Google drive Calibre folder"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:50
|
||||
msgid "Enable public registration"
|
||||
#: cps/templates/config_edit.html:38
|
||||
msgid "Metadata Watch Channel ID"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
msgid "Server Port"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr "Titulo"
|
||||
|
||||
#: cps/templates/config_edit.html:64
|
||||
msgid "No. of random books to show"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:68
|
||||
msgid "Regular expression for ignoring columns"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:72
|
||||
msgid "Regular expression for title sorting"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:86
|
||||
msgid "Enable uploading"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:90
|
||||
msgid "Enable anonymous browsing"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:94
|
||||
msgid "Enable public registration"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:96
|
||||
msgid "Default Settings for new users"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
|
||||
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
|
||||
msgid "Admin user"
|
||||
msgstr "Usuario Administrador"
|
||||
|
||||
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
|
||||
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
|
||||
msgid "Allow Downloads"
|
||||
msgstr "Permitir descargas"
|
||||
|
||||
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
|
||||
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
|
||||
msgid "Allow Uploads"
|
||||
msgstr "Permitir subidas de archivos"
|
||||
|
||||
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
|
||||
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
|
||||
msgid "Allow Edit"
|
||||
msgstr "Permitir editar"
|
||||
|
||||
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
|
||||
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
|
||||
msgid "Allow Changing Password"
|
||||
msgstr "Permitir cambiar la clave"
|
||||
|
||||
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
|
||||
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
|
||||
#: cps/templates/login.html:4
|
||||
msgid "Login"
|
||||
msgstr "Inicio de Sesion"
|
||||
@@ -648,23 +739,27 @@ msgstr "de"
|
||||
msgid "language"
|
||||
msgstr "Lenguaje"
|
||||
|
||||
#: cps/templates/detail.html:74
|
||||
#: cps/templates/detail.html:81
|
||||
msgid "Publishing date"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:106
|
||||
#: cps/templates/detail.html:115
|
||||
msgid "Read"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:123
|
||||
msgid "Description:"
|
||||
msgstr "Descripcion :"
|
||||
|
||||
#: cps/templates/detail.html:134
|
||||
#: cps/templates/detail.html:151
|
||||
msgid "Read in browser"
|
||||
msgstr "Ver en el navegador"
|
||||
|
||||
#: cps/templates/detail.html:154
|
||||
#: cps/templates/detail.html:171
|
||||
msgid "Add to shelf"
|
||||
msgstr "Agregar al estante"
|
||||
|
||||
#: cps/templates/detail.html:194
|
||||
#: cps/templates/detail.html:211
|
||||
msgid "Edit metadata"
|
||||
msgstr "Editar la metadata"
|
||||
|
||||
@@ -744,19 +839,29 @@ msgstr "Libros Recientes"
|
||||
msgid "Show Random Books"
|
||||
msgstr "Mostrar libros al azar"
|
||||
|
||||
#: cps/templates/index.xml:43 cps/templates/layout.html:140
|
||||
#: cps/templates/index.xml:43 cps/templates/index.xml:47
|
||||
#: cps/templates/layout.html:132
|
||||
msgid "Read Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:50 cps/templates/index.xml:54
|
||||
#: cps/templates/layout.html:133
|
||||
msgid "Unread Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:57 cps/templates/layout.html:144
|
||||
msgid "Authors"
|
||||
msgstr "Autores"
|
||||
|
||||
#: cps/templates/index.xml:47
|
||||
#: cps/templates/index.xml:61
|
||||
msgid "Books ordered by Author"
|
||||
msgstr "Libros ordenados por Autor"
|
||||
|
||||
#: cps/templates/index.xml:54
|
||||
#: cps/templates/index.xml:68
|
||||
msgid "Books ordered by category"
|
||||
msgstr "Libros ordenados por Categorias"
|
||||
|
||||
#: cps/templates/index.xml:61
|
||||
#: cps/templates/index.xml:75
|
||||
msgid "Books ordered by series"
|
||||
msgstr "Libros ordenados por Series"
|
||||
|
||||
@@ -764,10 +869,6 @@ msgstr "Libros ordenados por Series"
|
||||
msgid "Toggle navigation"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "Vamos!"
|
||||
|
||||
#: cps/templates/layout.html:68
|
||||
msgid "Advanced Search"
|
||||
msgstr "Busqueda avanzada"
|
||||
@@ -784,31 +885,31 @@ msgstr "Registro"
|
||||
msgid "Browse"
|
||||
msgstr "Explorar"
|
||||
|
||||
#: cps/templates/layout.html:132
|
||||
#: cps/templates/layout.html:136
|
||||
msgid "Discover"
|
||||
msgstr "Descubrir"
|
||||
|
||||
#: cps/templates/layout.html:135
|
||||
#: cps/templates/layout.html:139
|
||||
msgid "Categories"
|
||||
msgstr "Categoria"
|
||||
|
||||
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
|
||||
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
|
||||
msgid "Languages"
|
||||
msgstr "Lenguaje"
|
||||
|
||||
#: cps/templates/layout.html:145
|
||||
#: cps/templates/layout.html:149
|
||||
msgid "Public Shelves"
|
||||
msgstr "Estantes Publicos"
|
||||
|
||||
#: cps/templates/layout.html:149
|
||||
#: cps/templates/layout.html:153
|
||||
msgid "Your Shelves"
|
||||
msgstr "Sus Estantes"
|
||||
|
||||
#: cps/templates/layout.html:154
|
||||
#: cps/templates/layout.html:158
|
||||
msgid "Create a Shelf"
|
||||
msgstr "Crear un estante"
|
||||
|
||||
#: cps/templates/layout.html:155
|
||||
#: cps/templates/layout.html:159
|
||||
msgid "About"
|
||||
msgstr "Acerca de"
|
||||
|
||||
@@ -874,15 +975,15 @@ msgstr "Intente una busqueda diferente"
|
||||
msgid "Results for:"
|
||||
msgstr "Resultados para:"
|
||||
|
||||
#: cps/templates/search_form.html:23
|
||||
#: cps/templates/search_form.html:27
|
||||
msgid "Exclude Tags"
|
||||
msgstr "Excluir etiquetas"
|
||||
|
||||
#: cps/templates/search_form.html:43
|
||||
#: cps/templates/search_form.html:47
|
||||
msgid "Exclude Series"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/search_form.html:64
|
||||
#: cps/templates/search_form.html:68
|
||||
msgid "Exclude Languages"
|
||||
msgstr ""
|
||||
|
||||
@@ -907,37 +1008,37 @@ msgid "Drag 'n drop to rearrange order"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:3
|
||||
msgid "Linked libraries"
|
||||
msgstr "Librerias vinculadas"
|
||||
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Program library"
|
||||
msgstr "Librerias del programa"
|
||||
|
||||
#: cps/templates/stats.html:9
|
||||
msgid "Installed Version"
|
||||
msgstr "Version instalada"
|
||||
|
||||
#: cps/templates/stats.html:32
|
||||
msgid "Calibre library statistics"
|
||||
msgstr "Estadisticas de la Biblioteca"
|
||||
|
||||
#: cps/templates/stats.html:37
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Books in this Library"
|
||||
msgstr "Libros en esta Biblioteca"
|
||||
|
||||
#: cps/templates/stats.html:41
|
||||
#: cps/templates/stats.html:12
|
||||
msgid "Authors in this Library"
|
||||
msgstr "Autores en esta Biblioteca"
|
||||
|
||||
#: cps/templates/stats.html:45
|
||||
#: cps/templates/stats.html:16
|
||||
msgid "Categories in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:49
|
||||
#: cps/templates/stats.html:20
|
||||
msgid "Series in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:24
|
||||
msgid "Linked libraries"
|
||||
msgstr "Librerias vinculadas"
|
||||
|
||||
#: cps/templates/stats.html:28
|
||||
msgid "Program library"
|
||||
msgstr "Librerias del programa"
|
||||
|
||||
#: cps/templates/stats.html:29
|
||||
msgid "Installed Version"
|
||||
msgstr "Version instalada"
|
||||
|
||||
#: cps/templates/user_edit.html:23
|
||||
msgid "Kindle E-Mail"
|
||||
msgstr "Correo del Kindle"
|
||||
@@ -950,43 +1051,47 @@ msgstr "Mostrar lenguaje de los libros"
|
||||
msgid "Show all"
|
||||
msgstr "Mostrar Todo"
|
||||
|
||||
#: cps/templates/user_edit.html:45
|
||||
#: cps/templates/user_edit.html:47
|
||||
msgid "Show random books"
|
||||
msgstr "Mostrar libros al azar"
|
||||
|
||||
#: cps/templates/user_edit.html:49
|
||||
#: cps/templates/user_edit.html:51
|
||||
msgid "Show hot books"
|
||||
msgstr "Mostrar libros populares"
|
||||
|
||||
#: cps/templates/user_edit.html:53
|
||||
#: cps/templates/user_edit.html:55
|
||||
msgid "Show best rated books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:57
|
||||
#: cps/templates/user_edit.html:59
|
||||
msgid "Show language selection"
|
||||
msgstr "Mostrar lenguaje seleccionado"
|
||||
|
||||
#: cps/templates/user_edit.html:61
|
||||
#: cps/templates/user_edit.html:63
|
||||
msgid "Show series selection"
|
||||
msgstr "Mostrar series seleccionadas"
|
||||
|
||||
#: cps/templates/user_edit.html:65
|
||||
#: cps/templates/user_edit.html:67
|
||||
msgid "Show category selection"
|
||||
msgstr "Mostrar categorias elegidas"
|
||||
|
||||
#: cps/templates/user_edit.html:69
|
||||
#: cps/templates/user_edit.html:71
|
||||
msgid "Show author selection"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:73
|
||||
#: cps/templates/user_edit.html:75
|
||||
msgid "Show read and unread"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:79
|
||||
msgid "Show random books in detail view"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:105
|
||||
#: cps/templates/user_edit.html:112
|
||||
msgid "Delete this user"
|
||||
msgstr "Borrar este usuario"
|
||||
|
||||
#: cps/templates/user_edit.html:116
|
||||
#: cps/templates/user_edit.html:127
|
||||
msgid "Recent Downloads"
|
||||
msgstr "Descargas Recientes"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -20,7 +20,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Calibre-web\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
|
||||
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
|
||||
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
|
||||
"PO-Revision-Date: 2016-11-13 18:35+0100\n"
|
||||
"Last-Translator: Nicolas Roudninski <nicoroud@gmail.com>\n"
|
||||
"Language: fr\n"
|
||||
@@ -31,343 +31,356 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
|
||||
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
|
||||
msgid "not installed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:150
|
||||
#: cps/helper.py:164
|
||||
#, python-format
|
||||
msgid "Failed to send mail: %s"
|
||||
msgstr "Impossible d'envoyer le courriel : %s"
|
||||
|
||||
#: cps/helper.py:157
|
||||
#: cps/helper.py:171
|
||||
msgid "Calibre-web test email"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:158 cps/helper.py:168
|
||||
#: cps/helper.py:172 cps/helper.py:184
|
||||
msgid "This email has been sent via calibre web."
|
||||
msgstr "Ce message a été envoyé depuis calibre web."
|
||||
|
||||
#: cps/helper.py:167 cps/templates/detail.html:130
|
||||
#: cps/helper.py:181 cps/templates/detail.html:146
|
||||
msgid "Send to Kindle"
|
||||
msgstr "Envoyer ver Kindle"
|
||||
|
||||
#: cps/helper.py:185 cps/helper.py:200
|
||||
#: cps/helper.py:201 cps/helper.py:216
|
||||
msgid "Could not find any formats suitable for sending by email"
|
||||
msgstr "Impossible de trouver un format adapté à envoyer par courriel"
|
||||
|
||||
#: cps/helper.py:194
|
||||
#: cps/helper.py:210
|
||||
msgid "Could not convert epub to mobi"
|
||||
msgstr "Impossible de convertir epub vers mobi"
|
||||
|
||||
#: cps/ub.py:434
|
||||
#: cps/ub.py:488
|
||||
msgid "Guest"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:734
|
||||
#: cps/web.py:904
|
||||
msgid "Requesting update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:735
|
||||
#: cps/web.py:905
|
||||
msgid "Downloading update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:736
|
||||
#: cps/web.py:906
|
||||
msgid "Unzipping update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:737
|
||||
#: cps/web.py:907
|
||||
msgid "Files are replaced"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:738
|
||||
#: cps/web.py:908
|
||||
msgid "Database connections are closed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:739
|
||||
#: cps/web.py:909
|
||||
msgid "Server is stopped"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:740
|
||||
#: cps/web.py:910
|
||||
msgid "Update finished, please press okay and reload page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:810
|
||||
#: cps/web.py:983
|
||||
msgid "Latest Books"
|
||||
msgstr "Derniers livres"
|
||||
|
||||
#: cps/web.py:835
|
||||
#: cps/web.py:1014
|
||||
msgid "Hot Books (most downloaded)"
|
||||
msgstr "Livres populaires (les plus téléchargés)"
|
||||
|
||||
#: cps/web.py:845
|
||||
#: cps/web.py:1024
|
||||
msgid "Best rated books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:36 cps/web.py:854
|
||||
#: cps/templates/index.xml:36 cps/web.py:1033
|
||||
msgid "Random Books"
|
||||
msgstr "Livres au hasard"
|
||||
|
||||
#: cps/web.py:867
|
||||
#: cps/web.py:1046
|
||||
msgid "Author list"
|
||||
msgstr "Liste des auteurs"
|
||||
|
||||
#: cps/web.py:878
|
||||
#: cps/web.py:1057
|
||||
#, python-format
|
||||
msgid "Author: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
|
||||
#: cps/web.py:2115
|
||||
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
|
||||
#: cps/web.py:2579
|
||||
msgid "Error opening eBook. File does not exist or file is not accessible:"
|
||||
msgstr ""
|
||||
"Erreur d'ouverture du livre numérique. Le fichier n'existe pas ou n'est "
|
||||
"pas accessible :"
|
||||
|
||||
#: cps/templates/index.xml:57 cps/web.py:894
|
||||
#: cps/templates/index.xml:71 cps/web.py:1073
|
||||
msgid "Series list"
|
||||
msgstr "Liste des séries"
|
||||
|
||||
#: cps/web.py:906
|
||||
#: cps/web.py:1085
|
||||
#, python-format
|
||||
msgid "Series: %(serie)s"
|
||||
msgstr "Séries : %(serie)s"
|
||||
|
||||
#: cps/web.py:939
|
||||
#: cps/web.py:1118
|
||||
msgid "Available languages"
|
||||
msgstr "Langues disponibles"
|
||||
|
||||
#: cps/web.py:954
|
||||
#: cps/web.py:1133
|
||||
#, python-format
|
||||
msgid "Language: %(name)s"
|
||||
msgstr "Langue : %(name)s"
|
||||
|
||||
#: cps/templates/index.xml:50 cps/web.py:967
|
||||
#: cps/templates/index.xml:64 cps/web.py:1146
|
||||
msgid "Category list"
|
||||
msgstr "Liste des catégories"
|
||||
|
||||
#: cps/web.py:979
|
||||
#: cps/web.py:1158
|
||||
#, python-format
|
||||
msgid "Category: %(name)s"
|
||||
msgstr "Catégorie : %(name)s"
|
||||
|
||||
#: cps/web.py:1040
|
||||
#: cps/web.py:1267
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiques"
|
||||
|
||||
#: cps/web.py:1061
|
||||
#: cps/web.py:1375
|
||||
msgid "Server restarted, please reload page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1063
|
||||
#: cps/web.py:1377
|
||||
msgid "Performing shutdown of server, please close window"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1073
|
||||
#: cps/web.py:1392
|
||||
msgid "Update done"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1147 cps/web.py:1160
|
||||
#: cps/web.py:1470 cps/web.py:1483
|
||||
msgid "search"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
|
||||
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
|
||||
msgid "Read a Book"
|
||||
msgstr "Lire un livre"
|
||||
|
||||
#: cps/web.py:1276 cps/web.py:1713
|
||||
#: cps/web.py:1676 cps/web.py:2152
|
||||
msgid "Please fill out all fields!"
|
||||
msgstr "SVP, complétez tous les champs !"
|
||||
|
||||
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
|
||||
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
|
||||
msgid "register"
|
||||
msgstr "S'enregistrer"
|
||||
|
||||
#: cps/web.py:1292
|
||||
#: cps/web.py:1692
|
||||
msgid "An unknown error occured. Please try again later."
|
||||
msgstr "Une erreur a eu lieu. Merci de réessayez plus tard."
|
||||
|
||||
#: cps/web.py:1297
|
||||
#: cps/web.py:1697
|
||||
msgid "This username or email address is already in use."
|
||||
msgstr "Ce nom d'utilisateur ou cette adresse de courriel est déjà utilisée."
|
||||
|
||||
#: cps/web.py:1315
|
||||
#: cps/web.py:1715
|
||||
#, python-format
|
||||
msgid "you are now logged in as: '%(nickname)s'"
|
||||
msgstr "Vous êtes maintenant connecté sous : '%(nickname)s'"
|
||||
|
||||
#: cps/web.py:1320
|
||||
#: cps/web.py:1720
|
||||
msgid "Wrong Username or Password"
|
||||
msgstr "Mauvais nom d'utilisateur ou mot de passe"
|
||||
|
||||
#: cps/web.py:1322
|
||||
#: cps/web.py:1722
|
||||
msgid "login"
|
||||
msgstr "Connexion"
|
||||
|
||||
#: cps/web.py:1339
|
||||
#: cps/web.py:1739
|
||||
msgid "Please configure the SMTP mail settings first..."
|
||||
msgstr "Veillez configurer les paramètres smtp d'abord..."
|
||||
|
||||
#: cps/web.py:1343
|
||||
#: cps/web.py:1743
|
||||
#, python-format
|
||||
msgid "Book successfully send to %(kindlemail)s"
|
||||
msgstr "Livres envoyés à %(kindlemail)s avec succès"
|
||||
|
||||
#: cps/web.py:1347
|
||||
#: cps/web.py:1747
|
||||
#, python-format
|
||||
msgid "There was an error sending this book: %(res)s"
|
||||
msgstr "Il y a eu une erreur en envoyant ce livre : %(res)s"
|
||||
|
||||
#: cps/web.py:1349
|
||||
#: cps/web.py:1749 cps/web.py:2232
|
||||
msgid "Please configure your kindle email address first..."
|
||||
msgstr "Veuillez configurer votre adresse kindle d'abord..."
|
||||
|
||||
#: cps/web.py:1369
|
||||
#: cps/web.py:1774
|
||||
#, python-format
|
||||
msgid "Book has been added to shelf: %(sname)s"
|
||||
msgstr "Le livre a bien été ajouté à l'étagère : %(sname)s"
|
||||
|
||||
#: cps/web.py:1390
|
||||
#: cps/web.py:1793
|
||||
#, python-format
|
||||
msgid "Book has been removed from shelf: %(sname)s"
|
||||
msgstr "Le livre a été supprimé de l'étagère %(sname)s"
|
||||
|
||||
#: cps/web.py:1409 cps/web.py:1433
|
||||
#: cps/web.py:1812 cps/web.py:1836
|
||||
#, python-format
|
||||
msgid "A shelf with the name '%(title)s' already exists."
|
||||
msgstr "Une étagère de ce nom '%(title)s' existe déjà."
|
||||
|
||||
#: cps/web.py:1414
|
||||
#: cps/web.py:1817
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s created"
|
||||
msgstr "Étagère %(title)s créée"
|
||||
|
||||
#: cps/web.py:1416 cps/web.py:1444
|
||||
#: cps/web.py:1819 cps/web.py:1847
|
||||
msgid "There was an error"
|
||||
msgstr "Il y a eu une erreur"
|
||||
|
||||
#: cps/web.py:1417 cps/web.py:1419
|
||||
#: cps/web.py:1820 cps/web.py:1822
|
||||
msgid "create a shelf"
|
||||
msgstr "Créer une étagère"
|
||||
|
||||
#: cps/web.py:1442
|
||||
#: cps/web.py:1845
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s changed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1445 cps/web.py:1447
|
||||
#: cps/web.py:1848 cps/web.py:1850
|
||||
msgid "Edit a shelf"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1465
|
||||
#: cps/web.py:1868
|
||||
#, python-format
|
||||
msgid "successfully deleted shelf %(name)s"
|
||||
msgstr "L'étagère %(name)s a été supprimé avec succès"
|
||||
|
||||
#: cps/web.py:1487
|
||||
#: cps/web.py:1890
|
||||
#, python-format
|
||||
msgid "Shelf: '%(name)s'"
|
||||
msgstr "Étagère : '%(name)s'"
|
||||
|
||||
#: cps/web.py:1518
|
||||
#: cps/web.py:1921
|
||||
#, python-format
|
||||
msgid "Change order of Shelf: '%(name)s'"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1580
|
||||
#: cps/web.py:1985
|
||||
msgid "Found an existing account for this email address."
|
||||
msgstr "Un compte avec cette adresse de courriel existe déjà."
|
||||
|
||||
#: cps/web.py:1582 cps/web.py:1586
|
||||
#: cps/web.py:1987 cps/web.py:1991
|
||||
#, python-format
|
||||
msgid "%(name)s's profile"
|
||||
msgstr "Profil de %(name)s"
|
||||
|
||||
#: cps/web.py:1583
|
||||
#: cps/web.py:1988
|
||||
msgid "Profile updated"
|
||||
msgstr "Profil mis à jour"
|
||||
|
||||
#: cps/web.py:1597
|
||||
#: cps/web.py:2002
|
||||
msgid "Admin page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1668
|
||||
#: cps/web.py:2106
|
||||
msgid "Calibre-web configuration updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
|
||||
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
|
||||
msgid "Basic Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1679
|
||||
#: cps/web.py:2117
|
||||
msgid "DB location is not valid, please enter correct path"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
|
||||
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
|
||||
msgid "Add new user"
|
||||
msgstr "Ajouter un nouvel utilisateur"
|
||||
|
||||
#: cps/web.py:1753
|
||||
#: cps/web.py:2194
|
||||
#, python-format
|
||||
msgid "User '%(user)s' created"
|
||||
msgstr "Utilisateur '%(user)s' créé"
|
||||
|
||||
#: cps/web.py:1757
|
||||
#: cps/web.py:2198
|
||||
msgid "Found an existing account for this email address or nickname."
|
||||
msgstr "Un compte avec cette adresse de courriel ou ce surnom existe déjà."
|
||||
|
||||
#: cps/web.py:1779
|
||||
#: cps/web.py:2220
|
||||
msgid "Mail settings updated"
|
||||
msgstr "Paramètres de courriel mis à jour"
|
||||
|
||||
#: cps/web.py:1785
|
||||
#: cps/web.py:2227
|
||||
#, python-format
|
||||
msgid "Test E-Mail successfully send to %(kindlemail)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1788
|
||||
#: cps/web.py:2230
|
||||
#, python-format
|
||||
msgid "There was an error sending the Test E-Mail: %(res)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1789
|
||||
#: cps/web.py:2234
|
||||
msgid "E-Mail settings updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2235
|
||||
msgid "Edit mail settings"
|
||||
msgstr "Éditer les paramètres de courriel"
|
||||
|
||||
#: cps/web.py:1817
|
||||
#: cps/web.py:2263
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' deleted"
|
||||
msgstr "Utilisateur '%(nick)s' supprimé"
|
||||
|
||||
#: cps/web.py:1898
|
||||
#: cps/web.py:2349
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' updated"
|
||||
msgstr "Utilisateur '%(nick)s' mis à jour"
|
||||
|
||||
#: cps/web.py:1901
|
||||
#: cps/web.py:2352
|
||||
msgid "An unknown error occured."
|
||||
msgstr "Oups ! Une erreur inconnue a eu lieu."
|
||||
|
||||
#: cps/web.py:1904
|
||||
#: cps/web.py:2355
|
||||
#, python-format
|
||||
msgid "Edit User %(nick)s"
|
||||
msgstr "Éditer l'utilisateur %(nick)s"
|
||||
|
||||
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
|
||||
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
|
||||
msgid "edit metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2145
|
||||
#: cps/web.py:2598
|
||||
#, python-format
|
||||
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2604
|
||||
msgid "File to be uploaded must have an extension"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2621
|
||||
#, python-format
|
||||
msgid "Failed to create path %s (Permission denied)."
|
||||
msgstr "Impossible de créer le chemin %s (permission refusée)"
|
||||
|
||||
#: cps/web.py:2150
|
||||
#: cps/web.py:2626
|
||||
#, python-format
|
||||
msgid "Failed to store file %s (Permission denied)."
|
||||
msgstr "Impossible d'enregistrer le fichier %s (permission refusée)"
|
||||
|
||||
#: cps/web.py:2155
|
||||
#: cps/web.py:2631
|
||||
#, python-format
|
||||
msgid "Failed to delete file %s (Permission denied)."
|
||||
msgstr "Impossible de supprimer le fichier %s (permission refusée)"
|
||||
@@ -396,7 +409,7 @@ msgstr "DLS"
|
||||
msgid "Admin"
|
||||
msgstr "Administration"
|
||||
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:117
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:134
|
||||
msgid "Download"
|
||||
msgstr "Télécharger"
|
||||
|
||||
@@ -452,7 +465,7 @@ msgstr ""
|
||||
msgid "Calibre DB dir"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
|
||||
msgid "Log Level"
|
||||
msgstr ""
|
||||
|
||||
@@ -460,7 +473,7 @@ msgstr ""
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
|
||||
msgid "Books per page"
|
||||
msgstr ""
|
||||
|
||||
@@ -489,42 +502,46 @@ msgid "Newest commit timestamp"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:83
|
||||
msgid "Restart Calibre-web"
|
||||
msgid "Reconnect to Calibre DB"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
msgid "Stop Calibre-web"
|
||||
msgid "Restart Calibre-web"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:85
|
||||
msgid "Check for update"
|
||||
msgid "Stop Calibre-web"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:86
|
||||
msgid "Check for update"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:87
|
||||
msgid "Perform Update"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:96
|
||||
#: cps/templates/admin.html:97
|
||||
msgid "Do you really want to restart Calibre-web?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:101 cps/templates/admin.html:115
|
||||
#: cps/templates/admin.html:136
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/admin.html:137
|
||||
msgid "Ok"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
|
||||
#: cps/templates/admin.html:103 cps/templates/admin.html:117
|
||||
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
|
||||
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
|
||||
#: cps/templates/admin.html:114
|
||||
#: cps/templates/admin.html:115
|
||||
msgid "Do you really want to stop Calibre-web?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:127
|
||||
#: cps/templates/admin.html:128
|
||||
msgid "Updating, please do not reload page"
|
||||
msgstr ""
|
||||
|
||||
@@ -532,20 +549,21 @@ msgstr ""
|
||||
msgid "Book Title"
|
||||
msgstr "Titre du livre"
|
||||
|
||||
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
|
||||
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
|
||||
#: cps/templates/search_form.html:10
|
||||
msgid "Author"
|
||||
msgstr "Auteur"
|
||||
|
||||
#: cps/templates/book_edit.html:24
|
||||
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
|
||||
msgid "Tags"
|
||||
msgstr "Étiquette"
|
||||
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
|
||||
#: cps/templates/search_form.html:33
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
|
||||
#: cps/templates/search_form.html:37
|
||||
msgid "Series"
|
||||
msgstr "Séries"
|
||||
|
||||
@@ -577,69 +595,142 @@ msgstr "Non"
|
||||
msgid "view book after edit"
|
||||
msgstr "Voir le livre après l'édition"
|
||||
|
||||
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:75
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
|
||||
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
|
||||
msgid "Get metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:79
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
|
||||
msgid "Submit"
|
||||
msgstr "Soumettre"
|
||||
|
||||
#: cps/templates/book_edit.html:121
|
||||
msgid "Keyword"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:122
|
||||
msgid " Search keyword "
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "Allez !"
|
||||
|
||||
#: cps/templates/book_edit.html:125
|
||||
msgid "Click the cover to load metadata to the form"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:132
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:143
|
||||
msgid "Search error!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:144
|
||||
msgid "No Result! Please try anonther keyword."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
|
||||
#: cps/templates/search_form.html:14
|
||||
msgid "Publisher"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:148
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:7
|
||||
msgid "Location of Calibre database"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:11
|
||||
msgid "Server Port"
|
||||
#: cps/templates/config_edit.html:13
|
||||
msgid "Use google drive?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: cps/templates/config_edit.html:23
|
||||
msgid "No. of random books to show"
|
||||
#: cps/templates/config_edit.html:17
|
||||
msgid "Client id"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:28
|
||||
msgid "Regular expression for title sorting"
|
||||
#: cps/templates/config_edit.html:21
|
||||
msgid "Client secret"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:42
|
||||
msgid "Enable uploading"
|
||||
#: cps/templates/config_edit.html:25
|
||||
msgid "Calibre Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:46
|
||||
msgid "Enable anonymous browsing"
|
||||
#: cps/templates/config_edit.html:29
|
||||
msgid "Google drive Calibre folder"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:50
|
||||
msgid "Enable public registration"
|
||||
#: cps/templates/config_edit.html:38
|
||||
msgid "Metadata Watch Channel ID"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
msgid "Server Port"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
#: cps/templates/config_edit.html:64
|
||||
msgid "No. of random books to show"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:68
|
||||
msgid "Regular expression for ignoring columns"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:72
|
||||
msgid "Regular expression for title sorting"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:86
|
||||
msgid "Enable uploading"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:90
|
||||
msgid "Enable anonymous browsing"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:94
|
||||
msgid "Enable public registration"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:96
|
||||
msgid "Default Settings for new users"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
|
||||
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
|
||||
msgid "Admin user"
|
||||
msgstr "Utilisateur admin"
|
||||
|
||||
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
|
||||
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
|
||||
msgid "Allow Downloads"
|
||||
msgstr "Permettre les téléchargements"
|
||||
|
||||
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
|
||||
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
|
||||
msgid "Allow Uploads"
|
||||
msgstr "Permettre les téléversements"
|
||||
|
||||
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
|
||||
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
|
||||
msgid "Allow Edit"
|
||||
msgstr "Permettre l'édition"
|
||||
|
||||
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
|
||||
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
|
||||
msgid "Allow Changing Password"
|
||||
msgstr "Permettre le changement de mot de passe"
|
||||
|
||||
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
|
||||
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
|
||||
#: cps/templates/login.html:4
|
||||
msgid "Login"
|
||||
msgstr "Connexion"
|
||||
@@ -656,23 +747,27 @@ msgstr ""
|
||||
msgid "language"
|
||||
msgstr "Langue"
|
||||
|
||||
#: cps/templates/detail.html:74
|
||||
#: cps/templates/detail.html:81
|
||||
msgid "Publishing date"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:106
|
||||
#: cps/templates/detail.html:115
|
||||
msgid "Read"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:123
|
||||
msgid "Description:"
|
||||
msgstr "Description :"
|
||||
|
||||
#: cps/templates/detail.html:134
|
||||
#: cps/templates/detail.html:151
|
||||
msgid "Read in browser"
|
||||
msgstr "Lire dans le navigateur"
|
||||
|
||||
#: cps/templates/detail.html:154
|
||||
#: cps/templates/detail.html:171
|
||||
msgid "Add to shelf"
|
||||
msgstr "Ajouter à l'étagère"
|
||||
|
||||
#: cps/templates/detail.html:194
|
||||
#: cps/templates/detail.html:211
|
||||
msgid "Edit metadata"
|
||||
msgstr "Éditer les métadonnées"
|
||||
|
||||
@@ -752,19 +847,29 @@ msgstr "Les derniers livres"
|
||||
msgid "Show Random Books"
|
||||
msgstr "Montrer des livres au hasard"
|
||||
|
||||
#: cps/templates/index.xml:43 cps/templates/layout.html:140
|
||||
#: cps/templates/index.xml:43 cps/templates/index.xml:47
|
||||
#: cps/templates/layout.html:132
|
||||
msgid "Read Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:50 cps/templates/index.xml:54
|
||||
#: cps/templates/layout.html:133
|
||||
msgid "Unread Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:57 cps/templates/layout.html:144
|
||||
msgid "Authors"
|
||||
msgstr "Auteurs"
|
||||
|
||||
#: cps/templates/index.xml:47
|
||||
#: cps/templates/index.xml:61
|
||||
msgid "Books ordered by Author"
|
||||
msgstr "Livres classés par auteur"
|
||||
|
||||
#: cps/templates/index.xml:54
|
||||
#: cps/templates/index.xml:68
|
||||
msgid "Books ordered by category"
|
||||
msgstr "Livres classés par catégorie"
|
||||
|
||||
#: cps/templates/index.xml:61
|
||||
#: cps/templates/index.xml:75
|
||||
msgid "Books ordered by series"
|
||||
msgstr "Livres classés par série"
|
||||
|
||||
@@ -772,10 +877,6 @@ msgstr "Livres classés par série"
|
||||
msgid "Toggle navigation"
|
||||
msgstr "Basculer la navigation"
|
||||
|
||||
#: cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "Allez !"
|
||||
|
||||
#: cps/templates/layout.html:68
|
||||
msgid "Advanced Search"
|
||||
msgstr "Recherche avancée"
|
||||
@@ -792,31 +893,31 @@ msgstr "S'enregistrer"
|
||||
msgid "Browse"
|
||||
msgstr "Explorer"
|
||||
|
||||
#: cps/templates/layout.html:132
|
||||
#: cps/templates/layout.html:136
|
||||
msgid "Discover"
|
||||
msgstr "Découvrir"
|
||||
|
||||
#: cps/templates/layout.html:135
|
||||
#: cps/templates/layout.html:139
|
||||
msgid "Categories"
|
||||
msgstr "Catégories"
|
||||
|
||||
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
|
||||
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
|
||||
msgid "Languages"
|
||||
msgstr "Langues"
|
||||
|
||||
#: cps/templates/layout.html:145
|
||||
#: cps/templates/layout.html:149
|
||||
msgid "Public Shelves"
|
||||
msgstr "Étagères publiques"
|
||||
|
||||
#: cps/templates/layout.html:149
|
||||
#: cps/templates/layout.html:153
|
||||
msgid "Your Shelves"
|
||||
msgstr "Vos étagères"
|
||||
|
||||
#: cps/templates/layout.html:154
|
||||
#: cps/templates/layout.html:158
|
||||
msgid "Create a Shelf"
|
||||
msgstr "Créer une étagère"
|
||||
|
||||
#: cps/templates/layout.html:155
|
||||
#: cps/templates/layout.html:159
|
||||
msgid "About"
|
||||
msgstr "À popos"
|
||||
|
||||
@@ -882,15 +983,15 @@ msgstr "Essayer une recherche différente"
|
||||
msgid "Results for:"
|
||||
msgstr "Résultats pour :"
|
||||
|
||||
#: cps/templates/search_form.html:23
|
||||
#: cps/templates/search_form.html:27
|
||||
msgid "Exclude Tags"
|
||||
msgstr "Exclure des étiquettes"
|
||||
|
||||
#: cps/templates/search_form.html:43
|
||||
#: cps/templates/search_form.html:47
|
||||
msgid "Exclude Series"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/search_form.html:64
|
||||
#: cps/templates/search_form.html:68
|
||||
msgid "Exclude Languages"
|
||||
msgstr ""
|
||||
|
||||
@@ -915,37 +1016,37 @@ msgid "Drag 'n drop to rearrange order"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:3
|
||||
msgid "Linked libraries"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Program library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:9
|
||||
msgid "Installed Version"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:32
|
||||
msgid "Calibre library statistics"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:37
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Books in this Library"
|
||||
msgstr "Livres dans la bibiothèque"
|
||||
|
||||
#: cps/templates/stats.html:41
|
||||
#: cps/templates/stats.html:12
|
||||
msgid "Authors in this Library"
|
||||
msgstr "Auteurs dans la bibliothèque"
|
||||
|
||||
#: cps/templates/stats.html:45
|
||||
#: cps/templates/stats.html:16
|
||||
msgid "Categories in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:49
|
||||
#: cps/templates/stats.html:20
|
||||
msgid "Series in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:24
|
||||
msgid "Linked libraries"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:28
|
||||
msgid "Program library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:29
|
||||
msgid "Installed Version"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:23
|
||||
msgid "Kindle E-Mail"
|
||||
msgstr "Courriel Kindle"
|
||||
@@ -958,43 +1059,47 @@ msgstr "Montrer les livres dans la langue"
|
||||
msgid "Show all"
|
||||
msgstr "Montrer tout"
|
||||
|
||||
#: cps/templates/user_edit.html:45
|
||||
#: cps/templates/user_edit.html:47
|
||||
msgid "Show random books"
|
||||
msgstr "Montrer des livres au hasard"
|
||||
|
||||
#: cps/templates/user_edit.html:49
|
||||
#: cps/templates/user_edit.html:51
|
||||
msgid "Show hot books"
|
||||
msgstr "Montrer les livres populaires"
|
||||
|
||||
#: cps/templates/user_edit.html:53
|
||||
#: cps/templates/user_edit.html:55
|
||||
msgid "Show best rated books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:57
|
||||
#: cps/templates/user_edit.html:59
|
||||
msgid "Show language selection"
|
||||
msgstr "Montrer la sélection de la langue"
|
||||
|
||||
#: cps/templates/user_edit.html:61
|
||||
#: cps/templates/user_edit.html:63
|
||||
msgid "Show series selection"
|
||||
msgstr "Montrer la sélection des séries"
|
||||
|
||||
#: cps/templates/user_edit.html:65
|
||||
#: cps/templates/user_edit.html:67
|
||||
msgid "Show category selection"
|
||||
msgstr "Montrer la sélection des catégories"
|
||||
|
||||
#: cps/templates/user_edit.html:69
|
||||
#: cps/templates/user_edit.html:71
|
||||
msgid "Show author selection"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:73
|
||||
#: cps/templates/user_edit.html:75
|
||||
msgid "Show read and unread"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:79
|
||||
msgid "Show random books in detail view"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:105
|
||||
#: cps/templates/user_edit.html:112
|
||||
msgid "Delete this user"
|
||||
msgstr "Supprimer cet utilisateur"
|
||||
|
||||
#: cps/templates/user_edit.html:116
|
||||
#: cps/templates/user_edit.html:127
|
||||
msgid "Recent Downloads"
|
||||
msgstr "Téléchargements récents"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Calibre-web\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/janeczku/calibre-web\n"
|
||||
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
|
||||
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
|
||||
"PO-Revision-Date: 2017-01-06 17:00+0000\n"
|
||||
"Last-Translator: dalin <dalin.lin@gmail.com>\n"
|
||||
"Language: zh_Hans_CN\n"
|
||||
@@ -26,341 +26,354 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
|
||||
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
|
||||
msgid "not installed"
|
||||
msgstr "未安装"
|
||||
|
||||
#: cps/helper.py:150
|
||||
#: cps/helper.py:164
|
||||
#, python-format
|
||||
msgid "Failed to send mail: %s"
|
||||
msgstr "发送邮件失败: %s"
|
||||
|
||||
#: cps/helper.py:157
|
||||
#: cps/helper.py:171
|
||||
msgid "Calibre-web test email"
|
||||
msgstr "Calibre-web 测试邮件"
|
||||
|
||||
#: cps/helper.py:158 cps/helper.py:168
|
||||
#: cps/helper.py:172 cps/helper.py:184
|
||||
msgid "This email has been sent via calibre web."
|
||||
msgstr "此邮件由calibre web发送"
|
||||
|
||||
#: cps/helper.py:167 cps/templates/detail.html:130
|
||||
#: cps/helper.py:181 cps/templates/detail.html:146
|
||||
msgid "Send to Kindle"
|
||||
msgstr "发送到Kindle"
|
||||
|
||||
#: cps/helper.py:185 cps/helper.py:200
|
||||
#: cps/helper.py:201 cps/helper.py:216
|
||||
msgid "Could not find any formats suitable for sending by email"
|
||||
msgstr "无法找到适合邮件发送的格式"
|
||||
|
||||
#: cps/helper.py:194
|
||||
#: cps/helper.py:210
|
||||
msgid "Could not convert epub to mobi"
|
||||
msgstr "无法转换epub到mobi"
|
||||
|
||||
#: cps/ub.py:434
|
||||
#: cps/ub.py:488
|
||||
msgid "Guest"
|
||||
msgstr "游客"
|
||||
|
||||
#: cps/web.py:734
|
||||
#: cps/web.py:904
|
||||
msgid "Requesting update package"
|
||||
msgstr "正在请求更新包"
|
||||
|
||||
#: cps/web.py:735
|
||||
#: cps/web.py:905
|
||||
msgid "Downloading update package"
|
||||
msgstr "正在下载更新包"
|
||||
|
||||
#: cps/web.py:736
|
||||
#: cps/web.py:906
|
||||
msgid "Unzipping update package"
|
||||
msgstr "正在解压更新包"
|
||||
|
||||
#: cps/web.py:737
|
||||
#: cps/web.py:907
|
||||
msgid "Files are replaced"
|
||||
msgstr "文件已替换"
|
||||
|
||||
#: cps/web.py:738
|
||||
#: cps/web.py:908
|
||||
msgid "Database connections are closed"
|
||||
msgstr "数据库连接已关闭"
|
||||
|
||||
#: cps/web.py:739
|
||||
#: cps/web.py:909
|
||||
msgid "Server is stopped"
|
||||
msgstr "服务器已停止"
|
||||
|
||||
#: cps/web.py:740
|
||||
#: cps/web.py:910
|
||||
msgid "Update finished, please press okay and reload page"
|
||||
msgstr "更新完成,请按确定并刷新页面"
|
||||
|
||||
#: cps/web.py:810
|
||||
#: cps/web.py:983
|
||||
msgid "Latest Books"
|
||||
msgstr "最新书籍"
|
||||
|
||||
#: cps/web.py:835
|
||||
#: cps/web.py:1014
|
||||
msgid "Hot Books (most downloaded)"
|
||||
msgstr "热门书籍(最多下载)"
|
||||
|
||||
#: cps/web.py:845
|
||||
#: cps/web.py:1024
|
||||
msgid "Best rated books"
|
||||
msgstr "最高评分书籍"
|
||||
|
||||
#: cps/templates/index.xml:36 cps/web.py:854
|
||||
#: cps/templates/index.xml:36 cps/web.py:1033
|
||||
msgid "Random Books"
|
||||
msgstr "随机书籍"
|
||||
|
||||
#: cps/web.py:867
|
||||
#: cps/web.py:1046
|
||||
msgid "Author list"
|
||||
msgstr "作者列表"
|
||||
|
||||
#: cps/web.py:878
|
||||
#, python-forma
|
||||
#: cps/web.py:1057
|
||||
#, python-forma, python-format
|
||||
msgid "Author: %(name)s"
|
||||
msgstr "作者: %(name)s"
|
||||
|
||||
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
|
||||
#: cps/web.py:2115
|
||||
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
|
||||
#: cps/web.py:2579
|
||||
msgid "Error opening eBook. File does not exist or file is not accessible:"
|
||||
msgstr "无法打开电子书。 文件不存在或者文件不可访问:"
|
||||
|
||||
#: cps/templates/index.xml:57 cps/web.py:894
|
||||
#: cps/templates/index.xml:71 cps/web.py:1073
|
||||
msgid "Series list"
|
||||
msgstr "丛书列表"
|
||||
|
||||
#: cps/web.py:906
|
||||
#: cps/web.py:1085
|
||||
#, python-format
|
||||
msgid "Series: %(serie)s"
|
||||
msgstr "丛书: %(serie)s"
|
||||
|
||||
#: cps/web.py:939
|
||||
#: cps/web.py:1118
|
||||
msgid "Available languages"
|
||||
msgstr "可用语言"
|
||||
|
||||
#: cps/web.py:954
|
||||
#: cps/web.py:1133
|
||||
#, python-format
|
||||
msgid "Language: %(name)s"
|
||||
msgstr "语言: %(name)s"
|
||||
|
||||
#: cps/templates/index.xml:50 cps/web.py:967
|
||||
#: cps/templates/index.xml:64 cps/web.py:1146
|
||||
msgid "Category list"
|
||||
msgstr "分类列表"
|
||||
|
||||
#: cps/web.py:979
|
||||
#: cps/web.py:1158
|
||||
#, python-format
|
||||
msgid "Category: %(name)s"
|
||||
msgstr "分类: %(name)s"
|
||||
|
||||
#: cps/web.py:1040
|
||||
#: cps/web.py:1267
|
||||
msgid "Statistics"
|
||||
msgstr "统计"
|
||||
|
||||
#: cps/web.py:1061
|
||||
#: cps/web.py:1375
|
||||
msgid "Server restarted, please reload page"
|
||||
msgstr "服务器已重启,请刷新页面"
|
||||
|
||||
#: cps/web.py:1063
|
||||
#: cps/web.py:1377
|
||||
msgid "Performing shutdown of server, please close window"
|
||||
msgstr "正在关闭服务器,请关闭窗口"
|
||||
|
||||
#: cps/web.py:1073
|
||||
#: cps/web.py:1392
|
||||
msgid "Update done"
|
||||
msgstr "更新完成"
|
||||
|
||||
#: cps/web.py:1147 cps/web.py:1160
|
||||
#: cps/web.py:1470 cps/web.py:1483
|
||||
msgid "search"
|
||||
msgstr "搜索"
|
||||
|
||||
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
|
||||
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
|
||||
msgid "Read a Book"
|
||||
msgstr "阅读一本书"
|
||||
|
||||
#: cps/web.py:1276 cps/web.py:1713
|
||||
#: cps/web.py:1676 cps/web.py:2152
|
||||
msgid "Please fill out all fields!"
|
||||
msgstr "请填写所有字段"
|
||||
|
||||
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
|
||||
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
|
||||
msgid "register"
|
||||
msgstr "注册"
|
||||
|
||||
#: cps/web.py:1292
|
||||
#: cps/web.py:1692
|
||||
msgid "An unknown error occured. Please try again later."
|
||||
msgstr "发生一个未知错误。请稍后再试。"
|
||||
|
||||
#: cps/web.py:1297
|
||||
#: cps/web.py:1697
|
||||
msgid "This username or email address is already in use."
|
||||
msgstr "此用户名或邮箱已被使用。"
|
||||
|
||||
#: cps/web.py:1315
|
||||
#: cps/web.py:1715
|
||||
#, python-format
|
||||
msgid "you are now logged in as: '%(nickname)s'"
|
||||
msgstr "您现在已以'%(nickname)s'身份登录"
|
||||
|
||||
#: cps/web.py:1320
|
||||
#: cps/web.py:1720
|
||||
msgid "Wrong Username or Password"
|
||||
msgstr "用户名或密码错误"
|
||||
|
||||
#: cps/web.py:1322
|
||||
#: cps/web.py:1722
|
||||
msgid "login"
|
||||
msgstr "登录"
|
||||
|
||||
#: cps/web.py:1339
|
||||
#: cps/web.py:1739
|
||||
msgid "Please configure the SMTP mail settings first..."
|
||||
msgstr "请先配置SMTP邮箱..."
|
||||
|
||||
#: cps/web.py:1343
|
||||
#: cps/web.py:1743
|
||||
#, python-format
|
||||
msgid "Book successfully send to %(kindlemail)s"
|
||||
msgstr "此书已被成功发给 %(kindlemail)s"
|
||||
|
||||
#: cps/web.py:1347
|
||||
#: cps/web.py:1747
|
||||
#, python-format
|
||||
msgid "There was an error sending this book: %(res)s"
|
||||
msgstr "发送这本书的时候出现错误: %(res)s"
|
||||
|
||||
#: cps/web.py:1349
|
||||
#: cps/web.py:1749 cps/web.py:2232
|
||||
msgid "Please configure your kindle email address first..."
|
||||
msgstr "请先配置您的kindle电子邮箱地址..."
|
||||
|
||||
#: cps/web.py:1369
|
||||
#: cps/web.py:1774
|
||||
#, python-format
|
||||
msgid "Book has been added to shelf: %(sname)s"
|
||||
msgstr "此书已被添加到书架: %(sname)s"
|
||||
|
||||
#: cps/web.py:1390
|
||||
#: cps/web.py:1793
|
||||
#, python-format
|
||||
msgid "Book has been removed from shelf: %(sname)s"
|
||||
msgstr "此书已从书架 %(sname)s 中删除"
|
||||
|
||||
#: cps/web.py:1409 cps/web.py:1433
|
||||
#: cps/web.py:1812 cps/web.py:1836
|
||||
#, python-format
|
||||
msgid "A shelf with the name '%(title)s' already exists."
|
||||
msgstr "已存在书架 '%(title)s'。"
|
||||
|
||||
#: cps/web.py:1414
|
||||
#: cps/web.py:1817
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s created"
|
||||
msgstr "书架 %(title)s 已被创建"
|
||||
|
||||
#: cps/web.py:1416 cps/web.py:1444
|
||||
#: cps/web.py:1819 cps/web.py:1847
|
||||
msgid "There was an error"
|
||||
msgstr "发生错误"
|
||||
|
||||
#: cps/web.py:1417 cps/web.py:1419
|
||||
#: cps/web.py:1820 cps/web.py:1822
|
||||
msgid "create a shelf"
|
||||
msgstr "创建书架"
|
||||
|
||||
#: cps/web.py:1442
|
||||
#: cps/web.py:1845
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s changed"
|
||||
msgstr "书架 %(title)s 已被修改"
|
||||
|
||||
#: cps/web.py:1445 cps/web.py:1447
|
||||
#: cps/web.py:1848 cps/web.py:1850
|
||||
msgid "Edit a shelf"
|
||||
msgstr "编辑书架"
|
||||
|
||||
#: cps/web.py:1465
|
||||
#: cps/web.py:1868
|
||||
#, python-format
|
||||
msgid "successfully deleted shelf %(name)s"
|
||||
msgstr "成功删除书架 %(name)s"
|
||||
|
||||
#: cps/web.py:1487
|
||||
#: cps/web.py:1890
|
||||
#, python-format
|
||||
msgid "Shelf: '%(name)s'"
|
||||
msgstr "书架: '%(name)s'"
|
||||
|
||||
#: cps/web.py:1518
|
||||
#: cps/web.py:1921
|
||||
#, python-format
|
||||
msgid "Change order of Shelf: '%(name)s'"
|
||||
msgstr "修改书架 '%(name)s' 顺序"
|
||||
|
||||
#: cps/web.py:1580
|
||||
#: cps/web.py:1985
|
||||
msgid "Found an existing account for this email address."
|
||||
msgstr "找到已使用此邮箱的账号。"
|
||||
|
||||
#: cps/web.py:1582 cps/web.py:1586
|
||||
#: cps/web.py:1987 cps/web.py:1991
|
||||
#, python-format
|
||||
msgid "%(name)s's profile"
|
||||
msgstr "%(name)s 的资料"
|
||||
|
||||
#: cps/web.py:1583
|
||||
#: cps/web.py:1988
|
||||
msgid "Profile updated"
|
||||
msgstr "资料已更新"
|
||||
|
||||
#: cps/web.py:1597
|
||||
#: cps/web.py:2002
|
||||
msgid "Admin page"
|
||||
msgstr "管理页"
|
||||
|
||||
#: cps/web.py:1668
|
||||
#: cps/web.py:2106
|
||||
msgid "Calibre-web configuration updated"
|
||||
msgstr "Calibre-web配置已更新"
|
||||
|
||||
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
|
||||
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
|
||||
msgid "Basic Configuration"
|
||||
msgstr "基本配置"
|
||||
|
||||
#: cps/web.py:1679
|
||||
#: cps/web.py:2117
|
||||
msgid "DB location is not valid, please enter correct path"
|
||||
msgstr "DB位置无效,请输入正确路径"
|
||||
|
||||
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
|
||||
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
|
||||
msgid "Add new user"
|
||||
msgstr "添加新用户"
|
||||
|
||||
#: cps/web.py:1753
|
||||
#: cps/web.py:2194
|
||||
#, python-format
|
||||
msgid "User '%(user)s' created"
|
||||
msgstr "用户 '%(user)s' 已被创建"
|
||||
|
||||
#: cps/web.py:1757
|
||||
#: cps/web.py:2198
|
||||
msgid "Found an existing account for this email address or nickname."
|
||||
msgstr "已找到使用此邮箱或昵称的账号。"
|
||||
|
||||
#: cps/web.py:1779
|
||||
#: cps/web.py:2220
|
||||
msgid "Mail settings updated"
|
||||
msgstr "邮箱设置已更新"
|
||||
|
||||
#: cps/web.py:1785
|
||||
#: cps/web.py:2227
|
||||
#, python-format
|
||||
msgid "Test E-Mail successfully send to %(kindlemail)s"
|
||||
msgstr "测试邮件已成功发送到 %(kindlemail)s"
|
||||
|
||||
#: cps/web.py:1788
|
||||
#: cps/web.py:2230
|
||||
#, python-format
|
||||
msgid "There was an error sending the Test E-Mail: %(res)s"
|
||||
msgstr "发送测试邮件时发生错误: %(res)s"
|
||||
|
||||
#: cps/web.py:1789
|
||||
#: cps/web.py:2234
|
||||
msgid "E-Mail settings updated"
|
||||
msgstr "E-Mail 设置已更新"
|
||||
|
||||
#: cps/web.py:2235
|
||||
msgid "Edit mail settings"
|
||||
msgstr "编辑邮箱设置"
|
||||
|
||||
#: cps/web.py:1817
|
||||
#: cps/web.py:2263
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' deleted"
|
||||
msgstr "用户 '%(nick)s' 已被删除"
|
||||
|
||||
#: cps/web.py:1898
|
||||
#: cps/web.py:2349
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' updated"
|
||||
msgstr "用户 '%(nick)s' 已被更新"
|
||||
|
||||
#: cps/web.py:1901
|
||||
#: cps/web.py:2352
|
||||
msgid "An unknown error occured."
|
||||
msgstr "发生未知错误。"
|
||||
|
||||
#: cps/web.py:1904
|
||||
#: cps/web.py:2355
|
||||
#, python-format
|
||||
msgid "Edit User %(nick)s"
|
||||
msgstr "编辑用户 %(nick)s"
|
||||
|
||||
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
|
||||
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
|
||||
msgid "edit metadata"
|
||||
msgstr "编辑元数据"
|
||||
|
||||
#: cps/web.py:2145
|
||||
#: cps/web.py:2598
|
||||
#, python-format
|
||||
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
|
||||
msgstr "不能上传后缀为 \"%s\" 的文件到此服务器"
|
||||
|
||||
#: cps/web.py:2604
|
||||
msgid "File to be uploaded must have an extension"
|
||||
msgstr "要上传的文件必须有一个后缀"
|
||||
|
||||
#: cps/web.py:2621
|
||||
#, python-format
|
||||
msgid "Failed to create path %s (Permission denied)."
|
||||
msgstr "创建路径 %s 失败(权限拒绝)。"
|
||||
|
||||
#: cps/web.py:2150
|
||||
#: cps/web.py:2626
|
||||
#, python-format
|
||||
msgid "Failed to store file %s (Permission denied)."
|
||||
msgstr "存储文件 %s 失败(权限拒绝)。"
|
||||
|
||||
#: cps/web.py:2155
|
||||
#: cps/web.py:2631
|
||||
#, python-format
|
||||
msgid "Failed to delete file %s (Permission denied)."
|
||||
msgstr "删除文件 %s 失败(权限拒绝)。"
|
||||
@@ -389,7 +402,7 @@ msgstr ""
|
||||
msgid "Admin"
|
||||
msgstr "管理"
|
||||
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:117
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:134
|
||||
msgid "Download"
|
||||
msgstr "下载"
|
||||
|
||||
@@ -445,7 +458,7 @@ msgstr "配置"
|
||||
msgid "Calibre DB dir"
|
||||
msgstr "Calibre DB目录"
|
||||
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
|
||||
msgid "Log Level"
|
||||
msgstr "日志级别"
|
||||
|
||||
@@ -453,7 +466,7 @@ msgstr "日志级别"
|
||||
msgid "Port"
|
||||
msgstr "端口"
|
||||
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
|
||||
msgid "Books per page"
|
||||
msgstr "每页书籍数"
|
||||
|
||||
@@ -482,42 +495,46 @@ msgid "Newest commit timestamp"
|
||||
msgstr "最新提交时间戳"
|
||||
|
||||
#: cps/templates/admin.html:83
|
||||
msgid "Reconnect to Calibre DB"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
msgid "Restart Calibre-web"
|
||||
msgstr "重启 Calibre-web"
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
#: cps/templates/admin.html:85
|
||||
msgid "Stop Calibre-web"
|
||||
msgstr "停止 Calibre-web"
|
||||
|
||||
#: cps/templates/admin.html:85
|
||||
#: cps/templates/admin.html:86
|
||||
msgid "Check for update"
|
||||
msgstr "检查更新"
|
||||
|
||||
#: cps/templates/admin.html:86
|
||||
#: cps/templates/admin.html:87
|
||||
msgid "Perform Update"
|
||||
msgstr "执行更新"
|
||||
|
||||
#: cps/templates/admin.html:96
|
||||
#: cps/templates/admin.html:97
|
||||
msgid "Do you really want to restart Calibre-web?"
|
||||
msgstr "您确定要重启 Calibre-web 吗?"
|
||||
|
||||
#: cps/templates/admin.html:101 cps/templates/admin.html:115
|
||||
#: cps/templates/admin.html:136
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/admin.html:137
|
||||
msgid "Ok"
|
||||
msgstr "确定"
|
||||
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
|
||||
#: cps/templates/admin.html:103 cps/templates/admin.html:117
|
||||
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
|
||||
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
|
||||
msgid "Back"
|
||||
msgstr "后退"
|
||||
|
||||
#: cps/templates/admin.html:114
|
||||
#: cps/templates/admin.html:115
|
||||
msgid "Do you really want to stop Calibre-web?"
|
||||
msgstr "您确定要关闭 Calibre-web 吗?"
|
||||
|
||||
#: cps/templates/admin.html:127
|
||||
#: cps/templates/admin.html:128
|
||||
msgid "Updating, please do not reload page"
|
||||
msgstr "正在更新,请不要刷新页面"
|
||||
|
||||
@@ -525,20 +542,21 @@ msgstr "正在更新,请不要刷新页面"
|
||||
msgid "Book Title"
|
||||
msgstr "书名"
|
||||
|
||||
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
|
||||
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
|
||||
#: cps/templates/search_form.html:10
|
||||
msgid "Author"
|
||||
msgstr "作者"
|
||||
|
||||
#: cps/templates/book_edit.html:24
|
||||
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
|
||||
msgid "Description"
|
||||
msgstr "简介"
|
||||
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
|
||||
msgid "Tags"
|
||||
msgstr "标签"
|
||||
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
|
||||
#: cps/templates/search_form.html:33
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
|
||||
#: cps/templates/search_form.html:37
|
||||
msgid "Series"
|
||||
msgstr "丛书"
|
||||
|
||||
@@ -570,69 +588,142 @@ msgstr ""
|
||||
msgid "view book after edit"
|
||||
msgstr "编辑后查看书籍"
|
||||
|
||||
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:75
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
|
||||
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
|
||||
msgid "Get metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:79
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
|
||||
msgid "Submit"
|
||||
msgstr "提交"
|
||||
|
||||
#: cps/templates/book_edit.html:121
|
||||
msgid "Keyword"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:122
|
||||
msgid " Search keyword "
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "走起!"
|
||||
|
||||
#: cps/templates/book_edit.html:125
|
||||
msgid "Click the cover to load metadata to the form"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:132
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:143
|
||||
msgid "Search error!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:144
|
||||
msgid "No Result! Please try anonther keyword."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
|
||||
#: cps/templates/search_form.html:14
|
||||
msgid "Publisher"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:148
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:7
|
||||
msgid "Location of Calibre database"
|
||||
msgstr "Calibre 数据库位置"
|
||||
|
||||
#: cps/templates/config_edit.html:11
|
||||
#: cps/templates/config_edit.html:13
|
||||
msgid "Use google drive?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:17
|
||||
msgid "Client id"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:21
|
||||
msgid "Client secret"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:25
|
||||
msgid "Calibre Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:29
|
||||
msgid "Google drive Calibre folder"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:38
|
||||
msgid "Metadata Watch Channel ID"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
msgid "Server Port"
|
||||
msgstr "服务器端口"
|
||||
|
||||
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
|
||||
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr "标题"
|
||||
|
||||
#: cps/templates/config_edit.html:23
|
||||
#: cps/templates/config_edit.html:64
|
||||
msgid "No. of random books to show"
|
||||
msgstr "随机书籍显示数量"
|
||||
|
||||
#: cps/templates/config_edit.html:28
|
||||
#: cps/templates/config_edit.html:68
|
||||
msgid "Regular expression for ignoring columns"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:72
|
||||
msgid "Regular expression for title sorting"
|
||||
msgstr "标题排序的正则表达式"
|
||||
|
||||
#: cps/templates/config_edit.html:42
|
||||
#: cps/templates/config_edit.html:86
|
||||
msgid "Enable uploading"
|
||||
msgstr "启用上传"
|
||||
|
||||
#: cps/templates/config_edit.html:46
|
||||
#: cps/templates/config_edit.html:90
|
||||
msgid "Enable anonymous browsing"
|
||||
msgstr "启用匿名浏览"
|
||||
|
||||
#: cps/templates/config_edit.html:50
|
||||
#: cps/templates/config_edit.html:94
|
||||
msgid "Enable public registration"
|
||||
msgstr "启用注册"
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
#: cps/templates/config_edit.html:96
|
||||
msgid "Default Settings for new users"
|
||||
msgstr "新用户默认设置"
|
||||
|
||||
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
|
||||
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
|
||||
msgid "Admin user"
|
||||
msgstr "管理用户"
|
||||
|
||||
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
|
||||
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
|
||||
msgid "Allow Downloads"
|
||||
msgstr "允许下载"
|
||||
|
||||
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
|
||||
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
|
||||
msgid "Allow Uploads"
|
||||
msgstr "允许上传"
|
||||
|
||||
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
|
||||
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
|
||||
msgid "Allow Edit"
|
||||
msgstr "允许编辑"
|
||||
|
||||
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
|
||||
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
|
||||
msgid "Allow Changing Password"
|
||||
msgstr "允许修改密码"
|
||||
|
||||
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
|
||||
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
|
||||
#: cps/templates/login.html:4
|
||||
msgid "Login"
|
||||
msgstr "登录"
|
||||
@@ -649,23 +740,27 @@ msgstr ""
|
||||
msgid "language"
|
||||
msgstr "语言"
|
||||
|
||||
#: cps/templates/detail.html:74
|
||||
#: cps/templates/detail.html:81
|
||||
msgid "Publishing date"
|
||||
msgstr "出版日期"
|
||||
|
||||
#: cps/templates/detail.html:106
|
||||
#: cps/templates/detail.html:115
|
||||
msgid "Read"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:123
|
||||
msgid "Description:"
|
||||
msgstr "简介:"
|
||||
|
||||
#: cps/templates/detail.html:134
|
||||
#: cps/templates/detail.html:151
|
||||
msgid "Read in browser"
|
||||
msgstr "在浏览器中阅读"
|
||||
|
||||
#: cps/templates/detail.html:154
|
||||
#: cps/templates/detail.html:171
|
||||
msgid "Add to shelf"
|
||||
msgstr "添加到书架"
|
||||
|
||||
#: cps/templates/detail.html:194
|
||||
#: cps/templates/detail.html:211
|
||||
msgid "Edit metadata"
|
||||
msgstr "编辑元数据"
|
||||
|
||||
@@ -745,19 +840,29 @@ msgstr "最新书籍"
|
||||
msgid "Show Random Books"
|
||||
msgstr "显示随机书籍"
|
||||
|
||||
#: cps/templates/index.xml:43 cps/templates/layout.html:140
|
||||
#: cps/templates/index.xml:43 cps/templates/index.xml:47
|
||||
#: cps/templates/layout.html:132
|
||||
msgid "Read Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:50 cps/templates/index.xml:54
|
||||
#: cps/templates/layout.html:133
|
||||
msgid "Unread Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:57 cps/templates/layout.html:144
|
||||
msgid "Authors"
|
||||
msgstr "作者"
|
||||
|
||||
#: cps/templates/index.xml:47
|
||||
#: cps/templates/index.xml:61
|
||||
msgid "Books ordered by Author"
|
||||
msgstr "书籍按作者排序"
|
||||
|
||||
#: cps/templates/index.xml:54
|
||||
#: cps/templates/index.xml:68
|
||||
msgid "Books ordered by category"
|
||||
msgstr "书籍按分类排序"
|
||||
|
||||
#: cps/templates/index.xml:61
|
||||
#: cps/templates/index.xml:75
|
||||
msgid "Books ordered by series"
|
||||
msgstr "书籍按丛书排序"
|
||||
|
||||
@@ -765,10 +870,6 @@ msgstr "书籍按丛书排序"
|
||||
msgid "Toggle navigation"
|
||||
msgstr "切换导航"
|
||||
|
||||
#: cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr "走起!"
|
||||
|
||||
#: cps/templates/layout.html:68
|
||||
msgid "Advanced Search"
|
||||
msgstr "高级搜索"
|
||||
@@ -785,31 +886,31 @@ msgstr "注册"
|
||||
msgid "Browse"
|
||||
msgstr "浏览"
|
||||
|
||||
#: cps/templates/layout.html:132
|
||||
#: cps/templates/layout.html:136
|
||||
msgid "Discover"
|
||||
msgstr "发现"
|
||||
|
||||
#: cps/templates/layout.html:135
|
||||
#: cps/templates/layout.html:139
|
||||
msgid "Categories"
|
||||
msgstr "分类"
|
||||
|
||||
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
|
||||
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
|
||||
msgid "Languages"
|
||||
msgstr "语言"
|
||||
|
||||
#: cps/templates/layout.html:145
|
||||
#: cps/templates/layout.html:149
|
||||
msgid "Public Shelves"
|
||||
msgstr "公开书架"
|
||||
|
||||
#: cps/templates/layout.html:149
|
||||
#: cps/templates/layout.html:153
|
||||
msgid "Your Shelves"
|
||||
msgstr "您的书架"
|
||||
|
||||
#: cps/templates/layout.html:154
|
||||
#: cps/templates/layout.html:158
|
||||
msgid "Create a Shelf"
|
||||
msgstr "创建书架"
|
||||
|
||||
#: cps/templates/layout.html:155
|
||||
#: cps/templates/layout.html:159
|
||||
msgid "About"
|
||||
msgstr "关于"
|
||||
|
||||
@@ -875,15 +976,15 @@ msgstr "请尝试别的关键字"
|
||||
msgid "Results for:"
|
||||
msgstr "结果:"
|
||||
|
||||
#: cps/templates/search_form.html:23
|
||||
#: cps/templates/search_form.html:27
|
||||
msgid "Exclude Tags"
|
||||
msgstr "排除标签"
|
||||
|
||||
#: cps/templates/search_form.html:43
|
||||
#: cps/templates/search_form.html:47
|
||||
msgid "Exclude Series"
|
||||
msgstr "排除丛书"
|
||||
|
||||
#: cps/templates/search_form.html:64
|
||||
#: cps/templates/search_form.html:68
|
||||
msgid "Exclude Languages"
|
||||
msgstr "排除语言"
|
||||
|
||||
@@ -908,37 +1009,37 @@ msgid "Drag 'n drop to rearrange order"
|
||||
msgstr "拖拽以重新排序"
|
||||
|
||||
#: cps/templates/stats.html:3
|
||||
msgid "Linked libraries"
|
||||
msgstr "链接库"
|
||||
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Program library"
|
||||
msgstr "程序库"
|
||||
|
||||
#: cps/templates/stats.html:9
|
||||
msgid "Installed Version"
|
||||
msgstr "已安装版本"
|
||||
|
||||
#: cps/templates/stats.html:32
|
||||
msgid "Calibre library statistics"
|
||||
msgstr "Calibre书库统计"
|
||||
|
||||
#: cps/templates/stats.html:37
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Books in this Library"
|
||||
msgstr "本书在此书库"
|
||||
|
||||
#: cps/templates/stats.html:41
|
||||
#: cps/templates/stats.html:12
|
||||
msgid "Authors in this Library"
|
||||
msgstr "个作者在此书库"
|
||||
|
||||
#: cps/templates/stats.html:45
|
||||
#: cps/templates/stats.html:16
|
||||
msgid "Categories in this Library"
|
||||
msgstr "个分类在此书库"
|
||||
|
||||
#: cps/templates/stats.html:49
|
||||
#: cps/templates/stats.html:20
|
||||
msgid "Series in this Library"
|
||||
msgstr "个丛书在此书库"
|
||||
|
||||
#: cps/templates/stats.html:24
|
||||
msgid "Linked libraries"
|
||||
msgstr "链接库"
|
||||
|
||||
#: cps/templates/stats.html:28
|
||||
msgid "Program library"
|
||||
msgstr "程序库"
|
||||
|
||||
#: cps/templates/stats.html:29
|
||||
msgid "Installed Version"
|
||||
msgstr "已安装版本"
|
||||
|
||||
#: cps/templates/user_edit.html:23
|
||||
msgid "Kindle E-Mail"
|
||||
msgstr ""
|
||||
@@ -951,43 +1052,47 @@ msgstr "按语言显示书籍"
|
||||
msgid "Show all"
|
||||
msgstr "显示全部"
|
||||
|
||||
#: cps/templates/user_edit.html:45
|
||||
#: cps/templates/user_edit.html:47
|
||||
msgid "Show random books"
|
||||
msgstr "显示随机书籍"
|
||||
|
||||
#: cps/templates/user_edit.html:49
|
||||
#: cps/templates/user_edit.html:51
|
||||
msgid "Show hot books"
|
||||
msgstr "显示热门书籍"
|
||||
|
||||
#: cps/templates/user_edit.html:53
|
||||
#: cps/templates/user_edit.html:55
|
||||
msgid "Show best rated books"
|
||||
msgstr "显示最高评分书籍"
|
||||
|
||||
#: cps/templates/user_edit.html:57
|
||||
#: cps/templates/user_edit.html:59
|
||||
msgid "Show language selection"
|
||||
msgstr "显示语言选择"
|
||||
|
||||
#: cps/templates/user_edit.html:61
|
||||
#: cps/templates/user_edit.html:63
|
||||
msgid "Show series selection"
|
||||
msgstr "显示丛书选择"
|
||||
|
||||
#: cps/templates/user_edit.html:65
|
||||
#: cps/templates/user_edit.html:67
|
||||
msgid "Show category selection"
|
||||
msgstr "显示分类选择"
|
||||
|
||||
#: cps/templates/user_edit.html:69
|
||||
#: cps/templates/user_edit.html:71
|
||||
msgid "Show author selection"
|
||||
msgstr "显示作者选择"
|
||||
|
||||
#: cps/templates/user_edit.html:73
|
||||
#: cps/templates/user_edit.html:75
|
||||
msgid "Show read and unread"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:79
|
||||
msgid "Show random books in detail view"
|
||||
msgstr "在详情页显示随机书籍"
|
||||
|
||||
#: cps/templates/user_edit.html:105
|
||||
#: cps/templates/user_edit.html:112
|
||||
msgid "Delete this user"
|
||||
msgstr "删除此用户"
|
||||
|
||||
#: cps/templates/user_edit.html:116
|
||||
#: cps/templates/user_edit.html:127
|
||||
msgid "Recent Downloads"
|
||||
msgstr "最近下载"
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import os
|
||||
import logging
|
||||
from werkzeug.security import generate_password_hash
|
||||
from flask_babel import gettext as _
|
||||
import json
|
||||
#from builtins import str
|
||||
|
||||
dbpath = os.path.join(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + ".." + os.sep), "app.db")
|
||||
engine = create_engine('sqlite:///{0}'.format(dbpath), echo=False)
|
||||
@@ -22,6 +24,7 @@ ROLE_UPLOAD = 4
|
||||
ROLE_EDIT = 8
|
||||
ROLE_PASSWD = 16
|
||||
ROLE_ANONYMOUS = 32
|
||||
ROLE_EDIT_SHELFS = 64
|
||||
|
||||
DETAIL_RANDOM = 1
|
||||
SIDEBAR_LANGUAGE = 2
|
||||
@@ -31,6 +34,7 @@ SIDEBAR_HOT = 16
|
||||
SIDEBAR_RANDOM = 32
|
||||
SIDEBAR_AUTHOR = 64
|
||||
SIDEBAR_BEST_RATED = 128
|
||||
SIDEBAR_READ_AND_UNREAD = 256
|
||||
|
||||
DEFAULT_PASS = "admin123"
|
||||
DEFAULT_PORT = int(os.environ.get("CALIBRE_PORT", 8083))
|
||||
@@ -83,6 +87,12 @@ class UserBase:
|
||||
else:
|
||||
return False
|
||||
|
||||
def role_edit_shelfs(self):
|
||||
if self.role is not None:
|
||||
return True if self.role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
@@ -90,7 +100,7 @@ class UserBase:
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return unicode(self.id)
|
||||
return str(self.id)
|
||||
|
||||
def filter_language(self):
|
||||
return self.default_language
|
||||
@@ -137,6 +147,12 @@ class UserBase:
|
||||
else:
|
||||
return False
|
||||
|
||||
def show_read_and_unread(self):
|
||||
if self.sidebar_view is not None:
|
||||
return True if self.sidebar_view & SIDEBAR_READ_AND_UNREAD == SIDEBAR_READ_AND_UNREAD else False
|
||||
else:
|
||||
return False
|
||||
|
||||
def show_detail_random(self):
|
||||
if self.sidebar_view is not None:
|
||||
return True if self.sidebar_view & DETAIL_RANDOM == DETAIL_RANDOM else False
|
||||
@@ -178,7 +194,6 @@ class Anonymous(AnonymousUserMixin, UserBase):
|
||||
self.role = data.role
|
||||
self.sidebar_view = data.sidebar_view
|
||||
self.default_language = data.default_language
|
||||
self.default_language = data.default_language
|
||||
self.locale = data.locale
|
||||
self.anon_browse = settings.config_anonbrowse
|
||||
|
||||
@@ -217,6 +232,14 @@ class BookShelf(Base):
|
||||
def __repr__(self):
|
||||
return '<Book %r>' % self.id
|
||||
|
||||
class ReadBook(Base):
|
||||
__tablename__ = 'book_read_link'
|
||||
|
||||
id=Column(Integer, primary_key=True)
|
||||
book_id = Column(Integer, unique=False)
|
||||
user_id =Column(Integer, ForeignKey('user.id'), unique=False)
|
||||
is_read = Column(Boolean, unique=False)
|
||||
|
||||
|
||||
# Baseclass representing Downloads from calibre-web in app.db
|
||||
class Downloads(Base):
|
||||
@@ -253,6 +276,14 @@ class Settings(Base):
|
||||
config_anonbrowse = Column(SmallInteger, default=0)
|
||||
config_public_reg = Column(SmallInteger, default=0)
|
||||
config_default_role = Column(SmallInteger, default=0)
|
||||
config_columns_to_ignore = Column(String)
|
||||
config_use_google_drive = Column(Boolean)
|
||||
config_google_drive_client_id = Column(String)
|
||||
config_google_drive_client_secret = Column(String)
|
||||
config_google_drive_folder = Column(String)
|
||||
config_google_drive_calibre_url_base = Column(String)
|
||||
config_google_drive_watch_changes_response = Column(String)
|
||||
config_columns_to_ignore = Column(String)
|
||||
|
||||
def __repr__(self):
|
||||
pass
|
||||
@@ -279,7 +310,18 @@ class Config:
|
||||
self.config_anonbrowse = data.config_anonbrowse
|
||||
self.config_public_reg = data.config_public_reg
|
||||
self.config_default_role = data.config_default_role
|
||||
if self.config_calibre_dir is not None:
|
||||
self.config_columns_to_ignore = data.config_columns_to_ignore
|
||||
self.config_use_google_drive = data.config_use_google_drive
|
||||
self.config_google_drive_client_id = data.config_google_drive_client_id
|
||||
self.config_google_drive_client_secret = data.config_google_drive_client_secret
|
||||
self.config_google_drive_calibre_url_base = data.config_google_drive_calibre_url_base
|
||||
self.config_google_drive_folder = data.config_google_drive_folder
|
||||
if data.config_google_drive_watch_changes_response:
|
||||
self.config_google_drive_watch_changes_response = json.loads(data.config_google_drive_watch_changes_response)
|
||||
else:
|
||||
self.config_google_drive_watch_changes_response=None
|
||||
self.config_columns_to_ignore = data.config_columns_to_ignore
|
||||
if self.config_calibre_dir is not None and (not self.config_use_google_drive or os.path.exists(self.config_calibre_dir + '/metadata.db')):
|
||||
self.db_configured = True
|
||||
else:
|
||||
self.db_configured = False
|
||||
@@ -318,6 +360,12 @@ class Config:
|
||||
else:
|
||||
return False
|
||||
|
||||
def role_edit_shelfs(self):
|
||||
if self.config_default_role is not None:
|
||||
return True if self.config_default_role & ROLE_EDIT_SHELFS == ROLE_EDIT_SHELFS else False
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_Log_Level(self):
|
||||
ret_value=""
|
||||
if self.config_log_level == logging.INFO:
|
||||
@@ -335,6 +383,9 @@ class Config:
|
||||
# everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding
|
||||
# rows with SQL commands
|
||||
def migrate_Database():
|
||||
if not engine.dialect.has_table(engine.connect(), "book_read_link"):
|
||||
ReadBook.__table__.create(bind = engine)
|
||||
|
||||
try:
|
||||
session.query(exists().where(User.locale)).scalar()
|
||||
session.commit()
|
||||
@@ -360,6 +411,23 @@ def migrate_Database():
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_anonbrowse` SmallInteger DEFAULT 0")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_public_reg` SmallInteger DEFAULT 0")
|
||||
session.commit()
|
||||
|
||||
try:
|
||||
session.query(exists().where(Settings.config_use_google_drive)).scalar()
|
||||
except exc.OperationalError:
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_use_google_drive` INTEGER DEFAULT 0")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_client_id` String DEFAULT ''")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_client_secret` String DEFAULT ''")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_calibre_url_base` INTEGER DEFAULT 0")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_folder` String DEFAULT ''")
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_google_drive_watch_changes_response` String DEFAULT ''")
|
||||
try:
|
||||
session.query(exists().where(Settings.config_columns_to_ignore)).scalar()
|
||||
except exc.OperationalError:
|
||||
conn = engine.connect()
|
||||
conn.execute("ALTER TABLE Settings ADD column `config_columns_to_ignore` String DEFAULT ''")
|
||||
session.commit()
|
||||
try:
|
||||
session.query(exists().where(Settings.config_default_role)).scalar()
|
||||
session.commit()
|
||||
@@ -438,7 +506,7 @@ def create_anonymous_user():
|
||||
session.add(user)
|
||||
try:
|
||||
session.commit()
|
||||
except:
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
pass
|
||||
|
||||
@@ -449,14 +517,15 @@ def create_admin_user():
|
||||
user.nickname = "admin"
|
||||
user.role = ROLE_USER + ROLE_ADMIN + ROLE_DOWNLOAD + ROLE_UPLOAD + ROLE_EDIT + ROLE_PASSWD
|
||||
user.sidebar_view = DETAIL_RANDOM + SIDEBAR_LANGUAGE + SIDEBAR_SERIES + SIDEBAR_CATEGORY + SIDEBAR_HOT + \
|
||||
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED
|
||||
SIDEBAR_RANDOM + SIDEBAR_AUTHOR + SIDEBAR_BEST_RATED + SIDEBAR_READ_AND_UNREAD
|
||||
|
||||
|
||||
user.password = generate_password_hash(DEFAULT_PASS)
|
||||
|
||||
session.add(user)
|
||||
try:
|
||||
session.commit()
|
||||
except:
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
pass
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import hashlib
|
||||
from collections import namedtuple
|
||||
import book_formats
|
||||
|
||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id')
|
||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages')
|
||||
|
||||
"""
|
||||
:rtype: BookMeta
|
||||
|
||||
+558
-81
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
|
||||
+278
-173
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2017-02-20 19:47+0100\n"
|
||||
"POT-Creation-Date: 2017-03-19 19:20+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,341 +17,354 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.3.4\n"
|
||||
|
||||
#: cps/book_formats.py:111 cps/book_formats.py:115 cps/web.py:1030
|
||||
#: cps/book_formats.py:113 cps/book_formats.py:117 cps/web.py:1244
|
||||
msgid "not installed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:150
|
||||
#: cps/helper.py:164
|
||||
#, python-format
|
||||
msgid "Failed to send mail: %s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:157
|
||||
#: cps/helper.py:171
|
||||
msgid "Calibre-web test email"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:158 cps/helper.py:168
|
||||
#: cps/helper.py:172 cps/helper.py:184
|
||||
msgid "This email has been sent via calibre web."
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:167 cps/templates/detail.html:130
|
||||
#: cps/helper.py:181 cps/templates/detail.html:146
|
||||
msgid "Send to Kindle"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:185 cps/helper.py:200
|
||||
#: cps/helper.py:201 cps/helper.py:216
|
||||
msgid "Could not find any formats suitable for sending by email"
|
||||
msgstr ""
|
||||
|
||||
#: cps/helper.py:194
|
||||
#: cps/helper.py:210
|
||||
msgid "Could not convert epub to mobi"
|
||||
msgstr ""
|
||||
|
||||
#: cps/ub.py:434
|
||||
#: cps/ub.py:488
|
||||
msgid "Guest"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:734
|
||||
#: cps/web.py:904
|
||||
msgid "Requesting update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:735
|
||||
#: cps/web.py:905
|
||||
msgid "Downloading update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:736
|
||||
#: cps/web.py:906
|
||||
msgid "Unzipping update package"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:737
|
||||
#: cps/web.py:907
|
||||
msgid "Files are replaced"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:738
|
||||
#: cps/web.py:908
|
||||
msgid "Database connections are closed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:739
|
||||
#: cps/web.py:909
|
||||
msgid "Server is stopped"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:740
|
||||
#: cps/web.py:910
|
||||
msgid "Update finished, please press okay and reload page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:810
|
||||
#: cps/web.py:983
|
||||
msgid "Latest Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:835
|
||||
#: cps/web.py:1014
|
||||
msgid "Hot Books (most downloaded)"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:845
|
||||
#: cps/web.py:1024
|
||||
msgid "Best rated books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:36 cps/web.py:854
|
||||
#: cps/templates/index.xml:36 cps/web.py:1033
|
||||
msgid "Random Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:867
|
||||
#: cps/web.py:1046
|
||||
msgid "Author list"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:878
|
||||
#: cps/web.py:1057
|
||||
#, python-format
|
||||
msgid "Author: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:880 cps/web.py:908 cps/web.py:1007 cps/web.py:1235
|
||||
#: cps/web.py:2115
|
||||
#: cps/web.py:1059 cps/web.py:1087 cps/web.py:1221 cps/web.py:1626
|
||||
#: cps/web.py:2579
|
||||
msgid "Error opening eBook. File does not exist or file is not accessible:"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:57 cps/web.py:894
|
||||
#: cps/templates/index.xml:71 cps/web.py:1073
|
||||
msgid "Series list"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:906
|
||||
#: cps/web.py:1085
|
||||
#, python-format
|
||||
msgid "Series: %(serie)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:939
|
||||
#: cps/web.py:1118
|
||||
msgid "Available languages"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:954
|
||||
#: cps/web.py:1133
|
||||
#, python-format
|
||||
msgid "Language: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:50 cps/web.py:967
|
||||
#: cps/templates/index.xml:64 cps/web.py:1146
|
||||
msgid "Category list"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:979
|
||||
#: cps/web.py:1158
|
||||
#, python-format
|
||||
msgid "Category: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1040
|
||||
#: cps/web.py:1267
|
||||
msgid "Statistics"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1061
|
||||
#: cps/web.py:1375
|
||||
msgid "Server restarted, please reload page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1063
|
||||
#: cps/web.py:1377
|
||||
msgid "Performing shutdown of server, please close window"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1073
|
||||
#: cps/web.py:1392
|
||||
msgid "Update done"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1147 cps/web.py:1160
|
||||
#: cps/web.py:1470 cps/web.py:1483
|
||||
msgid "search"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1211 cps/web.py:1218 cps/web.py:1225 cps/web.py:1232
|
||||
#: cps/web.py:1602 cps/web.py:1609 cps/web.py:1616 cps/web.py:1623
|
||||
msgid "Read a Book"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1276 cps/web.py:1713
|
||||
#: cps/web.py:1676 cps/web.py:2152
|
||||
msgid "Please fill out all fields!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1277 cps/web.py:1293 cps/web.py:1298 cps/web.py:1300
|
||||
#: cps/web.py:1677 cps/web.py:1693 cps/web.py:1698 cps/web.py:1700
|
||||
msgid "register"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1292
|
||||
#: cps/web.py:1692
|
||||
msgid "An unknown error occured. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1297
|
||||
#: cps/web.py:1697
|
||||
msgid "This username or email address is already in use."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1315
|
||||
#: cps/web.py:1715
|
||||
#, python-format
|
||||
msgid "you are now logged in as: '%(nickname)s'"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1320
|
||||
#: cps/web.py:1720
|
||||
msgid "Wrong Username or Password"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1322
|
||||
#: cps/web.py:1722
|
||||
msgid "login"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1339
|
||||
#: cps/web.py:1739
|
||||
msgid "Please configure the SMTP mail settings first..."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1343
|
||||
#: cps/web.py:1743
|
||||
#, python-format
|
||||
msgid "Book successfully send to %(kindlemail)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1347
|
||||
#: cps/web.py:1747
|
||||
#, python-format
|
||||
msgid "There was an error sending this book: %(res)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1349
|
||||
#: cps/web.py:1749 cps/web.py:2232
|
||||
msgid "Please configure your kindle email address first..."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1369
|
||||
#: cps/web.py:1774
|
||||
#, python-format
|
||||
msgid "Book has been added to shelf: %(sname)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1390
|
||||
#: cps/web.py:1793
|
||||
#, python-format
|
||||
msgid "Book has been removed from shelf: %(sname)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1409 cps/web.py:1433
|
||||
#: cps/web.py:1812 cps/web.py:1836
|
||||
#, python-format
|
||||
msgid "A shelf with the name '%(title)s' already exists."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1414
|
||||
#: cps/web.py:1817
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s created"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1416 cps/web.py:1444
|
||||
#: cps/web.py:1819 cps/web.py:1847
|
||||
msgid "There was an error"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1417 cps/web.py:1419
|
||||
#: cps/web.py:1820 cps/web.py:1822
|
||||
msgid "create a shelf"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1442
|
||||
#: cps/web.py:1845
|
||||
#, python-format
|
||||
msgid "Shelf %(title)s changed"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1445 cps/web.py:1447
|
||||
#: cps/web.py:1848 cps/web.py:1850
|
||||
msgid "Edit a shelf"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1465
|
||||
#: cps/web.py:1868
|
||||
#, python-format
|
||||
msgid "successfully deleted shelf %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1487
|
||||
#: cps/web.py:1890
|
||||
#, python-format
|
||||
msgid "Shelf: '%(name)s'"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1518
|
||||
#: cps/web.py:1921
|
||||
#, python-format
|
||||
msgid "Change order of Shelf: '%(name)s'"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1580
|
||||
#: cps/web.py:1985
|
||||
msgid "Found an existing account for this email address."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1582 cps/web.py:1586
|
||||
#: cps/web.py:1987 cps/web.py:1991
|
||||
#, python-format
|
||||
msgid "%(name)s's profile"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1583
|
||||
#: cps/web.py:1988
|
||||
msgid "Profile updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1597
|
||||
#: cps/web.py:2002
|
||||
msgid "Admin page"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1668
|
||||
#: cps/web.py:2106
|
||||
msgid "Calibre-web configuration updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1675 cps/web.py:1681 cps/web.py:1694
|
||||
#: cps/web.py:2113 cps/web.py:2119 cps/web.py:2133
|
||||
msgid "Basic Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1679
|
||||
#: cps/web.py:2117
|
||||
msgid "DB location is not valid, please enter correct path"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:34 cps/web.py:1715 cps/web.py:1761
|
||||
#: cps/templates/admin.html:34 cps/web.py:2154 cps/web.py:2202
|
||||
msgid "Add new user"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1753
|
||||
#: cps/web.py:2194
|
||||
#, python-format
|
||||
msgid "User '%(user)s' created"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1757
|
||||
#: cps/web.py:2198
|
||||
msgid "Found an existing account for this email address or nickname."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1779
|
||||
#: cps/web.py:2220
|
||||
msgid "Mail settings updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1785
|
||||
#: cps/web.py:2227
|
||||
#, python-format
|
||||
msgid "Test E-Mail successfully send to %(kindlemail)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1788
|
||||
#: cps/web.py:2230
|
||||
#, python-format
|
||||
msgid "There was an error sending the Test E-Mail: %(res)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1789
|
||||
#: cps/web.py:2234
|
||||
msgid "E-Mail settings updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2235
|
||||
msgid "Edit mail settings"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1817
|
||||
#: cps/web.py:2263
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' deleted"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1898
|
||||
#: cps/web.py:2349
|
||||
#, python-format
|
||||
msgid "User '%(nick)s' updated"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1901
|
||||
#: cps/web.py:2352
|
||||
msgid "An unknown error occured."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:1904
|
||||
#: cps/web.py:2355
|
||||
#, python-format
|
||||
msgid "Edit User %(nick)s"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2110 cps/web.py:2113 cps/web.py:2188
|
||||
#: cps/web.py:2574 cps/web.py:2577 cps/web.py:2689
|
||||
msgid "edit metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2145
|
||||
#: cps/web.py:2598
|
||||
#, python-format
|
||||
msgid "File extension \"%s\" is not allowed to be uploaded to this server"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2604
|
||||
msgid "File to be uploaded must have an extension"
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2621
|
||||
#, python-format
|
||||
msgid "Failed to create path %s (Permission denied)."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2150
|
||||
#: cps/web.py:2626
|
||||
#, python-format
|
||||
msgid "Failed to store file %s (Permission denied)."
|
||||
msgstr ""
|
||||
|
||||
#: cps/web.py:2155
|
||||
#: cps/web.py:2631
|
||||
#, python-format
|
||||
msgid "Failed to delete file %s (Permission denied)."
|
||||
msgstr ""
|
||||
@@ -380,7 +393,7 @@ msgstr ""
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:117
|
||||
#: cps/templates/admin.html:13 cps/templates/detail.html:134
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
@@ -436,7 +449,7 @@ msgstr ""
|
||||
msgid "Calibre DB dir"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:32
|
||||
#: cps/templates/admin.html:61 cps/templates/config_edit.html:76
|
||||
msgid "Log Level"
|
||||
msgstr ""
|
||||
|
||||
@@ -444,7 +457,7 @@ msgstr ""
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:19
|
||||
#: cps/templates/admin.html:63 cps/templates/config_edit.html:60
|
||||
msgid "Books per page"
|
||||
msgstr ""
|
||||
|
||||
@@ -473,42 +486,46 @@ msgid "Newest commit timestamp"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:83
|
||||
msgid "Restart Calibre-web"
|
||||
msgid "Reconnect to Calibre DB"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:84
|
||||
msgid "Stop Calibre-web"
|
||||
msgid "Restart Calibre-web"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:85
|
||||
msgid "Check for update"
|
||||
msgid "Stop Calibre-web"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:86
|
||||
msgid "Check for update"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:87
|
||||
msgid "Perform Update"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:96
|
||||
#: cps/templates/admin.html:97
|
||||
msgid "Do you really want to restart Calibre-web?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:101 cps/templates/admin.html:115
|
||||
#: cps/templates/admin.html:136
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/admin.html:137
|
||||
msgid "Ok"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:102 cps/templates/admin.html:116
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:75
|
||||
#: cps/templates/admin.html:103 cps/templates/admin.html:117
|
||||
#: cps/templates/book_edit.html:109 cps/templates/config_edit.html:119
|
||||
#: cps/templates/email_edit.html:36 cps/templates/shelf_edit.html:17
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:111
|
||||
#: cps/templates/shelf_order.html:12 cps/templates/user_edit.html:120
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:114
|
||||
#: cps/templates/admin.html:115
|
||||
msgid "Do you really want to stop Calibre-web?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/admin.html:127
|
||||
#: cps/templates/admin.html:128
|
||||
msgid "Updating, please do not reload page"
|
||||
msgstr ""
|
||||
|
||||
@@ -516,20 +533,21 @@ msgstr ""
|
||||
msgid "Book Title"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:20 cps/templates/search_form.html:10
|
||||
#: cps/templates/book_edit.html:20 cps/templates/book_edit.html:145
|
||||
#: cps/templates/search_form.html:10
|
||||
msgid "Author"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:24
|
||||
#: cps/templates/book_edit.html:24 cps/templates/book_edit.html:147
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:13
|
||||
#: cps/templates/book_edit.html:28 cps/templates/search_form.html:17
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:138
|
||||
#: cps/templates/search_form.html:33
|
||||
#: cps/templates/book_edit.html:33 cps/templates/layout.html:142
|
||||
#: cps/templates/search_form.html:37
|
||||
msgid "Series"
|
||||
msgstr ""
|
||||
|
||||
@@ -561,69 +579,142 @@ msgstr ""
|
||||
msgid "view book after edit"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:107 cps/templates/config_edit.html:73
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:75
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:109
|
||||
#: cps/templates/book_edit.html:107 cps/templates/book_edit.html:118
|
||||
msgid "Get metadata"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:108 cps/templates/config_edit.html:117
|
||||
#: cps/templates/login.html:19 cps/templates/search_form.html:79
|
||||
#: cps/templates/shelf_edit.html:15 cps/templates/user_edit.html:118
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:121
|
||||
msgid "Keyword"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:122
|
||||
msgid " Search keyword "
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:124 cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:125
|
||||
msgid "Click the cover to load metadata to the form"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:129 cps/templates/book_edit.html:142
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:132
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:143
|
||||
msgid "Search error!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:144
|
||||
msgid "No Result! Please try anonther keyword."
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:146 cps/templates/detail.html:76
|
||||
#: cps/templates/search_form.html:14
|
||||
msgid "Publisher"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/book_edit.html:148
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:7
|
||||
msgid "Location of Calibre database"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:11
|
||||
msgid "Server Port"
|
||||
#: cps/templates/config_edit.html:13
|
||||
msgid "Use google drive?"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:15 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
#: cps/templates/config_edit.html:17
|
||||
msgid "Client id"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:23
|
||||
msgid "No. of random books to show"
|
||||
#: cps/templates/config_edit.html:21
|
||||
msgid "Client secret"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:28
|
||||
msgid "Regular expression for title sorting"
|
||||
#: cps/templates/config_edit.html:25
|
||||
msgid "Calibre Base URL"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:42
|
||||
msgid "Enable uploading"
|
||||
#: cps/templates/config_edit.html:29
|
||||
msgid "Google drive Calibre folder"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:46
|
||||
msgid "Enable anonymous browsing"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:50
|
||||
msgid "Enable public registration"
|
||||
#: cps/templates/config_edit.html:38
|
||||
msgid "Metadata Watch Channel ID"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:52
|
||||
msgid "Server Port"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:56 cps/templates/shelf_edit.html:7
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:64
|
||||
msgid "No. of random books to show"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:68
|
||||
msgid "Regular expression for ignoring columns"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:72
|
||||
msgid "Regular expression for title sorting"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:86
|
||||
msgid "Enable uploading"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:90
|
||||
msgid "Enable anonymous browsing"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:94
|
||||
msgid "Enable public registration"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:96
|
||||
msgid "Default Settings for new users"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:55 cps/templates/user_edit.html:80
|
||||
#: cps/templates/config_edit.html:99 cps/templates/user_edit.html:87
|
||||
msgid "Admin user"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:59 cps/templates/user_edit.html:85
|
||||
#: cps/templates/config_edit.html:103 cps/templates/user_edit.html:92
|
||||
msgid "Allow Downloads"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:63 cps/templates/user_edit.html:89
|
||||
#: cps/templates/config_edit.html:107 cps/templates/user_edit.html:96
|
||||
msgid "Allow Uploads"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:67 cps/templates/user_edit.html:93
|
||||
#: cps/templates/config_edit.html:111 cps/templates/user_edit.html:100
|
||||
msgid "Allow Edit"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:71 cps/templates/user_edit.html:98
|
||||
#: cps/templates/config_edit.html:115 cps/templates/user_edit.html:105
|
||||
msgid "Allow Changing Password"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/config_edit.html:78 cps/templates/layout.html:93
|
||||
#: cps/templates/config_edit.html:122 cps/templates/layout.html:93
|
||||
#: cps/templates/login.html:4
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
@@ -640,23 +731,27 @@ msgstr ""
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:74
|
||||
#: cps/templates/detail.html:81
|
||||
msgid "Publishing date"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:106
|
||||
#: cps/templates/detail.html:115
|
||||
msgid "Read"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:123
|
||||
msgid "Description:"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:134
|
||||
#: cps/templates/detail.html:151
|
||||
msgid "Read in browser"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:154
|
||||
#: cps/templates/detail.html:171
|
||||
msgid "Add to shelf"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/detail.html:194
|
||||
#: cps/templates/detail.html:211
|
||||
msgid "Edit metadata"
|
||||
msgstr ""
|
||||
|
||||
@@ -736,19 +831,29 @@ msgstr ""
|
||||
msgid "Show Random Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:43 cps/templates/layout.html:140
|
||||
#: cps/templates/index.xml:43 cps/templates/index.xml:47
|
||||
#: cps/templates/layout.html:132
|
||||
msgid "Read Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:50 cps/templates/index.xml:54
|
||||
#: cps/templates/layout.html:133
|
||||
msgid "Unread Books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:57 cps/templates/layout.html:144
|
||||
msgid "Authors"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:47
|
||||
#: cps/templates/index.xml:61
|
||||
msgid "Books ordered by Author"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:54
|
||||
#: cps/templates/index.xml:68
|
||||
msgid "Books ordered by category"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/index.xml:61
|
||||
#: cps/templates/index.xml:75
|
||||
msgid "Books ordered by series"
|
||||
msgstr ""
|
||||
|
||||
@@ -756,10 +861,6 @@ msgstr ""
|
||||
msgid "Toggle navigation"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:60
|
||||
msgid "Go!"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:68
|
||||
msgid "Advanced Search"
|
||||
msgstr ""
|
||||
@@ -776,31 +877,31 @@ msgstr ""
|
||||
msgid "Browse"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:132
|
||||
#: cps/templates/layout.html:136
|
||||
msgid "Discover"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:135
|
||||
#: cps/templates/layout.html:139
|
||||
msgid "Categories"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:142 cps/templates/search_form.html:54
|
||||
#: cps/templates/layout.html:146 cps/templates/search_form.html:58
|
||||
msgid "Languages"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:145
|
||||
#: cps/templates/layout.html:149
|
||||
msgid "Public Shelves"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:149
|
||||
#: cps/templates/layout.html:153
|
||||
msgid "Your Shelves"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:154
|
||||
#: cps/templates/layout.html:158
|
||||
msgid "Create a Shelf"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/layout.html:155
|
||||
#: cps/templates/layout.html:159
|
||||
msgid "About"
|
||||
msgstr ""
|
||||
|
||||
@@ -866,15 +967,15 @@ msgstr ""
|
||||
msgid "Results for:"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/search_form.html:23
|
||||
#: cps/templates/search_form.html:27
|
||||
msgid "Exclude Tags"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/search_form.html:43
|
||||
#: cps/templates/search_form.html:47
|
||||
msgid "Exclude Series"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/search_form.html:64
|
||||
#: cps/templates/search_form.html:68
|
||||
msgid "Exclude Languages"
|
||||
msgstr ""
|
||||
|
||||
@@ -899,37 +1000,37 @@ msgid "Drag 'n drop to rearrange order"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:3
|
||||
msgid "Linked libraries"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Program library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:9
|
||||
msgid "Installed Version"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:32
|
||||
msgid "Calibre library statistics"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:37
|
||||
#: cps/templates/stats.html:8
|
||||
msgid "Books in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:41
|
||||
#: cps/templates/stats.html:12
|
||||
msgid "Authors in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:45
|
||||
#: cps/templates/stats.html:16
|
||||
msgid "Categories in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:49
|
||||
#: cps/templates/stats.html:20
|
||||
msgid "Series in this Library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:24
|
||||
msgid "Linked libraries"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:28
|
||||
msgid "Program library"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/stats.html:29
|
||||
msgid "Installed Version"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:23
|
||||
msgid "Kindle E-Mail"
|
||||
msgstr ""
|
||||
@@ -942,43 +1043,47 @@ msgstr ""
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:45
|
||||
#: cps/templates/user_edit.html:47
|
||||
msgid "Show random books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:49
|
||||
#: cps/templates/user_edit.html:51
|
||||
msgid "Show hot books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:53
|
||||
#: cps/templates/user_edit.html:55
|
||||
msgid "Show best rated books"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:57
|
||||
#: cps/templates/user_edit.html:59
|
||||
msgid "Show language selection"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:61
|
||||
#: cps/templates/user_edit.html:63
|
||||
msgid "Show series selection"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:65
|
||||
#: cps/templates/user_edit.html:67
|
||||
msgid "Show category selection"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:69
|
||||
#: cps/templates/user_edit.html:71
|
||||
msgid "Show author selection"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:73
|
||||
#: cps/templates/user_edit.html:75
|
||||
msgid "Show read and unread"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:79
|
||||
msgid "Show random books in detail view"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:105
|
||||
#: cps/templates/user_edit.html:112
|
||||
msgid "Delete this user"
|
||||
msgstr ""
|
||||
|
||||
#: cps/templates/user_edit.html:116
|
||||
#: cps/templates/user_edit.html:127
|
||||
msgid "Recent Downloads"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -6,7 +6,8 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
|
||||
|
||||

|
||||
|
||||
##Features
|
||||
## Features
|
||||
|
||||
- Bootstrap 3 HTML5 interface
|
||||
- full graphical setup
|
||||
- User management
|
||||
@@ -28,13 +29,14 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Execute the command: `python cps.py`
|
||||
2. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
3. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
|
||||
4. Go to Login page
|
||||
1. Install required dependencies by executing `pip install -r requirements.txt`
|
||||
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
|
||||
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
|
||||
5. Go to Login page
|
||||
|
||||
**Default admin login:**
|
||||
*Username:* admin
|
||||
**Default admin login:**
|
||||
*Username:* admin
|
||||
*Password:* admin123
|
||||
|
||||
## Runtime Configuration Options
|
||||
@@ -56,10 +58,44 @@ Tick to enable uploading of PDF, epub, FB2. This requires the imagemagick librar
|
||||
## Requirements
|
||||
|
||||
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
|
||||
|
||||
@@ -131,4 +167,4 @@ Replace the user and ExecStart with your user and foldernames.
|
||||
|
||||
`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
|
||||
Vendored
-22
@@ -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.
|
||||
Vendored
-22
@@ -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.
|
||||
Vendored
-31
@@ -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.
|
||||
Vendored
-5
@@ -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"]
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
__version__ = '1.26.0'
|
||||
Vendored
-362
@@ -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
|
||||
Vendored
-1226
File diff suppressed because it is too large
Load Diff
Vendored
-553
@@ -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()
|
||||
Vendored
-152
@@ -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
|
||||
Vendored
-3004
File diff suppressed because it is too large
Load Diff
Vendored
-295
@@ -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']]
|
||||
Vendored
-358
@@ -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
|
||||
"""
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
__version__ = '5.0.6'
|
||||
Vendored
-28
@@ -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".
|
||||
Vendored
-29
@@ -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.
|
||||
Vendored
-24
@@ -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'
|
||||
Vendored
-51
@@ -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,)
|
||||
Vendored
-941
@@ -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)))
|
||||
Vendored
-1181
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
Vendored
-209
@@ -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)
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(Umin_daysqKU
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U
unit_patternsq&}q'u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(Umin_daysqKU
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U
unit_patternsq&}q'u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(Umin_daysqKU
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U
unit_patternsq&}q'u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(U
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U
unit_patternsq%}q&u.
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(U
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U
unit_patternsq%}q&u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(Umin_daysqKU
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U
unit_patternsq&}q'u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(U
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U
unit_patternsq%}q&u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(U
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U
unit_patternsq%}q&u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(U
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq}q Uvariantsq!}q"Ucurrency_namesq#}q$U
unit_patternsq%}q&u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
€}q(Ucurrency_symbolsq}qUscientific_formatsq}qUpercent_formatsq}qUnumber_symbolsq}q Ucurrency_names_pluralq
|
||||
}qU week_dataq}q
(Umin_daysqKU
weekend_startqKU first_dayqKUweekend_endqKuUzone_formatsq}qUcurrency_formatsq}qU_versionqM5 U languagesq}qUterritoriesq}U
|
||||
time_zonesq}qUscriptsq}qUdecimal_formatsq}qU
|
||||
meta_zonesq }q!Uvariantsq"}q#Ucurrency_namesq$}q%U
unit_patternsq&}q'u.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user