diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 681d953c9b..1a769ea97f 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -479,6 +479,7 @@ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600 from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.kobo.driver import KOBO +from calibre.devices.bambook.driver import BAMBOOK from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \ LibraryThing @@ -598,6 +599,7 @@ plugins += [ VELOCITYMICRO, PDNOVEL_KOBO, ITUNES, + BAMBOOK, ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ x.__name__.endswith('MetadataReader')] diff --git a/src/calibre/devices/bambook/__init__.py b/src/calibre/devices/bambook/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/devices/bambook/driver.py b/src/calibre/devices/bambook/driver.py new file mode 100644 index 0000000000..77f8bacbcd --- /dev/null +++ b/src/calibre/devices/bambook/driver.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL v3' +__copyright__ = '2010, Li Fanxi ' +__docformat__ = 'restructuredtext en' + +''' +Device driver for Sanda's Bambook +''' + +import time, os, hashlib +from itertools import cycle +from calibre.devices.interface import DevicePlugin +from calibre.devices.usbms.deviceconfig import DeviceConfig +from calibre.devices.bambook.libbambookcore import Bambook, text_encoding, CONN_CONNECTED +from calibre.devices.usbms.books import Book, BookList +from calibre.ebooks.metadata.book.json_codec import JsonCodec +from calibre.ptempfile import TemporaryDirectory, TemporaryFile +from calibre.constants import __appname__, __version__ + +class BAMBOOK(DeviceConfig, DevicePlugin): + name = 'Bambook Device Interface' + description = _('Communicate with the Sanda Bambook eBook reader.') + author = _('Li Fanxi') + supported_platforms = ['windows', 'linux'] + log_packets = False + + booklist_class = BookList + book_class = Book + + FORMATS = [ "snb" ] + VENDOR_ID = 0x230b + PRODUCT_ID = 0x0001 + BCD = None + CAN_SET_METADATA = False + THUMBNAIL_HEIGHT = 155 + +# path_sep = "/" + icon = I("devices/bambook.png") +# OPEN_FEEDBACK_MESSAGE = _( +# 'Connecting to Bambook device, please wait ...') + BACKLOADING_ERROR_MESSAGE = _( + 'Unable to add book to library directly from Bambook. ' + 'Please save the book to disk and add the file to library from disk.') + + METADATA_CACHE = '.calibre.bambook' + METADATA_FILE_GUID = 'calibremetadata.snb' + + bambook = None + + def reset(self, key='-1', log_packets=False, report_progress=None, + detected_device=None) : + self.open() + + def open(self): + # Disconnect first if connected + self.eject() + # Connect + self.bambook = Bambook() + self.bambook.Connect() + if self.bambook.GetState() != CONN_CONNECTED: + self.bambook = None + raise Exception(_("Unable to connect to Bambook.")) + + def eject(self): + if self.bambook: + self.bambook.Disconnect() + self.bambook = None + + def post_yank_cleanup(self): + self.eject() + + def set_progress_reporter(self, report_progress): + ''' + :param report_progress: Function that is called with a % progress + (number between 0 and 100) for various tasks + If it is called with -1 that means that the + task does not have any progress information + + ''' + self.report_progress = report_progress + + def get_device_information(self, end_session=True): + """ + Ask device for device information. See L{DeviceInfoQuery}. + + :return: (device name, device version, software version on device, mime type) + + """ + if self.bambook: + deviceInfo = self.bambook.GetDeviceInfo() + return (_("Bambook"), "SD928", deviceInfo.firmwareVersion, "MimeType") + + + def card_prefix(self, end_session=True): + ''' + Return a 2 element list of the prefix to paths on the cards. + If no card is present None is set for the card's prefix. + E.G. + ('/place', '/place2') + (None, 'place2') + ('place', None) + (None, None) + ''' + return (None, None) + + def total_space(self, end_session=True): + """ + Get total space available on the mountpoints: + 1. Main memory + 2. Memory Card A + 3. Memory Card B + + :return: A 3 element list with total space in bytes of (1, 2, 3). If a + particular device doesn't have any of these locations it should return 0. + + """ + deviceInfo = self.bambook.GetDeviceInfo() + return (deviceInfo.deviceVolume * 1024, 0, 0) + + def free_space(self, end_session=True): + """ + Get free space available on the mountpoints: + 1. Main memory + 2. Card A + 3. Card B + + :return: A 3 element list with free space in bytes of (1, 2, 3). If a + particular device doesn't have any of these locations it should return -1. + + """ + deviceInfo = self.bambook.GetDeviceInfo() + return (deviceInfo.spareVolume * 1024, -1, -1) + + + def books(self, oncard=None, end_session=True): + """ + Return a list of ebooks on the device. + + :param oncard: If 'carda' or 'cardb' return a list of ebooks on the + specific storage card, otherwise return list of ebooks + in main memory of device. If a card is specified and no + books are on the card return empty list. + + :return: A BookList. + + """ + # Bambook has no memroy card + if oncard: + return self.booklist_class(None, None, None) + + prefix = '' + booklist = self.booklist_class(oncard, prefix, self.settings) + need_sync = self.parse_metadata_cache(booklist) + + from calibre.ebooks.metadata.book.base import Metadata + devicebooks = self.bambook.GetBookList() + books = [] + for book in devicebooks: + if book.bookGuid == self.METADATA_FILE_GUID: + continue + b = self.book_class('', book.bookGuid) + b.title = book.bookName.decode(text_encoding) + b.authors = [ book.bookAuthor.decode(text_encoding) ] + b.size = 0 + b.datatime = time.gmtime() +# b.path = book.bookGuid + b.lpath = book.bookGuid + b.thumbnail = None + b.tags = None + b.comments = book.bookAbstract.decode(text_encoding) + books.append(b) + + # make a dict cache of paths so the lookup in the loop below is faster. + bl_cache = {} + + for idx, b in enumerate(booklist): + bl_cache[b.lpath] = idx + + def update_booklist(book, prefix): + changed = False + try: + idx = bl_cache.get(book.path, None) + if idx is not None: + bl_cache[book.path] = None + if self.update_metadata_item(book, booklist[idx]): + changed = True + else: + if booklist.add_book(book, + replace_metadata=False): + changed = True + except: # Probably a filename encoding error + import traceback + traceback.print_exc() + return changed + + for i, book in enumerate(books): + self.report_progress(i/float(len(books)), _('Getting list of books on device...')) + changed = update_booklist(book, prefix) + if changed: + need_sync = True + + # Remove books that are no longer in the filesystem. Cache contains + # indices into the booklist if book not in filesystem, None otherwise + # Do the operation in reverse order so indices remain valid + for idx in sorted(bl_cache.itervalues(), reverse=True): + if idx is not None: + need_sync = True + del booklist[idx] + + if need_sync: + self.sync_booklists((booklist, None, None)) + + self.report_progress(1.0, _('Getting list of books on device...')) + return booklist + + def upload_books(self, files, names, on_card=None, end_session=True, + metadata=None): + ''' + Upload a list of books to the device. If a file already + exists on the device, it should be replaced. + This method should raise a :class:`FreeSpaceError` if there is not enough + free space on the device. The text of the FreeSpaceError must contain the + word "card" if ``on_card`` is not None otherwise it must contain the word "memory". + + :param files: A list of paths and/or file-like objects. If they are paths and + the paths point to temporary files, they may have an additional + attribute, original_file_path pointing to the originals. They may have + another optional attribute, deleted_after_upload which if True means + that the file pointed to by original_file_path will be deleted after + being uploaded to the device. + :param names: A list of file names that the books should have + once uploaded to the device. len(names) == len(files) + :param metadata: If not None, it is a list of :class:`Metadata` objects. + The idea is to use the metadata to determine where on the device to + put the book. len(metadata) == len(files). Apart from the regular + cover (path to cover), there may also be a thumbnail attribute, which should + be used in preference. The thumbnail attribute is of the form + (width, height, cover_data as jpeg). + + :return: A list of 3-element tuples. The list is meant to be passed + to :meth:`add_books_to_metadata`. + ''' + self.report_progress(0, _('Transferring books to device...')) + booklist = [] + paths = [] + if self.bambook: + for (i, f) in enumerate(files): + self.report_progress((i+1) / float(len(files)), _('Transferring books to device...')) + if not hasattr(f, 'read'): + if self.bambook.VerifySNB(f): + guid = self.bambook.SendFile(f, self.get_guid(metadata[i].uuid)) + if guid: + paths.append(guid) + else: + print "Send fail" + else: + print "book invalid" + ret = zip(paths, cycle([on_card])) + self.report_progress(1.0, _('Transferring books to device...')) + return ret + + def add_books_to_metadata(self, locations, metadata, booklists): + metadata = iter(metadata) + for i, location in enumerate(locations): + self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...')) + info = metadata.next() + + # Extract the correct prefix from the pathname. To do this correctly, + # we must ensure that both the prefix and the path are normalized + # so that the comparison will work. Book's __init__ will fix up + # lpath, so we don't need to worry about that here. + + book = self.book_class('', location[0], other=info) + if book.size is None: + book.size = 0 + b = booklists[0].add_book(book, replace_metadata=True) + if b: + b._new_book = True + self.report_progress(1.0, _('Adding books to device metadata listing...')) + + def delete_books(self, paths, end_session=True): + ''' + Delete books at paths on device. + ''' + if self.bambook: + for i, path in enumerate(paths): + self.report_progress((i+1) / float(len(paths)), _('Removing books from device...')) + self.bambook.DeleteFile(path) + self.report_progress(1.0, _('Removing books from device...')) + + def remove_books_from_metadata(self, paths, booklists): + ''' + Remove books from the metadata list. This function must not communicate + with the device. + + :param paths: paths to books on the device. + :param booklists: A tuple containing the result of calls to + (:meth:`books(oncard=None)`, + :meth:`books(oncard='carda')`, + :meth`books(oncard='cardb')`). + + ''' + for i, path in enumerate(paths): + self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...')) + for bl in booklists: + for book in bl: + if book.lpath == path: + bl.remove_book(book) + self.report_progress(1.0, _('Removing books from device metadata listing...')) + + def sync_booklists(self, booklists, end_session=True): + ''' + Update metadata on device. + + :param booklists: A tuple containing the result of calls to + (:meth:`books(oncard=None)`, + :meth:`books(oncard='carda')`, + :meth`books(oncard='cardb')`). + + ''' + if not self.bambook: + return + + json_codec = JsonCodec() + + # Create stub virtual book for sync info + with TemporaryDirectory() as tdir: + snbcdir = os.path.join(tdir, 'snbc') + snbfdir = os.path.join(tdir, 'snbf') + os.mkdir(snbcdir) + os.mkdir(snbfdir) + + f = open(os.path.join(snbfdir, 'book.snbf'), 'wb') + f.write(''' + + calibre同步信息 + calibre + ZH-CN + + calibre + ''' + __appname__ + ' ' + __version__ + ''' + + + + + +''') + f.close() + f = open(os.path.join(snbfdir, 'toc.snbf'), 'wb') + f.write(''' + + 0 + + + + +'''); + f.close() + cache_name = os.path.join(snbcdir, self.METADATA_CACHE) + with open(cache_name, 'wb') as f: + json_codec.encode_to_file(f, booklists[0]) + + with TemporaryFile('.snb') as f: + if self.bambook.PackageSNB(f, tdir): + t = open('/tmp/abcd.snb', 'wb') + t2 = open(f, 'rb') + t.write(t2.read()) + t.close() + t2.close() + if not self.bambook.SendFile(f, self.METADATA_FILE_GUID): + print "Upload failed" + + # Clear the _new_book indication, as we are supposed to be done with + # adding books at this point + for blist in booklists: + if blist is not None: + for book in blist: + book._new_book = False + + self.report_progress(1.0, _('Sending metadata to device...')) + + def get_file(self, path, outfile, end_session=True): + ''' + Read the file at ``path`` on the device and write it to outfile. + + :param outfile: file object like ``sys.stdout`` or the result of an + :func:`open` call. + + ''' + if self.bambook: + with TemporaryDirectory() as tdir: + self.bambook.GetFile(path, tdir) + filepath = os.path.join(tdir, path) + f = file(filepath, 'rb') + outfile.write(f.read()) + f.close() + + # @classmethod + # def config_widget(cls): + # ''' + # Should return a QWidget. The QWidget contains the settings for the device interface + # ''' + # raise NotImplementedError() + + # @classmethod + # def save_settings(cls, settings_widget): + # ''' + # Should save settings to disk. Takes the widget created in + # :meth:`config_widget` and saves all settings to disk. + # ''' + # raise NotImplementedError() + + # @classmethod + # def settings(cls): + # ''' + # Should return an opts object. The opts object should have at least one attribute + # `format_map` which is an ordered list of formats for the device. + # ''' + # raise NotImplementedError() + + def parse_metadata_cache(self, bl): + bl = [] + need_sync = True + if not self.bambook: + return need_sync + + # Get the metadata virtual book from Bambook + with TemporaryDirectory() as tdir: + if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir): + cache_name = os.path.join(tdir, self.METADATA_CACHE) + if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID), + 'snbc/' + self.METADATA_CACHE, + cache_name): + json_codec = JsonCodec() + if os.access(cache_name, os.R_OK): + try: + with open(cache_name, 'rb') as f: + json_codec.decode_from_file(f, bl, self.book_class, '') + need_sync = False + except: + import traceback + traceback.print_exc() + bl = [] + return need_sync + + @classmethod + def update_metadata_item(cls, book, blb): + changed = False + if book.bookName.decode(text_encoding) != blb.title: + changed = True + if book.bookAuthor.decode(text_encoding) != blb.authors[0]: + changed = True + if book.bookAbstract.decode(text_encoding) != blb.comments: + changed = True + return changed + + @staticmethod + def get_guid(uuid): + guid = hashlib.md5(uuid).hexdigest()[0:15] + ".snb" + return guid diff --git a/src/calibre/devices/bambook/libbambookcore.py b/src/calibre/devices/bambook/libbambookcore.py new file mode 100644 index 0000000000..21b25302be --- /dev/null +++ b/src/calibre/devices/bambook/libbambookcore.py @@ -0,0 +1,491 @@ +# -*- coding: utf-8 -*- + +__license__ = 'GPL v3' +__copyright__ = '2010, Li Fanxi ' +__docformat__ = 'restructuredtext en' + +''' +Sanda library wrapper +''' + +import ctypes, uuid, hashlib +from threading import Event, Thread, Lock +from calibre.constants import iswindows, islinux + +try: + if iswindows: + text_encoding = 'mbcs' + lib_handle = ctypes.cdll.BambookCore + elif islinux: + text_encoding = 'utf-8' + lib_handle = ctypes.CDLL('libBambookCore.so') +except: + lib_handle = None + +# Constant +DEFAULT_BAMBOOK_IP = '192.168.250.2' +BAMBOOK_SDK_VERSION = 0x00090000 +BR_SUCC = 0 # 操作成功 +BR_FAIL = 1001 # 操作失败 +BR_NOT_IMPL = 1002 # 该功能还未实现 +BR_DISCONNECTED = 1003 # 与设备的连接已断开 +BR_PARAM_ERROR = 1004 # 调用函数传入的参数错误 +BR_TIMEOUT = 1005 # 操作或通讯超时 +BR_INVALID_HANDLE = 1006 # 传入的句柄无效 +BR_INVALID_FILE = 1007 # 传入的文件不存在或格式无效 +BR_INVALID_DIR = 1008 # 传入的目录不存在 +BR_BUSY = 1010 # 设备忙,另一个操作还未完成 +BR_EOF = 1011 # 文件或操作已结束 +BR_IO_ERROR = 1012 # 文件读写失败 +BR_FILE_NOT_INSIDE = 1013 # 指定的文件不在包里 + +# 当前连接状态 +CONN_CONNECTED = 0 # 已连接 +CONN_DISCONNECTED = 1 # 未连接或连接已断开 +CONN_CONNECTING = 2 # 正在连接 +CONN_WAIT_FOR_AUTH = 3 # 已连接,正在等待身份验证(暂未实现) + +#传输状态 +TRANS_STATUS_TRANS = 0 #正在传输 +TRANS_STATUS_DONE = 1 #传输完成 +TRANS_STATUS_ERR = 2 #传输出错 + +# Key Enums +BBKeyNum0 = 0 +BBKeyNum1 = 1 +BBKeyNum2 = 2 +BBKeyNum3 = 3 +BBKeyNum4 = 4 +BBKeyNum5 = 5 +BBKeyNum6 = 6 +BBKeyNum7 = 7 +BBKeyNum8 = 8 +BBKeyNum9 = 9 +BBKeyStar = 10 +BBKeyCross = 11 +BBKeyUp = 12 +BBKeyDown = 13 +BBKeyLeft = 14 +BBKeyRight = 15 +BBKeyPageUp = 16 +BBKeyPageDown = 17 +BBKeyOK = 18 +BBKeyESC = 19 +BBKeyBookshelf = 20 +BBKeyStore = 21 +BBKeyTTS = 22 +BBKeyMenu = 23 +BBKeyInteract =24 + +class DeviceInfo(ctypes.Structure): + _fields_ = [ ("cbSize", ctypes.c_int), + ("sn", ctypes.c_char * 20), + ("firmwareVersion", ctypes.c_char * 20), + ("deviceVolume", ctypes.c_int), + ("spareVolume", ctypes.c_int), + ] + def __init__(self): + self.cbSize = ctypes.sizeof(self) + +class PrivBookInfo(ctypes.Structure): + _fields_ = [ ("cbSize", ctypes.c_int), + ("bookGuid", ctypes.c_char * 20), + ("bookName", ctypes.c_char * 80), + ("bookAuthor", ctypes.c_char * 40), + ("bookAbstract", ctypes.c_char * 256), + ] + def Clone(self): + bookInfo = PrivBookInfo() + bookInfo.cbSize = self.cbSize + bookInfo.bookGuid = self.bookGuid + bookInfo.bookName = self.bookName + bookInfo.bookAuthor = self.bookAuthor + bookInfo.bookAbstract = self.bookAbstract + return bookInfo + + def __init__(self): + self.cbSize = ctypes.sizeof(self) + +# extern "C"_declspec(dllexport) BB_RESULT BambookConnect(const char* lpszIP, int timeOut, BB_HANDLE* hConn); +def BambookConnect(ip = DEFAULT_BAMBOOK_IP, timeout = 0): + handle = ctypes.c_int(0) + if lib_handle == None: + raise Exception(_('Bambook SDK has not been installed.')) + ret = lib_handle.BambookConnect(ip, timeout, ctypes.byref(handle)) + if ret == BR_SUCC: + return handle + else: + return None + +# extern "C" _declspec(dllexport) BB_RESULT BambookGetConnectStatus(BB_HANDLE hConn, int* status); +def BambookGetConnectStatus(handle): + status = ctypes.c_int(0) + ret = lib_handle.BambookGetConnectStatus(handle, ctypes.byref(status)) + if ret == BR_SUCC: + return status.value + else: + return None + +# extern "C" _declspec(dllexport) BB_RESULT BambookDisconnect(BB_HANDLE hConn); +def BambookDisconnect(handle): + ret = lib_handle.BambookDisconnect(handle) + if ret == BR_SUCC: + return True + else: + return False + +# extern "C" const char * BambookGetErrorString(BB_RESULT nCode) +def BambookGetErrorString(code): + func = lib_handle.BambookGetErrorString + func.restype = c_char_p + return func(code) + + +# extern "C" BB_RESULT BambookGetSDKVersion(uint32_t * version); +def BambookGetSDKVersion(): + version = ctypes.c_int(0) + lib_handle.BambookGetSDKVersion(ctypes.byref(version)) + return version.value + +# extern "C" BB_RESULT BambookGetDeviceInfo(BB_HANDLE hConn, DeviceInfo* pInfo); +def BambookGetDeviceInfo(handle): + deviceInfo = DeviceInfo() + ret = lib_handle.BambookGetDeviceInfo(handle, ctypes.byref(deviceInfo)) + if ret == BR_SUCC: + return deviceInfo + else: + return None + + +# extern "C" BB_RESULT BambookKeyPress(BB_HANDLE hConn, BambookKey key); +def BambookKeyPress(handle, key): + ret = lib_handle.BambookKeyPress(handle, key) + if ret == BR_SUCC: + return True + else: + return False + +# extern "C" BB_RESULT BambookGetFirstPrivBookInfo(BB_HANDLE hConn, PrivBookInfo * pInfo); +def BambookGetFirstPrivBookInfo(handle, bookInfo): + bookInfo.contents.cbSize = ctypes.sizeof(bookInfo.contents) + ret = lib_handle.BambookGetFirstPrivBookInfo(handle, bookInfo) + if ret == BR_SUCC: + return True + else: + return False + +# extern "C" BB_RESULT BambookGetNextPrivBookInfo(BB_HANDLE hConn, PrivBookInfo * pInfo); +def BambookGetNextPrivBookInfo(handle, bookInfo): + bookInfo.contents.cbSize = ctypes.sizeof(bookInfo.contents) + ret = lib_handle.BambookGetNextPrivBookInfo(handle, bookInfo) + if ret == BR_SUCC: + return True + elif ret == BR_EOF: + return False + else: + return False + +# extern "C" BB_RESULT BambookDeletePrivBook(BB_HANDLE hConn, const char * lpszBookID); +def BambookDeletePrivBook(handle, guid): + ret = lib_handle.BambookDeletePrivBook(handle, guid) + if ret == BR_SUCC: + return True + else: + return False + +class JobQueue: + jobs = {} + maxID = 0 + lock = Lock() + def __init__(self): + self.maxID = 0 + + def NewJob(self): + self.lock.acquire() + self.maxID = self.maxID + 1 + maxid = self.maxID + self.lock.release() + event = Event() + self.jobs[maxid] = (event, TRANS_STATUS_TRANS) + return maxid + + def FinishJob(self, jobID, status): + self.jobs[jobID][0].set() + self.jobs[jobID] = (self.jobs[jobID][0], status) + + def WaitJob(self, jobID): + self.jobs[jobID][0].wait() + return (self.jobs[jobID][1] == TRANS_STATUS_DONE) + + def DeleteJob(self, jobID): + del self.jobs[jobID] + +job = JobQueue() + +def BambookTransferCallback(status, progress, userData): + if status == TRANS_STATUS_DONE and progress == 100: + job.FinishJob(userData, status) + elif status == TRANS_STATUS_ERR: + job.FinishJob(userData, status) + +TransCallback = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int, ctypes.c_int) +bambookTransferCallback = TransCallback(BambookTransferCallback) + +# extern "C" BB_RESULT BambookAddPrivBook(BB_HANDLE hConn, const char * pszSnbFile, +# TransCallback pCallbackFunc, intptr_t userData); +def BambookAddPrivBook(handle, filename, callback, userData): + if isinstance(filename, unicode): + filename = filename.encode('ascii') + ret = lib_handle.BambookAddPrivBook(handle, filename, callback, userData) + if ret == BR_SUCC: + return True + else: + return False + +# extern "C" BB_RESULT BambookReplacePrivBook(BB_HANDLE hConn, const char * +# pszSnbFile, const char * lpszBookID, TransCallback pCallbackFunc, intptr_t userData); +def BambookReplacePrivBook(handle, filename, bookID, callback, userData): + if isinstance(filename, unicode): + filename = filename.encode('ascii') + ret = lib_handle.BambookReplacePrivBook(handle, filename, bookID, callback, userData) + if ret == BR_SUCC: + return True + else: + return False + +# extern "C" BB_RESULT BambookFetchPrivBook(BB_HANDLE hConn, const char * +# lpszBookID, const char * lpszFilePath, TransCallback pCallbackFunc, intptr_t userData); +def BambookFetchPrivBook(handle, bookID, filename, callback, userData): + if isinstance(filename, unicode): + filename = filename.encode('ascii') + ret = lib_handle.BambookFetchPrivBook(handle, bookID, filename, bambookTransferCallback, userData) + if ret == BR_SUCC: + return True + else: + return False + +# extern "C" BB_RESULT BambookVerifySnbFile(const char * snbName) +def BambookVerifySnbFile(filename): + if isinstance(filename, unicode): + filename = filename.encode('ascii') + if lib_handle.BambookVerifySnbFile(filename) == BR_SUCC: + return True + else: + return False + +# BB_RESULT BambookPackSnbFromDir ( const char * snbName,, const char * rootDir ); +def BambookPackSnbFromDir(snbFileName, rootDir): + ret = lib_handle.BambookPackSnbFromDir(snbFileName, rootDir) + if ret == BR_SUCC: + return True + else: + return False + +# BB_RESULT BambookUnpackFileFromSnb ( const char * snbName,, const char * relativePath, const char * outfname ); +def BambookUnpackFileFromSnb(snbFileName, relPath, outFileName): + ret = lib_handle.BambookUnpackFileFromSnb(snbFileName, relPath, outFileName) + if ret == BR_SUCC: + return True + else: + return False + +class Bambook: + def __init__(self): + self.handle = None + + def Connect(self, ip = DEFAULT_BAMBOOK_IP, timeout = 10000): + self.handle = BambookConnect(ip, timeout) + if self.handle and self.handle != 0: + return True + else: + return False + + def Disconnect(self): + if self.handle: + return BambookDisconnect(self.handle) + return False + + def GetState(self): + if self.handle: + return BambookGetConnectStatus(self.handle) + return CONN_DISCONNECTED + + def GetDeviceInfo(self): + if self.handle: + return BambookGetDeviceInfo(self.handle) + return None + + def SendFile(self, fileName, guid = None): + if self.handle: + taskID = job.NewJob() + if guid: + if BambookReplacePrivBook(self.handle, fileName, guid, + bambookTransferCallback, taskID): + if(job.WaitJob(taskID)): + job.DeleteJob(taskID) + return guid + else: + job.DeleteJob(taskID) + return None + else: + job.DeleteJob(taskID) + return None + else: + guid = hashlib.md5(str(uuid.uuid4())).hexdigest()[0:15] + ".snb" + if BambookReplacePrivBook(self.handle, fileName, guid, + bambookTransferCallback, taskID): + if job.WaitJob(taskID): + job.DeleteJob(taskID) + return guid + else: + job.DeleteJob(taskID) + return None + else: + job.DeleteJob(taskID) + return None + return False + + def GetFile(self, guid, fileName): + if self.handle: + taskID = job.NewJob() + ret = BambookFetchPrivBook(self.handle, guid, fileName, bambookTransferCallback, taskID) + if ret: + ret = job.WaitJob(taskID) + job.DeleteJob(taskID) + return ret + else: + job.DeleteJob(taskID) + return False + return False + + def DeleteFile(self, guid): + if self.handle: + ret = BambookDeletePrivBook(self.handle, guid) + return ret + return False + + def GetBookList(self): + if self.handle: + books = [] + bookInfo = PrivBookInfo() + bi = ctypes.pointer(bookInfo) + + ret = BambookGetFirstPrivBookInfo(self.handle, bi) + while ret: + books.append(bi.contents.Clone()) + ret = BambookGetNextPrivBookInfo(self.handle, bi) + return books + + @staticmethod + def GetSDKVersion(): + return BambookGetSDKVersion() + + @staticmethod + def VerifySNB(fileName): + return BambookVerifySnbFile(fileName); + + @staticmethod + def ExtractSNBContent(fileName, relPath, path): + return BambookUnpackFileFromSnb(fileName, relPath, path) + + @staticmethod + def ExtractSNB(fileName, path): + ret = BambookUnpackFileFromSnb(fileName, 'snbf/book.snbf', path + '/snbf/book.snbf') + if not ret: + return False + ret = BambookUnpackFileFromSnb(fileName, 'snbf/toc.snbf', path + '/snbf/toc.snbf') + if not ret: + return False + + return True + + @staticmethod + def PackageSNB(fileName, path): + return BambookPackSnbFromDir(fileName, path) + +def passed(): + print "> Pass" + +def failed(): + print "> Failed" + +if __name__ == "__main__": + + print "Bambook SDK Unit Test" + bb = Bambook() + + print "Disconnect State" + if bb.GetState() == CONN_DISCONNECTED: + passed() + else: + failed() + + print "Get SDK Version" + if bb.GetSDKVersion() == BAMBOOK_SDK_VERSION: + passed() + else: + failed() + + print "Verify SNB File" + if bb.VerifySNB(u'/tmp/f2pioq3qf68h475.snb'): + passed() + else: + failed() + + if not bb.VerifySNB('./libwrapper.py'): + passed() + else: + failed() + + print "Extract SNB File" + if bb.ExtractSNB('./test.snb', '/tmp'): + passed() + else: + failed() + + print "Packet SNB File" + if bb.PackageSNB('/tmp/tmp.snb', '/tmp/test') and bb.VerifySNB('/tmp/tmp.snb'): + passed() + else: + failed() + + print "Connect to Bambook" + if bb.Connect('192.168.250.2', 10000) and bb.GetState() == CONN_CONNECTED: + passed() + else: + failed() + + print "Get Bambook Info" + devInfo = bb.GetDeviceInfo() + if devInfo: +# print "Info Size: ", devInfo.cbSize +# print "SN: ", devInfo.sn +# print "Firmware: ", devInfo.firmwareVersion +# print "Capacity: ", devInfo.deviceVolume +# print "Free: ", devInfo.spareVolume + if devInfo.cbSize == 52 and devInfo.deviceVolume == 1714232: + passed() + else: + failed() + + print "Send file" + bb.SendFile('./test.snb') + + print "Get book list" + books = bb.GetBookList() + if len(books) > 10: + passed() + else: + failed() + + print "Get book" + if bb.GetFile('f2pioq3qf68h475.snb', '/tmp') and bb.VerifySNB('/tmp/f2pioq3qf68h475.snb'): + passed() + else: + failed() + + print "Disconnect" + if bb.Disconnect(): + passed() + else: + failed() diff --git a/src/calibre/devices/bambook/test.snb b/src/calibre/devices/bambook/test.snb new file mode 100644 index 0000000000..e4f9a833d6 Binary files /dev/null and b/src/calibre/devices/bambook/test.snb differ