Edit book: Add a new tool to automatically arrange all files in the book into folders based on their type. Access it via Tools->arrange into folders.

This commit is contained in:
Kovid Goyal 2014-01-14 17:00:41 +05:30
parent 7ab44aa617
commit c2e8bab271
6 changed files with 127 additions and 18 deletions

View File

@ -95,6 +95,7 @@ class Container(object): # {{{
book_type = 'oeb' book_type = 'oeb'
SUPPORTS_TITLEPAGES = True SUPPORTS_TITLEPAGES = True
SUPPORTS_FILENAMES = True
def __init__(self, rootpath, opfpath, log, clone_data=None): def __init__(self, rootpath, opfpath, log, clone_data=None):
self.root = clone_data['root'] if clone_data is not None else os.path.abspath(rootpath) self.root = clone_data['root'] if clone_data is not None else os.path.abspath(rootpath)
@ -1035,6 +1036,7 @@ class AZW3Container(Container):
book_type = 'azw3' book_type = 'azw3'
SUPPORTS_TITLEPAGES = False SUPPORTS_TITLEPAGES = False
SUPPORTS_FILENAMES = False
def __init__(self, pathtoazw3, log, clone_data=None, tdir=None): def __init__(self, pathtoazw3, log, clone_data=None, tdir=None):
if clone_data is not None: if clone_data is not None:

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import codecs, shutil, os import codecs, shutil, os, posixpath
from urlparse import urlparse from urlparse import urlparse
from collections import Counter, defaultdict from collections import Counter, defaultdict
@ -146,25 +146,50 @@ def replace_file(container, name, path, basename, force_mt=None):
with container.open(nname, 'wb') as dest: with container.open(nname, 'wb') as dest:
shutil.copyfileobj(src, dest) shutil.copyfileobj(src, dest)
def get_recommended_folders(container, names): def mt_to_category(container, mt):
' Return the folders that are recommended for the given filenames '
from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
counts = defaultdict(Counter)
def mt_to_category(mt):
if mt in OEB_DOCS: if mt in OEB_DOCS:
category = 'text' category = 'text'
elif mt in OEB_STYLES: elif mt in OEB_STYLES:
category = 'style' category = 'style'
elif mt in OEB_FONTS: elif mt in OEB_FONTS:
category = 'font' category = 'font'
elif mt == guess_type('a.opf'):
category = 'opf'
elif mt == guess_type('a.ncx'):
category = 'toc'
else: else:
category = mt.partition('/')[0] category = mt.partition('/')[0]
return category return category
def get_recommended_folders(container, names):
' Return the folders that are recommended for the given filenames '
from calibre.ebooks.oeb.polish.container import guess_type
counts = defaultdict(Counter)
for name, mt in container.mime_map.iteritems(): for name, mt in container.mime_map.iteritems():
folder = name.rpartition('/')[0] if '/' in name else '' folder = name.rpartition('/')[0] if '/' in name else ''
counts[mt_to_category(mt)][folder] += 1 counts[mt_to_category(container, mt)][folder] += 1
recommendations = {category:counter.most_common(1)[0][0] for category, counter in counts.iteritems()} recommendations = {category:counter.most_common(1)[0][0] for category, counter in counts.iteritems()}
return {n:recommendations.get(mt_to_category(guess_type(os.path.basename(n))), '') for n in names} return {n:recommendations.get(mt_to_category(container, guess_type(os.path.basename(n))), '') for n in names}
def rationalize_folders(container, folder_type_map):
all_names = set(container.mime_map)
new_names = set()
name_map = {}
for name in all_names:
category = mt_to_category(container, container.mime_map[name])
folder = folder_type_map.get(category, None)
if folder is not None:
bn = posixpath.basename(name)
new_name = posixpath.join(folder, bn)
if new_name != name:
c = 0
while new_name in all_names or new_name in new_names:
c += 1
n, ext = bn.rpartition('.')[0::2]
new_name = posixpath.join(folder, '%s_%d.%s' % (n, c, ext))
name_map[name] = new_name
new_names.add(new_name)
return name_map

