diff --git a/recipes/business_spectator.recipe b/recipes/business_spectator.recipe index ef58424c6c..9ed3f1f7ac 100644 --- a/recipes/business_spectator.recipe +++ b/recipes/business_spectator.recipe @@ -16,6 +16,7 @@ class BusinessSpectator(BasicNewsRecipe): oldest_article = 2 max_articles_per_feed = 100 no_stylesheets = True + auto_cleanup = True #delay = 1 use_embedded_content = False encoding = 'utf8' @@ -32,11 +33,11 @@ class BusinessSpectator(BasicNewsRecipe): ,'linearize_tables': False } - keep_only_tags = [dict(id='storyHeader'), dict(id='body-html')] + #keep_only_tags = [dict(id='storyHeader'), dict(id='body-html')] - remove_tags = [dict(attrs={'class':'hql'})] + #remove_tags = [dict(attrs={'class':'hql'})] - remove_attributes = ['width','height','style'] + #remove_attributes = ['width','height','style'] feeds = [ ('Top Stories', 'http://www.businessspectator.com.au/top-stories.rss'), @@ -46,3 +47,4 @@ class BusinessSpectator(BasicNewsRecipe): ('Daily Dossier', 'http://www.businessspectator.com.au/bs.nsf/RSS?readform&type=kgb&cat=dossier'), ('Australia', 'http://www.businessspectator.com.au/bs.nsf/RSS?readform&type=region&cat=australia'), ] + diff --git a/setup/extensions.py b/setup/extensions.py index bc6e41089e..2df62636ae 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -169,7 +169,15 @@ if iswindows: cflags=['/X'] ), Extension('wpd', - ['calibre/devices/mtp/windows/wpd.cpp'], + [ + 'calibre/devices/mtp/windows/utils.cpp', + 'calibre/devices/mtp/windows/device_enumeration.cpp', + 'calibre/devices/mtp/windows/device.cpp', + 'calibre/devices/mtp/windows/wpd.cpp', + ], + headers=[ + 'calibre/devices/mtp/windows/global.h', + ], libraries=['ole32', 'portabledeviceguids'], # needs_ddk=True, cflags=['/X'] @@ -291,7 +299,8 @@ class Build(Command): self.obj_dir = os.path.join(os.path.dirname(SRC), 'build', 'objects') if not os.path.exists(self.obj_dir): os.makedirs(self.obj_dir) - self.build_style(self.j(self.SRC, 'calibre', 'plugins')) + if not opts.only: + self.build_style(self.j(self.SRC, 'calibre', 'plugins')) for ext in extensions: if opts.only != 'all' and opts.only != ext.name: continue diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py index 1552361a56..3b801c190e 100644 --- a/setup/installer/linux/freeze2.py +++ b/setup/installer/linux/freeze2.py @@ -38,7 +38,7 @@ binary_includes = [ '/lib/libz.so.1', '/usr/lib/libtiff.so.5', '/lib/libbz2.so.1', - '/usr/lib/libpoppler.so.25', + '/usr/lib/libpoppler.so.27', '/usr/lib/libxml2.so.2', '/usr/lib/libopenjpeg.so.2', '/usr/lib/libxslt.so.1', diff --git a/setup/installer/osx/app/main.py b/setup/installer/osx/app/main.py index 504f7fc49a..5268041359 100644 --- a/setup/installer/osx/app/main.py +++ b/setup/installer/osx/app/main.py @@ -379,7 +379,7 @@ class Py2App(object): @flush def add_poppler(self): info('\nAdding poppler') - for x in ('libpoppler.26.dylib',): + for x in ('libpoppler.27.dylib',): self.install_dylib(os.path.join(SW, 'lib', x)) for x in ('pdftohtml', 'pdftoppm', 'pdfinfo'): self.install_dylib(os.path.join(SW, 'bin', x), False) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index c4d2675bda..b63b8785eb 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -28,7 +28,8 @@ isosx = 'darwin' in _plat isnewosx = isosx and getattr(sys, 'new_app_bundle', False) isfreebsd = 'freebsd' in _plat isnetbsd = 'netbsd' in _plat -isbsd = isfreebsd or isnetbsd +isdragonflybsd = 'dragonfly' in _plat +isbsd = isfreebsd or isnetbsd or isdragonflybsd islinux = not(iswindows or isosx or isbsd) isfrozen = hasattr(sys, 'frozen') isunix = isosx or islinux @@ -215,3 +216,13 @@ def get_windows_temp_path(): ans = buf.value return ans if ans else None +def get_windows_user_locale_name(): + import ctypes + k32 = ctypes.windll.kernel32 + n = 200 + buf = ctypes.create_unicode_buffer(u'\0'*n) + n = k32.GetUserDefaultLocaleName(buf, n) + if n == 0: + return None + return u'_'.join(buf.value.split(u'-')[:2]) + diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 12e7f40301..f9efe3354e 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -87,7 +87,7 @@ class ANDROID(USBMS): # Google 0x18d1 : { - 0x0001 : [0x0223, 0x9999], + 0x0001 : [0x0223, 0x230, 0x9999], 0x0003 : [0x0230], 0x4e11 : [0x0100, 0x226, 0x227], 0x4e12 : [0x0100, 0x226, 0x227], @@ -196,7 +196,7 @@ class ANDROID(USBMS): 'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON', 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', - 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI'] + 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', @@ -214,7 +214,8 @@ class ANDROID(USBMS): 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', - 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID'] + 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', + 'S5830I_CARD', 'MID7042'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', @@ -224,7 +225,7 @@ class ANDROID(USBMS): 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875', 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', - 'USB_FLASH_DRIVER', 'ANDROID'] + 'USB_FLASH_DRIVER', 'ANDROID', 'MID7042'] OSX_MAIN_MEM = 'Android Device Main Memory' diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index c448bf809d..9b8c7b9d42 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -92,6 +92,7 @@ class ControlError(ProtocolError): def __init__(self, query=None, response=None, desc=None): self.query = query self.response = response + self.desc = desc ProtocolError.__init__(self, desc) def __str__(self): diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 7f20b70b39..91dc0bf6ef 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -39,6 +39,7 @@ class MTPDeviceBase(DevicePlugin): def __init__(self, *args, **kwargs): DevicePlugin.__init__(self, *args, **kwargs) self.progress_reporter = None + self.current_friendly_name = None def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): @@ -47,3 +48,7 @@ class MTPDeviceBase(DevicePlugin): def set_progress_reporter(self, report_progress): self.progress_reporter = report_progress + def get_gui_name(self): + return self.current_friendly_name or self.name + + diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index e5c5415ce8..c94a2e2458 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -14,7 +14,7 @@ from collections import deque, OrderedDict from io import BytesIO from calibre import prints -from calibre.devices.errors import OpenFailed +from calibre.devices.errors import OpenFailed, DeviceError from calibre.devices.mtp.base import MTPDeviceBase, synchronous from calibre.devices.mtp.unix.detect import MTPDetect @@ -102,11 +102,6 @@ class MTP_DEVICE(MTPDeviceBase): if self.progress_reporter is not None: self.progress_reporter(p) - @synchronous - def get_gui_name(self): - if self.dev is None or not self.dev.friendly_name: return self.name - return self.dev.friendly_name - @synchronous def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): @@ -134,7 +129,7 @@ class MTP_DEVICE(MTPDeviceBase): @synchronous def post_yank_cleanup(self): - self.dev = self.filesystem_cache = None + self.dev = self.filesystem_cache = self.current_friendly_name = None @synchronous def startup(self): @@ -184,15 +179,18 @@ class MTP_DEVICE(MTPDeviceBase): self._carda_id = storage[1]['id'] if len(storage) > 2: self._cardb_id = storage[2]['id'] + self.current_friendly_name = self.dev.name + @synchronous + def read_filesystem_cache(self): try: files, errs = self.dev.get_filelist(self) if errs and not files: - raise OpenFailed('Failed to read files from device. Underlying errors:\n' + raise DeviceError('Failed to read files from device. Underlying errors:\n' +self.format_errorstack(errs)) folders, errs = self.dev.get_folderlist() if errs and not folders: - raise OpenFailed('Failed to read folders from device. Underlying errors:\n' + raise DeviceError('Failed to read folders from device. Underlying errors:\n' +self.format_errorstack(errs)) self.filesystem_cache = FilesystemCache(files, folders) except: @@ -202,15 +200,15 @@ class MTP_DEVICE(MTPDeviceBase): @synchronous def get_device_information(self, end_session=True): d = self.dev - return (d.friendly_name, d.device_version, d.device_version, '') + return (self.current_friendly_name, d.device_version, d.device_version, '') @synchronous def card_prefix(self, end_session=True): ans = [None, None] if self._carda_id is not None: - ans[0] = 'mtp:%d:'%self._carda_id + ans[0] = 'mtp:::%d:::'%self._carda_id if self._cardb_id is not None: - ans[1] = 'mtp:%d:'%self._cardb_id + ans[1] = 'mtp:::%d:::'%self._cardb_id return tuple(ans) @synchronous @@ -248,6 +246,7 @@ if __name__ == '__main__': devs = linux_scanner() mtp_devs = dev.detect(devs) dev.open(list(mtp_devs)[0], 'xxx') + dev.read_filesystem_cache() d = dev.dev print ("Opened device:", dev.get_gui_name()) print ("Storage info:") diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index 58e8df6686..7ea987782a 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -1,3 +1,11 @@ +/* + * libmtp.c + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + + #define UNICODE #include diff --git a/src/calibre/devices/mtp/windows/__init__.py b/src/calibre/devices/mtp/windows/__init__.py new file mode 100644 index 0000000000..743a9d0561 --- /dev/null +++ b/src/calibre/devices/mtp/windows/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + diff --git a/src/calibre/devices/mtp/windows/device.cpp b/src/calibre/devices/mtp/windows/device.cpp new file mode 100644 index 0000000000..98038764a7 --- /dev/null +++ b/src/calibre/devices/mtp/windows/device.cpp @@ -0,0 +1,137 @@ +/* + * device.cpp + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "global.h" + +extern IPortableDevice* wpd::open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information); +extern IPortableDeviceValues* wpd::get_client_information(); +extern PyObject* wpd::get_device_information(IPortableDevice *device); + +using namespace wpd; +// Device.__init__() {{{ +static void +dealloc(Device* self) +{ + if (self->pnp_id != NULL) free(self->pnp_id); + self->pnp_id = NULL; + + if (self->device != NULL) { + Py_BEGIN_ALLOW_THREADS; + self->device->Close(); self->device->Release(); + self->device = NULL; + Py_END_ALLOW_THREADS; + } + + if (self->client_information != NULL) { self->client_information->Release(); self->client_information = NULL; } + + Py_XDECREF(self->device_information); self->device_information = NULL; + + self->ob_type->tp_free((PyObject*)self); +} + +static int +init(Device *self, PyObject *args, PyObject *kwds) +{ + PyObject *pnp_id; + int ret = -1; + + if (!PyArg_ParseTuple(args, "O", &pnp_id)) return -1; + + self->pnp_id = unicode_to_wchar(pnp_id); + if (self->pnp_id == NULL) return -1; + + self->client_information = get_client_information(); + if (self->client_information != NULL) { + self->device = open_device(self->pnp_id, self->client_information); + if (self->device != NULL) { + self->device_information = get_device_information(self->device); + if (self->device_information != NULL) ret = 0; + } + } + + return ret; +} + +// }}} + +// update_device_data() {{{ +static PyObject* +update_data(Device *self, PyObject *args, PyObject *kwargs) { + PyObject *di = NULL; + di = get_device_information(self->device); + if (di == NULL) return NULL; + Py_XDECREF(self->device_information); self->device_information = di; + Py_RETURN_NONE; +} // }}} + +static PyMethodDef Device_methods[] = { + {"update_data", (PyCFunction)update_data, METH_VARARGS, + "update_data() -> Reread the basic device data from the device (total, space, free space, storage locations, etc.)" + }, + + {NULL} +}; + +// Device.data {{{ +static PyObject * +Device_data(Device *self, void *closure) { + Py_INCREF(self->device_information); return self->device_information; +} // }}} + + +static PyGetSetDef Device_getsetters[] = { + {(char *)"data", + (getter)Device_data, NULL, + (char *)"The basic device information.", + NULL}, + + {NULL} /* Sentinel */ +}; + + +PyTypeObject wpd::DeviceType = { // {{{ + PyObject_HEAD_INIT(NULL) + 0, /*ob_size*/ + "wpd.Device", /*tp_name*/ + sizeof(Device), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "Device", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + Device_methods, /* tp_methods */ + 0, /* tp_members */ + Device_getsetters, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)init, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; // }}} + diff --git a/src/calibre/devices/mtp/windows/device_enumeration.cpp b/src/calibre/devices/mtp/windows/device_enumeration.cpp new file mode 100644 index 0000000000..775464d4c8 --- /dev/null +++ b/src/calibre/devices/mtp/windows/device_enumeration.cpp @@ -0,0 +1,349 @@ +/* + * device_enumeration.cpp + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "global.h" + +namespace wpd { + +IPortableDeviceValues *get_client_information() { // {{{ + IPortableDeviceValues *client_information; + HRESULT hr; + + ENSURE_WPD(NULL); + + Py_BEGIN_ALLOW_THREADS; + hr = CoCreateInstance(CLSID_PortableDeviceValues, NULL, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&client_information)); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to create IPortableDeviceValues", hr); return NULL; } + + Py_BEGIN_ALLOW_THREADS; + hr = client_information->SetStringValue(WPD_CLIENT_NAME, client_info.name); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to set client name", hr); return NULL; } + Py_BEGIN_ALLOW_THREADS; + hr = client_information->SetUnsignedIntegerValue(WPD_CLIENT_MAJOR_VERSION, client_info.major_version); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to set major version", hr); return NULL; } + Py_BEGIN_ALLOW_THREADS; + hr = client_information->SetUnsignedIntegerValue(WPD_CLIENT_MINOR_VERSION, client_info.minor_version); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to set minor version", hr); return NULL; } + Py_BEGIN_ALLOW_THREADS; + hr = client_information->SetUnsignedIntegerValue(WPD_CLIENT_REVISION, client_info.revision); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to set revision", hr); return NULL; } + // Some device drivers need to impersonate the caller in order to function correctly. Since our application does not + // need to restrict its identity, specify SECURITY_IMPERSONATION so that we work with all devices. + Py_BEGIN_ALLOW_THREADS; + hr = client_information->SetUnsignedIntegerValue(WPD_CLIENT_SECURITY_QUALITY_OF_SERVICE, SECURITY_IMPERSONATION); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) { hresult_set_exc("Failed to set quality of service", hr); return NULL; } + return client_information; +} // }}} + +IPortableDevice *open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information) { // {{{ + IPortableDevice *device = NULL; + HRESULT hr; + + Py_BEGIN_ALLOW_THREADS; + hr = CoCreateInstance(CLSID_PortableDevice, NULL, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&device)); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) hresult_set_exc("Failed to create IPortableDevice", hr); + else { + Py_BEGIN_ALLOW_THREADS; + hr = device->Open(pnp_id, client_information); + Py_END_ALLOW_THREADS; + if FAILED(hr) { + Py_BEGIN_ALLOW_THREADS; + device->Release(); + Py_END_ALLOW_THREADS; + device = NULL; + hresult_set_exc((hr == E_ACCESSDENIED) ? "Read/write access to device is denied": "Failed to open device", hr); + } + } + + return device; + +} // }}} + +PyObject* get_storage_info(IPortableDevice *device) { // {{{ + HRESULT hr, hr2; + IPortableDeviceContent *content = NULL; + IEnumPortableDeviceObjectIDs *objects = NULL; + IPortableDeviceProperties *properties = NULL; + IPortableDeviceKeyCollection *storage_properties = NULL; + IPortableDeviceValues *values = NULL; + PyObject *ans = NULL, *storage = NULL, *so = NULL, *desc = NULL, *soid = NULL; + DWORD fetched, i; + PWSTR object_ids[10]; + GUID guid; + ULONGLONG capacity, free_space, capacity_objects, free_objects; + ULONG access; + LPWSTR storage_desc = NULL; + + storage = PyList_New(0); + if (storage == NULL) { PyErr_NoMemory(); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = device->Content(&content); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to get content interface from device", hr); goto end;} + + Py_BEGIN_ALLOW_THREADS; + hr = content->Properties(&properties); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to get properties interface", hr); goto end;} + + Py_BEGIN_ALLOW_THREADS; + hr = CoCreateInstance(CLSID_PortableDeviceKeyCollection, NULL, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&storage_properties)); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to create storage properties collection", hr); goto end;} + + Py_BEGIN_ALLOW_THREADS; + hr = storage_properties->Add(WPD_OBJECT_CONTENT_TYPE); + hr = storage_properties->Add(WPD_FUNCTIONAL_OBJECT_CATEGORY); + hr = storage_properties->Add(WPD_STORAGE_DESCRIPTION); + hr = storage_properties->Add(WPD_STORAGE_CAPACITY); + hr = storage_properties->Add(WPD_STORAGE_CAPACITY_IN_OBJECTS); + hr = storage_properties->Add(WPD_STORAGE_FREE_SPACE_IN_BYTES); + hr = storage_properties->Add(WPD_STORAGE_FREE_SPACE_IN_OBJECTS); + hr = storage_properties->Add(WPD_STORAGE_ACCESS_CAPABILITY); + hr = storage_properties->Add(WPD_STORAGE_FILE_SYSTEM_TYPE); + hr = storage_properties->Add(WPD_OBJECT_NAME); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to create collection of properties for storage query", hr); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = content->EnumObjects(0, WPD_DEVICE_OBJECT_ID, NULL, &objects); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to get objects from device", hr); goto end;} + + hr = S_OK; + while (hr == S_OK) { + Py_BEGIN_ALLOW_THREADS; + hr = objects->Next(10, object_ids, &fetched); + Py_END_ALLOW_THREADS; + if (SUCCEEDED(hr)) { + for(i = 0; i < fetched; i++) { + Py_BEGIN_ALLOW_THREADS; + hr2 = properties->GetValues(object_ids[i], storage_properties, &values); + Py_END_ALLOW_THREADS; + if SUCCEEDED(hr2) { + if ( + SUCCEEDED(values->GetGuidValue(WPD_OBJECT_CONTENT_TYPE, &guid)) && IsEqualGUID(guid, WPD_CONTENT_TYPE_FUNCTIONAL_OBJECT) && + SUCCEEDED(values->GetGuidValue(WPD_FUNCTIONAL_OBJECT_CATEGORY, &guid)) && IsEqualGUID(guid, WPD_FUNCTIONAL_CATEGORY_STORAGE) + ) { + capacity = 0; capacity_objects = 0; free_space = 0; free_objects = 0; + values->GetUnsignedLargeIntegerValue(WPD_STORAGE_CAPACITY, &capacity); + values->GetUnsignedLargeIntegerValue(WPD_STORAGE_CAPACITY_IN_OBJECTS, &capacity_objects); + values->GetUnsignedLargeIntegerValue(WPD_STORAGE_FREE_SPACE_IN_BYTES, &free_space); + values->GetUnsignedLargeIntegerValue(WPD_STORAGE_FREE_SPACE_IN_OBJECTS, &free_objects); + desc = Py_False; + if (SUCCEEDED(values->GetUnsignedIntegerValue(WPD_STORAGE_ACCESS_CAPABILITY, &access)) && access == WPD_STORAGE_ACCESS_CAPABILITY_READWRITE) desc = Py_True; + soid = PyUnicode_FromWideChar(object_ids[i], wcslen(object_ids[i])); + if (soid == NULL) { PyErr_NoMemory(); goto end; } + so = Py_BuildValue("{s:K,s:K,s:K,s:K,s:O,s:N}", + "capacity", capacity, "capacity_objects", capacity_objects, "free_space", free_space, "free_objects", free_objects, "rw", desc, "id", soid); + if (so == NULL) { PyErr_NoMemory(); goto end; } + if (SUCCEEDED(values->GetStringValue(WPD_STORAGE_DESCRIPTION, &storage_desc))) { + desc = PyUnicode_FromWideChar(storage_desc, wcslen(storage_desc)); + if (desc != NULL) { PyDict_SetItemString(so, "description", desc); Py_DECREF(desc);} + CoTaskMemFree(storage_desc); storage_desc = NULL; + } + if (SUCCEEDED(values->GetStringValue(WPD_OBJECT_NAME, &storage_desc))) { + desc = PyUnicode_FromWideChar(storage_desc, wcslen(storage_desc)); + if (desc != NULL) { PyDict_SetItemString(so, "name", desc); Py_DECREF(desc);} + CoTaskMemFree(storage_desc); storage_desc = NULL; + } + if (SUCCEEDED(values->GetStringValue(WPD_STORAGE_FILE_SYSTEM_TYPE, &storage_desc))) { + desc = PyUnicode_FromWideChar(storage_desc, wcslen(storage_desc)); + if (desc != NULL) { PyDict_SetItemString(so, "filesystem", desc); Py_DECREF(desc);} + CoTaskMemFree(storage_desc); storage_desc = NULL; + } + PyList_Append(storage, so); + Py_DECREF(so); + } + } + } + } + } + ans = storage; + +end: + if (content != NULL) content->Release(); + if (objects != NULL) objects->Release(); + if (properties != NULL) properties->Release(); + if (storage_properties != NULL) storage_properties->Release(); + if (values != NULL) values->Release(); + return ans; +} // }}} + +PyObject* get_device_information(IPortableDevice *device) { // {{{ + IPortableDeviceContent *content = NULL; + IPortableDeviceProperties *properties = NULL; + IPortableDeviceKeyCollection *keys = NULL; + IPortableDeviceValues *values = NULL; + IPortableDeviceCapabilities *capabilities = NULL; + IPortableDevicePropVariantCollection *categories = NULL; + HRESULT hr; + DWORD num_of_categories, i; + LPWSTR temp; + ULONG ti; + PyObject *t, *ans = NULL, *storage = NULL; + char *type; + + Py_BEGIN_ALLOW_THREADS; + hr = CoCreateInstance(CLSID_PortableDeviceKeyCollection, NULL, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&keys)); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to create IPortableDeviceKeyCollection", hr); goto end;} + + Py_BEGIN_ALLOW_THREADS; + hr = keys->Add(WPD_DEVICE_PROTOCOL); + // Despite the MSDN documentation, this does not exist in PortableDevice.h + // hr = keys->Add(WPD_DEVICE_TRANSPORT); + hr = keys->Add(WPD_DEVICE_FRIENDLY_NAME); + hr = keys->Add(WPD_DEVICE_MANUFACTURER); + hr = keys->Add(WPD_DEVICE_MODEL); + hr = keys->Add(WPD_DEVICE_SERIAL_NUMBER); + hr = keys->Add(WPD_DEVICE_FIRMWARE_VERSION); + hr = keys->Add(WPD_DEVICE_TYPE); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to add keys to IPortableDeviceKeyCollection", hr); goto end;} + + Py_BEGIN_ALLOW_THREADS; + hr = device->Content(&content); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to get IPortableDeviceContent", hr); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = content->Properties(&properties); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) {hresult_set_exc("Failed to get IPortableDeviceProperties", hr); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = properties->GetValues(WPD_DEVICE_OBJECT_ID, keys, &values); + Py_END_ALLOW_THREADS; + if(FAILED(hr)) {hresult_set_exc("Failed to get device info", hr); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = device->Capabilities(&capabilities); + Py_END_ALLOW_THREADS; + if(FAILED(hr)) {hresult_set_exc("Failed to get device capabilities", hr); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = capabilities->GetFunctionalCategories(&categories); + Py_END_ALLOW_THREADS; + if(FAILED(hr)) {hresult_set_exc("Failed to get device functional categories", hr); goto end; } + + Py_BEGIN_ALLOW_THREADS; + hr = categories->GetCount(&num_of_categories); + Py_END_ALLOW_THREADS; + if(FAILED(hr)) {hresult_set_exc("Failed to get device functional categories number", hr); goto end; } + + ans = PyDict_New(); + if (ans == NULL) {PyErr_NoMemory(); goto end;} + + if (SUCCEEDED(values->GetStringValue(WPD_DEVICE_PROTOCOL, &temp))) { + t = PyUnicode_FromWideChar(temp, wcslen(temp)); + if (t != NULL) {PyDict_SetItemString(ans, "protocol", t); Py_DECREF(t);} + CoTaskMemFree(temp); + } + + // if (SUCCEEDED(values->GetUnsignedIntegerValue(WPD_DEVICE_TRANSPORT, &ti))) { + // PyDict_SetItemString(ans, "isusb", (ti == WPD_DEVICE_TRANSPORT_USB) ? Py_True : Py_False); + // t = PyLong_FromUnsignedLong(ti); + // } + + if (SUCCEEDED(values->GetUnsignedIntegerValue(WPD_DEVICE_TYPE, &ti))) { + switch (ti) { + case WPD_DEVICE_TYPE_CAMERA: + type = "camera"; break; + case WPD_DEVICE_TYPE_MEDIA_PLAYER: + type = "media player"; break; + case WPD_DEVICE_TYPE_PHONE: + type = "phone"; break; + case WPD_DEVICE_TYPE_VIDEO: + type = "video"; break; + case WPD_DEVICE_TYPE_PERSONAL_INFORMATION_MANAGER: + type = "personal information manager"; break; + case WPD_DEVICE_TYPE_AUDIO_RECORDER: + type = "audio recorder"; break; + default: + type = "unknown"; + } + t = PyString_FromString(type); + if (t != NULL) { + PyDict_SetItemString(ans, "type", t); Py_DECREF(t); + } + } + + if (SUCCEEDED(values->GetStringValue(WPD_DEVICE_FRIENDLY_NAME, &temp))) { + t = PyUnicode_FromWideChar(temp, wcslen(temp)); + if (t != NULL) {PyDict_SetItemString(ans, "friendly_name", t); Py_DECREF(t);} + CoTaskMemFree(temp); + } + + if (SUCCEEDED(values->GetStringValue(WPD_DEVICE_MANUFACTURER, &temp))) { + t = PyUnicode_FromWideChar(temp, wcslen(temp)); + if (t != NULL) {PyDict_SetItemString(ans, "manufacturer_name", t); Py_DECREF(t);} + CoTaskMemFree(temp); + } + + if (SUCCEEDED(values->GetStringValue(WPD_DEVICE_MODEL, &temp))) { + t = PyUnicode_FromWideChar(temp, wcslen(temp)); + if (t != NULL) {PyDict_SetItemString(ans, "model_name", t); Py_DECREF(t);} + CoTaskMemFree(temp); + } + + if (SUCCEEDED(values->GetStringValue(WPD_DEVICE_SERIAL_NUMBER, &temp))) { + t = PyUnicode_FromWideChar(temp, wcslen(temp)); + if (t != NULL) {PyDict_SetItemString(ans, "serial_number", t); Py_DECREF(t);} + CoTaskMemFree(temp); + } + + if (SUCCEEDED(values->GetStringValue(WPD_DEVICE_FIRMWARE_VERSION, &temp))) { + t = PyUnicode_FromWideChar(temp, wcslen(temp)); + if (t != NULL) {PyDict_SetItemString(ans, "device_version", t); Py_DECREF(t);} + CoTaskMemFree(temp); + } + + t = Py_False; + for (i = 0; i < num_of_categories; i++) { + PROPVARIANT pv; + PropVariantInit(&pv); + if (SUCCEEDED(categories->GetAt(i, &pv)) && pv.puuid != NULL) { + if (IsEqualGUID(WPD_FUNCTIONAL_CATEGORY_STORAGE, *pv.puuid)) { + t = Py_True; + } + } + PropVariantClear(&pv); + if (t == Py_True) break; + } + PyDict_SetItemString(ans, "has_storage", t); + + if (t == Py_True) { + storage = get_storage_info(device); + if (storage == NULL) goto end; + PyDict_SetItemString(ans, "storage", storage); + + } + +end: + if (keys != NULL) keys->Release(); + if (values != NULL) values->Release(); + if (properties != NULL) properties->Release(); + if (content != NULL) content->Release(); + if (capabilities != NULL) capabilities->Release(); + if (categories != NULL) categories->Release(); + return ans; +} // }}} + +} // namespace wpd diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py new file mode 100644 index 0000000000..fcfc415c90 --- /dev/null +++ b/src/calibre/devices/mtp/windows/driver.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import time +from threading import RLock + +from calibre import as_unicode, prints +from calibre.constants import plugins, __appname__, numeric_version +from calibre.devices.errors import OpenFailed +from calibre.devices.mtp.base import MTPDeviceBase, synchronous + +class MTP_DEVICE(MTPDeviceBase): + + supported_platforms = ['windows'] + + def __init__(self, *args, **kwargs): + MTPDeviceBase.__init__(self, *args, **kwargs) + self.dev = None + self.lock = RLock() + self.blacklisted_devices = set() + self.ejected_devices = set() + self.currently_connected_pnp_id = None + self.detected_devices = {} + self.previous_devices_on_system = frozenset() + self.last_refresh_devices_time = time.time() + self.wpd = self.wpd_error = None + self._main_id = self._carda_id = self._cardb_id = None + + @synchronous + def startup(self): + self.wpd, self.wpd_error = plugins['wpd'] + if self.wpd is not None: + try: + self.wpd.init(__appname__, *(numeric_version[:3])) + except self.wpd.NoWPD: + self.wpd_error = _( + 'The Windows Portable Devices service is not available' + ' on your computer. You may need to install Windows' + ' Media Player 11 or newer and/or restart your computer') + except Exception as e: + self.wpd_error = as_unicode(e) + + @synchronous + def shutdown(self): + self.dev = self.filesystem_cache = None + if self.wpd is not None: + self.wpd.uninit() + + @synchronous + def detect_managed_devices(self, devices_on_system): + if self.wpd is None: return None + + devices_on_system = frozenset(devices_on_system) + if (devices_on_system != self.previous_devices_on_system or time.time() + - self.last_refresh_devices_time > 10): + self.previous_devices_on_system = devices_on_system + self.last_refresh_devices_time = time.time() + try: + pnp_ids = frozenset(self.wpd.enumerate_devices()) + except: + return None + + self.detected_devices = {dev:self.detected_devices.get(dev, None) + for dev in pnp_ids} + + # Get device data for detected devices. If there is an error, we will + # try again for that device the next time this method is called. + for dev in tuple(self.detected_devices.iterkeys()): + data = self.detected_devices.get(dev, None) + if data is None or data is False: + try: + data = self.wpd.device_info(dev) + except Exception as e: + prints('Failed to get device info for device:', dev, + as_unicode(e)) + data = {} if data is False else False + self.detected_devices[dev] = data + + # Remove devices that have been disconnected from ejected + # devices and blacklisted devices + self.ejected_devices = set(self.detected_devices).intersection( + self.ejected_devices) + self.blacklisted_devices = set(self.detected_devices).intersection( + self.blacklisted_devices) + + if self.currently_connected_pnp_id is not None: + return (self.currently_connected_pnp_id if + self.currently_connected_pnp_id in self.detected_devices + else None) + + for dev, data in self.detected_devices.iteritems(): + if dev in self.blacklisted_devices or dev in self.ejected_devices: + # Ignore blacklisted and ejected devices + continue + if data and self.is_suitable_wpd_device(data): + return dev + + return None + + def is_suitable_wpd_device(self, devdata): + # Check that protocol is MTP + protocol = devdata.get('protocol', '').lower() + if not protocol.startswith('mtp:'): return False + + # Check that the device has some read-write storage + if not devdata.get('has_storage', False): return False + has_rw_storage = False + for s in devdata.get('storage', []): + if s.get('rw', False): + has_rw_storage = True + break + if not has_rw_storage: return False + + return True + + @synchronous + def post_yank_cleanup(self): + self.currently_connected_pnp_id = self.current_friendly_name = None + self._main_id = self._carda_id = self._cardb_id = None + self.dev = self.filesystem_cache = None + + @synchronous + def eject(self): + if self.currently_connected_pnp_id is None: return + self.ejected_devices.add(self.currently_connected_pnp_id) + self.currently_connected_pnp_id = self.current_friendly_name = None + self._main_id = self._carda_id = self._cardb_id = None + self.dev = self.filesystem_cache = None + + @synchronous + def open(self, connected_device, library_uuid): + self.dev = self.filesystem_cache = None + try: + self.dev = self.wpd.Device(connected_device) + except self.wpd.WPDError: + time.sleep(2) + try: + self.dev = self.wpd.Device(connected_device) + except self.wpd.WPDError as e: + self.blacklisted_devices.add(connected_device) + raise OpenFailed('Failed to open %s with error: %s'%( + connected_device, as_unicode(e))) + devdata = self.dev.data + storage = [s for s in devdata.get('storage', []) if s.get('rw', False)] + if not storage: + self.blacklisted_devices.add(connected_device) + raise OpenFailed('No storage found for device %s'%(connected_device,)) + self._main_id = storage[0]['id'] + if len(storage) > 1: + self._carda_id = storage[1]['id'] + if len(storage) > 2: + self._cardb_id = storage[2]['id'] + self.current_friendly_name = devdata.get('friendly_name', None) + + @synchronous + def get_device_information(self, end_session=True): + d = self.dev.data + dv = d.get('device_version', '') + return (self.current_friendly_name, dv, dv, '') + + @synchronous + def card_prefix(self, end_session=True): + ans = [None, None] + if self._carda_id is not None: + ans[0] = 'mtp:::%s:::'%self._carda_id + if self._cardb_id is not None: + ans[1] = 'mtp:::%s:::'%self._cardb_id + return tuple(ans) + + @synchronous + def total_space(self, end_session=True): + ans = [0, 0, 0] + dd = self.dev.data + for s in dd.get('storage', []): + i = {self._main_id:0, self._carda_id:1, + self._cardb_id:2}.get(s.get('id', -1), None) + if i is not None: + ans[i] = s['capacity'] + return tuple(ans) + + @synchronous + def free_space(self, end_session=True): + self.dev.update_data() + ans = [0, 0, 0] + dd = self.dev.data + for s in dd.get('storage', []): + i = {self._main_id:0, self._carda_id:1, + self._cardb_id:2}.get(s.get('id', -1), None) + if i is not None: + ans[i] = s['free_space'] + return tuple(ans) + + + diff --git a/src/calibre/devices/mtp/windows/global.h b/src/calibre/devices/mtp/windows/global.h new file mode 100644 index 0000000000..75712a18f5 --- /dev/null +++ b/src/calibre/devices/mtp/windows/global.h @@ -0,0 +1,58 @@ +/* + * global.h + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#pragma once +#define UNICODE +#include +#include + +#include +#include +#include + +#define ENSURE_WPD(retval) \ + if (portable_device_manager == NULL) { PyErr_SetString(NoWPD, "No WPD service available."); return retval; } + +namespace wpd { + +// Module exception types +extern PyObject *WPDError, *NoWPD; + +// The global device manager +extern IPortableDeviceManager *portable_device_manager; + +// Application info +typedef struct { + wchar_t *name; + unsigned int major_version; + unsigned int minor_version; + unsigned int revision; +} ClientInfo; +extern ClientInfo client_info; + +// Device type +typedef struct { + PyObject_HEAD + // Type-specific fields go here. + wchar_t *pnp_id; + IPortableDeviceValues *client_information; + IPortableDevice *device; + PyObject *device_information; + +} Device; +extern PyTypeObject DeviceType; + +// Utility functions +PyObject *hresult_set_exc(const char *msg, HRESULT hr); +wchar_t *unicode_to_wchar(PyObject *o); + +extern IPortableDeviceValues* get_client_information(); +extern IPortableDevice* open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information); +extern PyObject* get_device_information(IPortableDevice *device); + +} + diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py new file mode 100644 index 0000000000..b3c25d0216 --- /dev/null +++ b/src/calibre/devices/mtp/windows/remote.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import subprocess, sys, os, pprint, signal, time, glob +pprint + +def build(mod='wpd'): + master = subprocess.Popen('ssh -MN getafix'.split()) + master2 = subprocess.Popen('ssh -MN xp_build'.split()) + try: + while not glob.glob(os.path.expanduser('~/.ssh/*kovid@xp_build*')): + time.sleep(0.05) + builder = subprocess.Popen('ssh xp_build ~/build-wpd'.split()) + if builder.wait() != 0: + raise Exception('Failed to build plugin') + while not glob.glob(os.path.expanduser('~/.ssh/*kovid@getafix*')): + time.sleep(0.05) + syncer = subprocess.Popen('ssh getafix ~/test-wpd'.split()) + if syncer.wait() != 0: + raise Exception('Failed to rsync to getafix') + subprocess.check_call( + ('scp xp_build:build/calibre/src/calibre/plugins/%s.pyd /tmp'%mod).split()) + subprocess.check_call( + ('scp /tmp/%s.pyd getafix:calibre/src/calibre/devices/mtp/windows'%mod).split()) + p = subprocess.Popen( + 'ssh getafix calibre-debug -e calibre/src/calibre/devices/mtp/windows/remote.py'.split()) + p.wait() + print() + finally: + for m in (master2, master): + m.send_signal(signal.SIGHUP) + for m in (master2, master): + m.wait() + +def main(): + fp, d = os.path.abspath(__file__), os.path.dirname + if b'CALIBRE_DEVELOP_FROM' not in os.environ: + env = os.environ.copy() + env[b'CALIBRE_DEVELOP_FROM'] = bytes(d(d(d(d(d(fp)))))) + subprocess.call(['calibre-debug', '-e', fp], env=env) + return + + sys.path.insert(0, os.path.dirname(fp)) + if 'wpd' in sys.modules: + del sys.modules['wpd'] + import wpd + from calibre.constants import plugins + plugins._plugins['wpd'] = (wpd, '') + sys.path.pop(0) + + from calibre.devices.scanner import win_scanner + from calibre.devices.mtp.windows.driver import MTP_DEVICE + dev = MTP_DEVICE(None) + dev.startup() + print (dev.wpd, dev.wpd_error) + + try: + devices = win_scanner() + pnp_id = dev.detect_managed_devices(devices) + # pprint.pprint(dev.detected_devices) + print ('Trying to connect to:', pnp_id) + dev.open(pnp_id, '') + print ('Connected to:', dev.get_gui_name()) + print ('Total space', dev.total_space()) + print ('Free space', dev.free_space()) + finally: + dev.shutdown() + +if __name__ == '__main__': + main() + diff --git a/src/calibre/devices/mtp/windows/utils.cpp b/src/calibre/devices/mtp/windows/utils.cpp new file mode 100644 index 0000000000..527885a6f5 --- /dev/null +++ b/src/calibre/devices/mtp/windows/utils.cpp @@ -0,0 +1,44 @@ +/* + * utils.cpp + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "global.h" + +using namespace wpd; + +PyObject *wpd::hresult_set_exc(const char *msg, HRESULT hr) { + PyObject *o = NULL, *mess; + LPWSTR desc = NULL; + + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&desc, 0, NULL); + if (desc == NULL) { + o = PyUnicode_FromString("No description available."); + } else { + o = PyUnicode_FromWideChar(desc, wcslen(desc)); + LocalFree(desc); + } + if (o == NULL) return PyErr_NoMemory(); + mess = PyUnicode_FromFormat("%s: hr=%lu facility=%u error_code=%u description: %U", msg, hr, HRESULT_FACILITY(hr), HRESULT_CODE(hr), o); + Py_XDECREF(o); + if (mess == NULL) return PyErr_NoMemory(); + PyErr_SetObject(WPDError, mess); + Py_DECREF(mess); + return NULL; +} + +wchar_t *wpd::unicode_to_wchar(PyObject *o) { + wchar_t *buf; + Py_ssize_t len; + if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The python object must be a unicode object"); return NULL;} + len = PyUnicode_GET_SIZE(o); + buf = (wchar_t *)calloc(len+2, sizeof(wchar_t)); + if (buf == NULL) { PyErr_NoMemory(); return NULL; } + len = PyUnicode_AsWideChar((PyUnicodeObject*)o, buf, len); + if (len == -1) { free(buf); PyErr_Format(PyExc_TypeError, "Invalid python unicode object."); return NULL; } + return buf; +} + diff --git a/src/calibre/devices/mtp/windows/wpd.cpp b/src/calibre/devices/mtp/windows/wpd.cpp index e2397a5797..2fb908ebd3 100644 --- a/src/calibre/devices/mtp/windows/wpd.cpp +++ b/src/calibre/devices/mtp/windows/wpd.cpp @@ -2,35 +2,50 @@ * mtp.c * Copyright (C) 2012 Kovid Goyal * - * Distributed under terms of the MIT license. + * Distributed under terms of the GPL3 license. */ +#include "global.h" -#define UNICODE -#include -#include +using namespace wpd; -#include -#include +// Module exception types +PyObject *wpd::WPDError = NULL, *wpd::NoWPD = NULL; +// The global device manager +IPortableDeviceManager *wpd::portable_device_manager = NULL; + +// Flag indicating if COM has been initialized static int _com_initialized = 0; -static PyObject *WPDError = NULL; -static PyObject *NoWPD = NULL; -static IPortableDeviceManager *portable_device_manager = NULL; +// Application Info +wpd::ClientInfo wpd::client_info = {NULL, 0, 0, 0}; +extern IPortableDeviceValues* wpd::get_client_information(); +extern IPortableDevice* wpd::open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information); +extern PyObject* wpd::get_device_information(IPortableDevice *device); + +// Module startup/shutdown {{{ static PyObject * wpd_init(PyObject *self, PyObject *args) { HRESULT hr; + PyObject *o; + if (!PyArg_ParseTuple(args, "OIII", &o, &client_info.major_version, &client_info.minor_version, &client_info.revision)) return NULL; + client_info.name = unicode_to_wchar(o); + if (client_info.name == NULL) return NULL; if (!_com_initialized) { + Py_BEGIN_ALLOW_THREADS; hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + Py_END_ALLOW_THREADS; if (SUCCEEDED(hr)) _com_initialized = 1; else {PyErr_SetString(WPDError, "Failed to initialize COM"); return NULL;} } if (portable_device_manager == NULL) { + Py_BEGIN_ALLOW_THREADS; hr = CoCreateInstance(CLSID_PortableDeviceManager, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&portable_device_manager)); + Py_END_ALLOW_THREADS; if (FAILED(hr)) { portable_device_manager = NULL; @@ -47,27 +62,122 @@ wpd_init(PyObject *self, PyObject *args) { static PyObject * wpd_uninit(PyObject *self, PyObject *args) { if (portable_device_manager != NULL) { + Py_BEGIN_ALLOW_THREADS; portable_device_manager->Release(); + Py_END_ALLOW_THREADS; portable_device_manager = NULL; } if (_com_initialized) { + Py_BEGIN_ALLOW_THREADS; CoUninitialize(); + Py_END_ALLOW_THREADS; _com_initialized = 0; } + if (client_info.name != NULL) { free(client_info.name); } + // hresult_set_exc("test", HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)); return NULL; + Py_RETURN_NONE; } +// }}} + +// enumerate_devices() {{{ +static PyObject * +wpd_enumerate_devices(PyObject *self, PyObject *args) { + PyObject *refresh = NULL, *ans = NULL, *temp; + HRESULT hr; + DWORD num_of_devices, i; + PWSTR *pnp_device_ids; + + ENSURE_WPD(NULL); + + Py_BEGIN_ALLOW_THREADS; + hr = portable_device_manager->RefreshDeviceList(); + Py_END_ALLOW_THREADS; + if (FAILED(hr)) return hresult_set_exc("Failed to refresh the list of portable devices", hr); + + hr = portable_device_manager->GetDevices(NULL, &num_of_devices); + num_of_devices += 15; // Incase new devices were connected between this call and the next + if (FAILED(hr)) return hresult_set_exc("Failed to get number of devices on the system", hr); + pnp_device_ids = (PWSTR*)calloc(num_of_devices, sizeof(PWSTR)); + if (pnp_device_ids == NULL) return PyErr_NoMemory(); + + Py_BEGIN_ALLOW_THREADS; + hr = portable_device_manager->GetDevices(pnp_device_ids, &num_of_devices); + Py_END_ALLOW_THREADS; + + if (SUCCEEDED(hr)) { + ans = PyTuple_New(num_of_devices); + if (ans != NULL) { + for(i = 0; i < num_of_devices; i++) { + temp = PyUnicode_FromWideChar(pnp_device_ids[i], wcslen(pnp_device_ids[i])); + if (temp == NULL) { PyErr_NoMemory(); Py_DECREF(ans); ans = NULL; break;} + PyTuple_SET_ITEM(ans, i, temp); + } + } + } else { + hresult_set_exc("Failed to get list of portable devices", hr); + } + + for (i = 0; i < num_of_devices; i++) { + Py_BEGIN_ALLOW_THREADS; + CoTaskMemFree(pnp_device_ids[i]); + Py_END_ALLOW_THREADS; + pnp_device_ids[i] = NULL; + } + free(pnp_device_ids); + pnp_device_ids = NULL; + + return Py_BuildValue("N", ans); +} // }}} + +// device_info() {{{ +static PyObject * +wpd_device_info(PyObject *self, PyObject *args) { + PyObject *py_pnp_id, *ans = NULL; + wchar_t *pnp_id; + IPortableDeviceValues *client_information = NULL; + IPortableDevice *device = NULL; + + ENSURE_WPD(NULL); + + if (!PyArg_ParseTuple(args, "O", &py_pnp_id)) return NULL; + pnp_id = unicode_to_wchar(py_pnp_id); + if (wcslen(pnp_id) < 1) { PyErr_SetString(WPDError, "The PNP id must not be empty."); return NULL; } + if (pnp_id == NULL) return NULL; + + client_information = get_client_information(); + if (client_information != NULL) { + device = open_device(pnp_id, client_information); + if (device != NULL) { + ans = get_device_information(device); + } + } + + if (pnp_id != NULL) free(pnp_id); + if (client_information != NULL) client_information->Release(); + if (device != NULL) {device->Close(); device->Release();} + return ans; +} // }}} static PyMethodDef wpd_methods[] = { {"init", wpd_init, METH_VARARGS, - "init()\n\n Initializes this module. Call this method *only* in the thread in which you intend to use this module. Also remember to call uninit before the thread exits." + "init(name, major_version, minor_version, revision)\n\n Initializes this module. Call this method *only* in the thread in which you intend to use this module. Also remember to call uninit before the thread exits." }, {"uninit", wpd_uninit, METH_VARARGS, "uninit()\n\n Uninitialize this module. Must be called in the same thread as init(). Do not use any function/objects from this module after uninit has been called." }, + {"enumerate_devices", wpd_enumerate_devices, METH_VARARGS, + "enumerate_devices()\n\n Get the list of device PnP ids for all connected devices recognized by the WPD service. Do not call too often as it is resource intensive." + }, + + {"device_info", wpd_device_info, METH_VARARGS, + "device_info(pnp_id)\n\n Return basic device information for the device identified by pnp_id (which you get from enumerate_devices)." + }, + {NULL, NULL, 0, NULL} }; @@ -76,6 +186,10 @@ PyMODINIT_FUNC initwpd(void) { PyObject *m; + wpd::DeviceType.tp_new = PyType_GenericNew; + if (PyType_Ready(&wpd::DeviceType) < 0) + return; + m = Py_InitModule3("wpd", wpd_methods, "Interface to the WPD windows service."); if (m == NULL) return; @@ -84,6 +198,10 @@ initwpd(void) { NoWPD = PyErr_NewException("wpd.NoWPD", NULL, NULL); if (NoWPD == NULL) return; + + Py_INCREF(&DeviceType); + PyModule_AddObject(m, "Device", (PyObject *)&DeviceType); + } diff --git a/src/calibre/devices/prst1/driver.py b/src/calibre/devices/prst1/driver.py index d3c92b5eff..b51e55b829 100644 --- a/src/calibre/devices/prst1/driver.py +++ b/src/calibre/devices/prst1/driver.py @@ -193,7 +193,11 @@ class PRST1(USBMS): time_offsets = {} for i, row in enumerate(cursor): - comp_date = int(os.path.getmtime(self.normalize_path(prefix + row[0])) * 1000); + try: + comp_date = int(os.path.getmtime(self.normalize_path(prefix + row[0])) * 1000); + except (OSError, IOError): + # In case the db has incorrect path info + continue device_date = int(row[1]); offset = device_date - comp_date time_offsets.setdefault(offset, 0) diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index 89bd4a0cec..49273dd8bc 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -10,7 +10,8 @@ from threading import RLock from collections import namedtuple from calibre import prints, as_unicode -from calibre.constants import iswindows, isosx, plugins, islinux, isfreebsd +from calibre.constants import (iswindows, isosx, plugins, islinux, isfreebsd, + isnetbsd) osx_scanner = win_scanner = linux_scanner = None @@ -253,13 +254,18 @@ freebsd_scanner = None if isfreebsd: freebsd_scanner = FreeBSDScanner() +netbsd_scanner = None + +''' NetBSD support currently not written yet ''' +if isnetbsd: + netbsd_scanner = None class DeviceScanner(object): def __init__(self, *args): if isosx and osx_scanner is None: raise RuntimeError('The Python extension usbobserver must be available on OS X.') - self.scanner = win_scanner if iswindows else osx_scanner if isosx else freebsd_scanner if isfreebsd else linux_scanner + self.scanner = win_scanner if iswindows else osx_scanner if isosx else freebsd_scanner if isfreebsd else netbsd_scanner if isnetbsd else linux_scanner self.devices = [] def scan(self): diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 972545033e..ba6a959c0e 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -11,11 +11,12 @@ import socket, select, json, inspect, os, traceback, time, sys, random import hashlib, threading from base64 import b64encode, b64decode from functools import wraps +from errno import EAGAIN, EINTR from calibre import prints from calibre.constants import numeric_version, DEBUG from calibre.devices.errors import (OpenFailed, ControlError, TimeoutError, - InitialConnectionError) + InitialConnectionError, PacketError) from calibre.devices.interface import DevicePlugin from calibre.devices.usbms.books import Book, CollectionsBookList from calibre.devices.usbms.deviceconfig import DeviceConfig @@ -85,6 +86,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): MAX_CLIENT_COMM_TIMEOUT = 60.0 # Wait at most N seconds for an answer MAX_UNSUCCESSFUL_CONNECTS = 5 + SEND_NOOP_EVERY_NTH_PROBE = 5 + DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes + opcodes = { 'NOOP' : 12, 'OK' : 0, @@ -120,7 +124,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): _('Use fixed network port') + ':::

