Working JSON metadata along side Sony metadata

This commit is contained in:
Charles Haley 2010-05-13 21:57:54 +01:00
parent cd6c46dba5
commit d1c040d546
9 changed files with 142 additions and 319 deletions

View File

@ -387,6 +387,9 @@ class BookList(list):
__getslice__ = None
__setslice__ = None
def __init__(self, oncard, prefix):
pass
def supports_tags(self):
''' Return True if the the device supports tags (collections) for this book list. '''
raise NotImplementedError()
@ -399,3 +402,17 @@ class BookList(list):
'''
raise NotImplementedError()
def add_book(self, book, collections=None):
'''
Add the book to the booklist. Intent is to maintain any device-internal
metadata
'''
if book not in self:
self.append(book)
def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the
same time
'''
self.remove(book)

View File

@ -1,2 +1,6 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
MEDIA_XML = 'database/cache/media.xml'
CACHE_XML = 'Sony Reader/database/cache.xml'

View File

@ -5,13 +5,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, time, functools
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 calibre.devices.interface import BookList as _BookList
from calibre.devices import strftime as _strftime
from calibre.devices import strptime
from calibre.devices.usbms.books import Book as _Book
from calibre.devices.prs505 import MEDIA_XML
from calibre.devices.prs505 import CACHE_XML
strftime = functools.partial(_strftime, zone=time.gmtime)
@ -50,62 +51,7 @@ class book_metadata_field(object):
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=lambda x : int(float(x)))
# When setting this attribute you must use an epoch
datetime = book_metadata_field("date", formatter=strptime, setter=strftime)
@dynamic_property
def title_sorter(self):
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)
@dynamic_property
def thumbnail(self):
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)
@dynamic_property
def path(self):
doc = """ Absolute path to book on device. Setting not supported. """
def fget(self):
return self.mountpath + self.rpath
return property(fget=fget, doc=doc)
class Book(_Book):
@dynamic_property
def db_id(self):
doc = '''The database id in the application database that this file corresponds to'''
@ -115,42 +61,26 @@ class Book(object):
return int(match.group(1))
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, report_progress=None):
_BookList.__init__(self)
def __init__(self, oncard, prefix):
_BookList.__init__(self, oncard, prefix)
if prefix is None:
return
db = CACHE_XML if oncard else MEDIA_XML
xml_file = open(prefix + db, 'rb')
xml_file.seek(0)
self.document = dom.parse(xml_file)
self.root_element = self.document.documentElement
self.mountpath = mountpath
self.mountpath = prefix
records = self.root_element.getElementsByTagName('records')
self.tag_order = {}
if records:
self.prefix = 'xs1:'
self.root_element = records[0]
else:
self.prefix = ''
nodes = self.root_element.childNodes
for i, book in enumerate(nodes):
if report_progress:
report_progress((i+1) / float(len(nodes)), _('Getting list of books on device...'))
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))
self.tag_order = {}
def max_id(self):
max = 0
@ -180,32 +110,32 @@ class BookList(_BookList):
return child
return None
def add_book(self, mi, name, collections, size, ctime):
def add_book(self, book, collections):
if book in self:
return
""" Add a node into the DOM tree, representing a book """
book = self.book_by_path(name)
if book is not None:
self.remove_book(name)
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP.get(name.rpartition('.')[-1].lower(), MIME_MAP['epub'])
mime = MIME_MAP.get(book.lpath.rpartition('.')[-1].lower(), MIME_MAP['epub'])
cid = self.max_id()+1
book.sony_id = cid
self.append(book)
try:
sourceid = str(self[0].sourceid) if len(self) else '1'
except:
sourceid = '1'
attrs = {
"title" : mi.title,
'titleSorter' : sortable_title(mi.title),
"author" : mi.format_authors() if mi.format_authors() else _('Unknown'),
"title" : book.title,
'titleSorter' : sortable_title(book.title),
"author" : book.format_authors() if book.format_authors() else _('Unknown'),
"page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size)
"mime":mime, "path":book.lpath, "size":str(book.size)
}
for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr])
try:
w, h, data = mi.thumbnail
w, h, data = book.thumbnail
except:
w, h, data = None, None, None
@ -218,14 +148,11 @@ class BookList(_BookList):
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)
tags = []
for item in collections:
item = item.strip()
mitem = getattr(mi, item, None)
mitem = getattr(book, item, None)
titems = []
if mitem:
if isinstance(mitem, list):
@ -241,37 +168,34 @@ class BookList(_BookList):
tags.extend(titems)
if tags:
tags = list(set(tags))
if hasattr(mi, 'tag_order'):
self.tag_order.update(mi.tag_order)
self.set_tags(book, tags)
if hasattr(book, 'tag_order'):
self.tag_order.update(book.tag_order)
self.set_playlists(cid, tags)
def _delete_book(self, node):
def _delete_node(self, node):
nid = node.getAttribute('id')
self.remove_from_playlists(nid)
node.parentNode.removeChild(node)
node.unlink()
def delete_book(self, cid):
def delete_node(self, lpath):
'''
Remove DOM node corresponding to book with C{id == cid}.
Remove DOM node corresponding to book with lpath.
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)
for child in self.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
if child.getAttribute('path') == lpath:
self._delete_node(child)
break
def remove_book(self, path):
def remove_book(self, book):
'''
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.rpath):
self.remove(book)
self._delete_book(book.elem)
break
self.delete_node(book.lpath)
def playlists(self):
ans = []
@ -343,11 +267,6 @@ class BookList(_BookList):
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):
@ -358,15 +277,6 @@ class BookList(_BookList):
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')
@ -378,24 +288,38 @@ class BookList(_BookList):
src = self.document.toxml('utf-8') + '\n'
stream.write(src.replace("'", '&apos;'))
def book_by_id(self, id):
for book in self:
if str(book.id) == str(id):
return book
def reorder_playlists(self):
sony_id_cache = {}
for child in self.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
sony_id_cache[child.getAttribute('id')] = child.getAttribute('path')
books_lpath_cache = {}
for book in self:
books_lpath_cache[book.lpath] = book
for title in self.tag_order.keys():
pl = self.playlist_by_title(title)
if not pl:
continue
db_ids = [i.getAttribute('id') for i in pl.childNodes if hasattr(i, 'getAttribute')]
pl_book_ids = [getattr(self.book_by_id(i), 'db_id', None) for i in db_ids]
# make a list of the ids
sony_ids = [id.getAttribute('id') \
for id in pl.childNodes if hasattr(id, 'getAttribute')]
# convert IDs in playlist to a list of lpaths
sony_paths = [sony_id_cache[id] for id in sony_ids]
# create list of books containing lpaths
books = [books_lpath_cache.get(p, None) for p in sony_paths]
# create dict of db_id -> sony_id
imap = {}
for i, j in zip(pl_book_ids, db_ids):
imap[i] = j
pl_book_ids = [i for i in pl_book_ids if i is not None]
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids]
for book, sony_id in zip(books, sony_ids):
if book is not None:
imap[book.application_id] = sony_id
# filter the list, removing books not on device but on playlist
books = [i for i in books if i is not None]
# filter the order specification to the books we have
ordered_ids = [db_id for db_id in self.tag_order[title] if db_id in imap]
# rewrite the playlist in the correct order
if len(ordered_ids) < len(pl.childNodes):
continue
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
@ -439,7 +363,6 @@ def fix_ids(main, carda, cardb):
except KeyError:
item.parentNode.removeChild(item)
item.unlink()
db.reorder_playlists()
regen_ids(main)