View File

@ -38,6 +38,7 @@ d['preview_mono_font_size'] = 14
d['preview_minimum_font_size'] = 8 d['preview_minimum_font_size'] = 8
d['remove_existing_links_when_linking_sheets'] = True d['remove_existing_links_when_linking_sheets'] = True
d['charmap_favorites'] = list(map(ord, '\xa0\u2002\u2003\u2009\xad' '‘’“”‹›«»‚„' '—–§¶†‡©®™' '→⇒•·°±−×÷¼½½¾' '…µ¢£€¿¡¨´¸ˆ˜' 'ÀÁÂÃÄÅÆÇÈÉÊË' 'ÌÍÎÏÐÑÒÓÔÕÖØ' 'ŒŠÙÚÛÜÝŸÞßàá' 'âãäåæçèéêëìí' 'îïðñòóôõöøœš' 'ùúûüýÿþªºαΩ∞')) # noqa d['charmap_favorites'] = list(map(ord, '\xa0\u2002\u2003\u2009\xad' '‘’“”‹›«»‚„' '—–§¶†‡©®™' '→⇒•·°±−×÷¼½½¾' '…µ¢£€¿¡¨´¸ˆ˜' 'ÀÁÂÃÄÅÆÇÈÉÊË' 'ÌÍÎÏÐÑÒÓÔÕÖØ' 'ŒŠÙÚÛÜÝŸÞßàá' 'âãäåæçèéêëìí' 'îïðñòóôõöøœš' 'ùúûüýÿþªºαΩ∞')) # noqa
d['folders_for_types'] = {'style':'styles', 'image':'images', 'font':'fonts', 'audio':'audio', 'video':'video'}
del d del d

View File

@ -21,7 +21,7 @@ from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish
from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type, OEB_FONTS from calibre.ebooks.oeb.polish.container import get_container as _gc, clone_container, guess_type, OEB_FONTS
from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage
from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all
from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders from calibre.ebooks.oeb.polish.replace import rename_files, replace_file, get_recommended_folders, rationalize_folders
from calibre.ebooks.oeb.polish.split import split, merge, AbortError from calibre.ebooks.oeb.polish.split import split, merge, AbortError
from calibre.ebooks.oeb.polish.toc import remove_names_from_toc, find_existing_toc from calibre.ebooks.oeb.polish.toc import remove_names_from_toc, find_existing_toc
from calibre.ebooks.oeb.polish.utils import link_stylesheets from calibre.ebooks.oeb.polish.utils import link_stylesheets
@ -36,6 +36,7 @@ from calibre.gui2.tweak_book.toc import TOCEditor
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data, NewBook
from calibre.gui2.tweak_book.preferences import Preferences from calibre.gui2.tweak_book.preferences import Preferences
from calibre.gui2.tweak_book.widgets import RationalizeFolders
def get_container(*args, **kwargs): def get_container(*args, **kwargs):
kwargs['tweak_mode'] = True kwargs['tweak_mode'] = True
@ -390,6 +391,26 @@ class Boss(QObject):
d.exec_() d.exec_()
# Renaming {{{ # Renaming {{{
def rationalize_folders(self):
c = current_container()
if not c.SUPPORTS_FILENAMES:
return error_dialog(self.gui, _('Not supported'),
_('The %s format does not support file and folder names internally, therefore'
' arranging files into folders is not allowed.') % c.book_type.upper(), show=True)
d = RationalizeFolders(self.gui)
if d.exec_() != d.Accepted:
return
self.commit_all_editors_to_container()
name_map = rationalize_folders(c, d.folder_map)
if not name_map:
return info_dialog(self.gui, _('Nothing to do'), _(
'The files in this book are already arranged into folders'), show=True)
self.add_savepoint(_('Arrange into folders'))
self.gui.blocking_job(
'rationalize_folders', _('Renaming and updating links...'), partial(self.rename_done, name_map),
rename_files, current_container(), name_map)
def rename_requested(self, oldname, newname): def rename_requested(self, oldname, newname):
self.commit_all_editors_to_container() self.commit_all_editors_to_container()
if guess_type(oldname) != guess_type(newname): if guess_type(oldname) != guess_type(newname):

