On device indication, temporary implementation

This commit is contained in:
Kovid Goyal 2010-05-09 14:37:57 -06:00
commit 6b6dcf8c40
13 changed files with 163 additions and 24 deletions

View File

@ -55,7 +55,13 @@ class JETBOOK(USBMS):
au = mi.format_authors() au = mi.format_authors()
if not au: if not au:
au = 'Unknown' 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 @classmethod
def metadata_from_path(cls, path): def metadata_from_path(cls, path):

View File

@ -60,11 +60,6 @@ class KINDLE(USBMS):
'replace') 'replace')
return mi return mi
def filename_callback(self, fname, mi):
if fname.startswith('.'):
return 'x'+fname[1:]
return fname
def get_annotations(self, path_map): def get_annotations(self, path_map):
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt'] MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
mbp_formats = set(MBP_FORMATS) mbp_formats = set(MBP_FORMATS)

View File

@ -121,14 +121,6 @@ class PRS505(CLI, Device):
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return bl 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, def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None): metadata=None):

View File

@ -46,6 +46,12 @@ class Book(object):
return self.title.encode('utf-8') + " by " + \ return self.title.encode('utf-8') + " by " + \
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8') 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): class BookList(_BookList):

View File

@ -784,8 +784,14 @@ class Device(DeviceConfig, DevicePlugin):
def filename_callback(self, default, mi): def filename_callback(self, default, mi):
''' '''
Callback to allow drivers to change the default file name 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 return default
def sanitize_path_components(self, components): def sanitize_path_components(self, components):
@ -826,7 +832,7 @@ class Device(DeviceConfig, DevicePlugin):
if not isinstance(template, unicode): if not isinstance(template, unicode):
template = template.decode('utf-8') template = template.decode('utf-8')
app_id = str(getattr(mdata, 'application_id', '')) 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, extra_components = get_components(template, mdata, fname,
length=250-len(app_id)-1) length=250-len(app_id)-1)
if not extra_components: if not extra_components:
@ -835,6 +841,9 @@ class Device(DeviceConfig, DevicePlugin):
else: else:
extra_components[-1] = sanitize(self.filename_callback(extra_components[-1]+ext, mdata)) 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: if special_tag is not None:
name = extra_components[-1] name = extra_components[-1]
extra_components = [] extra_components = []

View File

@ -25,7 +25,7 @@ NONE = QVariant() #: Null value to return from the data function of item models
UNDEFINED_QDATE = QDate(UNDEFINED_DATE) UNDEFINED_QDATE = QDate(UNDEFINED_DATE)
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher', ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher',
'tags', 'series', 'pubdate'] 'tags', 'series', 'pubdate', 'ondevice']
def _config(): def _config():
c = Config('gui', 'preferences for the calibre GUI') c = Config('gui', 'preferences for the calibre GUI')

View File