' + _('If checked, use the port number in the "Port" box, otherwise ' 'the driver will pick a random port') + '

', - _('Port') + ':::

' + + _('Port number: ') + ':::

' + _('Enter the port number the driver is to use if the "fixed port" box is checked') + '

', _('Print extra debug information') + ':::

' + _('Check this box if requested when reporting problems') + '

', @@ -131,7 +135,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): _('. Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add ' 'these values to the list to enable them. The collections will be ' 'given the name provided after the ":" character.')%dict( - abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR) + abt='abt', abtv=ALL_BY_TITLE, aba='aba', abav=ALL_BY_AUTHOR), + '', + _('Enable the no-activity timeout') + ':::

' + + _('If this box is checked, calibre will automatically disconnect if ' + 'a connected device does nothing for %d minutes. Unchecking this ' + ' box disables this timeout, so calibre will never automatically ' + 'disconnect.')%(DISCONNECT_AFTER_N_SECONDS/60,) + '

', ] EXTRA_CUSTOMIZATION_DEFAULT = [ False, @@ -141,7 +151,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): False, '9090', False, '', - '' + '', + '', + True, ] OPT_AUTOSTART = 0 OPT_PASSWORD = 2 @@ -149,6 +161,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): OPT_PORT_NUMBER = 5 OPT_EXTRA_DEBUG = 6 OPT_COLLECTIONS = 8 + OPT_AUTODISCONNECT = 10 def __init__(self, path): self.sync_lock = threading.RLock() @@ -165,7 +178,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): inspect.stack()[1][3]), end='') for a in args: try: - prints('', a, end='') + if isinstance(a, dict): + printable = {} + for k,v in a.iteritems(): + if isinstance(v, (str, unicode)) and len(v) > 50: + printable[k] = 'too long' + else: + printable[k] = v + prints('', printable, end=''); + else: + prints('', a, end='') except: prints('', 'value too long', end='') print() @@ -339,6 +361,27 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): pos += len(v) return data + def _send_byte_string(self, s): + if not isinstance(s, bytes): + self._debug('given a non-byte string!') + raise PacketError("Internal error: found a string that isn't bytes") + sent_len = 0; + total_len = len(s) + while sent_len < total_len: + try: + if sent_len == 0: + amt_sent = self.device_socket.send(s) + else: + amt_sent = self.device_socket.send(s[sent_len:]) + if amt_sent <= 0: + raise IOError('Bad write on device socket'); + sent_len += amt_sent + except socket.error as e: + self._debug('socket error', e, e.errno) + if e.args[0] != EAGAIN and e.args[0] != EINTR: + raise + time.sleep(0.1) # lets not hammer the OS too hard + def _call_client(self, op, arg, print_debug_info=True): if op != 'NOOP': self.noop_counter = 0 @@ -355,9 +398,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): if print_debug_info and extra_debug: self._debug('send string', s) self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) - self.device_socket.sendall(('%d' % len(s))+s) - self.device_socket.settimeout(None) + self._send_byte_string((b'%d' % len(s))+s) v = self._read_string_from_net() + self.device_socket.settimeout(None) if print_debug_info and extra_debug: self._debug('received string', v) if v: @@ -373,13 +416,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): except socket.error: self._debug('device went away') self._close_device_socket() - raise ControlError('Device closed the network connection') + raise ControlError(desc='Device closed the network connection') except: self._debug('other exception') traceback.print_exc() self._close_device_socket() raise - raise ControlError('Device responded with incorrect information') + raise ControlError(desc='Device responded with incorrect information') # Write a file as a series of base64-encoded strings. def _put_file(self, infile, lpath, book_metadata, this_book, total_books): @@ -475,7 +518,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.is_connected = False if self.is_connected: self.noop_counter += 1 - if only_presence and (self.noop_counter % 5) != 1: + if only_presence and ( + self.noop_counter % self.SEND_NOOP_EVERY_NTH_PROBE) != 1: try: ans = select.select((self.device_socket,), (), (), 0) if len(ans[0]) == 0: @@ -486,11 +530,16 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): # This will usually toss an exception if the socket is gone. except: pass - try: - if self._call_client('NOOP', dict())[0] is None: - self._close_device_socket() - except: + if (self.settings().extra_customization[self.OPT_AUTODISCONNECT] and + self.noop_counter > self.DISCONNECT_AFTER_N_SECONDS): self._close_device_socket() + self._debug('timeout -- disconnected') + else: + try: + if self._call_client('NOOP', dict())[0] is None: + self._close_device_socket() + except: + self._close_device_socket() return (self.is_connected, self) if getattr(self, 'listen_socket', None) is not None: ans = select.select((self.listen_socket,), (), (), 0) @@ -533,7 +582,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self._debug() if not self.is_connected: # We have been called to retry the connection. Give up immediately - raise ControlError('Attempt to open a closed device') + raise ControlError(desc='Attempt to open a closed device') self.current_library_uuid = library_uuid self.current_library_name = current_library_name() try: @@ -569,6 +618,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self._debug('Protocol error - bogus book packet length') self._close_device_socket() return False + self._debug('CC version #:', result.get('ccVersionNumber', 'unknown')) self.max_book_packet_len = result.get('maxBookContentPacketLen', self.BASE_PACKET_LEN) exts = result.get('acceptedExtensions', None) @@ -689,7 +739,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self._set_known_metadata(book) bl.add_book(book, replace_metadata=True) else: - raise ControlError('book metadata not returned') + raise ControlError(desc='book metadata not returned') return bl @synchronous('sync_lock') @@ -720,7 +770,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): print_debug_info=False) if opcode != 'OK': self._debug('protocol error', opcode, i) - raise ControlError('sync_booklists') + raise ControlError(desc='sync_booklists') @synchronous('sync_lock') def eject(self): @@ -748,7 +798,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): book = Book(self.PREFIX, lpath, other=mdata) length = self._put_file(infile, lpath, book, i, len(files)) if length < 0: - raise ControlError('Sending book %s to device failed' % lpath) + raise ControlError(desc='Sending book %s to device failed' % lpath) paths.append((lpath, length)) # No need to deal with covers. The client will get the thumbnails # in the mi structure @@ -789,7 +839,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): if opcode == 'OK': self._debug('removed book with UUID', result['uuid']) else: - raise ControlError('Protocol error - delete books') + raise ControlError(desc='Protocol error - delete books') @synchronous('sync_lock') def remove_books_from_metadata(self, paths, booklists): @@ -825,7 +875,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): else: eof = True else: - raise ControlError('request for book data failed') + raise ControlError(desc='request for book data failed') @synchronous('sync_lock') def set_plugboards(self, plugboards, pb_func): diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 7a61c7cd96..9ebdc87c81 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -31,7 +31,7 @@ BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'txtz', 'text', 'ht 'epub', 'fb2', 'djv', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', 'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'pmlz', 'mbp', 'tan', 'snb', 'xps', 'oxps', 'azw4', 'book', 'zbf', 'pobi', 'docx', 'md', - 'textile', 'markdown', 'ibook', 'iba', 'azw3'] + 'textile', 'markdown', 'ibook', 'iba', 'azw3', 'ps'] class HTMLRenderer(object): diff --git a/src/calibre/ebooks/conversion/plugins/mobi_output.py b/src/calibre/ebooks/conversion/plugins/mobi_output.py index f07e01a53c..ab00346be9 100644 --- a/src/calibre/ebooks/conversion/plugins/mobi_output.py +++ b/src/calibre/ebooks/conversion/plugins/mobi_output.py @@ -88,6 +88,15 @@ class MOBIOutput(OutputFormatPlugin): 'formats. This option tells calibre not to do this. ' 'Useful if your document contains lots of GIF/PNG images that ' 'become very large when converted to JPEG.')), + OptionRecommendation(name='mobi_file_type', choices=['old', 'both', + 'new'], recommended_value='old', + help=_('By default calibre generates MOBI files that contain the ' + 'old MOBI 6 format. This format is compatible with all ' + 'devices. However, by changing this setting, you can tell ' + 'calibre to generate MOBI files that contain both MOBI 6 and ' + 'the new KF8 format, or only the new KF8 format. KF8 has ' + 'more features than MOBI 6, but only works with newer Kindles.')), + ]) def check_for_periodical(self): @@ -165,11 +174,10 @@ class MOBIOutput(OutputFormatPlugin): toc.nodes[0].href = toc.nodes[0].nodes[0].href def convert(self, oeb, output_path, input_plugin, opts, log): - from calibre.utils.config import tweaks from calibre.ebooks.mobi.writer2.resources import Resources self.log, self.opts, self.oeb = log, opts, oeb - mobi_type = tweaks.get('test_mobi_output_type', 'old') + mobi_type = opts.mobi_file_type if self.is_periodical: mobi_type = 'old' # Amazon does not support KF8 periodicals create_kf8 = mobi_type in ('new', 'both') diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index d58b64ac53..72c9dc0d72 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -11,6 +11,7 @@ from collections import defaultdict from lxml import etree import cssutils +from cssutils.css import Property from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES, namespace, barename, XPath) @@ -276,10 +277,16 @@ class CSSFlattener(object): cssdict['font-family'] = node.attrib['face'] del node.attrib['face'] if 'color' in node.attrib: - cssdict['color'] = node.attrib['color'] + try: + cssdict['color'] = Property('color', node.attrib['color']).value + except ValueError: + pass del node.attrib['color'] if 'bgcolor' in node.attrib: - cssdict['background-color'] = node.attrib['bgcolor'] + try: + cssdict['background-color'] = Property('background-color', node.attrib['bgcolor']).value + except ValueError: + pass del node.attrib['bgcolor'] if cssdict.get('font-weight', '').lower() == 'medium': cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index 50b67008d9..ac2bf15164 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -25,7 +25,7 @@ class PluginWidget(Widget, Ui_Form): 'mobi_keep_original_images', 'mobi_ignore_margins', 'mobi_toc_at_start', 'dont_compress', 'no_inline_toc', 'share_not_sync', - 'personal_doc']#, 'mobi_navpoints_only_deepest'] + 'personal_doc', 'mobi_file_type'] ) self.db, self.book_id = db, book_id @@ -48,6 +48,7 @@ class PluginWidget(Widget, Ui_Form): self.font_family_model = font_family_model self.opt_masthead_font.setModel(self.font_family_model) ''' + self.opt_mobi_file_type.addItems(['old', 'both', 'new']) self.initialize_options(get_option, get_help, db, book_id) diff --git a/src/calibre/gui2/convert/mobi_output.ui b/src/calibre/gui2/convert/mobi_output.ui index 2c62b8c27a..8c1c107620 100644 --- a/src/calibre/gui2/convert/mobi_output.ui +++ b/src/calibre/gui2/convert/mobi_output.ui @@ -14,80 +14,10 @@ Form - - - - Kindle options - - - - - - - - Personal Doc tag: - - - - - - - - - - - - Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + - Put generated Table of Contents at &start of book instead of end - - - - - - - Ignore &margins - - - - - - - Use author &sort for author + Do not add Table of Contents to book @@ -104,17 +34,24 @@ - - + + - Disable compression of the file contents + Put generated Table of Contents at &start of book instead of end - - + + - Do not add Table of Contents to book + Ignore &margins + + + + + + + Use author &sort for author @@ -125,6 +62,55 @@ + + + + Disable compression of the file contents + + + + + + + Kindle options + + + + QFormLayout::ExpandingFieldsGrow + + + + + MOBI file &type: + + + opt_mobi_file_type + + + + + + + + + + Personal Doc tag: + + + + + + + + + + Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing + + + + + + diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 5c90e5dddd..31aa7f812b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -529,6 +529,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.remove_button.clicked.connect(self.s_r_remove_query) self.queries = JSONConfig("search_replace_queries") + self.saved_search_name = '' self.query_field.addItem("") self.query_field_values = sorted([q for q in self.queries], key=sort_key) self.query_field.addItems(self.query_field_values) @@ -1034,11 +1035,16 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): self.queries.commit() def s_r_save_query(self, *args): - dex = self.query_field_values.index(self.saved_search_name) + names = [''] + names.extend(self.query_field_values) + try: + dex = names.index(self.saved_search_name) + except: + dex = 0 name = '' while not name: name, ok = QInputDialog.getItem(self, _('Save search/replace'), - _('Search/replace name:'), self.query_field_values, dex, True) + _('Search/replace name:'), names, dex, True) if not ok: return if not name: @@ -1086,6 +1092,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): def s_r_query_change(self, item_name): if not item_name: self.s_r_reset_query_fields() + self.saved_search_name = '' return item = self.queries.get(unicode(item_name), None) if item is None: diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index 2387b7863d..2c4a409a22 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -1241,17 +1241,18 @@ not multiple and the destination field is multiple search_mode s_r_src_ident s_r_template + search_for + case_sensitive replace_with replace_func + destination_field replace_mode comma_separated s_r_dst_ident results_count - scrollArea11 - destination_field - search_for - case_sensitive starting_from + multiple_separator + scrollArea11 diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index bb69197b58..54067f4c0f 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -310,8 +310,18 @@ class MetadataSingleDialogBase(ResizableDialog): self.update_from_mi(mi) def cover_from_format(self, *args): - mi, ext = self.formats_manager.get_selected_format_metadata(self.db, - self.book_id) + try: + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + except (IOError, OSError) as err: + if getattr(err, 'errno', None) == errno.EACCES: # Permission denied + import traceback + fname = err.filename if err.filename else 'file' + error_dialog(self, _('Permission denied'), + _('Could not open %s. Is it being used by another' + ' program?')%fname, det_msg=traceback.format_exc(), + show=True) + return if mi is None: return cdata = None diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index dff2d0143d..d6d62af83c 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -410,6 +410,7 @@ class TagsModel(QAbstractItemModel): # {{{ # first letter can actually be more than one letter long. cl_list = [None] * len(data[key]) last_ordnum = 0 + last_c = ' ' for idx,tag in enumerate(data[key]): if not tag.sort: c = ' ' diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 49487518ef..6e4d129eaf 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -417,7 +417,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): vprefs.set('viewer_splitter_state', bytearray(self.splitter.saveState())) vprefs['multiplier'] = self.view.multiplier - vprefs['in_paged_mode1'] = not self.action_toggle_paged_mode.isChecked() + vprefs['in_paged_mode'] = not self.action_toggle_paged_mode.isChecked() def restore_state(self): state = vprefs.get('viewer_toolbar_state', None) @@ -434,8 +434,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): # specific location, ensure they are visible. self.tool_bar.setVisible(True) self.tool_bar2.setVisible(True) - self.action_toggle_paged_mode.setChecked(not vprefs.get('in_paged_mode1', - False)) + self.action_toggle_paged_mode.setChecked(not vprefs.get('in_paged_mode', + True)) self.toggle_paged_mode(self.action_toggle_paged_mode.isChecked(), at_start=True) diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index ef756a226a..31d9cb0928 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -440,8 +440,7 @@ class KindlePage(QWizardPage, KindleUI): x = unicode(self.to_address.text()).strip() parts = x.split('@') - if (self.send_email_widget.set_email_settings(True) and len(parts) >= 2 - and parts[0]): + if (len(parts) >= 2 and parts[0] and self.send_email_widget.set_email_settings(True)): conf = smtp_prefs() accounts = conf.parse().accounts if not accounts: accounts = {} @@ -676,8 +675,9 @@ class LibraryPage(QWizardPage, LibraryUI): self.language.blockSignals(True) self.language.clear() from calibre.utils.localization import (available_translations, - get_language, get_lang) + get_language, get_lang, get_lc_messages_path) lang = get_lang() + lang = get_lc_messages_path(lang) if lang else lang if lang is None or lang not in available_translations(): lang = 'en' def get_esc_lang(l): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index c210993936..7969a0e032 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -11,7 +11,7 @@ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, \ from collections import defaultdict import threading, random from itertools import repeat -from math import ceil +from math import ceil, floor from calibre import prints, force_unicode from calibre.ebooks.metadata import (title_sort, author_to_author_sort, @@ -640,12 +640,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if name and name != fname: changed = True break - if path == current_path and not changed: - return - tpath = os.path.join(self.library_path, *path.split('/')) if not os.path.exists(tpath): os.makedirs(tpath) + if path == current_path and not changed: + return + spath = os.path.join(self.library_path, *current_path.split('/')) if current_path and os.path.exists(spath): # Migrate existing files @@ -1150,7 +1150,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): `data`: Can be either a QImage, QPixmap, file object or bytestring ''' - path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg') + base_path = os.path.join(self.library_path, self.path(id, + index_is_id=True)) + if not os.path.exists(base_path): + self.set_path(id, index_is_id=True) + base_path = os.path.join(self.library_path, self.path(id, + index_is_id=True)) + self.dirtied([id]) + + path = os.path.join(base_path, 'cover.jpg') + if callable(getattr(data, 'save', None)): data.save(path) else: @@ -2080,7 +2089,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return 1.0 series_indices = [x[0] for x in series_indices] if tweaks['series_index_auto_increment'] == 'next': - return series_indices[-1] + 1 + return floor(series_indices[-1]) + 1 if tweaks['series_index_auto_increment'] == 'first_free': for i in range(1, 10000): if i not in series_indices: diff --git a/src/calibre/library/restore.py b/src/calibre/library/restore.py index 0b352f365b..ac1fce5783 100644 --- a/src/calibre/library/restore.py +++ b/src/calibre/library/restore.py @@ -42,7 +42,7 @@ class Restore(Thread): self.src_library_path = os.path.abspath(library_path) self.progress_callback = progress_callback self.db_id_regexp = re.compile(r'^.* \((\d+)\)$') - self.bad_ext_pat = re.compile(r'[^a-z0-9]+') + self.bad_ext_pat = re.compile(r'[^a-z0-9_]+') if not callable(self.progress_callback): self.progress_callback = lambda x, y: x self.dirs = [] diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 193ab07692..618e3557d2 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -22,6 +22,34 @@ def available_translations(): _available_translations = [x for x in stats if stats[x] > 0.1] return _available_translations +def get_system_locale(): + from calibre.constants import iswindows + lang = None + if iswindows: + from calibre.constants import get_windows_user_locale_name + try: + lang = get_windows_user_locale_name() + lang = lang.strip() + if not lang: lang = None + except: + pass # Windows XP does not have the GetUserDefaultLocaleName fn + if lang is None: + try: + lang = locale.getdefaultlocale(['LANGUAGE', 'LC_ALL', 'LC_CTYPE', + 'LC_MESSAGES', 'LANG'])[0] + except: + pass # This happens on Ubuntu apparently + if lang is None and os.environ.has_key('LANG'): # Needed for OS X + try: + lang = os.environ['LANG'] + except: + pass + if lang: + lang = lang.replace('-', '_') + lang = '_'.join(lang.split('_')[:2]) + return lang + + def get_lang(): 'Try to figure out what language to display the interface in' from calibre.utils.config_base import prefs @@ -30,15 +58,11 @@ def get_lang(): if lang: return lang try: - lang = locale.getdefaultlocale(['LANGUAGE', 'LC_ALL', 'LC_CTYPE', - 'LC_MESSAGES', 'LANG'])[0] + lang = get_system_locale() except: - pass # This happens on Ubuntu apparently - if lang is None and os.environ.has_key('LANG'): # Needed for OS X - try: - lang = os.environ['LANG'] - except: - pass + import traceback + traceback.print_exc() + lang = None if lang: match = re.match('[a-z]{2,3}(_[A-Z]{2}){0,1}', lang) if match: @@ -55,7 +79,7 @@ def get_lc_messages_path(lang): if lang in available_translations(): hlang = lang else: - xlang = lang.split('_')[0] + xlang = lang.split('_')[0].lower() if xlang in available_translations(): hlang = xlang return hlang