View File

@ -327,6 +327,8 @@ class Main(MainWindow):
_('Beautify all files')) _('Beautify all files'))
self.action_insert_char = reg('character-set.png', _('&Insert special character'), self.boss.insert_character, 'insert-character', (), self.action_insert_char = reg('character-set.png', _('&Insert special character'), self.boss.insert_character, 'insert-character', (),
_('Insert special character')) _('Insert special character'))
self.action_rationalize_folders = reg('mimetypes/dir.png', _('&Arrange into folders'), self.boss.rationalize_folders, 'rationalize-folders', (),
_('Arrange into folders'))
# Polish actions # Polish actions
group = _('Polish Book') group = _('Polish Book')
@ -440,6 +442,7 @@ class Main(MainWindow):
e.addAction(self.action_smarten_punctuation) e.addAction(self.action_smarten_punctuation)
e.addAction(self.action_fix_html_all) e.addAction(self.action_fix_html_all)
e.addAction(self.action_pretty_all) e.addAction(self.action_pretty_all)
e.addAction(self.action_rationalize_folders)
e.addAction(self.action_check_book) e.addAction(self.action_check_book)
e = b.addMenu(_('&View')) e = b.addMenu(_('&View'))

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import (QDialog, QDialogButtonBox) from PyQt4.Qt import (QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit)
from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book import tprefs
@ -46,3 +46,60 @@ class Dialog(QDialog):
def setup_ui(self): def setup_ui(self):
raise NotImplementedError('You must implement this method in Dialog subclasses') raise NotImplementedError('You must implement this method in Dialog subclasses')
class RationalizeFolders(Dialog):
TYPE_MAP = (
('text', _('Text (HTML) files')),
('style', _('Style (CSS) files')),
('image', _('Images')),
('font', _('Fonts')),
('audio', _('Audio')),
('video', _('Video')),
('opf', _('OPF file (metadata)')),
('toc', _('Table of contents file (NCX)')),
)
def __init__(self, parent=None):
Dialog.__init__(self, _('Arrange in folders'), 'rationalize-folders', parent=parent)
def setup_ui(self):
self.l = l = QGridLayout()
self.setLayout(l)
self.la = la = QLabel(_(
'Arrange the files in this book into sub-folders based on their types.'
' If you leave a folder blank, the files will be placed in the root.'))
la.setWordWrap(True)
l.addWidget(la, 0, 0, 1, -1)
folders = tprefs['folders_for_types']
for i, (typ, text) in enumerate(self.TYPE_MAP):
la = QLabel('&' + text)
setattr(self, '%s_label' % typ, la)
le = QLineEdit(self)
setattr(self, '%s_folder' % typ, le)
val = folders.get(typ, '')
if val and not val.endswith('/'):
val += '/'
le.setText(val)
la.setBuddy(le)
l.addWidget(la, i + 1, 0)
l.addWidget(le, i + 1, 1)
self.la2 = la = QLabel(_(
'Note that this will only arrange files inside the book,'
' it will not affect how they are displayed in the Files Browser'))
la.setWordWrap(True)
l.addWidget(la, i + 2, 0, 1, -1)
l.addWidget(self.bb, i + 3, 0, 1, -1)
@property
def folder_map(self):
ans = {}
for typ, x in self.TYPE_MAP:
val = unicode(getattr(self, '%s_folder' % typ).text()).strip().strip('/')
ans[typ] = val
return ans
def accept(self):
tprefs['folders_for_types'] = self.folder_map
return Dialog.accept(self)