@ -1,7 +1,7 @@
from __future__ import with_statement from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, traceback, Queue, time, socket, cStringIO import os, traceback, Queue, time, socket, cStringIO, re
from threading import Thread, RLock from threading import Thread, RLock
from itertools import repeat from itertools import repeat
from functools import partial from functools import partial
@ -978,3 +978,69 @@ class DeviceGUI(object):
getattr(f, 'close', lambda : True)() getattr(f, 'close', lambda : True)()
if memory and memory[1]: if memory and memory[1]:
self.library_view.model().delete_books_by_id(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

View File

@ -319,11 +319,13 @@ class BooksModel(QAbstractTableModel):
'publisher' : _("Publisher"), 'publisher' : _("Publisher"),
'tags' : _("Tags"), 'tags' : _("Tags"),
'series' : _("Series"), 'series' : _("Series"),
'ondevice' : _("On Device"),
} }
def __init__(self, parent=None, buffer=40): def __init__(self, parent=None, buffer=40):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
self.book_on_device = None
self.editable_cols = ['title', 'authors', 'rating', 'publisher', self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp', 'pubdate'] 'tags', 'series', 'timestamp', 'pubdate']
self.default_image = QImage(I('book.svg')) self.default_image = QImage(I('book.svg'))
@ -362,6 +364,9 @@ class BooksModel(QAbstractTableModel):
self.reset() self.reset()
self.emit(SIGNAL('columns_sorted()')) self.emit(SIGNAL('columns_sorted()'))
def set_book_on_device_func(self, func):
self.book_on_device = func
def set_database(self, db): def set_database(self, db):
self.db = db self.db = db
self.custom_columns = self.db.custom_column_label_map self.custom_columns = self.db.custom_column_label_map
@ -802,6 +807,8 @@ class BooksModel(QAbstractTableModel):
'series' : functools.partial(series, 'series' : functools.partial(series,
idx=self.db.FIELD_MAP['series'], idx=self.db.FIELD_MAP['series'],
siix=self.db.FIELD_MAP['series_index']), siix=self.db.FIELD_MAP['series_index']),
'ondevice' : functools.partial(text_type,
idx=self.db.FIELD_MAP['ondevice'], mult=False),
} }
self.dc_decorator = {} self.dc_decorator = {}
@ -1258,6 +1265,7 @@ class DeviceBooksModel(BooksModel):
self.marked_for_deletion = {} self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self) self.search_engine = OnDeviceSearch(self)
self.editable = True self.editable = True
self.book_in_library = None
def mark_for_deletion(self, job, rows): def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows) self.marked_for_deletion[job] = self.indices(rows)
@ -1345,8 +1353,11 @@ class DeviceBooksModel(BooksModel):
def tagscmp(x, y): def tagscmp(x, y):
x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags) x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags)
return cmp(x, y) 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 \ 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) self.map.sort(cmp=fcmp, reverse=descending)
if len(self.map) == len(self.db): if len(self.map) == len(self.db):
self.sorted_map = list(self.map) self.sorted_map = list(self.map)
@ -1360,7 +1371,7 @@ class DeviceBooksModel(BooksModel):
def columnCount(self, parent): def columnCount(self, parent):
if parent and parent.isValid(): if parent and parent.isValid():
return 0 return 0
return 5 return 6
def rowCount(self, parent): def rowCount(self, parent):
if parent and parent.isValid(): if parent and parent.isValid():
@ -1401,7 +1412,6 @@ class DeviceBooksModel(BooksModel):
''' '''
return [ self.map[r.row()] for r in rows] return [ self.map[r.row()] for r in rows]
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole: if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
@ -1417,7 +1427,7 @@ class DeviceBooksModel(BooksModel):
if role == Qt.EditRole: if role == Qt.EditRole:
return QVariant(au) return QVariant(au)
authors = string_to_authors(au) authors = string_to_authors(au)
return QVariant("\n".join(authors)) return QVariant(" & ".join(authors))
elif col == 2: elif col == 2:
size = self.db[self.map[row]].size size = self.db[self.map[row]].size
return QVariant(BooksView.human_readable(size)) return QVariant(BooksView.human_readable(size))
@ -1429,6 +1439,9 @@ class DeviceBooksModel(BooksModel):
tags = self.db[self.map[row]].tags tags = self.db[self.map[row]].tags
if tags: if tags:
return QVariant(', '.join(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]: elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid(): elif role == Qt.ToolTipRole and index.isValid():
@ -1449,6 +1462,7 @@ class DeviceBooksModel(BooksModel):
elif section == 2: text = _("Size (MB)") elif section == 2: text = _("Size (MB)")
elif section == 3: text = _("Date") elif section == 3: text = _("Date")
elif section == 4: text = _("Tags") elif section == 4: text = _("Tags")
elif section == 5: text = _("In Library")
return QVariant(text) return QVariant(text)
else: else:
return QVariant(section+1) return QVariant(section+1)
@ -1482,4 +1496,3 @@ class DeviceBooksModel(BooksModel):
def set_search_restriction(self, s): def set_search_restriction(self, s):
pass pass

View File

@ -520,7 +520,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
if self.system_tray_icon.isVisible() and opts.start_in_tray: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() self.hide_windows()
self.stack.setCurrentIndex(0) 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.set_database(db)
self.library_view.model().set_book_on_device_func(self.book_on_device)
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)])) self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)]))
if not self.library_view.restore_column_widths(): if not self.library_view.restore_column_widths():
@ -956,6 +959,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.status_bar.reset_info() self.status_bar.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0)) self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.eject_action.setEnabled(False) self.eject_action.setEnabled(False)
self.refresh_ondevice_info (clear_info = True)
def info_read(self, job): def info_read(self, job):
''' '''
@ -988,12 +992,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
else: else:
self.device_job_exception(job) self.device_job_exception(job)
return return
self.set_books_in_library(None, reset=True)
mainlist, cardalist, cardblist = job.result mainlist, cardalist, cardblist = job.result
self.memory_view.set_database(mainlist) 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.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.card_a_view.set_database(cardalist) 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_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
self.card_b_view.set_database(cardblist) 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) 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): for view in (self.memory_view, self.card_a_view, self.card_b_view):
view.sortByColumn(3, Qt.DescendingOrder) view.sortByColumn(3, Qt.DescendingOrder)
@ -1001,8 +1009,19 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
if not view.restore_column_widths(): if not view.restore_column_widths():
view.resizeColumnsToContents() view.resizeColumnsToContents()
view.resize_on_select = not view.isVisible() 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_news()
self.sync_catalogs() 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 ################################ ######################### Fetch annotations ################################
@ -2228,7 +2247,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def library_moved(self, newloc): def library_moved(self, newloc):
if newloc is None: return if newloc is None: return
db = LibraryDatabase2(newloc) 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.set_database(db)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clearMessage() self.status_bar.clearMessage()
self.search.clear_to_help() self.search.clear_to_help()
self.status_bar.reset_info() self.status_bar.reset_info()

