Support for collections on the 505

This commit is contained in:
Kovid Goyal 2007-11-09 21:08:23 +00:00
parent 9f463128a7
commit 41e7de3077
6 changed files with 376 additions and 8 deletions

View File

@ -193,8 +193,8 @@ class BookList(list):
7. tags (a list of strings, can be empty). 7. tags (a list of strings, can be empty).
''' '''
def __init__(self): __getslice__ = None
list.__init__(self) __setslice__ = None
def supports_tags(self): def supports_tags(self):
''' Return True if the the device supports tags (collections) for this book list. ''' ''' Return True if the the device supports tags (collections) for this book list. '''

View File

@ -220,7 +220,7 @@ class BookList(_BookList):
def remove_book(self, path): def remove_book(self, path):
''' '''
Remove DOM node corresponding to book with C{id == cid}. Remove DOM node corresponding to book with C{path == path}.
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:

View File

@ -0,0 +1,365 @@
## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## 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.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
'''
'''
import re
from uuid import uuid4 as _uuid
import xml.dom.minidom as dom
from base64 import b64decode as decode
from base64 import b64encode as encode
from libprs500.devices.interface import BookList as _BookList
from libprs500.devices import strftime, strptime
MIME_MAP = {
"lrf" : "application/x-sony-bbeb",
"rtf" : "application/rtf",
"pdf" : "application/pdf",
"txt" : "text/plain"
}
def uuid():
return str(_uuid()).replace('-', '', 1).upper()
def sortable_title(title):
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
class book_metadata_field(object):
""" Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None):
self.attr = attr
self.formatter = formatter
self.setter = setter
def __get__(self, obj, typ=None):
""" Return a string. String may be empty if self.attr is absent """
return self.formatter(obj.elem.getAttribute(self.attr)) if \
self.formatter else obj.elem.getAttribute(self.attr).strip()
def __set__(self, obj, val):
""" Set the attribute """
val = self.setter(val) if self.setter else val
if not isinstance(val, unicode):
val = unicode(val, 'utf8', 'replace')
obj.elem.setAttribute(self.attr, val)
class Book(object):
""" Provides a view onto the XML element that represents a book """
title = book_metadata_field("title")
authors = book_metadata_field("author", \
formatter=lambda x: x if x and x.strip() else "Unknown")
mime = book_metadata_field("mime")
rpath = book_metadata_field("path")
id = book_metadata_field("id", formatter=int)
sourceid = book_metadata_field("sourceid", formatter=int)
size = book_metadata_field("size", formatter=int)
# When setting this attribute you must use an epoch
datetime = book_metadata_field("date", formatter=strptime, setter=strftime)
@apply
def title_sorter():
doc = '''String to sort the title. If absent, title is returned'''
def fget(self):
src = self.elem.getAttribute('titleSorter').strip()
if not src:
src = self.title
return src
def fset(self, val):
self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
return property(doc=doc, fget=fget, fset=fset)
@apply
def thumbnail():
doc = \
"""
The thumbnail. Should be a height 68 image.
Setting is not supported.
"""
def fget(self):
th = self.elem.getElementsByTagName(self.prefix + "thumbnail")
if not len(th):
th = self.elem.getElementsByTagName("cache:thumbnail")
if len(th):
for n in th[0].childNodes:
if n.nodeType == n.ELEMENT_NODE:
th = n
break
rc = ""
for node in th.childNodes:
if node.nodeType == node.TEXT_NODE:
rc += node.data
return decode(rc)
return property(fget=fget, doc=doc)
@apply
def path():
doc = """ Absolute path to book on device. Setting not supported. """
def fget(self):
return self.mountpath + self.rpath
return property(fget=fget, doc=doc)
def __init__(self, node, mountpath, tags, prefix=""):
self.elem = node
self.prefix = prefix
self.tags = tags
self.mountpath = mountpath
def __str__(self):
""" Return a utf-8 encoded string with title author and path information """
return self.title.encode('utf-8') + " by " + \
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
class BookList(_BookList):
def __init__(self, xml_file, mountpath):
_BookList.__init__(self)
xml_file.seek(0)
self.document = dom.parse(xml_file)
self.root_element = self.document.documentElement
self.mountpath = mountpath
records = self.root_element.getElementsByTagName('records')
if records:
self.prefix = 'xs1:'
self.root_element = records[0]
else:
self.prefix = ''
for book in self.root_element.childNodes:
if hasattr(book, 'tagName') and book.tagName.endswith('text'):
tags = [i.getAttribute('title') for i in self.get_playlists(book.getAttribute('id'))]
self.append(Book(book, mountpath, tags, prefix=self.prefix))
def max_id(self):
max = 0
for child in self.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
nid = int(child.getAttribute('id'))
if nid > max:
max = nid
return max
def is_id_valid(self, id):
'''Return True iff there is an element with C{id==id}.'''
id = str(id)
for child in self.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
if child.getAttribute('id') == id:
return True
return False
def supports_tags(self):
return True
def add_book(self, info, name, size, ctime):
""" Add a node into the DOM tree, representing a book """
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name.rpartition('.')[-1]]
cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = {
"title" : info["title"],
'titleSorter' : sortable_title(info['title']),
"author" : info["authors"] if info['authors'] else 'Unknown', \
"page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size)
}
for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr])
try:
w, h, data = info["cover"]
except TypeError:
w, h, data = None, None, None
if data:
th = self.document.createElement(self.prefix + "thumbnail")
th.setAttribute("width", str(w))
th.setAttribute("height", str(h))
jpeg = self.document.createElement(self.prefix + "jpeg")
jpeg.appendChild(self.document.createTextNode(encode(data)))
th.appendChild(jpeg)
node.appendChild(th)
self.root_element.appendChild(node)
book = Book(node, self.mountpath, [], prefix=self.prefix)
book.datetime = ctime
self.append(book)
if info.has_key('tags'):
self.set_tags(book, info['tags'])
def _delete_book(self, node):
nid = node.getAttribute('id')
self.remove_from_playlists(nid)
node.parentNode.removeChild(node)
node.unlink()
def delete_book(self, cid):
'''
Remove DOM node corresponding to book with C{id == cid}.
Also remove book from any collections it is part of.
'''
for book in self:
if str(book.id) == str(cid):
self.remove(book)
self._delete_book(book.elem)
break
def remove_book(self, path):
'''
Remove DOM node corresponding to book with C{path == path}.
Also remove book from any collections it is part of.
'''
for book in self:
if path.endswith(book.path):
self.remove(book)
self._delete_book(book.elem)
break
def playlists(self):
ans = []
for c in self.root_element.childNodes:
if hasattr(c, 'tagName') and c.tagName.endswith('playlist'):
ans.append(c)
return ans
def playlist_items(self):
plitems = []
for pl in self.playlists():
for c in pl.childNodes:
if hasattr(c, 'tagName') and c.tagName.endswith('item'):
plitems.append(c)
return plitems
def purge_corrupted_files(self):
if not self.root_element:
return []
corrupted = self.root_element.getElementsByTagName(self.prefix+'corrupted')
paths = []
for c in corrupted:
paths.append(c.getAttribute('path'))
c.parentNode.removeChild(c)
c.unlink()
return paths
def purge_empty_playlists(self):
''' Remove all playlists that have no children. Also removes any invalid playlist items.'''
for pli in self.playlist_items():
if not self.is_id_valid(pli.getAttribute('id')):
pli.parentNode.removeChild(pli)
pli.unlink()
for pl in self.playlists():
empty = True
for c in pl.childNodes:
if hasattr(c, 'tagName') and c.tagName.endswith('item'):
empty = False
break
if empty:
pl.parentNode.removeChild(pl)
pl.unlink()
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('id', str(cid))
pl.setAttribute('title', title)
pl.setAttribute('uuid', uuid())
self.root_element.insertBefore(pl, self.root_element.childNodes[-1])
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):
tags = [t for t in tags if t]
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, bookid):
ans = []
for pl in self.playlists():
for item in pl.childNodes:
if hasattr(item, 'tagName') and item.tagName.endswith('item'):
if item.getAttribute('id') == str(bookid):
ans.append(pl)
return ans
def next_id(self):
return self.document.documentElement.getAttribute('nextID')
def set_next_id(self, id):
self.document.documentElement.setAttribute('nextID', str(id))
def write(self, stream):
""" Write XML representation of DOM tree to C{stream} """
src = self.document.toxml('utf-8') + '\n'
stream.write(src.replace("'", '''))
def fix_ids(main, card):
'''
Adjust ids the XML databases.
'''
if hasattr(main, 'purge_empty_playlists'):
main.purge_empty_playlists()
if hasattr(card, 'purge_empty_playlists'):
card.purge_empty_playlists()
def regen_ids(db):
if not hasattr(db, 'root_element'):
return
id_map = {}
db.purge_empty_playlists()
cid = 0 if db == main else 1
for child in db.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute('id'):
id_map[child.getAttribute('id')] = str(cid)
child.setAttribute("sourceid", '1')
child.setAttribute('id', str(cid))
cid += 1
for item in db.playlist_items():
oid = item.getAttribute('id')
try:
item.setAttribute('id', id_map[oid])
except KeyError:
item.parentNode.removeChild(item)
item.unlink()
regen_ids(main)
regen_ids(card)
main.set_next_id(str(main.max_id()+1))

View File

@ -20,7 +20,7 @@ from itertools import cycle
from libprs500.devices.interface import Device from libprs500.devices.interface import Device
from libprs500.devices.errors import DeviceError, FreeSpaceError from libprs500.devices.errors import DeviceError, FreeSpaceError
from libprs500.devices.prs500.books import BookList, fix_ids from libprs500.devices.prs505.books import BookList, fix_ids
from libprs500 import iswindows, islinux, isosx from libprs500 import iswindows, islinux, isosx
from libprs500.devices.libusb import get_device_by_id from libprs500.devices.libusb import get_device_by_id
from libprs500.devices.libusb import Error as USBError from libprs500.devices.libusb import Error as USBError
@ -270,10 +270,10 @@ class PRS505(Device):
return [] return []
db = self.__class__.CACHE_XML if oncard else self.__class__.MEDIA_XML db = self.__class__.CACHE_XML if oncard else self.__class__.MEDIA_XML
prefix = self._card_prefix if oncard else self._main_prefix prefix = self._card_prefix if oncard else self._main_prefix
f = open(prefix + db, 'rb') bl = BookList(open(prefix + db, 'rb'), prefix)
bl = BookList(root=self._card_prefix if oncard else self._main_prefix, sfile=f)
paths = bl.purge_corrupted_files() paths = bl.purge_corrupted_files()
for path in paths: for path in paths:
path = os.path.join(self._card_prefix if oncard else self._main_prefix, path)
if os.path.exists(path): if os.path.exists(path):
os.unlink(path) os.unlink(path)
return bl return bl
@ -366,6 +366,7 @@ class PRS505(Device):
on_card = 1 if location[3] else 0 on_card = 1 if location[3] else 0
name = path.rpartition('/')[2] name = path.rpartition('/')[2]
name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'database/media/books/') + name name = (cls.CARD_PATH_PREFIX+'/' if on_card else 'database/media/books/') + name
name = name.replace('//', '/')
booklists[on_card].add_book(info, name, *location[1:-1]) booklists[on_card].add_book(info, name, *location[1:-1])
fix_ids(*booklists) fix_ids(*booklists)
@ -377,6 +378,7 @@ class PRS505(Device):
def remove_books_from_metadata(cls, paths, booklists): def remove_books_from_metadata(cls, paths, booklists):
for path in paths: for path in paths:
for bl in booklists: for bl in booklists:
if hasattr(bl, 'remove_book'):
bl.remove_book(path) bl.remove_book(path)
fix_ids(*booklists) fix_ids(*booklists)

View File

@ -638,6 +638,7 @@ class DeviceBooksModel(BooksModel):
self.db[idx].authors = val self.db[idx].authors = val
elif col == 4: elif col == 4:
tags = [i.strip() for i in val.split(',')] tags = [i.strip() for i in val.split(',')]
tags = [t for t in tags if t]
self.db.set_tags(self.db[idx], tags) self.db.set_tags(self.db[idx], tags)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
self.emit(SIGNAL('booklist_dirtied()')) self.emit(SIGNAL('booklist_dirtied()'))

View File

@ -373,8 +373,8 @@ class Main(MainWindow, Ui_MainWindow):
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)
self.device_manager.remove_books_from_metadata(paths, self.booklists()) self.device_manager.remove_books_from_metadata(paths, self.booklists())
self.upload_booklists()
model.paths_deleted(paths) model.paths_deleted(paths)
self.upload_booklists()
############################################################################ ############################################################################