Pull from chaley custcols for metadata caching, connect to folder

This commit is contained in:
Kovid Goyal 2010-05-16 19:48:45 -06:00
commit 6eb25fe9e0
17 changed files with 496 additions and 501 deletions

View File

@ -455,7 +455,7 @@ from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3 from calibre.devices.teclast.driver import TECLAST_K3
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, KOBO, AVANT from calibre.devices.misc import PALMPRE, KOBO, AVANT
from calibre.devices.htc_td2.driver import HTC_TD2 from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon
from calibre.library.catalog import CSV_XML, EPUB_MOBI from calibre.library.catalog import CSV_XML, EPUB_MOBI
@ -540,7 +540,7 @@ plugins += [
PALMPRE, PALMPRE,
KOBO, KOBO,
AZBOOKA, AZBOOKA,
HTC_TD2, FOLDER_DEVICE_FOR_CONFIG,
AVANT, AVANT,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \

View File

@ -0,0 +1,96 @@
'''
Created on 15 May 2010
@author: charles
'''
import os
from calibre.devices.usbms.driver import USBMS, BookList
# This class is added to the standard device plugin chain, so that it can
# be configured. It has invalid vendor_id etc, so it will never match a
# device. The 'real' FOLDER_DEVICE will use the config from it.
class FOLDER_DEVICE_FOR_CONFIG(USBMS):
name = 'Folder Device Interface'
gui_name = 'Folder Device'
description = _('Use an arbitrary folder as a device.')
author = 'John Schember/Charles Haley'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb']
VENDOR_ID = 0xffff
PRODUCT_ID = 0xffff
BCD = 0xffff
class FOLDER_DEVICE(USBMS):
type = _('Device Interface')
name = 'Folder Device Interface'
gui_name = 'Folder Device'
description = _('Use an arbitrary folder as a device.')
author = 'John Schember/Charles Haley'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb']
VENDOR_ID = 0xffff
PRODUCT_ID = 0xffff
BCD = 0xffff
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
CAN_SET_METADATA = True
SUPPORTS_SUB_DIRS = True
#: Icon for this device
icon = I('sd.svg')
METADATA_CACHE = '.metadata.calibre'
_main_prefix = ''
_card_a_prefix = None
_card_b_prefix = None
is_connected = False
def __init__(self, path):
if not os.path.isdir(path):
raise IOError, 'Path is not a folder'
self._main_prefix = path
self.booklist_class = BookList
self.is_connected = True
@classmethod
def get_gui_name(cls):
if hasattr(cls, 'gui_name'):
return cls.gui_name
if hasattr(cls, '__name__'):
return cls.__name__
return cls.name
def disconnect_from_folder(self):
self._main_prefix = ''
self.is_connected = False
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False):
return self.is_connected, self
def open(self):
if not self._main_prefix:
return False
return True
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress
def card_prefix(self, end_session=True):
return (None, None)
def get_main_ebook_dir(self):
return ''
def eject(self):
self.is_connected = False
@classmethod
def settings(self):
return FOLDER_DEVICE_FOR_CONFIG._config().parse()

View File

@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.devices.usbms.driver import USBMS
class HTC_TD2(USBMS):
name = 'HTC TD2 Phone driver'
gui_name = 'HTC TD2'
description = _('Communicate with HTC TD2 phones.')
author = 'Charles Haley'
supported_platforms = ['osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'pdf']
VENDOR_ID = {
# HTC
0x0bb4 : { 0x0c30 : [0x000]},
}
EBOOK_DIR_MAIN = ['EBooks']
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
'send e-books to on the device. The first one that exists will '
'be used')
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
VENDOR_NAME = ['']
WINDOWS_MAIN_MEM = ['']
MAIN_MEMORY_VOLUME_LABEL = 'HTC Phone Internal Memory'
SUPPORTS_SUB_DIRS = True
def post_open_callback(self):
opts = self.settings()
dirs = opts.extra_customization
if not dirs:
dirs = self.EBOOK_DIR_MAIN
else:
dirs = [x.strip() for x in dirs.split(',')]
self.EBOOK_DIR_MAIN = dirs

View File