View File

@ -370,7 +370,7 @@ class ResultCache(SearchQueryParser):
location += 's' location += 's'
all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series',
'formats', 'isbn', 'rating', 'cover') 'formats', 'isbn', 'rating', 'cover', 'ondevice')
MAP = {} MAP = {}
for x in all: # get the db columns for the standard searchables for x in all: # get the db columns for the standard searchables
@ -518,6 +518,7 @@ class ResultCache(SearchQueryParser):
try: try:
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0] 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.has_cover(id, index_is_id=True))
self._data[id].append(db.book_on_device_string(id))
except IndexError: except IndexError:
return None return None
try: try:
@ -533,6 +534,7 @@ class ResultCache(SearchQueryParser):
for id in ids: for id in ids:
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0] 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.has_cover(id, index_is_id=True))
self._data[id].append(db.book_on_device_string(id))
self._map[0:0] = ids self._map[0:0] = ids
self._map_filtered[0:0] = ids self._map_filtered[0:0] = ids
@ -553,6 +555,7 @@ class ResultCache(SearchQueryParser):
for item in self._data: for item in self._data:
if item is not None: if item is not None:
item.append(db.has_cover(item[0], index_is_id=True)) 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] self._map = [i[0] for i in self._data if i is not None]
if field is not None: if field is not None:
self.sort(field, ascending) self.sort(field, ascending)

View File

@ -1070,6 +1070,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM tags')] 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): def conversion_options(self, id, format):
data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False) data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False)

View File

@ -222,6 +222,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.FIELD_MAP[col] = base = base+1 self.FIELD_MAP[col] = base = base+1
self.FIELD_MAP['cover'] = base+1 self.FIELD_MAP['cover'] = base+1
self.FIELD_MAP['ondevice'] = base+2
script = ''' script = '''
DROP VIEW IF EXISTS meta2; DROP VIEW IF EXISTS meta2;
@ -233,6 +234,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.executescript(script) self.conn.executescript(script)
self.conn.commit() self.conn.commit()
self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map) self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map)
self.search = self.data.search self.search = self.data.search
self.refresh = functools.partial(self.data.refresh, self) self.refresh = functools.partial(self.data.refresh, self)
@ -468,6 +470,27 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
im = PILImage.open(f) im = PILImage.open(f)
im.convert('RGB').save(path, 'JPEG') 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): def all_formats(self):
formats = self.conn.get('SELECT DISTINCT format from data') formats = self.conn.get('SELECT DISTINCT format from data')
if not formats: if not formats:

View File

@ -100,6 +100,7 @@ class SearchQueryParser(object):
'search', 'search',
'date', 'date',
'pubdate', 'pubdate',
'ondevice',
'all', 'all',
] ]