View File

@ -11,15 +11,14 @@ Device driver for the SONY PRS-505
import os
import re
import time
from itertools import cycle
from calibre.devices.usbms.cli import CLI
from calibre.devices.usbms.device import Device
from calibre.devices.usbms.driver import USBMS
from calibre.devices.prs505.books import BookList, fix_ids
from calibre.devices.prs505 import MEDIA_XML
from calibre.devices.prs505 import CACHE_XML
from calibre import __appname__
class PRS505(CLI, Device):
class PRS505(USBMS):
name = 'PRS-300/505 Device Interface'
gui_name = 'SONY Reader'
@ -46,9 +45,6 @@ class PRS505(CLI, Device):
MAIN_MEMORY_VOLUME_LABEL = 'Sony Reader Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Sony Reader Storage Card'
MEDIA_XML = 'database/cache/media.xml'
CACHE_XML = 'Sony Reader/database/cache.xml'
CARD_PATH_PREFIX = __appname__
SUPPORTS_SUB_DIRS = True
@ -60,67 +56,18 @@ class PRS505(CLI, Device):
'series, tags, authors'
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
METADATA_CACHE = "database/cache/metadata.calibre"
def initialize(self):
USBMS.initialize(self)
self.booklist_class = BookList
def windows_filter_pnp_id(self, pnp_id):
return '_LAUNCHER' in pnp_id
def open(self):
self.report_progress = lambda x, y: x
Device.open(self)
def write_cache(prefix):
try:
cachep = os.path.join(prefix, *(self.CACHE_XML.split('/')))
if not os.path.exists(cachep):
dname = os.path.dirname(cachep)
if not os.path.exists(dname):
try:
os.makedirs(dname, mode=0777)
except:
time.sleep(5)
os.makedirs(dname, mode=0777)
with open(cachep, 'wb') as f:
f.write(u'''<?xml version="1.0" encoding="UTF-8"?>
<cache xmlns="http://www.kinoma.com/FskCache/1">
</cache>
'''.encode('utf8'))
return True
except:
import traceback
traceback.print_exc()
return False
if self._card_a_prefix is not None:
if not write_cache(self._card_a_prefix):
self._card_a_prefix = None
if self._card_b_prefix is not None:
if not write_cache(self._card_b_prefix):
self._card_b_prefix = None
def get_device_information(self, end_session=True):
return (self.gui_name, '', '', '')
def books(self, oncard=None, end_session=True):
if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return []
elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return []
elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...'))
return []
db = self.__class__.CACHE_XML if oncard else self.__class__.MEDIA_XML
prefix = self._card_a_prefix if oncard == 'carda' else self._card_b_prefix if oncard == 'cardb' else self._main_prefix
bl = BookList(open(prefix + db, 'rb'), prefix, self.report_progress)
paths = bl.purge_corrupted_files()
for path in paths:
path = os.path.join(prefix, path)
if os.path.exists(path):
os.unlink(path)
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]
@ -129,90 +76,17 @@ class PRS505(CLI, Device):
fname = base + suffix + '.' + fname.rpartition('.')[-1]
return fname
def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None):
path = self._sanity_check(on_card, files)
paths, ctimes, sizes = [], [], []
names = iter(names)
metadata = iter(metadata)
for i, infile in enumerate(files):
mdata, fname = metadata.next(), names.next()
filepath = self.create_upload_path(path, mdata, fname)
paths.append(filepath)
self.put_file(infile, paths[-1], replace_file=True)
ctimes.append(os.path.getctime(paths[-1]))
sizes.append(os.stat(paths[-1]).st_size)
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
self.report_progress(1.0, _('Transferring books to device...'))
return zip(paths, sizes, ctimes, cycle([on_card]))
def add_books_to_metadata(self, locations, metadata, booklists):
if not locations or not metadata:
return
metadata = iter(metadata)
for location in locations:
info = metadata.next()
path = location[0]
oncard = location[3]
blist = 2 if oncard == 'cardb' else 1 if oncard == 'carda' else 0
if self._main_prefix and path.startswith(self._main_prefix):
name = path.replace(self._main_prefix, '')
elif self._card_a_prefix and path.startswith(self._card_a_prefix):
name = path.replace(self._card_a_prefix, '')
elif self._card_b_prefix and path.startswith(self._card_b_prefix):
name = path.replace(self._card_b_prefix, '')
name = name.replace('\\', '/')
name = name.replace('//', '/')
if name.startswith('/'):
name = name[1:]
opts = self.settings()
collections = opts.extra_customization.split(',') if opts.extra_customization else []
booklist = booklists[blist]
if not hasattr(booklist, 'add_book'):
raise ValueError(('Incorrect upload location %s. Did you choose the'
' correct card A or B, to send books to?')%oncard)
booklist.add_book(info, name, collections, *location[1:-1])
fix_ids(*booklists)
def delete_books(self, paths, end_session=True):
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
if os.path.exists(path):
os.unlink(path)
try:
os.removedirs(os.path.dirname(path))
except:
pass
self.report_progress(1.0, _('Removing books from device...'))
@classmethod
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)
def sync_booklists(self, booklists, end_session=True):
print 'in sync_booklists'
fix_ids(*booklists)
if not os.path.exists(self._main_prefix):
os.makedirs(self._main_prefix)
with open(self._main_prefix + self.__class__.MEDIA_XML, 'wb') as f:
with open(self._main_prefix + MEDIA_XML, 'wb') as f:
booklists[0].write(f)
def write_card_prefix(prefix, listid):
if prefix is not None and hasattr(booklists[listid], 'write'):
tgt = os.path.join(prefix, *(self.CACHE_XML.split('/')))
tgt = os.path.join(prefix, *(CACHE_XML.split('/')))
base = os.path.dirname(tgt)
if not os.path.exists(base):
os.makedirs(base)
@ -221,8 +95,7 @@ class PRS505(CLI, Device):
write_card_prefix(self._card_a_prefix, 1)
write_card_prefix(self._card_b_prefix, 2)
self.report_progress(1.0, _('Sending metadata to device...'))
USBMS.sync_booklists(self, booklists, end_session)
class PRS700(PRS505):
@ -241,5 +114,3 @@ class PRS700(PRS505):
OSX_MAIN_MEM = re.compile(r'Sony PRS-((700/[^:]+)|((6|9)00)) Media')
OSX_CARD_A_MEM = re.compile(r'Sony PRS-((700/[^:]+:)|((6|9)00 ))MS Media')
OSX_CARD_B_MEM = re.compile(r'Sony PRS-((700/[^:]+:)|((6|9)00 ))SD Media')

