mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04: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'
|
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:
|
||||||
|
@ -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)
|
if mt in OEB_DOCS:
|
||||||
def mt_to_category(mt):
|
category = 'text'
|
||||||
if mt in OEB_DOCS:
|
elif mt in OEB_STYLES:
|
||||||
category = 'text'
|
category = 'style'
|
||||||
elif mt in OEB_STYLES:
|
elif mt in OEB_FONTS:
|
||||||
category = 'style'
|
category = 'font'
|
||||||
elif mt in OEB_FONTS:
|
elif mt == guess_type('a.opf'):
|
||||||
category = 'font'
|
category = 'opf'
|
||||||
else:
|
elif mt == guess_type('a.ncx'):
|
||||||
category = mt.partition('/')[0]
|
category = 'toc'
|
||||||
return category
|
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():
|
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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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'))
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user