mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-11-03 19:17:02 -05:00 
			
		
		
		
	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:
		
							parent
							
								
									7ab44aa617
								
							
						
					
					
						commit
						c2e8bab271
					
				@ -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:
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ __license__   = 'GPL v3'
 | 
			
		||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
 | 
			
		||||
__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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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):
 | 
			
		||||
 | 
			
		||||
@ -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'))
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
 | 
			
		||||
__license__ = 'GPL v3'
 | 
			
		||||
__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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user