View File

@ -33,6 +33,8 @@ class Book(MetaInformation):
self.path = os.path.join(prefix, lpath)
if os.sep == '\\':
self.path = self.path.replace('/', '\\')
self.lpath = lpath.replace('\\', '/')
else:
self.lpath = lpath
self.mime = mime_type_ext(path_to_ext(lpath))
self.size = os.stat(self.path).st_size if size == None else size

View File

@ -31,32 +31,36 @@ class USBMS(CLI, Device):
CAN_SET_METADATA = True
METADATA_CACHE = 'metadata.calibre'
def initialize(self):
Device.initialize(self)
self.booklist_class = BookList
def get_device_information(self, end_session=True):
self.report_progress(1.0, _('Get device information...'))
return (self.get_gui_name(), '', '', '')
def books(self, oncard=None, end_session=True):
from calibre.ebooks.metadata.meta import path_to_ext
bl = BookList()
metadata = BookList()
need_sync = False
if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return bl
return []
elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return bl
return []
elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...'))
return bl
return []
prefix = self._card_a_prefix if oncard == 'carda' else self._card_b_prefix if oncard == 'cardb' else self._main_prefix
metadata = self.booklist_class(oncard, prefix)
ebook_dirs = self.EBOOK_DIR_CARD_A if oncard == 'carda' else \
self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \
self.get_main_ebook_dir()
bl, need_sync = self.parse_metadata_cache(prefix, self.METADATA_CACHE)
bl, need_sync = self.parse_metadata_cache(prefix, self.METADATA_CACHE,
self.booklist_class(oncard, prefix))
# make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {}
@ -109,7 +113,6 @@ class USBMS(CLI, Device):
# find on the device. If need_sync is True then there were either items
# on the device that were not in bl or some of the items were changed.
if self.count_found_in_bl != len(bl) or need_sync:
print 'resync'
if oncard == 'cardb':
self.sync_booklists((None, None, metadata))
elif oncard == 'carda':
@ -122,7 +125,6 @@ class USBMS(CLI, Device):
def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None):
path = self._sanity_check(on_card, files)
paths = []
@ -145,7 +147,6 @@ class USBMS(CLI, Device):
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
self.report_progress(1.0, _('Transferring books to device...'))
return zip(paths, cycle([on_card]))
def upload_cover(self, path, filename, metadata):
@ -174,11 +175,10 @@ class USBMS(CLI, Device):
lpath = path.partition(prefix)[2]
if lpath.startswith(os.sep):
lpath = lpath[len(os.sep):]
lpath = lpath.replace('\\', '/')
book = Book(prefix, lpath, other=info)
if book not in booklists[blist]:
booklists[blist].append(book)
opts = self.settings()
collections = opts.extra_customization.split(',') if opts.extra_customization else []
booklists[blist].add_book(book, collections, *location[1:-1])
self.report_progress(1.0, _('Adding books to device metadata listing...'))
@ -209,7 +209,7 @@ class USBMS(CLI, Device):
for bl in booklists:
for book in bl:
if path.endswith(book.path):
bl.remove(book)
bl.remove_book(book)
self.report_progress(1.0, _('Removing books from device metadata listing...'))
def sync_booklists(self, booklists, end_session=True):
@ -217,7 +217,7 @@ class USBMS(CLI, Device):
os.makedirs(self._main_prefix)
def write_prefix(prefix, listid):
if prefix is not None and isinstance(booklists[listid], BookList):
if prefix is not None and isinstance(booklists[listid], self.booklist_class):
if not os.path.exists(prefix):
os.makedirs(prefix)
js = [item.to_json() for item in booklists[listid]]
@ -230,9 +230,8 @@ class USBMS(CLI, Device):
self.report_progress(1.0, _('Sending metadata to device...'))
@classmethod
def parse_metadata_cache(cls, prefix, name):
def parse_metadata_cache(cls, prefix, name, bl):
js = []
bl = BookList()
need_sync = False
try:
with open(os.path.join(prefix, name), 'rb') as f:
@ -249,7 +248,7 @@ class USBMS(CLI, Device):
except:
import traceback
traceback.print_exc()
bl = BookList()
bl = []
return bl, need_sync
@classmethod

View File

@ -254,11 +254,11 @@ class MetaInformation(object):
setattr(self, x, getattr(mi, x, None))
def print_all_attributes(self):
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language',
for x in ('author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
'rights', 'publication_type', 'uuid',
'rights', 'publication_type', 'uuid', 'tag_order',
):
prints(x, getattr(self, x, 'None'))
@ -278,7 +278,7 @@ class MetaInformation(object):
'isbn', 'application_id', 'manifest', 'spine', 'toc',
'cover', 'language', 'guide', 'book_producer',
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
'publication_type', 'uuid',):
'publication_type', 'uuid', 'tag_order'):
if hasattr(mi, attr):
val = getattr(mi, attr)
if val is not None:

View File

@ -821,7 +821,9 @@ class DeviceGUI(object):
def sync_to_device(self, on_card, delete_from_library,
specific_format=None, send_ids=None, do_auto_convert=True):
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
ids = [self.library_view.model().id(r) \
for r in self.library_view.selectionModel().selectedRows()] \
if send_ids is None else send_ids
if not self.device_manager or not ids or len(ids) == 0:
return
@ -842,8 +844,7 @@ class DeviceGUI(object):
ids = iter(ids)
for mi in metadata:
if mi.cover and os.access(mi.cover, os.R_OK):
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
'rb').read())
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover, 'rb').read())
imetadata = iter(metadata)
files = [getattr(f, 'name', None) for f in _files]
@ -890,7 +891,9 @@ class DeviceGUI(object):
bad.append(self.library_view.model().db.title(id, index_is_id=True))
if auto != []:
format = specific_format if specific_format in list(set(settings.format_map).intersection(set(available_output_formats()))) else None
format = specific_format if specific_format in \
list(set(settings.format_map).intersection(set(available_output_formats()))) \
else None
if not format:
for fmt in settings.format_map:
if fmt in list(set(settings.format_map).intersection(set(available_output_formats()))):
@ -1039,7 +1042,8 @@ class DeviceGUI(object):
loc[i] = True
break
if mi.authors and \
re.sub('(?u)\W|[_]', '', mi.authors.lower()) in cache['authors']:
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
in cache['authors']:
loc[i] = True
break
return loc

View File

@ -17,7 +17,7 @@ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
SIGNAL, QObject, QSize, QModelIndex, QDate
from calibre import strftime
from calibre.ebooks.metadata import fmt_sidx, authors_to_string
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.gui2 import NONE, TableView, config, error_dialog, UNDEFINED_QDATE
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
@ -1378,7 +1378,10 @@ class DeviceBooksModel(BooksModel):
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 \
def authorcmp(x, y):
x, y = authors_to_string(self.db[x].authors), authors_to_string(self.db[y].authors)
return cmp(x, y)
fcmp = strcmp('title_sorter') if col == 0 else authorcmp if col == 1 else \
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):
@ -1446,9 +1449,9 @@ class DeviceBooksModel(BooksModel):
au = self.db[self.map[row]].authors
if not au:
au = self.unknown
if role == Qt.EditRole:
# if role == Qt.EditRole:
# return QVariant(au)
return QVariant(authors_to_string(au))
return QVariant(" & ".join(au))
elif col == 2:
size = self.db[self.map[row]].size
return QVariant(BooksView.human_readable(size))
@ -1501,7 +1504,7 @@ class DeviceBooksModel(BooksModel):
self.db[idx].title = val
self.db[idx].title_sorter = val
elif col == 1:
self.db[idx].authors = val
self.db[idx].authors = string_to_authors(val)
elif col == 4:
tags = [i.strip() for i in val.split(',')]
tags = [t for t in tags if t]