mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
fd2888af18
commit
e810b58f40
@ -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
|
||||
|
@ -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 = {}
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
213
src/calibre/library/save_to_disk.py
Normal file
213
src/calibre/library/save_to_disk.py
Normal 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
|
||||
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user