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()
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):

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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 = []

View File

@ -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')

View File

@ -1,7 +1,7 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__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 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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

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