From d88067518ed3b961ae55063726ad6c11f64239db Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 13:48:17 -0600 Subject: [PATCH 01/14] Fix Kobo driver regression --- src/calibre/devices/misc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index c1a2ee66da..23cb05aeb4 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -48,6 +48,7 @@ class KOBO(USBMS): WINDOWS_MAIN_MEM = '.KOBOEREADER' EBOOK_DIR_MAIN = '' + SUPPORTS_SUB_DIRS = True class AVANT(USBMS): name = 'Booq Avant Device Interface' From e02d86fe906f1b01b798e59ccaa61fd766288d78 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 14:42:16 -0600 Subject: [PATCH 02/14] update NIN Online. Fixes #5547 (Updated recipe for NIN online) --- resources/recipes/nin.recipe | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/resources/recipes/nin.recipe b/resources/recipes/nin.recipe index 9e1aa57733..70fd998a09 100644 --- a/resources/recipes/nin.recipe +++ b/resources/recipes/nin.recipe @@ -5,7 +5,7 @@ __copyright__ = '2008-2010, Darko Miletic ' www.nin.co.rs ''' -import re, urllib +import re from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe @@ -16,13 +16,13 @@ class Nin(BasicNewsRecipe): publisher = 'NIN d.o.o.' category = 'news, politics, Serbia' no_stylesheets = True + delay = 1 oldest_article = 15 encoding = 'utf-8' needs_subscription = True remove_empty_feeds = True PREFIX = 'http://www.nin.co.rs' INDEX = PREFIX + '/?change_lang=ls' - LOGIN = PREFIX + '/?logout=true' use_embedded_content = False language = 'sr' publication_type = 'magazine' @@ -41,14 +41,12 @@ class Nin(BasicNewsRecipe): def get_browser(self): br = BasicNewsRecipe.get_browser() - br.open(self.INDEX) if self.username is not None and self.password is not None: - data = urllib.urlencode({ 'login_name':self.username - ,'login_password':self.password - ,'imageField.x':'32' - ,'imageField.y':'15' - }) - br.open(self.LOGIN,data) + br.open(self.INDEX) + br.select_form(name='form1') + br['login_name' ] = self.username + br['login_password'] = self.password + br.submit() return br keep_only_tags =[dict(name='td', attrs={'width':'520'})] From 2bae16552c314ac7427ca48cb55e3c3943ce30bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 15:51:40 -0600 Subject: [PATCH 03/14] Agro Gerilla by Darko Miletic. Fixes #5548 (New recipe for serbian Blog Agro gerila) --- resources/recipes/agrogerila.recipe | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 resources/recipes/agrogerila.recipe diff --git a/resources/recipes/agrogerila.recipe b/resources/recipes/agrogerila.recipe new file mode 100644 index 0000000000..8ca13af4dd --- /dev/null +++ b/resources/recipes/agrogerila.recipe @@ -0,0 +1,40 @@ + +__license__ = 'GPL v3' +__copyright__ = '2010, Darko Miletic ' +''' +boljevac.blogspot.com +''' + +import re +from calibre.web.feeds.news import BasicNewsRecipe + +class AgroGerila(BasicNewsRecipe): + title = 'Agro Gerila' + __author__ = 'Darko Miletic' + description = 'Politicki nekorektan blog.' + oldest_article = 45 + max_articles_per_feed = 100 + language = 'sr' + encoding = 'utf-8' + no_stylesheets = True + use_embedded_content = True + publication_type = 'blog' + extra_css = ' @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: "Trebuchet MS",Trebuchet,Verdana,sans1,sans-serif} .article_description{font-family: sans1, sans-serif} img{margin-bottom: 0.8em; border: 1px solid #333333; padding: 4px } ' + + conversion_options = { + 'comment' : description + , 'tags' : 'film, blog, srbija' + , 'publisher': 'Dry-Na-Nord' + , 'language' : language + } + + preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] + + feeds = [(u'Posts', u'http://boljevac.blogspot.com/feeds/posts/default')] + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + return self.adeify_images(soup) + + From f21d1287e9c579a901ca29a783e66a83c2f7fabe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 18:48:59 -0600 Subject: [PATCH 04/14] Fix #5550 (unicode error in libero.recipe) --- resources/recipes/libero.recipe | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/recipes/libero.recipe b/resources/recipes/libero.recipe index f5801d06bc..4354940746 100644 --- a/resources/recipes/libero.recipe +++ b/resources/recipes/libero.recipe @@ -15,7 +15,7 @@ class LiberoNews(BasicNewsRecipe): description = 'Italian daily newspaper' cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif' - title = u'Libero ' + title = u'Libero' publisher = 'EDITORIALE LIBERO s.r.l 2006' category = 'News, politics, culture, economy, general interest' @@ -48,7 +48,7 @@ class LiberoNews(BasicNewsRecipe): (u'Tecnologia', u'http://www.libero-news.it/rss.jsp?sezione=20'), (u'LifeStyle', u'http://www.libero-news.it/rss.jsp?sezione=22'), (u'Sport', u'http://www.libero-news.it/rss.jsp?sezione=23'), - (u'Costume e Società', u' http://www.libero-news.it/rss.jsp?sezione=24'), + (u'Costume e Societ\xc3\xa0', u' http://www.libero-news.it/rss.jsp?sezione=24'), (u'Milano', u'http://www.libero-news.it/rss.jsp?sezione=26'), (u'Roma', u'http://www.libero-news.it/rss.jsp?sezione=27'), (u'Alimentazione', u'http://www.libero-news.it/rss.jsp?sezione=29') From 2047bfbe9b4454826b92243755f5a8a7ab7e9b34 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 19:18:48 -0600 Subject: [PATCH 05/14] Beginnings of add books wizard --- src/calibre/gui2/add_wizard/__init__.py | 174 +++++++++++++++++++++++ src/calibre/gui2/add_wizard/scan.ui | 25 ++++ src/calibre/gui2/add_wizard/welcome.ui | 134 ++++++++++++++++++ src/calibre/library/add_to_library.py | 178 ++++++++++++++++++++++++ 4 files changed, 511 insertions(+) create mode 100644 src/calibre/gui2/add_wizard/__init__.py create mode 100644 src/calibre/gui2/add_wizard/scan.ui create mode 100644 src/calibre/gui2/add_wizard/welcome.ui create mode 100644 src/calibre/library/add_to_library.py diff --git a/src/calibre/gui2/add_wizard/__init__.py b/src/calibre/gui2/add_wizard/__init__.py new file mode 100644 index 0000000000..f7518db3fc --- /dev/null +++ b/src/calibre/gui2/add_wizard/__init__.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os + +from PyQt4.Qt import QWizard, QWizardPage, QIcon, QPixmap, Qt, QThread, \ + pyqtSignal + +from calibre.gui2 import error_dialog, choose_dir, gprefs +from calibre.constants import filesystem_encoding +from calibre.library.add_to_library import find_folders_under, \ + find_books_in_folder, hash_merge_format_collections + +class WizardPage(QWizardPage): # {{{ + + def __init__(self, db, parent): + QWizardPage.__init__(self, parent) + self.db = db + self.register = parent.register + self.setupUi(self) + + self.do_init() + + def do_init(self): + pass + +# }}} + +# Scan root folder Page {{{ + +from calibre.gui2.add_wizard.scan_ui import Ui_WizardPage as ScanWidget + +class RecursiveFinder(QThread): + + activity_changed = pyqtSignal(object, object) # description and total count + activity_iterated = pyqtSignal(object, object) # item desc, progress number + + def __init__(self, parent=None): + QThread.__init__(self, parent) + self.canceled = False + self.cancel_callback = lambda : self.canceled + self.folders = set([]) + self.books = [] + + def cancel(self, *args): + self.canceled = True + + def set_params(self, root, db, one_per_folder): + self.root, self.db = root, db + self.one_per_folder = one_per_folder + + def run(self): + self.activity_changed.emit(_('Searching for sub-folders'), 0) + self.folders = find_folders_under(self.root, self.db, + cancel_callback=self.cancel_callback) + if self.canceled: + return + self.activity_changed.emit(_('Searching for books'), len(self.folders)) + for i, folder in enumerate(self.folders): + if self.canceled: + break + books_in_folder = find_books_in_folder(folder, self.one_per_folder, + cancel_callback=self.cancel_callback) + if self.canceled: + break + self.books.extend(books_in_folder) + self.activity_iterated.emit(folder, i) + + self.activity_changed.emit( + _('Looking for duplicates based on file hash'), 0) + + self.books = hash_merge_format_collections(self.books, + cancel_callback=self.cancel_callback) + + + +class ScanPage(WizardPage, ScanWidget): + + ID = 2 + +# }}} + +# Welcome Page {{{ + +from calibre.gui2.add_wizard.welcome_ui import Ui_WizardPage as WelcomeWidget + +class WelcomePage(WizardPage, WelcomeWidget): + + ID = 1 + + def do_init(self): + # Root folder must be filled + self.registerField('root_folder*', self.opt_root_folder) + + self.register['root_folder'] = self.get_root_folder + self.register['one_per_folder'] = self.get_one_per_folder + + self.button_choose_root_folder.clicked.connect(self.choose_root_folder) + + def choose_root_folder(self, *args): + x = self.get_root_folder() + if x is None: + x = '~' + x = choose_dir(self, 'add wizard choose root folder', + _('Choose root folder'), default_dir=x) + if x is not None: + self.opt_root_folder.setText(os.path.abspath(x)) + + def initializePage(self): + opf = gprefs.get('add wizard one per folder', True) + self.opt_one_per_folder.setChecked(opf) + self.opt_many_per_folder.setChecked(not opf) + add_dir = gprefs.get('add wizard root folder', None) + if add_dir is not None: + self.opt_root_folder.setText(add_dir) + + def get_root_folder(self): + x = unicode(self.opt_root_folder.text()).strip() + if not x: + return None + return os.path.abspath(x.encode(filesystem_encoding)) + + def get_one_per_folder(self): + return self.opt_one_per_folder.isChecked() + + def validatePage(self): + x = self.get_root_folder() + xu = x.decode(filesystem_encoding) + if x and os.access(x, os.R_OK) and os.path.isdir(x): + gprefs['add wizard root folder'] = xu + gprefs['add wizard one per folder'] = self.get_one_per_folder() + return True + error_dialog(self, _('Invalid root folder'), + xu + _('is not a valid root folder'), show=True) + return False + +# }}} + +class Wizard(QWizard): # {{{ + + def __init__(self, db, parent=None): + QWizard.__init__(self, parent) + self.setModal(True) + self.setWindowTitle(_('Add books to calibre')) + self.setWindowIcon(QIcon(I('add_book.svg'))) + self.setPixmap(self.LogoPixmap, QPixmap(P('content_server/calibre.png')).scaledToHeight(80, + Qt.SmoothTransformation)) + self.setPixmap(self.WatermarkPixmap, + QPixmap(I('welcome_wizard.svg'))) + + self.register = {} + + for attr, cls in [ + ('welcome_page', WelcomePage), + ('scan_page', ScanPage), + ]: + setattr(self, attr, cls(db, self)) + self.setPage(getattr(cls, 'ID'), getattr(self, attr)) + +# }}} + +# Test Wizard {{{ +if __name__ == '__main__': + from PyQt4.Qt import QApplication + from calibre.library import db + app = QApplication([]) + w = Wizard(db()) + w.exec_() +# }}} + diff --git a/src/calibre/gui2/add_wizard/scan.ui b/src/calibre/gui2/add_wizard/scan.ui new file mode 100644 index 0000000000..b697ff9894 --- /dev/null +++ b/src/calibre/gui2/add_wizard/scan.ui @@ -0,0 +1,25 @@ + + + WizardPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Scanning root folder for books + + + This may take a few minutes + + + + + diff --git a/src/calibre/gui2/add_wizard/welcome.ui b/src/calibre/gui2/add_wizard/welcome.ui new file mode 100644 index 0000000000..52fcabb714 --- /dev/null +++ b/src/calibre/gui2/add_wizard/welcome.ui @@ -0,0 +1,134 @@ + + + WizardPage + + + + 0 + 0 + 704 + 468 + + + + WizardPage + + + Choose the location to add books from + + + Select a folder on your hard disk + + + + + + <p>calibre can scan your computer for existing books automatically. These books will then be <b>copied</b> into the calibre library. This wizard will help you customize the scanning and import process for your existing book collection.</p> +<p>Choose a root folder. Books will be searched for only inside this folder and any sub-folders.</p> +<p>Make sure that the folder you chose for your calibre library <b>is not</b> under the root folder you choose.</p> + + + true + + + + + + + &Root folder: + + + opt_root_folder + + + + + + + This folder and its sub-folders will be scanned for books to import into calibre's library + + + + + + + Choose root folder + + + ... + + + + :/images/document_open.svg:/images/document_open.svg + + + + + + + Handle multiple files per book + + + + + + &One book per folder, assumes every ebook file in a folder is the same book in a different format + + + + + + + &Multiple books per folder, assumes every ebook file is a different book + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/calibre/library/add_to_library.py b/src/calibre/library/add_to_library.py new file mode 100644 index 0000000000..8451241e3c --- /dev/null +++ b/src/calibre/library/add_to_library.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os +from hashlib import sha1 + +from calibre.constants import filesystem_encoding +from calibre.ebooks import BOOK_EXTENSIONS + +def find_folders_under(root, db, add_root=True, # {{{ + follow_links=False, cancel_callback=lambda : False): + ''' + Find all folders under the specified root path, ignoring any folders under + the library path of db + + root must be a bytestring in filesystem_encoding + + If follow_links is True, follow symbolic links. WARNING; this can lead to + infinite recursion. + + cancel_callback must be a no argument callable that returns True to cancel + the search + ''' + assert not isinstance(root, unicode) # root must be in filesystem encoding + lp = db.library_path + if isinstance(lp, unicode): + try: + lp = lp.encode(filesystem_encoding) + except: + lp = None + if lp: + lp = os.path.abspath(lp) + + root = os.path.abspath(root) + + ans = set([]) + for dirpath, dirnames, __ in os.walk(root, topdown=True, followlinks=follow_links): + if cancel_callback(): + break + for x in list(dirnames): + path = os.path.join(dirpath, x) + if lp and path.startswith(lp): + dirnames.remove(x) + if lp and dirpath.startswith(lp): + continue + ans.add(dirpath) + + if not add_root: + ans.remove(root) + + return ans + +# }}} + +class FormatCollection(object): # {{{ + + def __init__(self, parent_folder, formats): + self.path_map = {} + for x in set(formats): + fmt = os.path.splitext(x)[1].lower() + if fmt: + fmt = fmt[1:] + self.path_map[fmt] = x + self.parent_folder = None + self.hash_map = {} + for fmt, path in self.format_map.items(): + self.hash_map[fmt] = self.hash_of_file(path) + + def hash_of_file(self, path): + with open(path, 'rb') as f: + return sha1(f.read()).digest() + + @property + def hashes(self): + return frozenset(self.formats.values()) + + @property + def is_empty(self): + return len(self) == 0 + + def __iter__(self): + for x in self.path_map: + yield x + + def __len__(self): + return len(self.path_map) + + def remove(self, fmt): + self.hash_map.pop(fmt, None) + self.path_map.pop(fmt, None) + + def matches(self, other): + if not self.hashes.intersection(other.hashes): + return False + for fmt in self: + if self.hash_map[fmt] != other.hash_map.get(fmt, False): + return False + return True + + def merge(self, other): + for fmt in list(other): + self.path_map[fmt] = other.path_map[fmt] + self.hash_map[fmt] = other.hash_map[fmt] + other.remove(fmt) + +# }}} + +def books_in_folder(folder, one_per_folder, # {{{ + cancel_callback=lambda : False): + assert not isinstance(folder, unicode) + + dirpath = os.path.abspath(folder) + if one_per_folder: + formats = set([]) + for path in os.listdir(dirpath): + if cancel_callback(): + return [] + path = os.path.abspath(os.path.join(dirpath, path)) + if os.path.isdir(path) or not os.access(path, os.R_OK): + continue + ext = os.path.splitext(path)[1] + if not ext: + continue + ext = ext[1:].lower() + if ext not in BOOK_EXTENSIONS and ext != 'opf': + continue + formats.add(path) + return [FormatCollection(folder, formats)] + else: + books = {} + for path in os.listdir(dirpath): + if cancel_callback(): + return + path = os.path.abspath(os.path.join(dirpath, path)) + if os.path.isdir(path) or not os.access(path, os.R_OK): + continue + ext = os.path.splitext(path)[1] + if not ext: + continue + ext = ext[1:].lower() + if ext not in BOOK_EXTENSIONS: + continue + + key = os.path.splitext(path)[0] + if not books.has_key(key): + books[key] = set([]) + books[key].add(path) + + return [FormatCollection(folder, x) for x in books.values() if x] + +# }}} + +def hash_merge_format_collections(collections, cancel_callback=lambda:False): + ans = [] + + collections = list(collections) + l = len(collections) + for i in range(l): + if cancel_callback(): + return collections + one = collections[i] + if one.is_empty: + continue + for j in range(i+1, l): + if cancel_callback(): + return collections + two = collections[j] + if two.is_empty: + continue + if one.matches(two): + one.merge(two) + ans.append(one) + + return ans From 86f56c4a852ece9c190c936bbd4cd7d002d586fb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 19:51:52 -0600 Subject: [PATCH 06/14] Icons for connect/disconnect folder actions --- src/calibre/gui2/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 31fe4bbbbd..8f4ff6617f 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -426,12 +426,12 @@ class DeviceMenu(QMenu): self.addMenu(self.email_to_menu) self.addSeparator() - mitem = self.addAction(_('Connect to folder')) + mitem = self.addAction(QIcon(I('document_open.svg')), _('Connect to folder')) mitem.setEnabled(True) mitem.triggered.connect(lambda x : self.connect_to_folder.emit()) self.connect_to_folder_action = mitem - mitem = self.addAction(_('Disconnect from folder')) + mitem = self.addAction(QIcon(I('eject.svg')), _('Disconnect from folder')) mitem.setEnabled(False) mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit()) self.disconnect_from_folder_action = mitem From db60769702eac408d656a92f62d0e27e76bb0f68 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 19:57:01 -0600 Subject: [PATCH 07/14] Don't resort when editing columns in the main GUI --- src/calibre/gui2/library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index da6b03737d..54b505867f 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -695,8 +695,8 @@ class BooksModel(QAbstractTableModel): self.db.set(row, column, val) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ index, index) - if column == self.sorted_on[0]: - self.resort() + #if column == self.sorted_on[0]: + # self.resort() return True From 66d260445898ba40d77aecd0bd5860d02a73f589 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 21:18:45 -0600 Subject: [PATCH 08/14] Remove unnecessary method definitions from folder driver --- src/calibre/devices/folder_device/driver.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/calibre/devices/folder_device/driver.py b/src/calibre/devices/folder_device/driver.py index f85fca55e1..0dcbae87ce 100644 --- a/src/calibre/devices/folder_device/driver.py +++ b/src/calibre/devices/folder_device/driver.py @@ -58,14 +58,6 @@ class FOLDER_DEVICE(USBMS): self.booklist_class = BookList self.is_connected = True - @classmethod - def get_gui_name(cls): - if hasattr(cls, 'gui_name'): - return cls.gui_name - if hasattr(cls, '__name__'): - return cls.__name__ - return cls.name - def disconnect_from_folder(self): self._main_prefix = '' self.is_connected = False @@ -85,9 +77,6 @@ class FOLDER_DEVICE(USBMS): def card_prefix(self, end_session=True): return (None, None) - def get_main_ebook_dir(self): - return '' - def eject(self): self.is_connected = False From fcdcd68adfd99bee04846755243709fa36f522f2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 21:21:21 -0600 Subject: [PATCH 09/14] Plugin customization GUI: Sort plugins by name --- src/calibre/gui2/dialogs/config/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index 1cb6aad283..ff50ff7718 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -109,6 +109,9 @@ class PluginModel(QAbstractItemModel): self._data[plugin.type].append(plugin) self.categories = sorted(self._data.keys()) + for plugins in self._data.values(): + plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower())) + def index(self, row, column, parent): if not self.hasIndex(row, column, parent): return QModelIndex() From f4a02e09d6ec8d576cfdd0f830c1493531c82d71 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 21:47:00 -0600 Subject: [PATCH 10/14] Cleanup BookList classes --- src/calibre/devices/interface.py | 16 +++++++++++++++- src/calibre/devices/usbms/books.py | 12 +----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index b38b62e20c..356ebfc876 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -387,7 +387,7 @@ class BookList(list): __getslice__ = None __setslice__ = None - def __init__(self, oncard, prefix): + def __init__(self, oncard, prefix, settings): pass def supports_tags(self): @@ -402,3 +402,17 @@ class BookList(list): ''' raise NotImplementedError() + def add_book(self, book, replace_metadata): + ''' + Add the book to the booklist. Intent is to maintain any device-internal + metadata. Return True if booklists must be sync'ed + ''' + raise NotImplementedError() + + def remove_book(self, book): + ''' + Remove a book from the booklist. Correct any device metadata at the + same time + ''' + raise NotImplementedError() + diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index b153300282..edd5907713 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -108,9 +108,6 @@ class Book(MetaInformation): class BookList(_BookList): - def __init__(self, oncard, prefix, settings): - pass - def supports_tags(self): return True @@ -118,16 +115,9 @@ class BookList(_BookList): book.tags = tags def add_book(self, book, replace_metadata): - ''' - Add the book to the booklist. Intent is to maintain any device-internal - metadata. Return True if booklists must be sync'ed - ''' if book not in self: self.append(book) + return True def remove_book(self, book): - ''' - Remove a book from the booklist. Correct any device metadata at the - same time - ''' self.remove(book) From feb5a6f0595c37c38bfff33a057b00a029afe9c5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 21:55:09 -0600 Subject: [PATCH 11/14] USBMS: books emthod should always returna n object of type BookList --- src/calibre/devices/usbms/driver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 332f337a2f..c8f48511a4 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -45,15 +45,17 @@ class USBMS(CLI, Device): def books(self, oncard=None, end_session=True): from calibre.ebooks.metadata.meta import path_to_ext + dummy_bl = BookList(None, None, None) + if oncard == 'carda' and not self._card_a_prefix: self.report_progress(1.0, _('Getting list of books on device...')) - return [] + return dummy_bl elif oncard == 'cardb' and not self._card_b_prefix: self.report_progress(1.0, _('Getting list of books on device...')) - return [] + return dummy_bl elif oncard and oncard != 'carda' and oncard != 'cardb': self.report_progress(1.0, _('Getting list of books on device...')) - return [] + return dummy_bl prefix = self._card_a_prefix if oncard == 'carda' else \ self._card_b_prefix if oncard == 'cardb' \ From 473ccd8a715ce8660c052d536a81e861d261d7ec Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 22:07:23 -0600 Subject: [PATCH 12/14] usbms.driver: Report progress as 50% not 5000%. Also remove spurious changed=True, since add_book now correctly reports changed --- src/calibre/devices/usbms/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index c8f48511a4..c7c4e06834 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -89,7 +89,6 @@ class USBMS(CLI, Device): self.count_found_in_bl += 1 else: item = self.book_from_path(prefix, lpath) - changed = True if metadata.add_book(item, replace_metadata=False): changed = True except: # Probably a filename encoding error @@ -108,7 +107,7 @@ class USBMS(CLI, Device): if self.SUPPORTS_SUB_DIRS: for path, dirs, files in os.walk(ebook_dir): for filename in files: - self.report_progress(50.0, _('Getting list of books on device...')) + self.report_progress(0.5, _('Getting list of books on device...')) changed = update_booklist(filename, path, prefix) if changed: need_sync = True @@ -250,6 +249,7 @@ class USBMS(CLI, Device): @classmethod def normalize_path(cls, path): + 'Return path with platform native path separators' if path is None: return None if os.sep == '\\': From 84e5059b11657aacc36a30e867c8c31cb096c870 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 22:26:59 -0600 Subject: [PATCH 13/14] More usbms.driver cleanups --- src/calibre/devices/prs505/driver.py | 2 +- src/calibre/devices/usbms/books.py | 2 +- src/calibre/devices/usbms/driver.py | 17 +++++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index d2823ff4a4..9926e5f61c 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -27,7 +27,7 @@ class PRS505(USBMS): supported_platforms = ['windows', 'osx', 'linux'] path_sep = '/' - booklist_class = PRS_BookList # See USBMS for some explanation of this + booklist_class = PRS_BookList # See usbms.driver for some explanation of this FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt'] diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index edd5907713..3ecee3755f 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -37,7 +37,7 @@ class Book(MetaInformation): else: self.lpath = lpath self.mime = mime_type_ext(path_to_ext(lpath)) - self.size = None # will be set later + self.size = size # will be set later if None self.datetime = time.gmtime() if other: diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index c7c4e06834..1d5343024c 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -15,6 +15,7 @@ import re import json from itertools import cycle +from calibre import prints from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.device import Device from calibre.devices.usbms.books import BookList, Book @@ -122,7 +123,7 @@ class USBMS(CLI, Device): # if count != len(bl) then there were items in it that we did not # find on the device. If need_sync is True then there were either items # on the device that were not in bl or some of the items were changed. - print "count found in cache: %d, count of files in cache: %d, must_sync_cache: %s" % (self.count_found_in_bl, len(bl), need_sync) + #print "count found in cache: %d, count of files in cache: %d, must_sync_cache: %s" % (self.count_found_in_bl, len(bl), need_sync) if self.count_found_in_bl != len(bl) or need_sync: if oncard == 'cardb': self.sync_booklists((None, None, metadata)) @@ -146,7 +147,9 @@ class USBMS(CLI, Device): mdata, fname = metadata.next(), names.next() filepath = self.normalize_path(self.create_upload_path(path, mdata, fname)) paths.append(filepath) - self.put_file(self.normalize_path(infile), filepath, replace_file=True) + if not hasattr(infile, 'read'): + infile = self.normalize_path(infile) + self.put_file(infile, filepath, replace_file=True) try: self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], mdata) @@ -188,7 +191,8 @@ class USBMS(CLI, Device): if not prefix and self._card_b_prefix: prefix = self._card_b_prefix if path.startswith(self._card_b_prefix) else None if prefix is None: - print 'in add_books_to_metadata. Prefix is None!', path, self._main_prefix + prints('in add_books_to_metadata. Prefix is None!', path, + self._main_prefix) continue lpath = path.partition(prefix)[2] if lpath.startswith('/') or lpath.startswith('\\'): @@ -238,7 +242,8 @@ class USBMS(CLI, Device): if prefix is not None and isinstance(booklists[listid], self.booklist_class): if not os.path.exists(prefix): os.makedirs(self.normalize_path(prefix)) - js = [item.to_json() for item in booklists[listid]] + js = [item.to_json() for item in booklists[listid] if + hasattr(item, 'to_json')] with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f: json.dump(js, f, indent=2, encoding='utf-8') write_prefix(self._main_prefix, 0) @@ -314,6 +319,6 @@ class USBMS(CLI, Device): if mi is None: mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], [_('Unknown')]) - mi.size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size - book = cls.book_class(prefix, path, other=mi) + size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size + book = cls.book_class(prefix, path, other=mi, size=size) return book From 0d4506f9c146990e51dea9172608229ab6bc97ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 16 May 2010 22:34:51 -0600 Subject: [PATCH 14/14] Change line endings --- src/calibre/devices/folder_device/driver.py | 170 ++++++++++---------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/src/calibre/devices/folder_device/driver.py b/src/calibre/devices/folder_device/driver.py index 0dcbae87ce..6cc825dd9b 100644 --- a/src/calibre/devices/folder_device/driver.py +++ b/src/calibre/devices/folder_device/driver.py @@ -1,85 +1,85 @@ -''' -Created on 15 May 2010 - -@author: charles -''' -import os - -from calibre.devices.usbms.driver import USBMS, BookList - -# This class is added to the standard device plugin chain, so that it can -# be configured. It has invalid vendor_id etc, so it will never match a -# device. The 'real' FOLDER_DEVICE will use the config from it. -class FOLDER_DEVICE_FOR_CONFIG(USBMS): - name = 'Folder Device Interface' - gui_name = 'Folder Device' - description = _('Use an arbitrary folder as a device.') - author = 'John Schember/Charles Haley' - supported_platforms = ['windows', 'osx', 'linux'] - FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb'] - VENDOR_ID = 0xffff - PRODUCT_ID = 0xffff - BCD = 0xffff - - -class FOLDER_DEVICE(USBMS): - type = _('Device Interface') - - name = 'Folder Device Interface' - gui_name = 'Folder Device' - description = _('Use an arbitrary folder as a device.') - author = 'John Schember/Charles Haley' - supported_platforms = ['windows', 'osx', 'linux'] - FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb'] - - VENDOR_ID = 0xffff - PRODUCT_ID = 0xffff - BCD = 0xffff - - THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device - - CAN_SET_METADATA = True - SUPPORTS_SUB_DIRS = True - - #: Icon for this device - icon = I('sd.svg') - METADATA_CACHE = '.metadata.calibre' - - _main_prefix = '' - _card_a_prefix = None - _card_b_prefix = None - - is_connected = False - - def __init__(self, path): - if not os.path.isdir(path): - raise IOError, 'Path is not a folder' - self._main_prefix = path - self.booklist_class = BookList - self.is_connected = True - - def disconnect_from_folder(self): - self._main_prefix = '' - self.is_connected = False - - def is_usb_connected(self, devices_on_system, debug=False, - only_presence=False): - return self.is_connected, self - - def open(self): - if not self._main_prefix: - return False - return True - - def set_progress_reporter(self, report_progress): - self.report_progress = report_progress - - def card_prefix(self, end_session=True): - return (None, None) - - def eject(self): - self.is_connected = False - - @classmethod - def settings(self): - return FOLDER_DEVICE_FOR_CONFIG._config().parse() +''' +Created on 15 May 2010 + +@author: charles +''' +import os + +from calibre.devices.usbms.driver import USBMS, BookList + +# This class is added to the standard device plugin chain, so that it can +# be configured. It has invalid vendor_id etc, so it will never match a +# device. The 'real' FOLDER_DEVICE will use the config from it. +class FOLDER_DEVICE_FOR_CONFIG(USBMS): + name = 'Folder Device Interface' + gui_name = 'Folder Device' + description = _('Use an arbitrary folder as a device.') + author = 'John Schember/Charles Haley' + supported_platforms = ['windows', 'osx', 'linux'] + FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb'] + VENDOR_ID = 0xffff + PRODUCT_ID = 0xffff + BCD = 0xffff + + +class FOLDER_DEVICE(USBMS): + type = _('Device Interface') + + name = 'Folder Device Interface' + gui_name = 'Folder Device' + description = _('Use an arbitrary folder as a device.') + author = 'John Schember/Charles Haley' + supported_platforms = ['windows', 'osx', 'linux'] + FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb'] + + VENDOR_ID = 0xffff + PRODUCT_ID = 0xffff + BCD = 0xffff + + THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device + + CAN_SET_METADATA = True + SUPPORTS_SUB_DIRS = True + + #: Icon for this device + icon = I('sd.svg') + METADATA_CACHE = '.metadata.calibre' + + _main_prefix = '' + _card_a_prefix = None + _card_b_prefix = None + + is_connected = False + + def __init__(self, path): + if not os.path.isdir(path): + raise IOError, 'Path is not a folder' + self._main_prefix = path + self.booklist_class = BookList + self.is_connected = True + + def disconnect_from_folder(self): + self._main_prefix = '' + self.is_connected = False + + def is_usb_connected(self, devices_on_system, debug=False, + only_presence=False): + return self.is_connected, self + + def open(self): + if not self._main_prefix: + return False + return True + + def set_progress_reporter(self, report_progress): + self.report_progress = report_progress + + def card_prefix(self, end_session=True): + return (None, None) + + def eject(self): + self.is_connected = False + + @classmethod + def settings(self): + return FOLDER_DEVICE_FOR_CONFIG._config().parse()