IGN:calibredb export now supports using templates to control output directory structure/filenames. Fix pop-up menuon search box being colored.

This commit is contained in:
Kovid Goyal 2009-08-17 14:11:29 -06:00
parent fd2888af18
commit e810b58f40
7 changed files with 269 additions and 11 deletions

View File

@ -75,7 +75,7 @@ def sanitize_file_name(name, substitute='_', as_unicode=False):
'''
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
The set of invalid characters is the union of the invalid characters in Windows,
OS X and Linux. Also removes leading an trailing whitespace.
OS X and Linux. Also removes leading and trailing whitespace.
**WARNING:** This function also replaces path separators, so only pass file names
and not full paths to it.
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings

View File

@ -520,7 +520,8 @@ class Device(DeviceConfig, DevicePlugin):
main, carda, cardb = self.find_device_nodes()
if main is None:
raise DeviceError(_('Unable to detect the %s disk drive.')
raise DeviceError(_('Unable to detect the %s disk drive. Your '
' kernel is probably exporting a deprecated version of SYSFS.')
%self.__class__.__name__)
self._linux_mount_map = {}

View File

@ -301,6 +301,15 @@ class MetaInformation(object):
def authors_from_string(self, raw):
self.authors = string_to_authors(raw)
def format_authors(self):
return authors_to_string(self.authors)
def format_tags(self):
return u', '.join([unicode(t) for t in self.tags])
def format_rating(self):
return unicode(self.rating)
def __unicode__(self):
ans = []
def fmt(x, y):

View File

@ -1121,13 +1121,14 @@ class SearchBox(QLineEdit):
def normalize_state(self):
self.setText('')
self.setPalette(self.default_palette)
self.setStyleSheet('QLineEdit { background-color: white; }')
def clear_to_help(self):
self.setPalette(self.gray)
self.setText(self.help_text)
self.home(False)
self.initial_state = True
self.setStyleSheet("background-color: white")
self.setStyleSheet('QLineEdit { background-color: white; }')
self.emit(SIGNAL('cleared()'))
def clear(self):
@ -1135,8 +1136,8 @@ class SearchBox(QLineEdit):
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
def search_done(self, ok):
col = 'rgba(0,255,0,25%)' if ok else 'rgb(255,0,0,25%)'
self.setStyleSheet('background-color: '+col)
col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)'
self.setStyleSheet('QLineEdit { background-color: %s; }' % col)
def keyPressEvent(self, event):
if self.initial_state:

View File

@ -11,7 +11,7 @@ import sys, os, cStringIO
from textwrap import TextWrapper
from urllib import quote
from calibre import terminal_controller, preferred_encoding
from calibre import terminal_controller, preferred_encoding, prints
from calibre.utils.config import OptionParser, prefs
try:
from calibre.utils.single_qt_application import send_message
@ -488,10 +488,21 @@ show_metadata command.
do_set_metadata(get_db(dbpath, opts), id, opf)
return 0
def do_export(db, ids, dir, single_dir, by_author):
def do_export(db, ids, dir, opts):
if ids is None:
ids = list(db.all_ids())
db.export_to_dir(dir, ids, byauthor=by_author, single_dir=single_dir, index_is_id=True)
from calibre.library.save_to_disk import save_to_disk
failures = save_to_disk(db, ids, dir, opts=opts)
if failures:
prints('Failed to save the following books:')
for id, title, tb in failures:
prints(str(id)+':', title)
if tb:
prints('\t'+'\n\t'.join(tb.splitlines()))
else:
prints('\tRequested formats not available')
prints(' ')
def command_export(args, dbpath):
parser = get_parser(_('''\
@ -507,8 +518,21 @@ an opf file). You can get id numbers from the list command.
help=(_('Export books to the specified directory. Default is')+' %default'))
parser.add_option('--single-dir', default=False, action='store_true',
help=_('Export all books into a single directory'))
parser.add_option('--by-author', default=False, action='store_true',
help=_('Create file names as author - title instead of title - author'))
from calibre.library.save_to_disk import config
c = config()
for pref in ['asciiize', 'update_metadata', 'write_opf', 'save_cover']:
opt = c.get_option(pref)
switch = '--dont-'+pref.replace('_', '-')
parser.add_option(switch, default=True, action='store_false',
help=opt.help+' '+_('Specifying this switch will turn '
'this behavior off.'), dest=pref)
for pref in ['timefmt', 'template', 'formats']:
opt = c.get_option(pref)
switch = '--'+pref
parser.add_option(switch, default=opt.default,
help=opt.help, dest=pref)
opts, args = parser.parse_args(sys.argv[1:]+args)
if (len(args) < 2 and not opts.all):
parser.print_help()
@ -517,7 +541,7 @@ an opf file). You can get id numbers from the list command.
return 1
ids = None if opts.all else map(int, args[1].split(','))
dir = os.path.abspath(os.path.expanduser(opts.to_dir))
do_export(get_db(dbpath, opts), ids, dir, opts.single_dir, opts.by_author)
do_export(get_db(dbpath, opts), ids, dir, opts)
return 0
def main(args=sys.argv):

