mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add support for generating Stanza feeds to calibredb
This commit is contained in:
parent
ea6d462d34
commit
754c0c63c1
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Command line interface to the calibre database.
|
Command line interface to the calibre database.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sys, os
|
import sys, os, cStringIO
|
||||||
from textwrap import TextWrapper
|
from textwrap import TextWrapper
|
||||||
|
|
||||||
from calibre import terminal_controller, preferred_encoding
|
from calibre import terminal_controller, preferred_encoding
|
||||||
@ -20,8 +20,9 @@ from calibre.ebooks.metadata.meta import get_metadata
|
|||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.library.database import text_to_tokens
|
from calibre.library.database import text_to_tokens
|
||||||
from calibre.ebooks.metadata.opf import OPFCreator, OPFReader
|
from calibre.ebooks.metadata.opf import OPFCreator, OPFReader
|
||||||
|
from calibre.utils.genshi.template import MarkupTemplate
|
||||||
|
|
||||||
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats', 'isbn', 'path'])
|
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats', 'isbn', 'cover'])
|
||||||
|
|
||||||
XML_TEMPLATE = '''\
|
XML_TEMPLATE = '''\
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@ -58,6 +59,33 @@ XML_TEMPLATE = '''\
|
|||||||
</calibredb>
|
</calibredb>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
STANZA_TEMPLATE='''\
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
<title>calibre Library</title>
|
||||||
|
<author>
|
||||||
|
<name>calibre</name>
|
||||||
|
<uri>http://calibre.kovidgoyal.net</uri>
|
||||||
|
</author>
|
||||||
|
<subtitle>
|
||||||
|
${subtitle}
|
||||||
|
</subtitle>
|
||||||
|
<py:for each="record in data">
|
||||||
|
<entry>
|
||||||
|
<title>${record['title']}</title>
|
||||||
|
<id>urn:calibre:${record['id']}</id>
|
||||||
|
<author><name>${record['authors']}</name></author>
|
||||||
|
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%S+0000')}</updated>
|
||||||
|
<link type="application/epub+zip" href="${record['fmt_epub'].replace(sep, '/')}" />
|
||||||
|
<link py:if="record['cover']" rel="x-stanza-cover-image" type="image/png" href="${record['cover'].replace(sep, '/')}" />
|
||||||
|
<content py:if="record['comments']" type="xhtml">
|
||||||
|
<pre>${record['comments']}</pre>
|
||||||
|
</content>
|
||||||
|
</entry>
|
||||||
|
</py:for>
|
||||||
|
</feed>
|
||||||
|
'''
|
||||||
|
|
||||||
def get_parser(usage):
|
def get_parser(usage):
|
||||||
parser = OptionParser(usage)
|
parser = OptionParser(usage)
|
||||||
go = parser.add_option_group('GLOBAL OPTIONS')
|
go = parser.add_option_group('GLOBAL OPTIONS')
|
||||||
@ -72,26 +100,21 @@ def get_db(dbpath, options):
|
|||||||
print _('Using library at'), dbpath
|
print _('Using library at'), dbpath
|
||||||
return LibraryDatabase2(dbpath, row_factory=True)
|
return LibraryDatabase2(dbpath, row_factory=True)
|
||||||
|
|
||||||
def do_list(db, fields, sort_by, ascending, search_text, line_width, separator):
|
def do_list(db, fields, sort_by, ascending, search_text, line_width, separator,
|
||||||
|
prefix, output_format, subtitle='Books in the calibre database'):
|
||||||
db.refresh(sort_by, ascending)
|
db.refresh(sort_by, ascending)
|
||||||
if search_text:
|
if search_text:
|
||||||
filters, OR = text_to_tokens(search_text)
|
filters, OR = text_to_tokens(search_text)
|
||||||
db.filter(filters, False, OR)
|
db.filter(filters, False, OR)
|
||||||
|
authors_to_string = output_format in ['stanza', 'text']
|
||||||
|
data = db.get_data_as_dict(prefix, authors_as_string=authors_to_string)
|
||||||
fields = ['id'] + fields
|
fields = ['id'] + fields
|
||||||
|
if output_format == 'text':
|
||||||
widths = list(map(lambda x : 0, fields))
|
widths = list(map(lambda x : 0, fields))
|
||||||
data = []
|
for record in data:
|
||||||
for record in db.data:
|
for f in record.keys():
|
||||||
data.append({})
|
record[f] = unicode(record[f])
|
||||||
for field in fields:
|
record[f] = record[f].replace('\n', ' ')
|
||||||
if field == 'path':
|
|
||||||
path = db.path(record['id'], index_is_id=True)
|
|
||||||
path += os.sep + db.construct_file_name(record['id']) + '.[%s]'
|
|
||||||
formats = db.formats(record['id'], index_is_id=True)
|
|
||||||
if not formats:
|
|
||||||
formats = ''
|
|
||||||
data[-1][field] = path%formats.lower()
|
|
||||||
else:
|
|
||||||
data[-1][field] = record[field]
|
|
||||||
for i in data:
|
for i in data:
|
||||||
for j, field in enumerate(fields):
|
for j, field in enumerate(fields):
|
||||||
widths[j] = max(widths[j], len(unicode(i[str(field)])))
|
widths[j] = max(widths[j], len(unicode(i[str(field)])))
|
||||||
@ -117,6 +140,7 @@ def do_list(db, fields, sort_by, ascending, search_text, line_width, separator):
|
|||||||
print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL
|
print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL
|
||||||
|
|
||||||
wrappers = map(lambda x: TextWrapper(x-1), widths)
|
wrappers = map(lambda x: TextWrapper(x-1), widths)
|
||||||
|
o = cStringIO.StringIO()
|
||||||
|
|
||||||
for record in data:
|
for record in data:
|
||||||
text = [wrappers[i].wrap(unicode(record[field]).encode('utf-8')) for i, field in enumerate(fields)]
|
text = [wrappers[i].wrap(unicode(record[field]).encode('utf-8')) for i, field in enumerate(fields)]
|
||||||
@ -125,9 +149,18 @@ def do_list(db, fields, sort_by, ascending, search_text, line_width, separator):
|
|||||||
for i, field in enumerate(text):
|
for i, field in enumerate(text):
|
||||||
ft = text[i][l] if l < len(text[i]) else ''
|
ft = text[i][l] if l < len(text[i]) else ''
|
||||||
filler = '%*s'%(widths[i]-len(ft)-1, '')
|
filler = '%*s'%(widths[i]-len(ft)-1, '')
|
||||||
sys.stdout.write(ft)
|
o.write(ft)
|
||||||
sys.stdout.write(filler+separator)
|
o.write(filler+separator)
|
||||||
print
|
print >>o
|
||||||
|
return o.getvalue()
|
||||||
|
elif output_format == 'xml':
|
||||||
|
template = MarkupTemplate(XML_TEMPLATE)
|
||||||
|
return template.generate(data=data).render('xml')
|
||||||
|
elif output_format == 'stanza':
|
||||||
|
data = [i for i in data if i.has_key('fmt_epub')]
|
||||||
|
template = MarkupTemplate(STANZA_TEMPLATE)
|
||||||
|
return template.generate(data=data, subtitle=subtitle, sep=os.sep).render('xml')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def command_list(args, dbpath):
|
def command_list(args, dbpath):
|
||||||
@ -139,7 +172,7 @@ List the books available in the calibre database.
|
|||||||
'''
|
'''
|
||||||
))
|
))
|
||||||
parser.add_option('-f', '--fields', default='title,authors',
|
parser.add_option('-f', '--fields', default='title,authors',
|
||||||
help=_('The fields to display when listing books in the database. Should be a comma separated list of fields.\nAvailable fields: %s\nDefault: %%default. The special field "all" can be used to select all fields.')%','.join(FIELDS))
|
help=_('The fields to display when listing books in the database. Should be a comma separated list of fields.\nAvailable fields: %s\nDefault: %%default. The special field "all" can be used to select all fields. Only has effect in the text output format.')%','.join(FIELDS))
|
||||||
parser.add_option('--sort-by', default='timestamp',
|
parser.add_option('--sort-by', default='timestamp',
|
||||||
help=_('The field by which to sort the results.\nAvailable fields: %s\nDefault: %%default')%','.join(FIELDS))
|
help=_('The field by which to sort the results.\nAvailable fields: %s\nDefault: %%default')%','.join(FIELDS))
|
||||||
parser.add_option('--ascending', default=False, action='store_true',
|
parser.add_option('--ascending', default=False, action='store_true',
|
||||||
@ -149,6 +182,10 @@ List the books available in the calibre database.
|
|||||||
parser.add_option('-w', '--line-width', default=-1, type=int,
|
parser.add_option('-w', '--line-width', default=-1, type=int,
|
||||||
help=_('The maximum width of a single line in the output. Defaults to detecting screen size.'))
|
help=_('The maximum width of a single line in the output. Defaults to detecting screen size.'))
|
||||||
parser.add_option('--separator', default=' ', help=_('The string used to separate fields. Default is a space.'))
|
parser.add_option('--separator', default=' ', help=_('The string used to separate fields. Default is a space.'))
|
||||||
|
parser.add_option('--prefix', default=None, help=_('The prefix for all file paths. Default is the absolute path to the library folder.'))
|
||||||
|
of = ['text', 'xml', 'stanza']
|
||||||
|
parser.add_option('--output-format', choices=of, default='text',
|
||||||
|
help=_('The format in which to output the data. Available choices: %s. Defaults is text.')%of)
|
||||||
opts, args = parser.parse_args(sys.argv[:1] + args)
|
opts, args = parser.parse_args(sys.argv[:1] + args)
|
||||||
fields = [str(f.strip().lower()) for f in opts.fields.split(',')]
|
fields = [str(f.strip().lower()) for f in opts.fields.split(',')]
|
||||||
if 'all' in fields:
|
if 'all' in fields:
|
||||||
@ -166,7 +203,8 @@ List the books available in the calibre database.
|
|||||||
print >>sys.stderr, _('Invalid sort field. Available fields:'), ','.join(FIELDS)
|
print >>sys.stderr, _('Invalid sort field. Available fields:'), ','.join(FIELDS)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
do_list(db, fields, opts.sort_by, opts.ascending, opts.search, opts.line_width, opts.separator)
|
print do_list(db, fields, opts.sort_by, opts.ascending, opts.search, opts.line_width, opts.separator,
|
||||||
|
opts.prefix, opts.output_format)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@ -205,9 +243,10 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates):
|
|||||||
metadata.append(mi)
|
metadata.append(mi)
|
||||||
|
|
||||||
file_duplicates = db.add_books(files, formats, metadata, add_duplicates=add_duplicates)
|
file_duplicates = db.add_books(files, formats, metadata, add_duplicates=add_duplicates)
|
||||||
if not file_duplicates:
|
if not file_duplicates[0]:
|
||||||
file_duplicates = []
|
file_duplicates = []
|
||||||
|
else:
|
||||||
|
file_duplicates = file_duplicates[0]
|
||||||
|
|
||||||
dir_dups = []
|
dir_dups = []
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
@ -452,45 +491,9 @@ an opf file). You can get id numbers from the list command.
|
|||||||
do_export(get_db(dbpath, opts), ids, dir, opts.single_dir, opts.by_author)
|
do_export(get_db(dbpath, opts), ids, dir, opts.single_dir, opts.by_author)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def do_export_db(db):
|
|
||||||
db.refresh('timestamp', True)
|
|
||||||
data = []
|
|
||||||
for record in db.data:
|
|
||||||
x = {}
|
|
||||||
for field in FIELDS:
|
|
||||||
if field != 'path':
|
|
||||||
x[field] = record[field]
|
|
||||||
data.append(x)
|
|
||||||
x['id'] = record[0]
|
|
||||||
x['formats'] = []
|
|
||||||
x['authors'] = [i.replace('|', ',') for i in x['authors'].split(',')]
|
|
||||||
x['tags'] = [i.replace('|', ',').strip() for i in x['tags'].split(',')] if x['tags'] else []
|
|
||||||
path = os.path.join(db.library_path, db.path(record['id'], index_is_id=True))
|
|
||||||
x['cover'] = os.path.join(path, 'cover.jpg')
|
|
||||||
if not os.path.exists(x['cover']):
|
|
||||||
x['cover'] = None
|
|
||||||
path += os.sep + db.construct_file_name(record['id']) + '.%s'
|
|
||||||
formats = db.formats(record['id'], index_is_id=True)
|
|
||||||
if formats:
|
|
||||||
for fmt in formats.split(','):
|
|
||||||
x['formats'].append(path%fmt.lower())
|
|
||||||
from calibre.utils.genshi.template import MarkupTemplate
|
|
||||||
template = MarkupTemplate(XML_TEMPLATE)
|
|
||||||
print template.generate(data=data).render('xml')
|
|
||||||
|
|
||||||
def command_export_db(args, dbpath):
|
|
||||||
parser = get_parser(_('''\
|
|
||||||
%prog export_db [options]
|
|
||||||
|
|
||||||
Export the metadata in the database as an XML file.
|
|
||||||
'''))
|
|
||||||
opts, args = parser.parse_args(sys.argv[1:]+args)
|
|
||||||
do_export_db(get_db(dbpath, opts))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
commands = ('list', 'add', 'remove', 'add_format', 'remove_format',
|
commands = ('list', 'add', 'remove', 'add_format', 'remove_format',
|
||||||
'show_metadata', 'set_metadata', 'export', 'export_db')
|
'show_metadata', 'set_metadata', 'export')
|
||||||
parser = OptionParser(_(
|
parser = OptionParser(_(
|
||||||
'''\
|
'''\
|
||||||
%%prog command [options] [arguments]
|
%%prog command [options] [arguments]
|
||||||
|
@ -16,7 +16,7 @@ from PyQt4.QtGui import QApplication, QPixmap, QImage
|
|||||||
__app = None
|
__app = None
|
||||||
|
|
||||||
from calibre.library.database import LibraryDatabase
|
from calibre.library.database import LibraryDatabase
|
||||||
from calibre.ebooks.metadata import string_to_authors
|
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
||||||
from calibre.constants import preferred_encoding, iswindows, isosx
|
from calibre.constants import preferred_encoding, iswindows, isosx
|
||||||
|
|
||||||
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
@ -569,6 +569,11 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
return img
|
return img
|
||||||
return f if as_file else f.read()
|
return f if as_file else f.read()
|
||||||
|
|
||||||
|
def has_cover(self, index, index_is_id=False):
|
||||||
|
id = index if index_is_id else self.id(index)
|
||||||
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
|
return os.access(path, os.R_OK)
|
||||||
|
|
||||||
def set_cover(self, id, data):
|
def set_cover(self, id, data):
|
||||||
'''
|
'''
|
||||||
Set the cover for this book.
|
Set the cover for this book.
|
||||||
@ -1004,6 +1009,46 @@ class LibraryDatabase2(LibraryDatabase):
|
|||||||
progress.hide()
|
progress.hide()
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_as_dict(self, prefix=None, authors_as_string=False):
|
||||||
|
'''
|
||||||
|
Return all metadata stored in the database as a dict. Includes paths to
|
||||||
|
the cover and each format.
|
||||||
|
|
||||||
|
:param prefix: The prefix for all paths. By default, the prefix is the absolute path
|
||||||
|
to the library folder.
|
||||||
|
'''
|
||||||
|
if prefix is None:
|
||||||
|
prefix = self.library_path
|
||||||
|
FIELDS = set(['title', 'authors', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'isbn'])
|
||||||
|
data = []
|
||||||
|
if len(self.data) == 0:
|
||||||
|
self.refresh('timestamp', True)
|
||||||
|
for record in self.data:
|
||||||
|
if record is None:
|
||||||
|
continue
|
||||||
|
x = {}
|
||||||
|
for field in FIELDS:
|
||||||
|
x[field] = record[field]
|
||||||
|
data.append(x)
|
||||||
|
x['id'] = record[0]
|
||||||
|
x['formats'] = []
|
||||||
|
x['authors'] = [i.replace('|', ',') for i in x['authors'].split(',')]
|
||||||
|
if authors_as_string:
|
||||||
|
x['authors'] = authors_to_string(x['authors'])
|
||||||
|
x['tags'] = [i.replace('|', ',').strip() for i in x['tags'].split(',')] if x['tags'] else []
|
||||||
|
path = os.path.join(prefix, self.path(record['id'], index_is_id=True))
|
||||||
|
x['cover'] = os.path.join(path, 'cover.jpg')
|
||||||
|
if not self.has_cover(x['id'], index_is_id=True):
|
||||||
|
x['cover'] = None
|
||||||
|
path += os.sep + self.construct_file_name(record['id']) + '.%s'
|
||||||
|
formats = self.formats(record['id'], index_is_id=True)
|
||||||
|
if formats:
|
||||||
|
for fmt in formats.split(','):
|
||||||
|
x['formats'].append(path%fmt.lower())
|
||||||
|
x['fmt_'+fmt.lower()] = path%fmt.lower()
|
||||||
|
x['available_formats'] = [i.upper() for i in formats.split(',')]
|
||||||
|
return data
|
||||||
|
|
||||||
def migrate_old(self, db, progress):
|
def migrate_old(self, db, progress):
|
||||||
header = _(u'<p>Migrating old database to ebook library in %s<br><center>')%self.library_path
|
header = _(u'<p>Migrating old database to ebook library in %s<br><center>')%self.library_path
|
||||||
progress.setValue(0)
|
progress.setValue(0)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user