@ -387,6 +387,9 @@ class BookList(list):
__getslice__ = None __getslice__ = None
__setslice__ = None __setslice__ = None
def __init__(self, oncard, prefix):
pass
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. '''
raise NotImplementedError() raise NotImplementedError()

View File

@ -1,2 +1,6 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __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,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re, time, functools import re, time, functools
from uuid import uuid4 as _uuid from uuid import uuid4 as _uuid
import xml.dom.minidom as dom import xml.dom.minidom as dom
from base64 import b64decode as decode
from base64 import b64encode as encode from base64 import b64encode as encode
from calibre.devices.interface import BookList as _BookList from calibre.devices.usbms.books import BookList as _BookList
from calibre.devices import strftime as _strftime from calibre.devices import strftime as _strftime
from calibre.devices import strptime from calibre.devices.prs505 import MEDIA_XML, CACHE_XML
from calibre.devices.errors import PathError
strftime = functools.partial(_strftime, zone=time.gmtime) strftime = functools.partial(_strftime, zone=time.gmtime)
@ -30,127 +30,43 @@ def uuid():
def sortable_title(title): def sortable_title(title):
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip() 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=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)
@dynamic_property
def db_id(self):
doc = '''The database id in the application database that this file corresponds to'''
def fget(self):
match = re.search(r'_(\d+)$', self.rpath.rpartition('.')[0])
if match:
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): class BookList(_BookList):
def __init__(self, xml_file, mountpath, report_progress=None): def __init__(self, oncard, prefix, settings):
_BookList.__init__(self) _BookList.__init__(self, oncard, prefix, settings)
xml_file.seek(0) if prefix is None:
self.document = dom.parse(xml_file) return
self.sony_id_cache = {}
self.books_lpath_cache = {}
opts = settings()
self.collections = opts.extra_customization.split(',') if opts.extra_customization else []
db = CACHE_XML if oncard else MEDIA_XML
with open(prefix + db, 'rb') as xml_file:
xml_file.seek(0)
self.document = dom.parse(xml_file)
self.root_element = self.document.documentElement self.root_element = self.document.documentElement
self.mountpath = mountpath self.mountpath = prefix
records = self.root_element.getElementsByTagName('records') records = self.root_element.getElementsByTagName('records')
self.tag_order = {}
if records: if records:
self.prefix = 'xs1:' self.prefix = 'xs1:'
self.root_element = records[0] self.root_element = records[0]
else: else:
self.prefix = '' self.prefix = ''
for child in self.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
self.sony_id_cache[child.getAttribute('id')] = child.getAttribute('path')
# set the key to none. Will be filled in later when booklist is built
self.books_lpath_cache[child.getAttribute('path')] = None
self.tag_order = {}
paths = self.purge_corrupted_files()
for path in paths:
try:
self.del_file(path, end_session=False)
except PathError: # Incase this is a refetch without a sync in between
continue
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))
def max_id(self): def max_id(self):
max = 0 max = 0
@ -173,39 +89,44 @@ class BookList(_BookList):
def supports_tags(self): def supports_tags(self):
return True return True
def book_by_path(self, path): def add_book(self, book, replace_metadata):
for child in self.root_element.childNodes: # Add a node into the DOM tree, representing a book. Also add to booklist
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"): if book in self:
if path == child.getAttribute('path'): # replacing metadata for book
return child self.delete_node(book.lpath)
return None else:
self.append(book)
def add_book(self, mi, name, collections, size, ctime): if not replace_metadata:
""" Add a node into the DOM tree, representing a book """ if self.books_lpath_cache.has_key(book.lpath):
book = self.book_by_path(name) self.books_lpath_cache[book.lpath] = book
if book is not None: return
self.remove_book(name) # Book not in metadata. Add it. Note that we don't need to worry about
# extra books in the Sony metadata. The reader deletes them for us when
node = self.document.createElement(self.prefix + "text") # we disconnect. That said, if it becomes important one day, we can do
mime = MIME_MAP.get(name.rpartition('.')[-1].lower(), MIME_MAP['epub']) # it by scanning the books_lpath_cache for None entries and removing the
# corresponding nodes.
self.books_lpath_cache[book.lpath] = book
cid = self.max_id()+1 cid = self.max_id()+1
node = self.document.createElement(self.prefix + "text")
self.sony_id_cache[cid] = book.lpath
mime = MIME_MAP.get(book.lpath.rpartition('.')[-1].lower(), MIME_MAP['epub'])
try: try:
sourceid = str(self[0].sourceid) if len(self) else '1' sourceid = str(self[0].sourceid) if len(self) else '1'
except: except:
sourceid = '1' sourceid = '1'
attrs = { attrs = {
"title" : mi.title, "title" : book.title,
'titleSorter' : sortable_title(mi.title), 'titleSorter' : sortable_title(book.title),
"author" : mi.format_authors() if mi.format_authors() else _('Unknown'), "author" : book.format_authors() if book.format_authors() else _('Unknown'),
"page":"0", "part":"0", "scale":"0", \ "page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \ "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(): 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])
try: try:
w, h, data = mi.thumbnail w, h, data = book.thumbnail
except: except:
w, h, data = None, None, None w, h, data = None, None, None
@ -218,14 +139,11 @@ class BookList(_BookList):
th.appendChild(jpeg) th.appendChild(jpeg)
node.appendChild(th) node.appendChild(th)
self.root_element.appendChild(node) self.root_element.appendChild(node)
book = Book(node, self.mountpath, [], prefix=self.prefix)
book.datetime = ctime
self.append(book)
tags = [] tags = []
for item in collections: for item in self.collections:
item = item.strip() item = item.strip()
mitem = getattr(mi, item, None) mitem = getattr(book, item, None)
titems = [] titems = []
if mitem: if mitem:
if isinstance(mitem, list): if isinstance(mitem, list):
@ -241,37 +159,36 @@ class BookList(_BookList):
tags.extend(titems) tags.extend(titems)
if tags: if tags:
tags = list(set(tags)) tags = list(set(tags))
if hasattr(mi, 'tag_order'): if hasattr(book, 'tag_order'):
self.tag_order.update(mi.tag_order) self.tag_order.update(book.tag_order)
self.set_tags(book, tags) self.set_playlists(cid, tags)
return True # metadata cache has changed. Must sync at end
def _delete_book(self, node): def _delete_node(self, node):
nid = node.getAttribute('id') nid = node.getAttribute('id')
self.remove_from_playlists(nid) self.remove_from_playlists(nid)
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
node.unlink() 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. Also remove book from any collections it is part of.
''' '''
for book in self: for child in self.root_element.childNodes:
if str(book.id) == str(cid): if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
self.remove(book) if child.getAttribute('path') == lpath:
self._delete_book(book.elem) self._delete_node(child)
break break
def remove_book(self, path): def remove_book(self, book):
''' '''
Remove DOM node corresponding to book with C{path == path}. 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, and remove
from the booklist
''' '''
for book in self: self.remove(book)
if path.endswith(book.rpath): self.delete_node(book.lpath)
self.remove(book)
self._delete_book(book.elem)
break
def playlists(self): def playlists(self):
ans = [] ans = []
@ -358,15 +275,6 @@ class BookList(_BookList):
item.setAttribute('id', str(id)) item.setAttribute('id', str(id))
coll.appendChild(item) 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): def next_id(self):
return self.document.documentElement.getAttribute('nextID') return self.document.documentElement.getAttribute('nextID')
@ -378,27 +286,36 @@ class BookList(_BookList):
src = self.document.toxml('utf-8') + '\n' src = self.document.toxml('utf-8') + '\n'
stream.write(src.replace("'", '&apos;')) 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): def reorder_playlists(self):
for title in self.tag_order.keys(): for title in self.tag_order.keys():
pl = self.playlist_by_title(title) pl = self.playlist_by_title(title)
if not pl: if not pl:
continue continue
db_ids = [i.getAttribute('id') for i in pl.childNodes if hasattr(i, 'getAttribute')] # make a list of the ids
pl_book_ids = [getattr(self.book_by_id(i), 'db_id', None) for i in db_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 = [self.sony_id_cache[id] for id in sony_ids]
# create list of books containing lpaths
books = [self.books_lpath_cache.get(p, None) for p in sony_paths]
# create dict of db_id -> sony_id
imap = {} imap = {}
for i, j in zip(pl_book_ids, db_ids): for book, sony_id in zip(books, sony_ids):
imap[i] = j if book is not None:
pl_book_ids = [i for i in pl_book_ids if i is not None] db_id = book.application_id
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids] if db_id is None:
db_id = book.db_id
if db_id 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): if len(ordered_ids) < len(pl.childNodes):
continue continue
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')] children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
for child in children: for child in children:
pl.removeChild(child) pl.removeChild(child)
child.unlink() child.unlink()
@ -439,8 +356,12 @@ def fix_ids(main, carda, cardb):
except KeyError: except KeyError:
item.parentNode.removeChild(item) item.parentNode.removeChild(item)
item.unlink() item.unlink()
db.reorder_playlists() db.reorder_playlists()
db.sony_id_cache = {}
for child in db.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
db.sony_id_cache[child.getAttribute('id')] = child.getAttribute('path')
regen_ids(main) regen_ids(main)
regen_ids(carda) regen_ids(carda)

View File

@ -11,15 +11,14 @@ Device driver for the SONY PRS-505
import os import os
import re import re
import time
from itertools import cycle
from calibre.devices.usbms.cli import CLI from calibre.devices.usbms.driver import USBMS
from calibre.devices.usbms.device import Device from calibre.devices.prs505.books import BookList as PRS_BookList, fix_ids
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__ from calibre import __appname__
class PRS505(CLI, Device): class PRS505(USBMS):
name = 'PRS-300/505 Device Interface' name = 'PRS-300/505 Device Interface'
gui_name = 'SONY Reader' gui_name = 'SONY Reader'
@ -28,6 +27,8 @@ class PRS505(CLI, Device):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
path_sep = '/' path_sep = '/'
booklist_class = PRS_BookList # See USBMS for some explanation of this
FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt'] FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt']
VENDOR_ID = [0x054c] #: SONY Vendor Id VENDOR_ID = [0x054c] #: SONY Vendor Id
@ -46,9 +47,6 @@ class PRS505(CLI, Device):
MAIN_MEMORY_VOLUME_LABEL = 'Sony Reader Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'Sony Reader Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Sony Reader Storage Card' 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__ CARD_PATH_PREFIX = __appname__
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
@ -63,64 +61,9 @@ class PRS505(CLI, Device):
def windows_filter_pnp_id(self, pnp_id): def windows_filter_pnp_id(self, pnp_id):
return '_LAUNCHER' in 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): def get_device_information(self, end_session=True):
return (self.gui_name, '', '', '') 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): def filename_callback(self, fname, mi):
if getattr(mi, 'application_id', None) is not None: if getattr(mi, 'application_id', None) is not None:
base = fname.rpartition('.')[0] base = fname.rpartition('.')[0]
@ -129,90 +72,16 @@ class PRS505(CLI, Device):
fname = base + suffix + '.' + fname.rpartition('.')[-1] fname = base + suffix + '.' + fname.rpartition('.')[-1]
return fname 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): def sync_booklists(self, booklists, end_session=True):
fix_ids(*booklists) fix_ids(*booklists)
if not os.path.exists(self._main_prefix): if not os.path.exists(self._main_prefix):
os.makedirs(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) booklists[0].write(f)
def write_card_prefix(prefix, listid): def write_card_prefix(prefix, listid):
if prefix is not None and hasattr(booklists[listid], 'write'): 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) base = os.path.dirname(tgt)
if not os.path.exists(base): if not os.path.exists(base):
os.makedirs(base) os.makedirs(base)
@ -221,8 +90,7 @@ class PRS505(CLI, Device):
write_card_prefix(self._card_a_prefix, 1) write_card_prefix(self._card_a_prefix, 1)
write_card_prefix(self._card_b_prefix, 2) 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): class PRS700(PRS505):
@ -241,5 +109,3 @@ class PRS700(PRS505):
OSX_MAIN_MEM = re.compile(r'Sony PRS-((700/[^:]+)|((6|9)00)) Media') 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_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') OSX_CARD_B_MEM = re.compile(r'Sony PRS-((700/[^:]+:)|((6|9)00 ))SD Media')

View File

@ -31,14 +31,14 @@ class Book(MetaInformation):
MetaInformation.__init__(self, '') MetaInformation.__init__(self, '')
self.path = os.path.join(prefix, lpath) self.path = os.path.join(prefix, lpath)
self.lpath = 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.mime = mime_type_ext(path_to_ext(lpath))
self.size = os.stat(self.path).st_size if size == None else size self.size = None # will be set later
self.db_id = None self.datetime = time.gmtime()
try:
self.datetime = time.gmtime(os.path.getctime(self.path))
except ValueError:
self.datetime = time.gmtime()
if other: if other:
self.smart_update(other) self.smart_update(other)
@ -66,6 +66,16 @@ class Book(MetaInformation):
return spath == opath return spath == opath
@dynamic_property
def db_id(self):
doc = '''The database id in the application database that this file corresponds to'''
def fget(self):
match = re.search(r'_(\d+)$', self.lpath.rpartition('.')[0])
if match:
return int(match.group(1))
return None
return property(fget=fget, doc=doc)
@dynamic_property @dynamic_property
def title_sorter(self): def title_sorter(self):
doc = '''String to sort the title. If absent, title is returned''' doc = '''String to sort the title. If absent, title is returned'''
@ -77,13 +87,6 @@ class Book(MetaInformation):
def thumbnail(self): def thumbnail(self):
return None return None
# 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')
def smart_update(self, other): def smart_update(self, other):
''' '''
Merge the information in C{other} into self. In case of conflicts, the information Merge the information in C{other} into self. In case of conflicts, the information
@ -105,9 +108,26 @@ class Book(MetaInformation):
class BookList(_BookList): class BookList(_BookList):
def __init__(self, oncard, prefix, settings):
pass
def supports_tags(self): def supports_tags(self):
return True return True
def set_tags(self, book, tags): def set_tags(self, book, tags):
book.tags = tags book.tags = tags
def add_book(self, book, replace_metadata):
'''
Add the book to the booklist. Intent is to maintain any device-internal
metadata. Return True if booklists must be sync'ed
'''
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

@ -113,15 +113,17 @@ class Device(DeviceConfig, DevicePlugin):
def _windows_space(cls, prefix): def _windows_space(cls, prefix):
if not prefix: if not prefix:
return 0, 0 return 0, 0
if prefix.endswith(os.sep):
prefix = prefix[:-1]
win32file = __import__('win32file', globals(), locals(), [], -1) win32file = __import__('win32file', globals(), locals(), [], -1)
try: try:
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix[:-1]) win32file.GetDiskFreeSpace(prefix)
except Exception, err: except Exception, err:
if getattr(err, 'args', [None])[0] == 21: # Disk not ready if getattr(err, 'args', [None])[0] == 21: # Disk not ready
time.sleep(3) time.sleep(3)
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix[:-1]) win32file.GetDiskFreeSpace(prefix)
else: raise else: raise
mult = sectors_per_cluster * bytes_per_sector mult = sectors_per_cluster * bytes_per_sector
return total_clusters * mult, free_clusters * mult return total_clusters * mult, free_clusters * mult

View File

@ -27,6 +27,13 @@ class USBMS(CLI, Device):
author = _('John Schember') author = _('John Schember')
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
# Store type instances of BookList and Book. We must do this because
# a) we need to override these classes in some device drivers, and
# b) the classmethods seem only to see real attributes declared in the
# class, not attributes stored in the class
booklist_class = BookList
book_class = Book
FORMATS = [] FORMATS = []
CAN_SET_METADATA = True CAN_SET_METADATA = True
METADATA_CACHE = 'metadata.calibre' METADATA_CACHE = 'metadata.calibre'
@ -37,48 +44,52 @@ class USBMS(CLI, Device):
def books(self, oncard=None, end_session=True): def books(self, oncard=None, end_session=True):
from calibre.ebooks.metadata.meta import path_to_ext 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: if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return bl return []
elif oncard == 'cardb' and not self._card_b_prefix: elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return bl return []
elif oncard and oncard != 'carda' and oncard != 'cardb': elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...')) 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
prefix = self._card_a_prefix if oncard == 'carda' else self._card_b_prefix if oncard == 'cardb' else self._main_prefix
ebook_dirs = self.EBOOK_DIR_CARD_A if oncard == 'carda' else \ ebook_dirs = self.EBOOK_DIR_CARD_A if oncard == 'carda' else \
self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \ self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \
self.get_main_ebook_dir() self.get_main_ebook_dir()
# build a temporary list of books from the metadata cache
bl, need_sync = self.parse_metadata_cache(prefix, self.METADATA_CACHE) bl, need_sync = self.parse_metadata_cache(prefix, self.METADATA_CACHE)
# make a dict cache of paths so the lookup in the loop below is faster. # make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {} bl_cache = {}
for idx,b in enumerate(bl): for idx,b in enumerate(bl):
bl_cache[b.path] = idx bl_cache[b.lpath] = idx
self.count_found_in_bl = 0 self.count_found_in_bl = 0
# Make the real booklist that will be filled in below
metadata = self.booklist_class(oncard, prefix, self.settings)
def update_booklist(filename, path, prefix): def update_booklist(filename, path, prefix):
changed = False changed = False
if path_to_ext(filename) in self.FORMATS: if path_to_ext(filename) in self.FORMATS:
try: try:
lpath = os.path.join(path, filename).partition(prefix)[2] lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2]
if lpath.startswith(os.sep): if lpath.startswith(os.sep):
lpath = lpath[len(os.sep):] lpath = lpath[len(os.sep):]
p = os.path.join(prefix, lpath) idx = bl_cache.get(lpath.replace('\\', '/'), None)
if p in bl_cache: if idx is not None:
item, changed = self.__class__.update_metadata_item(bl[bl_cache[p]]) item, changed = self.update_metadata_item(bl[idx])
self.count_found_in_bl += 1 self.count_found_in_bl += 1
else: else:
item = self.__class__.book_from_path(prefix, lpath) item = self.book_from_path(prefix, lpath)
changed = True
if metadata.add_book(item, replace_metadata=False):
changed = True changed = True
metadata.append(item)
except: # Probably a filename encoding error except: # Probably a filename encoding error
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@ -87,7 +98,9 @@ class USBMS(CLI, Device):
if isinstance(ebook_dirs, basestring): if isinstance(ebook_dirs, basestring):
ebook_dirs = [ebook_dirs] ebook_dirs = [ebook_dirs]
for ebook_dir in ebook_dirs: for ebook_dir in ebook_dirs:
ebook_dir = os.path.join(prefix, *(ebook_dir.split('/'))) if ebook_dir else prefix ebook_dir = self.normalize_path( \
os.path.join(prefix, *(ebook_dir.split('/'))) \
if ebook_dir else prefix)
if not os.path.exists(ebook_dir): continue if not os.path.exists(ebook_dir): continue
# Get all books in the ebook_dir directory # Get all books in the ebook_dir directory
if self.SUPPORTS_SUB_DIRS: if self.SUPPORTS_SUB_DIRS:
@ -108,6 +121,7 @@ class USBMS(CLI, Device):
# if count != len(bl) then there were items in it that we did not # if count != len(bl) then there were items in it that we did not
# find on the device. If need_sync is True then there were either items # 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. # on the device that were not in bl or some of the items were changed.
print "count found in cache: %d, count of files in cache: %d, must_sync_cache: %s" % (self.count_found_in_bl, len(bl), need_sync)
if self.count_found_in_bl != len(bl) or need_sync: if self.count_found_in_bl != len(bl) or need_sync:
if oncard == 'cardb': if oncard == 'cardb':
self.sync_booklists((None, None, metadata)) self.sync_booklists((None, None, metadata))
@ -117,12 +131,10 @@ class USBMS(CLI, Device):
self.sync_booklists((metadata, None, None)) self.sync_booklists((metadata, None, None))
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
#print 'at return', now() - start_time
return metadata return metadata
def upload_books(self, files, names, on_card=None, end_session=True, def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None): metadata=None):
path = self._sanity_check(on_card, files) path = self._sanity_check(on_card, files)
paths = [] paths = []
@ -131,13 +143,12 @@ class USBMS(CLI, Device):
for i, infile in enumerate(files): for i, infile in enumerate(files):
mdata, fname = metadata.next(), names.next() mdata, fname = metadata.next(), names.next()
filepath = self.create_upload_path(path, mdata, fname) filepath = self.normalize_path(self.create_upload_path(path, mdata, fname))
paths.append(filepath) paths.append(filepath)
self.put_file(self.normalize_path(infile), filepath, replace_file=True)
self.put_file(infile, filepath, replace_file=True)
try: try:
self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], mdata) self.upload_cover(os.path.dirname(filepath),
os.path.splitext(os.path.basename(filepath))[0], mdata)
except: # Failure to upload cover is not catastrophic except: # Failure to upload cover is not catastrophic
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@ -145,7 +156,6 @@ class USBMS(CLI, Device):
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...')) self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
self.report_progress(1.0, _('Transferring books to device...')) self.report_progress(1.0, _('Transferring books to device...'))
return zip(paths, cycle([on_card])) return zip(paths, cycle([on_card]))
def upload_cover(self, path, filename, metadata): def upload_cover(self, path, filename, metadata):
@ -166,25 +176,32 @@ class USBMS(CLI, Device):
blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0 blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0
if self._main_prefix: if self._main_prefix:
# Normalize path and prefix
if self._main_prefix.find('\\') >= 0:
path = path.replace('/', '\\')
else:
path = path.replace('\\', '/')
prefix = self._main_prefix if path.startswith(self._main_prefix) else None prefix = self._main_prefix if path.startswith(self._main_prefix) else None
if not prefix and self._card_a_prefix: if not prefix and self._card_a_prefix:
prefix = self._card_a_prefix if path.startswith(self._card_a_prefix) else None prefix = self._card_a_prefix if path.startswith(self._card_a_prefix) else None
if not prefix and self._card_b_prefix: if not prefix and self._card_b_prefix:
prefix = self._card_b_prefix if path.startswith(self._card_b_prefix) else None prefix = self._card_b_prefix if path.startswith(self._card_b_prefix) else None
if prefix is None:
print 'in add_books_to_metadata. Prefix is None!', path, self._main_prefix
continue
lpath = path.partition(prefix)[2] lpath = path.partition(prefix)[2]
if lpath.startswith(os.sep): if lpath.startswith('/') or lpath.startswith('\\'):
lpath = lpath[len(os.sep):] lpath = lpath[1:]
book = self.book_class(prefix, lpath, other=info)
book = Book(prefix, lpath, other=info) if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size
if book not in booklists[blist]: booklists[blist].add_book(book, replace_metadata=True)
booklists[blist].append(book)
self.report_progress(1.0, _('Adding books to device metadata listing...')) self.report_progress(1.0, _('Adding books to device metadata listing...'))
def delete_books(self, paths, end_session=True): def delete_books(self, paths, end_session=True):
for i, path in enumerate(paths): for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...')) self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
path = self.normalize_path(path)
if os.path.exists(path): if os.path.exists(path):
# Delete the ebook # Delete the ebook
os.unlink(path) os.unlink(path)
@ -209,19 +226,19 @@ class USBMS(CLI, Device):
for bl in booklists: for bl in booklists:
for book in bl: for book in bl:
if path.endswith(book.path): if path.endswith(book.path):
bl.remove(book) bl.remove_book(book)
self.report_progress(1.0, _('Removing books from device metadata listing...')) self.report_progress(1.0, _('Removing books from device metadata listing...'))
def sync_booklists(self, booklists, end_session=True): def sync_booklists(self, booklists, end_session=True):
if not os.path.exists(self._main_prefix): if not os.path.exists(self.normalize_path(self._main_prefix)):
os.makedirs(self._main_prefix) os.makedirs(self.normalize_path(self._main_prefix))
def write_prefix(prefix, listid): 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): if not os.path.exists(prefix):
os.makedirs(prefix) os.makedirs(self.normalize_path(prefix))
js = [item.to_json() for item in booklists[listid]] js = [item.to_json() for item in booklists[listid]]
with open(os.path.join(prefix, self.METADATA_CACHE), 'wb') as f: with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
json.dump(js, f, indent=2, encoding='utf-8') json.dump(js, f, indent=2, encoding='utf-8')
write_prefix(self._main_prefix, 0) write_prefix(self._main_prefix, 0)
write_prefix(self._card_a_prefix, 1) write_prefix(self._card_a_prefix, 1)
@ -229,37 +246,45 @@ class USBMS(CLI, Device):
self.report_progress(1.0, _('Sending metadata to device...')) self.report_progress(1.0, _('Sending metadata to device...'))
@classmethod
def normalize_path(cls, path):
if path is None:
return None
if os.sep == '\\':
path = path.replace('/', '\\')
else:
path = path.replace('\\', '/')
return path
@classmethod @classmethod
def parse_metadata_cache(cls, prefix, name): def parse_metadata_cache(cls, prefix, name):
bl = []
js = [] js = []
bl = BookList()
need_sync = False need_sync = False
try: try:
with open(os.path.join(prefix, name), 'rb') as f: with open(cls.normalize_path(os.path.join(prefix, name)), 'rb') as f:
js = json.load(f, encoding='utf-8') js = json.load(f, encoding='utf-8')
for item in js: for item in js:
lpath = item.get('lpath', None) book = cls.book_class(prefix, item.get('lpath', None))
if not lpath or not os.path.exists(os.path.join(prefix, lpath)):
need_sync = True
continue
book = Book(prefix, lpath)
for key in item.keys(): for key in item.keys():
setattr(book, key, item[key]) setattr(book, key, item[key])
bl.append(book) bl.append(book)
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
bl = BookList() bl = []
need_sync = True
return bl, need_sync return bl, need_sync
@classmethod @classmethod
def update_metadata_item(cls, item): def update_metadata_item(cls, item):
changed = False changed = False
size = os.stat(item.path).st_size size = os.stat(cls.normalize_path(item.path)).st_size
if size != item.size: if size != item.size:
changed = True changed = True
mi = cls.metadata_from_path(item.path) mi = cls.metadata_from_path(item.path)
item.smart_update(mi) item.smart_update(mi)
item.size = size
return item, changed return item, changed
@classmethod @classmethod
@ -278,15 +303,15 @@ class USBMS(CLI, Device):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
if cls.settings().read_metadata or cls.MUST_READ_METADATA: if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(os.path.join(prefix, path)) mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path)))
else: else:
from calibre.ebooks.metadata.meta import metadata_from_filename from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(os.path.basename(path), mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)),
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+')) re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+'))
if mi is None: if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], mi = MetaInformation(os.path.splitext(os.path.basename(path))[0],
[_('Unknown')]) [_('Unknown')])
mi.size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
book = Book(prefix, path, other=mi) book = cls.book_class(prefix, path, other=mi)
return book return book

View File

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

View File

@ -25,6 +25,7 @@ from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError from calibre.devices.errors import FreeSpaceError
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \ from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
config as email_config config as email_config
from calibre.devices.folder_device.driver import FOLDER_DEVICE
class DeviceJob(BaseJob): class DeviceJob(BaseJob):
@ -207,6 +208,23 @@ class DeviceManager(Thread):
return self.create_job(self._get_device_information, done, return self.create_job(self._get_device_information, done,
description=_('Get device information')) description=_('Get device information'))
def connect_to_folder(self, path):
dev = FOLDER_DEVICE(path)
try:
dev.open()
except:
print 'Unable to open device', dev
traceback.print_exc()
return False
self.connected_device = dev
self.connected_slot(True)
return True
def disconnect_folder(self):
if self.connected_device is not None:
if hasattr(self.connected_device, 'disconnect_from_folder'):
self.connected_device.disconnect_from_folder()
def _books(self): def _books(self):
'''Get metadata from device''' '''Get metadata from device'''
mainlist = self.device.books(oncard=None, end_session=False) mainlist = self.device.books(oncard=None, end_session=False)
@ -309,6 +327,8 @@ class DeviceAction(QAction):
class DeviceMenu(QMenu): class DeviceMenu(QMenu):
fetch_annotations = pyqtSignal() fetch_annotations = pyqtSignal()
connect_to_folder = pyqtSignal()
disconnect_from_folder = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
QMenu.__init__(self, parent) QMenu.__init__(self, parent)
@ -404,6 +424,18 @@ class DeviceMenu(QMenu):
if opts.accounts: if opts.accounts:
self.addSeparator() self.addSeparator()
self.addMenu(self.email_to_menu) self.addMenu(self.email_to_menu)
self.addSeparator()
mitem = self.addAction(_('Connect to folder'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem
mitem = self.addAction(_('Disconnect from folder'))
mitem.setEnabled(False)
mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit())
self.disconnect_from_folder_action = mitem
self.addSeparator() self.addSeparator()
annot = self.addAction(_('Fetch annotations (experimental)')) annot = self.addAction(_('Fetch annotations (experimental)'))
annot.setEnabled(False) annot.setEnabled(False)
@ -523,7 +555,8 @@ class DeviceGUI(object):
d = ChooseFormatDialog(self, _('Choose format to send to device'), d = ChooseFormatDialog(self, _('Choose format to send to device'),
self.device_manager.device.settings().format_map) self.device_manager.device.settings().format_map)
d.exec_() d.exec_()
fmt = d.format().lower() if d.format():
fmt = d.format().lower()
dest, sub_dest = dest.split(':') dest, sub_dest = dest.split(':')
if dest in ('main', 'carda', 'cardb'): if dest in ('main', 'carda', 'cardb'):
if not self.device_connected or not self.device_manager: if not self.device_connected or not self.device_manager:
@ -821,7 +854,9 @@ class DeviceGUI(object):
def sync_to_device(self, on_card, delete_from_library, def sync_to_device(self, on_card, delete_from_library,
specific_format=None, send_ids=None, do_auto_convert=True): 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: if not self.device_manager or not ids or len(ids) == 0:
return return
@ -842,8 +877,7 @@ class DeviceGUI(object):
ids = iter(ids) ids = iter(ids)
for mi in metadata: for mi in metadata:
if mi.cover and os.access(mi.cover, os.R_OK): if mi.cover and os.access(mi.cover, os.R_OK):
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover, mi.thumbnail = self.cover_to_thumbnail(open(mi.cover, 'rb').read())
'rb').read())
imetadata = iter(metadata) imetadata = iter(metadata)
files = [getattr(f, 'name', None) for f in _files] files = [getattr(f, 'name', None) for f in _files]
@ -890,7 +924,9 @@ class DeviceGUI(object):
bad.append(self.library_view.model().db.title(id, index_is_id=True)) bad.append(self.library_view.model().db.title(id, index_is_id=True))
if auto != []: 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: if not format:
for fmt in settings.format_map: for fmt in settings.format_map:
if fmt in list(set(settings.format_map).intersection(set(available_output_formats()))): if fmt in list(set(settings.format_map).intersection(set(available_output_formats()))):
@ -995,83 +1031,111 @@ class DeviceGUI(object):
if changed: if changed:
self.library_view.model().refresh_ids(list(changed)) self.library_view.model().refresh_ids(list(changed))
def book_on_device(self, index, format=None, reset=False): def book_on_device(self, id, format=None, reset=False):
loc = [None, None, None] loc = [None, None, None]
if reset: if reset:
self.book_on_device_cache = None self.book_db_title_cache = None
self.book_db_uuid_cache = None
return return
if self.book_on_device_cache is None: if self.book_db_title_cache is None:
self.book_on_device_cache = [] self.book_db_title_cache = []
self.book_db_uuid_cache = []
for i, l in enumerate(self.booklists()): for i, l in enumerate(self.booklists()):
self.book_on_device_cache.append({}) self.book_db_title_cache.append({})
self.book_db_uuid_cache.append(set())
for book in l: for book in l:
book_title = book.title.lower() if book.title else '' book_title = book.title.lower() if book.title else ''
book_title = re.sub('(?u)\W|[_]', '', book_title) book_title = re.sub('(?u)\W|[_]', '', book_title)
if book_title not in self.book_on_device_cache[i]: if book_title not in self.book_db_title_cache[i]:
self.book_on_device_cache[i][book_title] = \ self.book_db_title_cache[i][book_title] = \
{'authors':set(), 'db_ids':set(), 'uuids':set()} {'authors':set(), 'db_ids':set(), 'uuids':set()}
book_authors = authors_to_string(book.authors).lower() book_authors = authors_to_string(book.authors).lower()
book_authors = re.sub('(?u)\W|[_]', '', book_authors) book_authors = re.sub('(?u)\W|[_]', '', book_authors)
self.book_on_device_cache[i][book_title]['authors'].add(book_authors) self.book_db_title_cache[i][book_title]['authors'].add(book_authors)
id = getattr(book, 'application_id', None) db_id = getattr(book, 'application_id', None)
if id is None: if db_id is None:
id = book.db_id db_id = book.db_id
if id is not None: if db_id is not None:
self.book_on_device_cache[i][book_title]['db_ids'].add(id) self.book_db_title_cache[i][book_title]['db_ids'].add(db_id)
uuid = getattr(book, 'uuid', None) uuid = getattr(book, 'uuid', None)
if uuid is None: if uuid is not None:
self.book_on_device_cache[i][book_title]['uuids'].add(uuid) self.book_db_uuid_cache[i].add(uuid)
db = self.library_view.model().db mi = self.library_view.model().db.get_metadata(id, index_is_id=True)
db_title = db.title(index, index_is_id=True).lower()
db_title = re.sub('(?u)\W|[_]', '', db_title)
db_authors = db.authors(index, index_is_id=True)
db_authors = db_authors.lower() if db_authors else ''
db_authors = re.sub('(?u)\W|[_]', '', db_authors)
db_uuid = db.uuid(index, index_is_id=True)
for i, l in enumerate(self.booklists()): for i, l in enumerate(self.booklists()):
d = self.book_on_device_cache[i].get(db_title, None) if mi.uuid in self.book_db_uuid_cache[i]:
if d: loc[i] = True
if db_uuid in d['uuids'] or \ continue
index in d['db_ids'] or \ db_title = re.sub('(?u)\W|[_]', '', mi.title.lower())
db_authors in d['authors']: cache = self.book_db_title_cache[i].get(db_title, None)
if cache:
if id in cache['db_ids']:
loc[i] = True
break
if mi.authors and \
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
in cache['authors']:
loc[i] = True loc[i] = True
break break
return loc return loc
def set_books_in_library(self, booklists, reset=False): def set_books_in_library(self, booklists, reset=False):
if reset: if reset:
# First build a self.book_in_library_cache of the library, so the search isn't On**2 # First build a cache of the library, so the search isn't On**2
self.book_in_library_cache = {} self.db_book_title_cache = {}
for id, title in self.library_view.model().db.all_titles(): self.db_book_uuid_cache = set()
title = re.sub('(?u)\W|[_]', '', title.lower()) db = self.library_view.model().db
if title not in self.book_in_library_cache: for id in db.data.iterallids():
self.book_in_library_cache[title] = {'authors':set(), 'db_ids':set(), 'uuids':set()} mi = db.get_metadata(id, index_is_id=True)
au = self.library_view.model().db.authors(id, index_is_id=True) title = re.sub('(?u)\W|[_]', '', mi.title.lower())
authors = au.lower() if au else '' if title not in self.db_book_title_cache:
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}}
authors = authors_to_string(mi.authors).lower() if mi.authors else ''
authors = re.sub('(?u)\W|[_]', '', authors) authors = re.sub('(?u)\W|[_]', '', authors)
self.book_in_library_cache[title]['authors'].add(authors) self.db_book_title_cache[title]['authors'][authors] = mi
self.book_in_library_cache[title]['db_ids'].add(id) self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
self.book_in_library_cache[title]['uuids'].add(self.library_view.model().db.uuid(id, index_is_id=True)) self.db_book_uuid_cache.add(mi.uuid)
# Now iterate through all the books on the device, setting the in_library field # Now iterate through all the books on the device, setting the
# in_library field Fastest and most accurate key is the uuid. Second is
# the application_id, which is really the db key, but as this can
# accidentally match across libraries we also verify the title. The
# db_id exists on Sony devices. Fallback is title and author match
resend_metadata = False
for booklist in booklists: for booklist in booklists:
for book in booklist: for book in booklist:
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
book.in_library = True
continue
book_title = book.title.lower() if book.title else '' book_title = book.title.lower() if book.title else ''
book_title = re.sub('(?u)\W|[_]', '', book_title) book_title = re.sub('(?u)\W|[_]', '', book_title)
book.in_library = False book.in_library = False
d = self.book_in_library_cache.get(book_title, None) d = self.db_book_title_cache.get(book_title, None)
if d is not None: if d is not None:
if getattr(book, 'uuid', None) in d['uuids'] or \ if getattr(book, 'application_id', None) in d['db_ids']:
getattr(book, 'application_id', None) in d['db_ids']:
book.in_library = True book.in_library = True
book.smart_update(d['db_ids'][book.application_id])
resend_metadata = True
continue continue
if book.db_id in d['db_ids']: if book.db_id in d['db_ids']:
book.in_library = True book.in_library = True
book.smart_update(d['db_ids'][book.db_id])
resend_metadata = True
continue continue
book_authors = authors_to_string(book.authors).lower() if book.authors else '' book_authors = authors_to_string(book.authors).lower() if book.authors else ''
book_authors = re.sub('(?u)\W|[_]', '', book_authors) book_authors = re.sub('(?u)\W|[_]', '', book_authors)
if book_authors in d['authors']: if book_authors in d['authors']:
book.in_library = True book.in_library = True
book.smart_update(d['authors'][book_authors])
resend_metadata = True
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort:
pass
if resend_metadata:
# Correcting metadata cache on device.
if self.device_manager.is_device_connected:
self.device_manager.sync_booklists(None, booklists)

View File

@ -17,7 +17,7 @@ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
SIGNAL, QObject, QSize, QModelIndex, QDate SIGNAL, QObject, QSize, QModelIndex, QDate
from calibre import strftime 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.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.gui2 import NONE, TableView, config, error_dialog, UNDEFINED_QDATE from calibre.gui2 import NONE, TableView, config, error_dialog, UNDEFINED_QDATE
from calibre.gui2.dialogs.comments_dialog import CommentsDialog from calibre.gui2.dialogs.comments_dialog import CommentsDialog
@ -371,7 +371,7 @@ class BooksModel(QAbstractTableModel):
def set_device_connected(self, is_connected): def set_device_connected(self, is_connected):
self.device_connected = is_connected self.device_connected = is_connected
self.read_config() self.read_config()
self.refresh(reset=True) self.db.refresh_ondevice()
self.database_changed.emit(self.db) self.database_changed.emit(self.db)
def set_book_on_device_func(self, func): def set_book_on_device_func(self, func):
@ -1378,7 +1378,17 @@ class DeviceBooksModel(BooksModel):
def libcmp(x, y): def libcmp(x, y):
x, y = self.db[x].in_library, self.db[y].in_library x, y = self.db[x].in_library, self.db[y].in_library
return cmp(x, y) return cmp(x, y)
fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \ def authorcmp(x, y):
ax = getattr(self.db[x], 'author_sort', None)
ay = getattr(self.db[y], 'author_sort', None)
if ax and ay:
x = ax
y = ay
else:
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 sizecmp if col == 2 else datecmp if col == 3 else tagscmp if col == 4 else libcmp
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):
@ -1446,9 +1456,9 @@ class DeviceBooksModel(BooksModel):
au = self.db[self.map[row]].authors au = self.db[self.map[row]].authors
if not au: if not au:
au = self.unknown au = self.unknown
if role == Qt.EditRole: # if role == Qt.EditRole:
return QVariant(authors_to_string(au)) # return QVariant(au)
return QVariant(" & ".join(au)) return QVariant(authors_to_string(au))
elif col == 2: elif col == 2:
size = self.db[self.map[row]].size size = self.db[self.map[row]].size
return QVariant(BooksView.human_readable(size)) return QVariant(BooksView.human_readable(size))
@ -1501,7 +1511,7 @@ class DeviceBooksModel(BooksModel):
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 = string_to_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] tags = [t for t in tags if t]

View File

@ -669,6 +669,15 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
def connect_to_folder(self):
dir = choose_dir(self, 'Select Device Folder', 'Select folder to open')
if dir is not None:
self.device_manager.connect_to_folder(dir)
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
def disconnect_from_folder(self):
self.device_manager.disconnect_folder()
def _sync_action_triggered(self, *args): def _sync_action_triggered(self, *args):
m = getattr(self, '_sync_menu', None) m = getattr(self, '_sync_menu', None)
if m is not None: if m is not None:
@ -681,6 +690,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.dispatch_sync_event) self.dispatch_sync_event)
self._sync_menu.fetch_annotations.connect(self.fetch_annotations) self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder)
def add_spare_server(self, *args): def add_spare_server(self, *args):
self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0))) self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
@ -939,6 +950,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
Called when a device is connected to the computer. Called when a device is connected to the computer.
''' '''
if connected: if connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
self.device_manager.get_device_information(\ self.device_manager.get_device_information(\
Dispatcher(self.info_read)) Dispatcher(self.info_read))
self.set_default_thumbnail(\ self.set_default_thumbnail(\
@ -952,8 +964,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.device_manager.device) self.device_manager.device)
self.location_view.model().device_connected(self.device_manager.device) self.location_view.model().device_connected(self.device_manager.device)
self.eject_action.setEnabled(True) self.eject_action.setEnabled(True)
self.refresh_ondevice_info (device_connected = True) self.refresh_ondevice_info (device_connected = True, reset_only = True)
else: else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
self.save_device_view_settings() self.save_device_view_settings()
self.device_connected = False self.device_connected = False
self._sync_menu.enable_device_actions(False) self._sync_menu.enable_device_actions(False)
@ -1022,10 +1036,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
############################################################################ ############################################################################
### Force the library view to refresh, taking into consideration books information ### Force the library view to refresh, taking into consideration books information
def refresh_ondevice_info(self, device_connected): def refresh_ondevice_info(self, device_connected, reset_only = False):
# Save current column widths because we might be turning on OnDevice
self.library_view.write_settings()
self.book_on_device(None, reset=True) self.book_on_device(None, reset=True)
if reset_only:
return
self.library_view.write_settings()
self.library_view.model().set_device_connected(device_connected) self.library_view.model().set_device_connected(device_connected)
############################################################################ ############################################################################
@ -1508,6 +1523,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
sm = view.selectionModel() sm = view.selectionModel()
sm.select(ci, sm.Select) sm.select(ci, sm.Select)
else: else:
if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> '
'from your device. Are you sure?')
+'</p>', 'library_delete_books', self):
return
if self.stack.currentIndex() == 1: if self.stack.currentIndex() == 1:
view = self.memory_view view = self.memory_view
elif self.stack.currentIndex() == 2: elif self.stack.currentIndex() == 2:

View File

@ -557,6 +557,12 @@ class ResultCache(SearchQueryParser):
def count(self): def count(self):
return len(self._map) return len(self._map)
def refresh_ondevice(self, db):
ondevice_col = self.FIELD_MAP['ondevice']
for item in self._data:
if item is not None:
item[ondevice_col] = db.book_on_device_string(item[0])
def refresh(self, db, field=None, ascending=True): def refresh(self, db, field=None, ascending=True):
temp = db.conn.get('SELECT * FROM meta2') temp = db.conn.get('SELECT * FROM meta2')
self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else [] self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else []

View File

@ -245,6 +245,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.has_id = self.data.has_id self.has_id = self.data.has_id
self.count = self.data.count self.count = self.data.count
self.refresh_ondevice = functools.partial(self.data.refresh_ondevice, self)
self.refresh() self.refresh()
self.last_update_check = self.last_modified() self.last_update_check = self.last_modified()
@ -470,14 +472,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
im = PILImage.open(f) im = PILImage.open(f)
im.convert('RGB').save(path, 'JPEG') im.convert('RGB').save(path, 'JPEG')
def book_on_device(self, index): def book_on_device(self, id):
if callable(self.book_on_device_func): if callable(self.book_on_device_func):
return self.book_on_device_func(index) return self.book_on_device_func(id)
return None return None
def book_on_device_string(self, index): def book_on_device_string(self, id):
loc = [] loc = []
on = self.book_on_device(index) on = self.book_on_device(id)
if on is not None: if on is not None:
m, a, b = on m, a, b = on
if m is not None: if m is not None: