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`.
|
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,
|
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
|
**WARNING:** This function also replaces path separators, so only pass file names
|
||||||
and not full paths to it.
|
and not full paths to it.
|
||||||
*NOTE:* This function always returns byte strings, not unicode objects. The byte strings
|
*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()
|
main, carda, cardb = self.find_device_nodes()
|
||||||
if main is None:
|
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.__class__.__name__)
|
||||||
|
|
||||||
self._linux_mount_map = {}
|
self._linux_mount_map = {}
|
||||||
|
@ -301,6 +301,15 @@ class MetaInformation(object):
|
|||||||
def authors_from_string(self, raw):
|
def authors_from_string(self, raw):
|
||||||
self.authors = string_to_authors(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):
|
def __unicode__(self):
|
||||||
ans = []
|
ans = []
|
||||||
def fmt(x, y):
|
def fmt(x, y):
|
||||||
|
@ -1121,13 +1121,14 @@ class SearchBox(QLineEdit):
|
|||||||
def normalize_state(self):
|
def normalize_state(self):
|
||||||
self.setText('')
|
self.setText('')
|
||||||
self.setPalette(self.default_palette)
|
self.setPalette(self.default_palette)
|
||||||
|
self.setStyleSheet('QLineEdit { background-color: white; }')
|
||||||
|
|
||||||
def clear_to_help(self):
|
def clear_to_help(self):
|
||||||
self.setPalette(self.gray)
|
self.setPalette(self.gray)
|
||||||
self.setText(self.help_text)
|
self.setText(self.help_text)
|
||||||
self.home(False)
|
self.home(False)
|
||||||
self.initial_state = True
|
self.initial_state = True
|
||||||
self.setStyleSheet("background-color: white")
|
self.setStyleSheet('QLineEdit { background-color: white; }')
|
||||||
self.emit(SIGNAL('cleared()'))
|
self.emit(SIGNAL('cleared()'))
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
@ -1135,8 +1136,8 @@ class SearchBox(QLineEdit):
|
|||||||
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
|
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
|
||||||
|
|
||||||
def search_done(self, ok):
|
def search_done(self, ok):
|
||||||
col = 'rgba(0,255,0,25%)' if ok else 'rgb(255,0,0,25%)'
|
col = 'rgba(0,255,0,20%)' if ok else 'rgb(255,0,0,20%)'
|
||||||
self.setStyleSheet('background-color: '+col)
|
self.setStyleSheet('QLineEdit { background-color: %s; }' % col)
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
if self.initial_state:
|
if self.initial_state:
|
||||||
|
@ -11,7 +11,7 @@ import sys, os, cStringIO
|
|||||||
from textwrap import TextWrapper
|
from textwrap import TextWrapper
|
||||||
from urllib import quote
|
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
|
from calibre.utils.config import OptionParser, prefs
|
||||||
try:
|
try:
|
||||||
from calibre.utils.single_qt_application import send_message
|
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)
|
do_set_metadata(get_db(dbpath, opts), id, opf)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def do_export(db, ids, dir, single_dir, by_author):
|
def do_export(db, ids, dir, opts):
|
||||||
if ids is None:
|
if ids is None:
|
||||||
ids = list(db.all_ids())
|
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):
|
def command_export(args, dbpath):
|
||||||
parser = get_parser(_('''\
|
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'))
|
help=(_('Export books to the specified directory. Default is')+' %default'))
|
||||||
parser.add_option('--single-dir', default=False, action='store_true',
|
parser.add_option('--single-dir', default=False, action='store_true',
|
||||||
help=_('Export all books into a single directory'))
|
help=_('Export all books into a single directory'))
|
||||||
parser.add_option('--by-author', default=False, action='store_true',
|
from calibre.library.save_to_disk import config
|
||||||
help=_('Create file names as author - title instead of title - author'))
|
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)
|
opts, args = parser.parse_args(sys.argv[1:]+args)
|
||||||
if (len(args) < 2 and not opts.all):
|
if (len(args) < 2 and not opts.all):
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
@ -517,7 +541,7 @@ an opf file). You can get id numbers from the list command.
|
|||||||
return 1
|
return 1
|
||||||
ids = None if opts.all else map(int, args[1].split(','))
|
ids = None if opts.all else map(int, args[1].split(','))
|
||||||
dir = os.path.abspath(os.path.expanduser(opts.to_dir))
|
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
|
return 0
|
||||||
|
|
||||||
def main(args=sys.argv):
|
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 True
|
||||||
return False
|
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=''):
|
def add_group(self, name, description=''):
|
||||||
if name in self.group_list:
|
if name in self.group_list:
|
||||||
raise ValueError('A group by the name %s already exists in this set'%name)
|
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.add_group = self.option_set.add_group
|
||||||
self.remove_opt = self.remove = self.option_set.remove_opt
|
self.remove_opt = self.remove = self.option_set.remove_opt
|
||||||
self.parse_string = self.option_set.parse_string
|
self.parse_string = self.option_set.parse_string
|
||||||
|
self.get_option = self.option_set.get_option
|
||||||
|
|
||||||
def update(self, other):
|
def update(self, other):
|
||||||
self.option_set.update(other.option_set)
|
self.option_set.update(other.option_set)
|
||||||
@ -381,6 +390,7 @@ class ConfigInterface(object):
|
|||||||
def smart_update(self, opts1, opts2):
|
def smart_update(self, opts1, opts2):
|
||||||
self.option_set.smart_update(opts1, opts2)
|
self.option_set.smart_update(opts1, opts2)
|
||||||
|
|
||||||
|
|
||||||
class Config(ConfigInterface):
|
class Config(ConfigInterface):
|
||||||
'''
|
'''
|
||||||
A file based configuration.
|
A file based configuration.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user