diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index 604c4c3c37..d6554cf015 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -95,6 +95,7 @@ class Container(object): # {{{ book_type = 'oeb' SUPPORTS_TITLEPAGES = True + SUPPORTS_FILENAMES = True 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) @@ -1035,6 +1036,7 @@ class AZW3Container(Container): book_type = 'azw3' SUPPORTS_TITLEPAGES = False + SUPPORTS_FILENAMES = False def __init__(self, pathtoazw3, log, clone_data=None, tdir=None): if clone_data is not None: diff --git a/src/calibre/ebooks/oeb/polish/replace.py b/src/calibre/ebooks/oeb/polish/replace.py index d6d3e14a19..77cc8c7073 100644 --- a/src/calibre/ebooks/oeb/polish/replace.py +++ b/src/calibre/ebooks/oeb/polish/replace.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import codecs, shutil, os +import codecs, shutil, os, posixpath from urlparse import urlparse 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: shutil.copyfileobj(src, dest) -def get_recommended_folders(container, names): - ' Return the folders that are recommended for the given filenames ' +def mt_to_category(container, mt): from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES - counts = defaultdict(Counter) - def mt_to_category(mt): - if mt in OEB_DOCS: - category = 'text' - elif mt in OEB_STYLES: - category = 'style' - elif mt in OEB_FONTS: - category = 'font' - else: - category = mt.partition('/')[0] - return category + if mt in OEB_DOCS: + category = 'text' + elif mt in OEB_STYLES: + category = 'style' + elif mt in OEB_FONTS: + category = 'font' + elif mt == guess_type('a.opf'): + category = 'opf' + elif mt == guess_type('a.ncx'): + category = 'toc' + else: + category = mt.partition('/')[0] + 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(): 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()} - 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 diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index d38e56f91b..e4a6586c94 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -38,6 +38,7 @@ d['preview_mono_font_size'] = 14 d['preview_minimum_font_size'] = 8 d['remove_existing_links_when_linking_sheets'] = True 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 diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index fd04f3cb62..9e310ac6ee 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -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.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.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.toc import remove_names_from_toc, find_existing_toc 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.insert_resource import get_resource_data, NewBook from calibre.gui2.tweak_book.preferences import Preferences +from calibre.gui2.tweak_book.widgets import RationalizeFolders def get_container(*args, **kwargs): kwargs['tweak_mode'] = True @@ -390,6 +391,26 @@ class Boss(QObject): d.exec_() # 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): self.commit_all_editors_to_container() if guess_type(oldname) != guess_type(newname): diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 5c1ad54d62..c22580f289 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -327,6 +327,8 @@ class Main(MainWindow): _('Beautify all files')) self.action_insert_char = reg('character-set.png', _('&Insert special character'), self.boss.insert_character, 'insert-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 group = _('Polish Book') @@ -440,6 +442,7 @@ class Main(MainWindow): e.addAction(self.action_smarten_punctuation) e.addAction(self.action_fix_html_all) e.addAction(self.action_pretty_all) + e.addAction(self.action_rationalize_folders) e.addAction(self.action_check_book) e = b.addMenu(_('&View')) diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index 606e699bd7..c7a9ba0033 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -from PyQt4.Qt import (QDialog, QDialogButtonBox) +from PyQt4.Qt import (QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit) from calibre.gui2.tweak_book import tprefs @@ -46,3 +46,60 @@ class Dialog(QDialog): def setup_ui(self): 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)