diff --git a/src/calibre/devices/jetbook/driver.py b/src/calibre/devices/jetbook/driver.py index e4fd840dc0..71b825f5d8 100644 --- a/src/calibre/devices/jetbook/driver.py +++ b/src/calibre/devices/jetbook/driver.py @@ -55,7 +55,13 @@ class JETBOOK(USBMS): au = mi.format_authors() if not au: au = 'Unknown' - return '%s#%s%s' % (au, title, fileext) + suffix = '' + if getattr(mi, 'application_id', None) is not None: + base = fname.rpartition('.')[0] + suffix = '_%s'%mi.application_id + if base.endswith(suffix): + suffix = '' + return '%s#%s%s%s' % (au, title, fileext, suffix) @classmethod def metadata_from_path(cls, path): diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 83eae78de0..c88a443689 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -60,11 +60,6 @@ class KINDLE(USBMS): 'replace') return mi - def filename_callback(self, fname, mi): - if fname.startswith('.'): - return 'x'+fname[1:] - return fname - def get_annotations(self, path_map): MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt'] mbp_formats = set(MBP_FORMATS) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index f4fc4b0d29..e73a341909 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -121,14 +121,6 @@ class PRS505(CLI, Device): self.report_progress(1.0, _('Getting list of books on device...')) return bl - def filename_callback(self, fname, mi): - if getattr(mi, 'application_id', None) is not None: - base = fname.rpartition('.')[0] - suffix = '_%s'%mi.application_id - if not base.endswith(suffix): - fname = base + suffix + '.' + fname.rpartition('.')[-1] - return fname - def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 4cde8dbe57..1ddc00729f 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -46,6 +46,12 @@ class Book(object): return self.title.encode('utf-8') + " by " + \ self.authors.encode('utf-8') + " at " + self.path.encode('utf-8') + @property + def db_id(self): + '''The database id in the application database that this file corresponds to''' + match = re.search(r'_(\d+)$', self.rpath.rpartition('.')[0]) + if match: + return int(match.group(1)) class BookList(_BookList): diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 897baf82ca..fbc61afc9f 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -784,8 +784,14 @@ class Device(DeviceConfig, DevicePlugin): def filename_callback(self, default, mi): ''' Callback to allow drivers to change the default file name - set by :method:`create_upload_path`. + set by :method:`create_upload_path`. By default, add the DB_ID + to the end of the string. Helps with ondevice doc matching ''' + if getattr(mi, 'application_id', None) is not None: + base = default.rpartition('.')[0] + suffix = '_%s'%mi.application_id + if not base.endswith(suffix): + default = base + suffix + '.' + default.rpartition('.')[-1] return default def sanitize_path_components(self, components): @@ -826,7 +832,7 @@ class Device(DeviceConfig, DevicePlugin): if not isinstance(template, unicode): template = template.decode('utf-8') app_id = str(getattr(mdata, 'application_id', '')) - # The SONY readers need to have the db id in the created filename + # The db id will be in the created filename extra_components = get_components(template, mdata, fname, length=250-len(app_id)-1) if not extra_components: @@ -835,6 +841,9 @@ class Device(DeviceConfig, DevicePlugin): else: extra_components[-1] = sanitize(self.filename_callback(extra_components[-1]+ext, mdata)) + if extra_components[-1] and extra_components[-1][0] in ('.', '_'): + extra_components[-1] = 'x' + extra_components[-1][1:] + if special_tag is not None: name = extra_components[-1] extra_components = [] diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index c97906495c..1eff46aca1 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -25,7 +25,7 @@ NONE = QVariant() #: Null value to return from the data function of item models UNDEFINED_QDATE = QDate(UNDEFINED_DATE) ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher', - 'tags', 'series', 'pubdate'] + 'tags', 'series', 'pubdate', 'ondevice'] def _config(): c = Config('gui', 'preferences for the calibre GUI') diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index b87a5e451b..855d05ff58 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1,7 +1,7 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, traceback, Queue, time, socket, cStringIO +import os, traceback, Queue, time, socket, cStringIO, re from threading import Thread, RLock from itertools import repeat from functools import partial @@ -978,3 +978,69 @@ class DeviceGUI(object): getattr(f, 'close', lambda : True)() if memory and memory[1]: self.library_view.model().delete_books_by_id(memory[1]) + + def book_on_device(self, index, format=None, reset=False): + loc = [None, None, None] + + if reset: + self.book_on_device_cache = None + return + + if self.book_on_device_cache is None: + self.book_on_device_cache = [] + for i, l in enumerate(self.booklists()): + self.book_on_device_cache.append({}) + for book in l: + book_title = book.title.lower() if book.title else '' + book_title = re.sub('(?u)\W|[_]', '', book_title) + if book_title not in self.book_on_device_cache[i]: + self.book_on_device_cache[i][book_title] = \ + {'authors':set(), 'db_ids':set()} + book_authors = authors_to_string(book.authors).lower() + book_authors = re.sub('(?u)\W|[_]', '', book_authors) + self.book_on_device_cache[i][book_title]['authors'].add(book_authors) + self.book_on_device_cache[i][book_title]['db_ids'].add(book.db_id) + + db_title = self.library_view.model().db.title(index, index_is_id=True).lower() + db_title = re.sub('(?u)\W|[_]', '', db_title) + au = self.library_view.model().db.authors(index, index_is_id=True) + db_authors = au.lower() if au else '' + db_authors = re.sub('(?u)\W|[_]', '', db_authors) + for i, l in enumerate(self.booklists()): + d = self.book_on_device_cache[i].get(db_title, None) + if d and (index in d['db_ids'] or db_authors in d['authors']): + loc[i] = True + break + return loc + + def set_books_in_library(self, booklist, reset = False): + if reset: + self.book_in_library_cache = None + return + + # First build a self.book_in_library_cache of the library, so the search isn't On**2 + self.book_in_library_cache = {} + for id, title in self.library_view.model().db.all_titles(): + title = re.sub('(?u)\W|[_]', '', title.lower()) + if title not in self.book_in_library_cache: + self.book_in_library_cache[title] = {'authors':set(), 'db_ids':set()} + au = self.library_view.model().db.authors(id, index_is_id=True) + authors = au.lower() if au else '' + authors = re.sub('(?u)\W|[_]', '', authors) + self.book_in_library_cache[title]['authors'].add(authors) + self.book_in_library_cache[title]['db_ids'].add(id) + + # Now iterate through all the books on the device, setting the in_library field + for book in booklist: + book_title = book.title.lower() if book.title else '' + book_title = re.sub('(?u)\W|[_]', '', book_title) + book.in_library = False + d = self.book_in_library_cache.get(book_title, None) + if d is not None: + if book.db_id in d['db_ids']: + book.in_library = True + continue + book_authors = authors_to_string(book.authors).lower() if book.authors else '' + book_authors = re.sub('(?u)\W|[_]', '', book_authors) + if book_authors in d['authors']: + book.in_library = True diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index e40403f1f4..90dc3eb1ea 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -319,11 +319,13 @@ class BooksModel(QAbstractTableModel): 'publisher' : _("Publisher"), 'tags' : _("Tags"), 'series' : _("Series"), + 'ondevice' : _("On Device"), } def __init__(self, parent=None, buffer=40): QAbstractTableModel.__init__(self, parent) self.db = None + self.book_on_device = None self.editable_cols = ['title', 'authors', 'rating', 'publisher', 'tags', 'series', 'timestamp', 'pubdate'] self.default_image = QImage(I('book.svg')) @@ -362,6 +364,9 @@ class BooksModel(QAbstractTableModel): self.reset() self.emit(SIGNAL('columns_sorted()')) + def set_book_on_device_func(self, func): + self.book_on_device = func + def set_database(self, db): self.db = db self.custom_columns = self.db.custom_column_label_map @@ -802,6 +807,8 @@ class BooksModel(QAbstractTableModel): 'series' : functools.partial(series, idx=self.db.FIELD_MAP['series'], siix=self.db.FIELD_MAP['series_index']), + 'ondevice' : functools.partial(text_type, + idx=self.db.FIELD_MAP['ondevice'], mult=False), } self.dc_decorator = {} @@ -1258,6 +1265,7 @@ class DeviceBooksModel(BooksModel): self.marked_for_deletion = {} self.search_engine = OnDeviceSearch(self) self.editable = True + self.book_in_library = None def mark_for_deletion(self, job, rows): self.marked_for_deletion[job] = self.indices(rows) @@ -1345,8 +1353,11 @@ class DeviceBooksModel(BooksModel): def tagscmp(x, y): x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags) return cmp(x, y) + def libcmp(x, y): + x, y = self.db[x].in_library, self.db[y].in_library + return cmp(x, y) fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \ - sizecmp if col == 2 else datecmp if col == 3 else tagscmp + sizecmp if col == 2 else datecmp if col == 3 else tagscmp if col == 4 else libcmp self.map.sort(cmp=fcmp, reverse=descending) if len(self.map) == len(self.db): self.sorted_map = list(self.map) @@ -1360,7 +1371,7 @@ class DeviceBooksModel(BooksModel): def columnCount(self, parent): if parent and parent.isValid(): return 0 - return 5 + return 6 def rowCount(self, parent): if parent and parent.isValid(): @@ -1401,7 +1412,6 @@ class DeviceBooksModel(BooksModel): ''' return [ self.map[r.row()] for r in rows] - def data(self, index, role): if role == Qt.DisplayRole or role == Qt.EditRole: row, col = index.row(), index.column() @@ -1417,7 +1427,7 @@ class DeviceBooksModel(BooksModel): if role == Qt.EditRole: return QVariant(au) authors = string_to_authors(au) - return QVariant("\n".join(authors)) + return QVariant(" & ".join(authors)) elif col == 2: size = self.db[self.map[row]].size return QVariant(BooksView.human_readable(size)) @@ -1429,6 +1439,9 @@ class DeviceBooksModel(BooksModel): tags = self.db[self.map[row]].tags if tags: return QVariant(', '.join(tags)) + elif col == 5: + return QVariant(_('Yes')) \ + if self.db[self.map[row]].in_library else QVariant(_('No')) elif role == Qt.TextAlignmentRole and index.column() in [2, 3]: return QVariant(Qt.AlignRight | Qt.AlignVCenter) elif role == Qt.ToolTipRole and index.isValid(): @@ -1449,6 +1462,7 @@ class DeviceBooksModel(BooksModel): elif section == 2: text = _("Size (MB)") elif section == 3: text = _("Date") elif section == 4: text = _("Tags") + elif section == 5: text = _("In Library") return QVariant(text) else: return QVariant(section+1) @@ -1482,4 +1496,3 @@ class DeviceBooksModel(BooksModel): def set_search_restriction(self, s): pass - diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 131a3f74f4..4f5e71174c 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -520,7 +520,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() self.stack.setCurrentIndex(0) + self.book_on_device(None, reset=True) + db.set_book_on_device_func(self.book_on_device) self.library_view.set_database(db) + self.library_view.model().set_book_on_device_func(self.book_on_device) prefs['library_path'] = self.library_path self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)])) if not self.library_view.restore_column_widths(): @@ -956,6 +959,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.status_bar.reset_info() self.location_view.setCurrentIndex(self.location_view.model().index(0)) self.eject_action.setEnabled(False) + self.refresh_ondevice_info (clear_info = True) def info_read(self, job): ''' @@ -988,12 +992,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): else: self.device_job_exception(job) return + self.set_books_in_library(None, reset=True) mainlist, cardalist, cardblist = job.result self.memory_view.set_database(mainlist) + self.set_books_in_library(mainlist) self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA) self.card_a_view.set_database(cardalist) + self.set_books_in_library(cardalist) self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA) self.card_b_view.set_database(cardblist) + self.set_books_in_library(cardblist) self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA) for view in (self.memory_view, self.card_a_view, self.card_b_view): view.sortByColumn(3, Qt.DescendingOrder) @@ -1001,8 +1009,19 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): if not view.restore_column_widths(): view.resizeColumnsToContents() view.resize_on_select = not view.isVisible() + if view.model().rowCount(None) > 1: + view.resizeRowToContents(0) + height = view.rowHeight(0) + view.verticalHeader().setDefaultSectionSize(height) self.sync_news() self.sync_catalogs() + self.refresh_ondevice_info() + + ############################################################################ + ### Force the library view to refresh, taking into consideration books information + def refresh_ondevice_info(self, clear_flags = False): + self.book_on_device(None, reset=True) + self.library_view.model().refresh() ############################################################################ ######################### Fetch annotations ################################ @@ -2228,7 +2247,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): def library_moved(self, newloc): if newloc is None: return db = LibraryDatabase2(newloc) + self.book_on_device(None, reset=True) + db.set_book_on_device_func(self.book_on_device) self.library_view.set_database(db) + self.library_view.model().set_book_on_device_func(self.book_on_device) self.status_bar.clearMessage() self.search.clear_to_help() self.status_bar.reset_info() diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 2e7c7c4ca8..9ed150733a 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -370,7 +370,7 @@ class ResultCache(SearchQueryParser): location += 's' all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', - 'formats', 'isbn', 'rating', 'cover') + 'formats', 'isbn', 'rating', 'cover', 'ondevice') MAP = {} for x in all: # get the db columns for the standard searchables @@ -518,6 +518,7 @@ class ResultCache(SearchQueryParser): try: self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0] self._data[id].append(db.has_cover(id, index_is_id=True)) + self._data[id].append(db.book_on_device_string(id)) except IndexError: return None try: @@ -533,6 +534,7 @@ class ResultCache(SearchQueryParser): for id in ids: self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0] self._data[id].append(db.has_cover(id, index_is_id=True)) + self._data[id].append(db.book_on_device_string(id)) self._map[0:0] = ids self._map_filtered[0:0] = ids @@ -553,6 +555,7 @@ class ResultCache(SearchQueryParser): for item in self._data: if item is not None: item.append(db.has_cover(item[0], index_is_id=True)) + item.append(db.book_on_device_string(item[0])) self._map = [i[0] for i in self._data if i is not None] if field is not None: self.sort(field, ascending) diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index cfd2213eed..6147101567 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -1070,6 +1070,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; return [ (i[0], i[1]) for i in \ self.conn.get('SELECT id, name FROM tags')] + def all_titles(self): + return [ (i[0], i[1]) for i in \ + self.conn.get('SELECT id, title FROM books')] def conversion_options(self, id, format): data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d52fcb3051..fd59503eed 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -222,6 +222,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.FIELD_MAP[col] = base = base+1 self.FIELD_MAP['cover'] = base+1 + self.FIELD_MAP['ondevice'] = base+2 script = ''' DROP VIEW IF EXISTS meta2; @@ -233,6 +234,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.executescript(script) self.conn.commit() + self.book_on_device_func = None self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map) self.search = self.data.search self.refresh = functools.partial(self.data.refresh, self) @@ -468,6 +470,27 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): im = PILImage.open(f) im.convert('RGB').save(path, 'JPEG') + def book_on_device(self, index): + if callable(self.book_on_device_func): + return self.book_on_device_func(index) + return None + + def book_on_device_string(self, index): + loc = [] + on = self.book_on_device(index) + if on is not None: + m, a, b = on + if m is not None: + loc.append(_('Main')) + if a is not None: + loc.append(_('Card A')) + if b is not None: + loc.append(_('Card B')) + return ', '.join(loc) + + def set_book_on_device_func(self, func): + self.book_on_device_func = func + def all_formats(self): formats = self.conn.get('SELECT DISTINCT format from data') if not formats: diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 79324e6b8b..11991727b7 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -100,6 +100,7 @@ class SearchQueryParser(object): 'search', 'date', 'pubdate', + 'ondevice', 'all', ]