View File

@ -0,0 +1,213 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, traceback, sys, cStringIO
from calibre.utils.config import Config, StringConfig
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
ascii_filename, sanitize_file_name
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.meta import set_metadata
from calibre import strftime
DEFAULT_TEMPLATE = '{author_sort}/{title} - {authors}'
FORMAT_ARGS = dict(
title='',
authors='',
author_sort='',
tags='',
series='',
series_index='',
rating='',
isbn='',
publisher='',
timestamp='',
pubdate='',
id=''
)
def config(defaults=None):
if defaults is None:
c = Config('save_to_disk', _('Options to control saving to disk'))
else:
c = StringConfig(defaults)
x = c.add_opt
x('update_metadata', default=True,
help=_('Normally, calibre will update the metadata in the saved files from what is'
' in the calibre library. Makes saving to disk slower.'))
x('write_opf', default=True,
help=_('Normally, calibre will write the metadata into a separate OPF file along with the'
' actual e-book files.'))
x('save_cover', default=True,
help=_('Normally, calibre will save the cover in a separate file along with the '
'actual e-book file(s).'))
x('formats', default='all',
help=_('Comma separated list of formats to save for each book.'
' By default all available books are saved.'))
x('template', default=DEFAULT_TEMPLATE,
help=_('The template to control the filename and directory structure of the saved files. '
'Default is "%s" which will save books into a per-author '
'subdirectory with filenames containing title and author. '
'Available controls are: {%s}')%(DEFAULT_TEMPLATE, ', '.join(FORMAT_ARGS)))
x('asciiize', default=True,
help=_('Normally, calibre will convert all non English characters into English equivalents '
'for the file names. '
'WARNING: If you turn this off, you may experience errors when '
'saving, depending on how well the filesystem you are saving '
'to supports unicode.'))
x('timefmt', default='%b, %Y',
help=_('The format in which to display dates. %d - day, %b - month, '
'%Y - year. Default is: %b, %Y'))
return c
def preprocess_template(template):
template = template.replace('//', '/')
template = template.replace('{author}', '{authors}')
template = template.replace('{tag}', '{tags}')
return template
def get_components(template, mi, id, timefmt='%b %Y', length=250, sanitize_func=ascii_filename):
format_args = dict(**FORMAT_ARGS)
if mi.title:
format_args['title'] = mi.title
if mi.authors:
format_args['authors'] = mi.format_authors()
if mi.author_sort:
format_args['author_sort'] = mi.author_sort
if mi.tags:
format_args['tags'] = mi.format_tags()
if mi.series:
format_args['series'] = mi.series
if mi.series_index is not None:
format_args['series_index'] = mi.format_series_index()
if mi.rating is not None:
format_args['rating'] = mi.format_rating()
if mi.isbn:
format_args['isbn'] = mi.isbn
if mi.publisher:
format_args['publisher'] = mi.publisher
if hasattr(mi.timestamp, 'timetuple'):
format_args['timestamp'] = strftime(timefmt, mi.timestamp.timetuple())
if hasattr(mi.pubdate, 'timetuple'):
format_args['timestamp'] = strftime(timefmt, mi.pubdate.timetuple())
format_args['id'] = str(id)
components = [x.strip() for x in template.split('/') if x.strip()]
components = [x.format(**format_args).strip() for x in components]
components = [sanitize_func(x) for x in components if x]
if not components:
components = [str(id)]
return shorten_components_to(length, components)
def save_book_to_disk(id, db, root, opts, length):
mi = db.get_metadata(id, index_is_id=True)
available_formats = db.formats(id, index_is_id=True)
if not available_formats:
available_formats = []
else:
available_formats = [x.lower().strip() for x in
available_formats.split(',')]
if opts.formats == 'all':
asked_formats = available_formats
else:
asked_formats = [x.lower().strip() for x in opts.formats.split(',')]
formats = set(available_formats).intersection(set(asked_formats))
if not formats:
return True, id, mi.title
components = get_components(opts.template, mi, id, opts.timefmt, length,
ascii_filename if opts.asciiize else sanitize_file_name)
base_path = os.path.join(root, *components)
base_name = os.path.basename(base_path)
dirpath = os.path.dirname(base_path)
if not os.path.exists(dirpath):
os.makedirs(dirpath)
cdata = db.cover(id, index_is_id=True)
if opts.save_cover:
if cdata is not None:
with open(base_path+'.jpg', 'wb') as f:
f.write(cdata)
mi.cover = base_name+'.jpg'
else:
mi.cover = None
if opts.write_opf:
opf = metadata_to_opf(mi)
with open(base_path+'.opf', 'wb') as f:
f.write(opf)
if cdata is not None:
mi.cover_data = ('jpg', cdata)
mi.cover = None
written = False
for fmt in formats:
data = db.format(id, fmt, index_is_id=True)
if data is None:
continue
else:
written = True
if opts.update_metadata:
stream = cStringIO.StringIO()
stream.write(data)
stream.seek(0)
try:
set_metadata(stream, mi, fmt)
except:
traceback.print_exc()
stream.seek(0)
data = stream.read()
with open(base_path+'.'+fmt, 'wb') as f:
f.write(data)
return not written, id, mi.title
def save_to_disk(db, ids, root, opts=None, callback=None):
'''
Save books from the database ``db`` to the path specified by ``root``.
:param:`ids` iterable of book ids to save from the database.
:param:`callback` is an optional callable that is called on after each
book is processed with the arguments: id, title and failed
:return: A list of failures. Each element of the list is a tuple
(id, title, traceback)
'''
if opts is None:
opts = config().parse()
if isinstance(root, unicode):
root = root.encode(sys.getfilesystemencoding())
root = os.path.abspath(root)
opts.template = preprocess_template(opts.template)
length = 1000 if supports_long_names(root) else 250
length -= len(root)
if length < 5:
raise ValueError('%r is too long.'%root)
failures = []
for x in ids:
tb = ''
try:
failed, id, title = save_book_to_disk(x, db, root, opts, length)
except:
failed, id, title = True, x, db.title(x, index_is_id=True)
tb = traceback.format_exc()
if failed:
failures.append((id, title, tb))
if callable(callback):
if not callback(int(id), title, failed):
break
return failures

View File

@ -216,6 +216,14 @@ class OptionSet(object):
return True
return False
def get_option(self, name_or_option_object):
idx = self.preferences.index(name_or_option_object)
if idx > -1:
return self.preferences[idx]
for p in self.preferences:
if p.name == name_or_option_object:
return p
def add_group(self, name, description=''):
if name in self.group_list:
raise ValueError('A group by the name %s already exists in this set'%name)
@ -370,6 +378,7 @@ class ConfigInterface(object):
self.add_group = self.option_set.add_group
self.remove_opt = self.remove = self.option_set.remove_opt
self.parse_string = self.option_set.parse_string
self.get_option = self.option_set.get_option
def update(self, other):
self.option_set.update(other.option_set)
@ -381,6 +390,7 @@ class ConfigInterface(object):
def smart_update(self, opts1, opts2):
self.option_set.smart_update(opts1, opts2)
class Config(ConfigInterface):
'''
A file based configuration.