mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add support for collections. Bug fixes.
This commit is contained in:
parent
55602e7daf
commit
89e77eb685
@ -13,7 +13,7 @@
|
|||||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
''' E-book management software'''
|
''' E-book management software'''
|
||||||
__version__ = "0.3.92"
|
__version__ = "0.3.93"
|
||||||
__docformat__ = "epytext"
|
__docformat__ = "epytext"
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
__appname__ = 'libprs500'
|
__appname__ = 'libprs500'
|
||||||
|
@ -102,8 +102,7 @@ class Device(object):
|
|||||||
@param oncard: If True return a list of ebooks on the storage card,
|
@param oncard: If True return a list of ebooks on the storage card,
|
||||||
otherwise return list of ebooks in main memory of device.
|
otherwise return list of ebooks in main memory of device.
|
||||||
If True and no books on card return empty list.
|
If True and no books on card return empty list.
|
||||||
@return: A list of Books. Each Book object must have the fields:
|
@return: A BookList.
|
||||||
title, authors, size, datetime (a UTC time tuple), path, thumbnail (can be None).
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -129,9 +128,10 @@ class Device(object):
|
|||||||
the device.
|
the device.
|
||||||
@param locations: Result of a call to L{upload_books}
|
@param locations: Result of a call to L{upload_books}
|
||||||
@param metadata: List of dictionaries. Each dictionary must have the
|
@param metadata: List of dictionaries. Each dictionary must have the
|
||||||
keys C{title}, C{authors}, C{cover}. The value of the C{cover} element
|
keys C{title}, C{authors}, C{cover}, C{tags}. The value of the C{cover}
|
||||||
can be None or a three element tuple (width, height, data)
|
element can be None or a three element tuple (width, height, data)
|
||||||
where data is the image data in JPEG format as a string.
|
where data is the image data in JPEG format as a string. C{tags} must be
|
||||||
|
a possibly empty list of strings.
|
||||||
@param booklists: A tuple containing the result of calls to
|
@param booklists: A tuple containing the result of calls to
|
||||||
(L{books}(oncard=False), L{books}(oncard=True)).
|
(L{books}(oncard=False), L{books}(oncard=True)).
|
||||||
'''
|
'''
|
||||||
@ -161,4 +161,31 @@ class Device(object):
|
|||||||
(L{books}(oncard=False), L{books}(oncard=True)).
|
(L{books}(oncard=False), L{books}(oncard=True)).
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class BookList(list):
|
||||||
|
'''
|
||||||
|
A list of books. Each Book object must have the fields:
|
||||||
|
1. title
|
||||||
|
2. authors
|
||||||
|
3. size (file size of the book)
|
||||||
|
4. datetime (a UTC time tuple)
|
||||||
|
5. path (path on the device to the book)
|
||||||
|
6. thumbnail (can be None)
|
||||||
|
7. tags (a list of strings, can be empty).
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
list.__init__(self)
|
||||||
|
|
||||||
|
def supports_tags(self):
|
||||||
|
''' Return True if the the device supports tags (collections) for this book list. '''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def set_tags(self, book, tags):
|
||||||
|
'''
|
||||||
|
Set the tags for C{book} to C{tags}.
|
||||||
|
@param tags: A list of strings. Can be empty.
|
||||||
|
@param book: A book object that is in this BookList.
|
||||||
|
'''
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ from base64 import b64encode as encode
|
|||||||
import time, re
|
import time, re
|
||||||
|
|
||||||
from libprs500.devices.errors import ProtocolError
|
from libprs500.devices.errors import ProtocolError
|
||||||
|
from libprs500.devices.interface import BookList as _BookList
|
||||||
|
|
||||||
MIME_MAP = { \
|
MIME_MAP = { \
|
||||||
"lrf":"application/x-sony-bbeb", \
|
"lrf":"application/x-sony-bbeb", \
|
||||||
@ -64,7 +65,6 @@ class Book(object):
|
|||||||
datetime = book_metadata_field("date", \
|
datetime = book_metadata_field("date", \
|
||||||
formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"),
|
formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"),
|
||||||
setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x)))
|
setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x)))
|
||||||
|
|
||||||
@apply
|
@apply
|
||||||
def title_sorter():
|
def title_sorter():
|
||||||
doc = '''String to sort the title. If absent, title is returned'''
|
doc = '''String to sort the title. If absent, title is returned'''
|
||||||
@ -105,10 +105,11 @@ class Book(object):
|
|||||||
return self.root + self.rpath
|
return self.root + self.rpath
|
||||||
return property(fget=fget, doc=doc)
|
return property(fget=fget, doc=doc)
|
||||||
|
|
||||||
def __init__(self, node, prefix="", root="/Data/media/"):
|
def __init__(self, node, tags=[], prefix="", root="/Data/media/"):
|
||||||
self.elem = node
|
self.elem = node
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
self.root = root
|
self.root = root
|
||||||
|
self.tags = tags
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
""" Return a utf-8 encoded string with title author and path information """
|
""" Return a utf-8 encoded string with title author and path information """
|
||||||
@ -117,33 +118,22 @@ class Book(object):
|
|||||||
|
|
||||||
|
|
||||||
def fix_ids(media, cache):
|
def fix_ids(media, cache):
|
||||||
'''
|
'''
|
||||||
Update ids in media, cache to be consistent with their
|
Adjust ids in cache to correspond with media.
|
||||||
current structure
|
|
||||||
'''
|
'''
|
||||||
media.purge_empty_playlists()
|
media.purge_empty_playlists()
|
||||||
plitems = media.playlist_items()
|
if cache.root:
|
||||||
cid = 0
|
sourceid = media.max_id()
|
||||||
for child in media.root.childNodes:
|
cid = sourceid + 1
|
||||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
|
||||||
old_id = child.getAttribute('id')
|
|
||||||
for item in plitems:
|
|
||||||
if item.hasAttribute('id') and item.getAttribute('id') == old_id:
|
|
||||||
item.setAttribute('id', str(cid))
|
|
||||||
child.setAttribute("id", str(cid))
|
|
||||||
cid += 1
|
|
||||||
mmaxid = cid - 1
|
|
||||||
cid = mmaxid + 2
|
|
||||||
if len(cache):
|
|
||||||
for child in cache.root.childNodes:
|
for child in cache.root.childNodes:
|
||||||
if child.nodeType == child.ELEMENT_NODE and \
|
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("sourceid"):
|
||||||
child.hasAttribute("sourceid"):
|
child.setAttribute("sourceid", str(sourceid))
|
||||||
child.setAttribute("sourceid", str(mmaxid+1))
|
|
||||||
child.setAttribute("id", str(cid))
|
child.setAttribute("id", str(cid))
|
||||||
cid += 1
|
cid += 1
|
||||||
media.document.documentElement.setAttribute("nextID", str(cid))
|
media.set_next_id(str(cid))
|
||||||
|
|
||||||
class BookList(list):
|
|
||||||
|
class BookList(_BookList):
|
||||||
"""
|
"""
|
||||||
A list of L{Book}s. Created from an XML file. Can write list
|
A list of L{Book}s. Created from an XML file. Can write list
|
||||||
to an XML file.
|
to an XML file.
|
||||||
@ -152,7 +142,8 @@ class BookList(list):
|
|||||||
__setslice__ = None
|
__setslice__ = None
|
||||||
|
|
||||||
def __init__(self, root="/Data/media/", sfile=None):
|
def __init__(self, root="/Data/media/", sfile=None):
|
||||||
list.__init__(self)
|
_BookList.__init__(self)
|
||||||
|
self.root = self.document = self.proot = None
|
||||||
if sfile:
|
if sfile:
|
||||||
sfile.seek(0)
|
sfile.seek(0)
|
||||||
self.document = dom.parse(sfile)
|
self.document = dom.parse(sfile)
|
||||||
@ -160,50 +151,30 @@ class BookList(list):
|
|||||||
self.prefix = ''
|
self.prefix = ''
|
||||||
records = self.root.getElementsByTagName('records')
|
records = self.root.getElementsByTagName('records')
|
||||||
if records:
|
if records:
|
||||||
|
self.prefix = 'xs1:'
|
||||||
self.root = records[0]
|
self.root = records[0]
|
||||||
for child in self.root.childNodes:
|
|
||||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
|
||||||
self.prefix = child.tagName.partition(':')[0] + ':'
|
|
||||||
break
|
|
||||||
if not self.prefix:
|
|
||||||
raise ProtocolError, 'Could not determine prefix in media.xml'
|
|
||||||
self.proot = root
|
self.proot = root
|
||||||
|
|
||||||
for book in self.document.getElementsByTagName(self.prefix + "text"):
|
for book in self.document.getElementsByTagName(self.prefix + "text"):
|
||||||
self.append(Book(book, root=root, prefix=self.prefix))
|
id = book.getAttribute('id')
|
||||||
|
pl = [i.getAttribute('title') for i in self.get_playlists(id)]
|
||||||
|
self.append(Book(book, root=root, prefix=self.prefix, tags=pl))
|
||||||
|
|
||||||
|
def supports_tags(self):
|
||||||
|
return bool(self.prefix)
|
||||||
|
|
||||||
def max_id(self):
|
def playlists(self):
|
||||||
""" Highest id in underlying XML file """
|
return self.root.getElementsByTagName(self.prefix+'playlist')
|
||||||
cid = -1
|
|
||||||
for child in self.root.childNodes:
|
|
||||||
if child.nodeType == child.ELEMENT_NODE and \
|
|
||||||
child.hasAttribute("id"):
|
|
||||||
c = int(child.getAttribute("id"))
|
|
||||||
if c > cid:
|
|
||||||
cid = c
|
|
||||||
return cid
|
|
||||||
|
|
||||||
def has_id(self, cid):
|
|
||||||
"""
|
|
||||||
Check if a book with id C{ == cid} exists already.
|
|
||||||
This *does not* check if id exists in the underlying XML file
|
|
||||||
"""
|
|
||||||
ans = False
|
|
||||||
for book in self:
|
|
||||||
if book.id == cid:
|
|
||||||
ans = True
|
|
||||||
break
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def playlist_items(self):
|
def playlist_items(self):
|
||||||
playlists = self.root.getElementsByTagName(self.prefix+'playlist')
|
|
||||||
plitems = []
|
plitems = []
|
||||||
for pl in playlists:
|
for pl in self.playlists():
|
||||||
plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
|
plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
|
||||||
return plitems
|
return plitems
|
||||||
|
|
||||||
def purge_corrupted_files(self):
|
def purge_corrupted_files(self):
|
||||||
|
if not self.root:
|
||||||
|
return []
|
||||||
corrupted = self.root.getElementsByTagName(self.prefix+'corrupted')
|
corrupted = self.root.getElementsByTagName(self.prefix+'corrupted')
|
||||||
paths = []
|
paths = []
|
||||||
proot = self.proot if self.proot.endswith('/') else self.proot + '/'
|
proot = self.proot if self.proot.endswith('/') else self.proot + '/'
|
||||||
@ -215,8 +186,7 @@ class BookList(list):
|
|||||||
|
|
||||||
def purge_empty_playlists(self):
|
def purge_empty_playlists(self):
|
||||||
''' Remove all playlist entries that have no children. '''
|
''' Remove all playlist entries that have no children. '''
|
||||||
playlists = self.root.getElementsByTagName(self.prefix+'playlist')
|
for pl in self.playlists():
|
||||||
for pl in playlists:
|
|
||||||
if not pl.getElementsByTagName(self.prefix + 'item'):
|
if not pl.getElementsByTagName(self.prefix + 'item'):
|
||||||
pl.parentNode.removeChild(pl)
|
pl.parentNode.removeChild(pl)
|
||||||
pl.unlink()
|
pl.unlink()
|
||||||
@ -225,10 +195,8 @@ class BookList(list):
|
|||||||
nid = node.getAttribute('id')
|
nid = node.getAttribute('id')
|
||||||
node.parentNode.removeChild(node)
|
node.parentNode.removeChild(node)
|
||||||
node.unlink()
|
node.unlink()
|
||||||
for pli in self.playlist_items():
|
self.remove_from_playlists(nid)
|
||||||
if pli.getAttribute('id') == nid:
|
|
||||||
pli.parentNode.removeChild(pli)
|
|
||||||
pli.unlink()
|
|
||||||
|
|
||||||
def delete_book(self, cid):
|
def delete_book(self, cid):
|
||||||
'''
|
'''
|
||||||
@ -247,11 +215,26 @@ class BookList(list):
|
|||||||
Also remove book from any collections it is part of.
|
Also remove book from any collections it is part of.
|
||||||
'''
|
'''
|
||||||
for book in self:
|
for book in self:
|
||||||
if book.path == path:
|
if path.endswith(book.path):
|
||||||
self.remove(book)
|
self.remove(book)
|
||||||
self._delete_book(book.elem)
|
self._delete_book(book.elem)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def next_id(self):
|
||||||
|
return self.document.documentElement.getAttribute('nextID')
|
||||||
|
|
||||||
|
def set_next_id(self, id):
|
||||||
|
self.document.documentElement.setAttribute('nextID', str(id))
|
||||||
|
|
||||||
|
def max_id(self):
|
||||||
|
max = 0
|
||||||
|
for child in self.root.childNodes:
|
||||||
|
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||||
|
nid = int(child.getAttribute('id'))
|
||||||
|
if nid > max:
|
||||||
|
max = nid
|
||||||
|
return max
|
||||||
|
|
||||||
def add_book(self, info, name, size, ctime):
|
def add_book(self, info, name, size, ctime):
|
||||||
""" Add a node into DOM tree representing a book """
|
""" Add a node into DOM tree representing a book """
|
||||||
node = self.document.createElement(self.prefix + "text")
|
node = self.document.createElement(self.prefix + "text")
|
||||||
@ -266,7 +249,6 @@ class BookList(list):
|
|||||||
"sourceid":sourceid, "id":str(cid), "date":"", \
|
"sourceid":sourceid, "id":str(cid), "date":"", \
|
||||||
"mime":mime, "path":name, "size":str(size)
|
"mime":mime, "path":name, "size":str(size)
|
||||||
}
|
}
|
||||||
print name
|
|
||||||
for attr in attrs.keys():
|
for attr in attrs.keys():
|
||||||
node.setAttributeNode(self.document.createAttribute(attr))
|
node.setAttributeNode(self.document.createAttribute(attr))
|
||||||
node.setAttribute(attr, attrs[attr])
|
node.setAttribute(attr, attrs[attr])
|
||||||
@ -287,6 +269,63 @@ class BookList(list):
|
|||||||
book = Book(node, root=self.proot, prefix=self.prefix)
|
book = Book(node, root=self.proot, prefix=self.prefix)
|
||||||
book.datetime = ctime
|
book.datetime = ctime
|
||||||
self.append(book)
|
self.append(book)
|
||||||
|
self.set_next_id(cid+1)
|
||||||
|
if self.prefix: # Playlists only supportted in main memory
|
||||||
|
self.set_playlists(book.id, info['tags'])
|
||||||
|
|
||||||
|
|
||||||
|
def playlist_by_title(self, title):
|
||||||
|
for pl in self.playlists():
|
||||||
|
if pl.getAttribute('title').lower() == title.lower():
|
||||||
|
return pl
|
||||||
|
|
||||||
|
def add_playlist(self, title):
|
||||||
|
cid = self.max_id()+1
|
||||||
|
pl = self.document.createElement(self.prefix+'playlist')
|
||||||
|
pl.setAttribute('sourceid', '0')
|
||||||
|
pl.setAttribute('id', str(cid))
|
||||||
|
pl.setAttribute('title', title)
|
||||||
|
for child in self.root.childNodes:
|
||||||
|
try:
|
||||||
|
if child.getAttribute('id') == '1':
|
||||||
|
self.root.insertBefore(pl, child)
|
||||||
|
self.set_next_id(cid+1)
|
||||||
|
break
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
return pl
|
||||||
|
|
||||||
|
|
||||||
|
def remove_from_playlists(self, id):
|
||||||
|
for pli in self.playlist_items():
|
||||||
|
if pli.getAttribute('id') == str(id):
|
||||||
|
pli.parentNode.removeChild(pli)
|
||||||
|
pli.unlink()
|
||||||
|
|
||||||
|
def set_tags(self, book, tags):
|
||||||
|
book.tags = tags
|
||||||
|
self.set_playlists(book.id, tags)
|
||||||
|
|
||||||
|
def set_playlists(self, id, collections):
|
||||||
|
self.remove_from_playlists(id)
|
||||||
|
for collection in set(collections):
|
||||||
|
coll = self.playlist_by_title(collection)
|
||||||
|
if not coll:
|
||||||
|
coll = self.add_playlist(collection)
|
||||||
|
item = self.document.createElement(self.prefix+'item')
|
||||||
|
item.setAttribute('id', str(id))
|
||||||
|
coll.appendChild(item)
|
||||||
|
|
||||||
|
def get_playlists(self, id):
|
||||||
|
ans = []
|
||||||
|
for pl in self.playlists():
|
||||||
|
for item in pl.getElementsByTagName(self.prefix+'item'):
|
||||||
|
if item.getAttribute('id') == str(id):
|
||||||
|
ans.append(pl)
|
||||||
|
continue
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def write(self, stream):
|
def write(self, stream):
|
||||||
""" Write XML representation of DOM tree to C{stream} """
|
""" Write XML representation of DOM tree to C{stream} """
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
## You should have received a copy of the GNU General Public License along
|
## You should have received a copy of the GNU General Public License along
|
||||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
### End point description for PRS-500 procductId=667
|
### End point description for PRS-500 procductId=667
|
||||||
### Endpoint Descriptor:
|
### Endpoint Descriptor:
|
||||||
@ -802,12 +803,12 @@ class PRS500(Device):
|
|||||||
else:
|
else:
|
||||||
self.get_file(self.MEDIA_XML, tfile, end_session=False)
|
self.get_file(self.MEDIA_XML, tfile, end_session=False)
|
||||||
bl = BookList(root=root, sfile=tfile)
|
bl = BookList(root=root, sfile=tfile)
|
||||||
paths = bl.purge_corrupted_files()
|
paths = bl.purge_corrupted_files()
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
self.del_file(path, end_session=False)
|
self.del_file(path, end_session=False)
|
||||||
except PathError: # Incase this is a refetch without a sync in between
|
except PathError: # Incase this is a refetch without a sync in between
|
||||||
continue
|
continue
|
||||||
return bl
|
return bl
|
||||||
|
|
||||||
@safe
|
@safe
|
||||||
@ -828,8 +829,9 @@ class PRS500(Device):
|
|||||||
@param booklists: A tuple containing the result of calls to
|
@param booklists: A tuple containing the result of calls to
|
||||||
(L{books}(oncard=False), L{books}(oncard=True)).
|
(L{books}(oncard=False), L{books}(oncard=True)).
|
||||||
'''
|
'''
|
||||||
|
fix_ids(*booklists)
|
||||||
self.upload_book_list(booklists[0], end_session=False)
|
self.upload_book_list(booklists[0], end_session=False)
|
||||||
if len(booklists[1]):
|
if booklists[1].root:
|
||||||
self.upload_book_list(booklists[1], end_session=False)
|
self.upload_book_list(booklists[1], end_session=False)
|
||||||
|
|
||||||
@safe
|
@safe
|
||||||
@ -940,7 +942,7 @@ class PRS500(Device):
|
|||||||
raise ArgumentError("Cannot upload list to card as "+\
|
raise ArgumentError("Cannot upload list to card as "+\
|
||||||
"card is not present")
|
"card is not present")
|
||||||
path = card + self.CACHE_XML
|
path = card + self.CACHE_XML
|
||||||
f = TemporaryFile()
|
f = StringIO()
|
||||||
booklist.write(f)
|
booklist.write(f)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
self.put_file(f, path, replace_file=True, end_session=False)
|
self.put_file(f, path, replace_file=True, end_session=False)
|
||||||
|
@ -1230,7 +1230,8 @@ class HTMLConverter(object):
|
|||||||
self.process_table(tag, tag_css)
|
self.process_table(tag, tag_css)
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
print 'WARNING: An error occurred while processing a table:', err
|
print 'WARNING: An error occurred while processing a table:', err
|
||||||
print 'Ignoring table markup'
|
print 'Ignoring table markup for table:'
|
||||||
|
print str(tag)[:100]
|
||||||
self.in_table = False
|
self.in_table = False
|
||||||
self.process_children(tag, tag_css)
|
self.process_children(tag, tag_css)
|
||||||
else:
|
else:
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
## You should have received a copy of the GNU General Public License along
|
## You should have received a copy of the GNU General Public License along
|
||||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
from libprs500.gui2 import qstring_to_unicode
|
||||||
import os, textwrap, traceback, time, re, sre_constants
|
import os, textwrap, traceback, time, re, sre_constants
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
@ -19,7 +20,7 @@ from math import cos, sin, pi
|
|||||||
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
|
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
|
||||||
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
|
||||||
QPen, QStyle, QPainter, QLineEdit, QApplication, \
|
QPen, QStyle, QPainter, QLineEdit, QApplication, \
|
||||||
QPalette, QItemSelectionModel
|
QPalette
|
||||||
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
|
||||||
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
|
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
|
||||||
|
|
||||||
@ -195,6 +196,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
row = row.row()
|
row = row.row()
|
||||||
au = self.db.authors(row)
|
au = self.db.authors(row)
|
||||||
|
tags = self.db.tags(row)
|
||||||
if not au:
|
if not au:
|
||||||
au = 'Unknown'
|
au = 'Unknown'
|
||||||
au = au.split(',')
|
au = au.split(',')
|
||||||
@ -204,11 +206,17 @@ class BooksModel(QAbstractTableModel):
|
|||||||
au = t
|
au = t
|
||||||
else:
|
else:
|
||||||
au = ' & '.join(au)
|
au = ' & '.join(au)
|
||||||
|
if not tags:
|
||||||
|
tags = []
|
||||||
|
else:
|
||||||
|
tags = tags.split(',')
|
||||||
mi = {
|
mi = {
|
||||||
'title' : self.db.title(row),
|
'title' : self.db.title(row),
|
||||||
'authors' : au,
|
'authors' : au,
|
||||||
'cover' : self.db.cover(row),
|
'cover' : self.db.cover(row),
|
||||||
|
'tags' : tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata.append(mi)
|
metadata.append(mi)
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
@ -334,7 +342,8 @@ class BooksView(QTableView):
|
|||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
self.setItemDelegateForColumn(4, LibraryDelegate(self))
|
if self.__class__.__name__ == 'BooksView': # Subclasses may not have rating as col 4
|
||||||
|
self.setItemDelegateForColumn(4, LibraryDelegate(self))
|
||||||
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
||||||
self._model.current_changed)
|
self._model.current_changed)
|
||||||
# Adding and removing rows should resize rows to contents
|
# Adding and removing rows should resize rows to contents
|
||||||
@ -398,6 +407,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
self.unknown = str(self.trUtf8('Unknown'))
|
self.unknown = str(self.trUtf8('Unknown'))
|
||||||
self.marked_for_deletion = {}
|
self.marked_for_deletion = {}
|
||||||
|
|
||||||
|
|
||||||
def mark_for_deletion(self, id, rows):
|
def mark_for_deletion(self, id, rows):
|
||||||
self.marked_for_deletion[id] = self.indices(rows)
|
self.marked_for_deletion[id] = self.indices(rows)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@ -415,16 +425,16 @@ class DeviceBooksModel(BooksModel):
|
|||||||
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
|
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
|
||||||
|
|
||||||
def path_about_to_be_deleted(self, path):
|
def path_about_to_be_deleted(self, path):
|
||||||
for row in range(len(self.map)):
|
for row in range(len(self.map)):
|
||||||
if self.db[self.map[row]].path == path:
|
if self.db[self.map[row]].path == path:
|
||||||
print row, path
|
#print row, path
|
||||||
|
#print self.rowCount(None)
|
||||||
self.beginRemoveRows(QModelIndex(), row, row)
|
self.beginRemoveRows(QModelIndex(), row, row)
|
||||||
self.map.pop(row)
|
self.map.pop(row)
|
||||||
|
self.endRemoveRows()
|
||||||
|
#print self.rowCount(None)
|
||||||
return
|
return
|
||||||
|
|
||||||
def path_deleted(self):
|
|
||||||
self.endRemoveRows()
|
|
||||||
|
|
||||||
def indices_to_be_deleted(self):
|
def indices_to_be_deleted(self):
|
||||||
ans = []
|
ans = []
|
||||||
for v in self.marked_for_deletion.values():
|
for v in self.marked_for_deletion.values():
|
||||||
@ -433,8 +443,12 @@ class DeviceBooksModel(BooksModel):
|
|||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
if self.map[index.row()] in self.indices_to_be_deleted():
|
||||||
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
||||||
return BooksModel.flags(self, index)
|
flags = QAbstractTableModel.flags(self, index)
|
||||||
|
if index.isValid():
|
||||||
|
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
|
||||||
|
flags |= Qt.ItemIsEditable
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
def search(self, text, refinement, reset=True):
|
def search(self, text, refinement, reset=True):
|
||||||
@ -443,7 +457,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
result = []
|
result = []
|
||||||
for i in base:
|
for i in base:
|
||||||
add = True
|
add = True
|
||||||
q = self.db[i].title + ' ' + self.db[i].authors
|
q = self.db[i].title + ' ' + self.db[i].authors + ' ' + ', '.join(self.db[i].tags)
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if not token.search(q):
|
if not token.search(q):
|
||||||
add = False
|
add = False
|
||||||
@ -478,8 +492,11 @@ class DeviceBooksModel(BooksModel):
|
|||||||
def sizecmp(x, y):
|
def sizecmp(x, y):
|
||||||
x, y = int(self.db[x].size), int(self.db[y].size)
|
x, y = int(self.db[x].size), int(self.db[y].size)
|
||||||
return cmp(x, y)
|
return cmp(x, y)
|
||||||
|
def tagscmp(x, y):
|
||||||
|
x, y = ','.join(self.db[x].tags), ','.join(self.db[y].tags)
|
||||||
|
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
|
sizecmp if col == 2 else datecmp if col == 3 else tagscmp
|
||||||
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)
|
||||||
@ -491,7 +508,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
return 4
|
return 5
|
||||||
|
|
||||||
def rowCount(self, parent):
|
def rowCount(self, parent):
|
||||||
return len(self.map)
|
return len(self.map)
|
||||||
@ -516,6 +533,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
dt = datetime(*dt[0:6])
|
dt = datetime(*dt[0:6])
|
||||||
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
||||||
data['Timestamp'] = dt.ctime()
|
data['Timestamp'] = dt.ctime()
|
||||||
|
data['Tags'] = ', '.join(item.tags)
|
||||||
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
||||||
|
|
||||||
def paths(self, rows):
|
def paths(self, rows):
|
||||||
@ -556,30 +574,51 @@ class DeviceBooksModel(BooksModel):
|
|||||||
dt = datetime(*dt[0:6])
|
dt = datetime(*dt[0:6])
|
||||||
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
|
||||||
return QVariant(dt.strftime(BooksView.TIME_FMT))
|
return QVariant(dt.strftime(BooksView.TIME_FMT))
|
||||||
|
elif col == 4:
|
||||||
|
tags = self.db[self.map[row]].tags
|
||||||
|
if tags:
|
||||||
|
return QVariant(', '.join(tags))
|
||||||
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():
|
||||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
if self.map[index.row()] in self.indices_to_be_deleted():
|
||||||
return QVariant('Marked for deletion')
|
return QVariant('Marked for deletion')
|
||||||
if index.column() in [0, 1]:
|
col = index.column()
|
||||||
|
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
|
||||||
return QVariant("Double click to <b>edit</b> me<br><br>")
|
return QVariant("Double click to <b>edit</b> me<br><br>")
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role != Qt.DisplayRole:
|
||||||
|
return NONE
|
||||||
|
text = ""
|
||||||
|
if orientation == Qt.Horizontal:
|
||||||
|
if section == 0: text = "Title"
|
||||||
|
elif section == 1: text = "Author(s)"
|
||||||
|
elif section == 2: text = "Size (MB)"
|
||||||
|
elif section == 3: text = "Date"
|
||||||
|
elif section == 4: text = "Tags"
|
||||||
|
return QVariant(self.trUtf8(text))
|
||||||
|
else:
|
||||||
|
return QVariant(section+1)
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
done = False
|
done = False
|
||||||
if role == Qt.EditRole:
|
if role == Qt.EditRole:
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
if col in [2, 3]:
|
if col in [2, 3]:
|
||||||
return False
|
return False
|
||||||
val = unicode(value.toString().toUtf8(), 'utf-8').strip()
|
val = qstring_to_unicode(value.toString()).strip()
|
||||||
idx = self.map[row]
|
idx = self.map[row]
|
||||||
if col == 0:
|
if col == 0:
|
||||||
self.db[idx].title = val
|
self.db[idx].title = val
|
||||||
self.db[idx].title_sorter = val
|
self.db[idx].title_sorter = val
|
||||||
elif col == 1:
|
elif col == 1:
|
||||||
self.db[idx].authors = val
|
self.db[idx].authors = val
|
||||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
elif col == 4:
|
||||||
index, index)
|
tags = [i.strip() for i in val.split(',')]
|
||||||
|
self.db.set_tags(self.db[idx], tags)
|
||||||
|
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
|
||||||
self.emit(SIGNAL('booklist_dirtied()'))
|
self.emit(SIGNAL('booklist_dirtied()'))
|
||||||
if col == self.sorted_on[0]:
|
if col == self.sorted_on[0]:
|
||||||
self.sort(col, self.sorted_on[1])
|
self.sort(col, self.sorted_on[1])
|
||||||
|
@ -61,6 +61,7 @@ class Main(QObject, Ui_MainWindow):
|
|||||||
self.device_error_dialog = error_dialog(self.window, 'Error communicating with device', ' ')
|
self.device_error_dialog = error_dialog(self.window, 'Error communicating with device', ' ')
|
||||||
self.device_error_dialog.setModal(Qt.NonModal)
|
self.device_error_dialog.setModal(Qt.NonModal)
|
||||||
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
||||||
|
self.device_connected = False
|
||||||
####################### Location View ########################
|
####################### Location View ########################
|
||||||
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
|
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
|
||||||
self.location_selected)
|
self.location_selected)
|
||||||
@ -156,7 +157,9 @@ class Main(QObject, Ui_MainWindow):
|
|||||||
self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT)
|
self.set_default_thumbnail(cls.THUMBNAIL_HEIGHT)
|
||||||
self.status_bar.showMessage('Device: '+cls.__name__+' detected.', 3000)
|
self.status_bar.showMessage('Device: '+cls.__name__+' detected.', 3000)
|
||||||
self.action_sync.setEnabled(True)
|
self.action_sync.setEnabled(True)
|
||||||
|
self.device_connected = True
|
||||||
else:
|
else:
|
||||||
|
self.device_connected = False
|
||||||
self.job_manager.terminate_device_jobs()
|
self.job_manager.terminate_device_jobs()
|
||||||
self.device_manager.device_removed()
|
self.device_manager.device_removed()
|
||||||
self.location_view.model().update_devices()
|
self.location_view.model().update_devices()
|
||||||
@ -326,15 +329,12 @@ class Main(QObject, Ui_MainWindow):
|
|||||||
self.device_job_exception(id, description, exception, formatted_traceback)
|
self.device_job_exception(id, description, exception, formatted_traceback)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.upload_booklists()
|
|
||||||
|
|
||||||
if self.delete_memory.has_key(id):
|
if self.delete_memory.has_key(id):
|
||||||
paths, model = self.delete_memory.pop(id)
|
paths, model = self.delete_memory.pop(id)
|
||||||
for path in paths:
|
for path in paths:
|
||||||
model.path_about_to_be_deleted(path)
|
model.path_about_to_be_deleted(path)
|
||||||
self.device_manager.remove_books_from_metadata((path,), self.booklists())
|
self.device_manager.remove_books_from_metadata((path,), self.booklists())
|
||||||
model.path_deleted()
|
self.upload_booklists()
|
||||||
|
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
@ -437,6 +437,13 @@ class Main(QObject, Ui_MainWindow):
|
|||||||
view.resize_on_select = False
|
view.resize_on_select = False
|
||||||
self.status_bar.reset_info()
|
self.status_bar.reset_info()
|
||||||
self.current_view().clearSelection()
|
self.current_view().clearSelection()
|
||||||
|
if location == 'library':
|
||||||
|
if self.device_connected:
|
||||||
|
self.action_sync.setEnabled(True)
|
||||||
|
self.action_edit.setEnabled(True)
|
||||||
|
else:
|
||||||
|
self.action_sync.setEnabled(False)
|
||||||
|
self.action_edit.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
def wrap_traceback(self, tb):
|
def wrap_traceback(self, tb):
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
## You should have received a copy of the GNU General Public License along
|
## You should have received a copy of the GNU General Public License along
|
||||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
from libprs500.gui2.dialogs.jobs import JobsDialog
|
|
||||||
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
|
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
|
||||||
@ -78,29 +76,12 @@ class BookInfoDisplay(QFrame):
|
|||||||
self.clear_message()
|
self.clear_message()
|
||||||
self.setVisible(True)
|
self.setVisible(True)
|
||||||
|
|
||||||
class BusyIndicator(QLabel):
|
|
||||||
def __init__(self, movie, jobs_dialog):
|
|
||||||
QLabel.__init__(self)
|
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
|
||||||
self.setToolTip('Click to see list of active jobs.')
|
|
||||||
self.setMovie(movie)
|
|
||||||
movie.start()
|
|
||||||
movie.setPaused(True)
|
|
||||||
self.jobs_dialog = jobs_dialog
|
|
||||||
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
|
||||||
if self.jobs_dialog.isVisible():
|
|
||||||
self.jobs_dialog.hide()
|
|
||||||
else:
|
|
||||||
self.jobs_dialog.show()
|
|
||||||
|
|
||||||
|
|
||||||
class MovieButton(QFrame):
|
class MovieButton(QFrame):
|
||||||
def __init__(self, movie, jobs_dialog):
|
def __init__(self, movie, jobs_dialog):
|
||||||
QFrame.__init__(self)
|
QFrame.__init__(self)
|
||||||
self.setLayout(QVBoxLayout())
|
self.setLayout(QVBoxLayout())
|
||||||
self.movie_widget = BusyIndicator(movie, jobs_dialog)
|
self.movie_widget = QLabel()
|
||||||
|
self.movie_widget.setMovie(movie)
|
||||||
self.movie = movie
|
self.movie = movie
|
||||||
self.layout().addWidget(self.movie_widget)
|
self.layout().addWidget(self.movie_widget)
|
||||||
self.jobs = QLabel('<b>Jobs: 0')
|
self.jobs = QLabel('<b>Jobs: 0')
|
||||||
@ -110,6 +91,18 @@ class MovieButton(QFrame):
|
|||||||
self.jobs.setMargin(0)
|
self.jobs.setMargin(0)
|
||||||
self.layout().setMargin(0)
|
self.layout().setMargin(0)
|
||||||
self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||||
|
self.jobs_dialog = jobs_dialog
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.setToolTip('Click to see list of active jobs.')
|
||||||
|
movie.start()
|
||||||
|
movie.setPaused(True)
|
||||||
|
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
if self.jobs_dialog.isVisible():
|
||||||
|
self.jobs_dialog.hide()
|
||||||
|
else:
|
||||||
|
self.jobs_dialog.show()
|
||||||
|
|
||||||
|
|
||||||
class StatusBar(QStatusBar):
|
class StatusBar(QStatusBar):
|
||||||
|
@ -57,7 +57,7 @@ class LibraryDatabase(object):
|
|||||||
Iterator over the books in the old pre 0.4.0 database.
|
Iterator over the books in the old pre 0.4.0 database.
|
||||||
'''
|
'''
|
||||||
conn = sqlite.connect(path)
|
conn = sqlite.connect(path)
|
||||||
cur = conn.execute('select * from books_meta;')
|
cur = conn.execute('select * from books_meta order by id;')
|
||||||
book = cur.fetchone()
|
book = cur.fetchone()
|
||||||
while book:
|
while book:
|
||||||
id = book[0]
|
id = book[0]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user