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).
'''
def __init__(self):
list.__init__(self)
__getslice__ = None
__setslice__ = None
def supports_tags(self):
''' 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):
'''
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.
'''
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.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.devices.libusb import get_device_by_id
from libprs500.devices.libusb import Error as USBError
@ -270,10 +270,10 @@ class PRS505(Device):
return []
db = self.__class__.CACHE_XML if oncard else self.__class__.MEDIA_XML
prefix = self._card_prefix if oncard else self._main_prefix
f = open(prefix + db, 'rb')
bl = BookList(root=self._card_prefix if oncard else self._main_prefix, sfile=f)
bl = BookList(open(prefix + db, 'rb'), prefix)
paths = bl.purge_corrupted_files()
for path in paths:
path = os.path.join(self._card_prefix if oncard else self._main_prefix, path)
if os.path.exists(path):
os.unlink(path)
return bl
@ -366,6 +366,7 @@ class PRS505(Device):
on_card = 1 if location[3] else 0
name = path.rpartition('/')[2]
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])
fix_ids(*booklists)
@ -377,6 +378,7 @@ class PRS505(Device):
def remove_books_from_metadata(cls, paths, booklists):
for path in paths:
for bl in booklists:
if hasattr(bl, 'remove_book'):
bl.remove_book(path)
fix_ids(*booklists)

View File

@ -638,6 +638,7 @@ class DeviceBooksModel(BooksModel):
self.db[idx].authors = val
elif col == 4:
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.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
self.emit(SIGNAL('booklist_dirtied()'))

View File

@ -373,8 +373,8 @@ class Main(MainWindow, Ui_MainWindow):
if self.delete_memory.has_key(id):
paths, model = self.delete_memory.pop(id)
self.device_manager.remove_books_from_metadata(paths, self.booklists())
self.upload_booklists()
model.paths_deleted(paths)
self.upload_booklists()
############################################################################