This commit is contained in:
GRiker 2012-08-14 11:16:06 -06:00
commit 31af65d019
35 changed files with 1316 additions and 172 deletions

View File

@ -16,6 +16,7 @@ class BusinessSpectator(BasicNewsRecipe):
oldest_article = 2 oldest_article = 2
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
auto_cleanup = True
#delay = 1 #delay = 1
use_embedded_content = False use_embedded_content = False
encoding = 'utf8' encoding = 'utf8'
@ -32,11 +33,11 @@ class BusinessSpectator(BasicNewsRecipe):
,'linearize_tables': False ,'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 = [ feeds = [
('Top Stories', 'http://www.businessspectator.com.au/top-stories.rss'), ('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'), ('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'), ('Australia', 'http://www.businessspectator.com.au/bs.nsf/RSS?readform&type=region&cat=australia'),
] ]

View File

@ -169,7 +169,15 @@ if iswindows:
cflags=['/X'] cflags=['/X']
), ),
Extension('wpd', 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'], libraries=['ole32', 'portabledeviceguids'],
# needs_ddk=True, # needs_ddk=True,
cflags=['/X'] cflags=['/X']
@ -291,6 +299,7 @@ class Build(Command):
self.obj_dir = os.path.join(os.path.dirname(SRC), 'build', 'objects') self.obj_dir = os.path.join(os.path.dirname(SRC), 'build', 'objects')
if not os.path.exists(self.obj_dir): if not os.path.exists(self.obj_dir):
os.makedirs(self.obj_dir) os.makedirs(self.obj_dir)
if not opts.only:
self.build_style(self.j(self.SRC, 'calibre', 'plugins')) self.build_style(self.j(self.SRC, 'calibre', 'plugins'))
for ext in extensions: for ext in extensions:
if opts.only != 'all' and opts.only != ext.name: if opts.only != 'all' and opts.only != ext.name:

View File

@ -38,7 +38,7 @@ binary_includes = [
'/lib/libz.so.1', '/lib/libz.so.1',
'/usr/lib/libtiff.so.5', '/usr/lib/libtiff.so.5',
'/lib/libbz2.so.1', '/lib/libbz2.so.1',
'/usr/lib/libpoppler.so.25', '/usr/lib/libpoppler.so.27',
'/usr/lib/libxml2.so.2', '/usr/lib/libxml2.so.2',
'/usr/lib/libopenjpeg.so.2', '/usr/lib/libopenjpeg.so.2',
'/usr/lib/libxslt.so.1', '/usr/lib/libxslt.so.1',

View File

@ -379,7 +379,7 @@ class Py2App(object):
@flush @flush
def add_poppler(self): def add_poppler(self):
info('\nAdding poppler') info('\nAdding poppler')
for x in ('libpoppler.26.dylib',): for x in ('libpoppler.27.dylib',):
self.install_dylib(os.path.join(SW, 'lib', x)) self.install_dylib(os.path.join(SW, 'lib', x))
for x in ('pdftohtml', 'pdftoppm', 'pdfinfo'): for x in ('pdftohtml', 'pdftoppm', 'pdfinfo'):
self.install_dylib(os.path.join(SW, 'bin', x), False) self.install_dylib(os.path.join(SW, 'bin', x), False)

View File

@ -28,7 +28,8 @@ isosx = 'darwin' in _plat
isnewosx = isosx and getattr(sys, 'new_app_bundle', False) isnewosx = isosx and getattr(sys, 'new_app_bundle', False)
isfreebsd = 'freebsd' in _plat isfreebsd = 'freebsd' in _plat
isnetbsd = 'netbsd' 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) islinux = not(iswindows or isosx or isbsd)
isfrozen = hasattr(sys, 'frozen') isfrozen = hasattr(sys, 'frozen')
isunix = isosx or islinux isunix = isosx or islinux
@ -215,3 +216,13 @@ def get_windows_temp_path():
ans = buf.value ans = buf.value
return ans if ans else None 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])

View File

@ -87,7 +87,7 @@ class ANDROID(USBMS):
# Google # Google
0x18d1 : { 0x18d1 : {
0x0001 : [0x0223, 0x9999], 0x0001 : [0x0223, 0x230, 0x9999],
0x0003 : [0x0230], 0x0003 : [0x0230],
0x4e11 : [0x0100, 0x226, 0x227], 0x4e11 : [0x0100, 0x226, 0x227],
0x4e12 : [0x0100, 0x226, 0x227], 0x4e12 : [0x0100, 0x226, 0x227],
@ -196,7 +196,7 @@ class ANDROID(USBMS):
'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON', 'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON',
'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP',
'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', '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', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', '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', 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER',
'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX',
'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', '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', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_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', 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC',
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', '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' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -92,6 +92,7 @@ class ControlError(ProtocolError):
def __init__(self, query=None, response=None, desc=None): def __init__(self, query=None, response=None, desc=None):
self.query = query self.query = query
self.response = response self.response = response
self.desc = desc
ProtocolError.__init__(self, desc) ProtocolError.__init__(self, desc)
def __str__(self): def __str__(self):

View File

@ -39,6 +39,7 @@ class MTPDeviceBase(DevicePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
DevicePlugin.__init__(self, *args, **kwargs) DevicePlugin.__init__(self, *args, **kwargs)
self.progress_reporter = None self.progress_reporter = None
self.current_friendly_name = None
def reset(self, key='-1', log_packets=False, report_progress=None, def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None): detected_device=None):
@ -47,3 +48,7 @@ class MTPDeviceBase(DevicePlugin):
def set_progress_reporter(self, report_progress): def set_progress_reporter(self, report_progress):
self.progress_reporter = report_progress self.progress_reporter = report_progress
def get_gui_name(self):
return self.current_friendly_name or self.name

View File

@ -14,7 +14,7 @@ from collections import deque, OrderedDict
from io import BytesIO from io import BytesIO
from calibre import prints 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.base import MTPDeviceBase, synchronous
from calibre.devices.mtp.unix.detect import MTPDetect from calibre.devices.mtp.unix.detect import MTPDetect
@ -102,11 +102,6 @@ class MTP_DEVICE(MTPDeviceBase):
if self.progress_reporter is not None: if self.progress_reporter is not None:
self.progress_reporter(p) 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 @synchronous
def is_usb_connected(self, devices_on_system, debug=False, def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False): only_presence=False):
@ -134,7 +129,7 @@ class MTP_DEVICE(MTPDeviceBase):
@synchronous @synchronous
def post_yank_cleanup(self): def post_yank_cleanup(self):
self.dev = self.filesystem_cache = None self.dev = self.filesystem_cache = self.current_friendly_name = None
@synchronous @synchronous
def startup(self): def startup(self):
@ -184,15 +179,18 @@ class MTP_DEVICE(MTPDeviceBase):
self._carda_id = storage[1]['id'] self._carda_id = storage[1]['id']
if len(storage) > 2: if len(storage) > 2:
self._cardb_id = storage[2]['id'] self._cardb_id = storage[2]['id']
self.current_friendly_name = self.dev.name
@synchronous
def read_filesystem_cache(self):
try: try:
files, errs = self.dev.get_filelist(self) files, errs = self.dev.get_filelist(self)
if errs and not files: 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)) +self.format_errorstack(errs))
folders, errs = self.dev.get_folderlist() folders, errs = self.dev.get_folderlist()
if errs and not folders: 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.format_errorstack(errs))
self.filesystem_cache = FilesystemCache(files, folders) self.filesystem_cache = FilesystemCache(files, folders)
except: except:
@ -202,15 +200,15 @@ class MTP_DEVICE(MTPDeviceBase):
@synchronous @synchronous
def get_device_information(self, end_session=True): def get_device_information(self, end_session=True):
d = self.dev 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 @synchronous
def card_prefix(self, end_session=True): def card_prefix(self, end_session=True):
ans = [None, None] ans = [None, None]
if self._carda_id is not 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: if self._cardb_id is not None:
ans[1] = 'mtp:%d:'%self._cardb_id ans[1] = 'mtp:::%d:::'%self._cardb_id
return tuple(ans) return tuple(ans)
@synchronous @synchronous
@ -248,6 +246,7 @@ if __name__ == '__main__':
devs = linux_scanner() devs = linux_scanner()
mtp_devs = dev.detect(devs) mtp_devs = dev.detect(devs)
dev.open(list(mtp_devs)[0], 'xxx') dev.open(list(mtp_devs)[0], 'xxx')
dev.read_filesystem_cache()
d = dev.dev d = dev.dev
print ("Opened device:", dev.get_gui_name()) print ("Opened device:", dev.get_gui_name())
print ("Storage info:") print ("Storage info:")

View File

@ -1,3 +1,11 @@
/*
* libmtp.c
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#define UNICODE #define UNICODE
#include <Python.h> #include <Python.h>

View File

@ -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 <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,137 @@
/*
* device.cpp
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* 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 */
}; // }}}

View File

@ -0,0 +1,349 @@
/*
* device_enumeration.cpp
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* 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

View File

@ -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 <kovid at kovidgoyal.net>'
__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)

View File

@ -0,0 +1,58 @@
/*
* global.h
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#pragma once
#define UNICODE
#include <Windows.h>
#include <Python.h>
#include <Objbase.h>
#include <PortableDeviceApi.h>
#include <PortableDevice.h>
#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);
}

View File

@ -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 <kovid at kovidgoyal.net>'
__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()

View File

@ -0,0 +1,44 @@
/*
* utils.cpp
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
*
* 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;
}

View File

@ -2,35 +2,50 @@
* mtp.c * mtp.c
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net> * Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
* *
* Distributed under terms of the MIT license. * Distributed under terms of the GPL3 license.
*/ */
#include "global.h"
#define UNICODE using namespace wpd;
#include <Windows.h>
#include <Python.h>
#include <Objbase.h> // Module exception types
#include <PortableDeviceApi.h> 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 int _com_initialized = 0;
static PyObject *WPDError = NULL; // Application Info
static PyObject *NoWPD = NULL; wpd::ClientInfo wpd::client_info = {NULL, 0, 0, 0};
static IPortableDeviceManager *portable_device_manager = NULL;
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 * static PyObject *
wpd_init(PyObject *self, PyObject *args) { wpd_init(PyObject *self, PyObject *args) {
HRESULT hr; 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) { if (!_com_initialized) {
Py_BEGIN_ALLOW_THREADS;
hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
Py_END_ALLOW_THREADS;
if (SUCCEEDED(hr)) _com_initialized = 1; if (SUCCEEDED(hr)) _com_initialized = 1;
else {PyErr_SetString(WPDError, "Failed to initialize COM"); return NULL;} else {PyErr_SetString(WPDError, "Failed to initialize COM"); return NULL;}
} }
if (portable_device_manager == NULL) { if (portable_device_manager == NULL) {
Py_BEGIN_ALLOW_THREADS;
hr = CoCreateInstance(CLSID_PortableDeviceManager, NULL, hr = CoCreateInstance(CLSID_PortableDeviceManager, NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&portable_device_manager)); CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&portable_device_manager));
Py_END_ALLOW_THREADS;
if (FAILED(hr)) { if (FAILED(hr)) {
portable_device_manager = NULL; portable_device_manager = NULL;
@ -47,27 +62,122 @@ wpd_init(PyObject *self, PyObject *args) {
static PyObject * static PyObject *
wpd_uninit(PyObject *self, PyObject *args) { wpd_uninit(PyObject *self, PyObject *args) {
if (portable_device_manager != NULL) { if (portable_device_manager != NULL) {
Py_BEGIN_ALLOW_THREADS;
portable_device_manager->Release(); portable_device_manager->Release();
Py_END_ALLOW_THREADS;
portable_device_manager = NULL; portable_device_manager = NULL;
} }
if (_com_initialized) { if (_com_initialized) {
Py_BEGIN_ALLOW_THREADS;
CoUninitialize(); CoUninitialize();
Py_END_ALLOW_THREADS;
_com_initialized = 0; _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; 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[] = { static PyMethodDef wpd_methods[] = {
{"init", wpd_init, METH_VARARGS, {"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", 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." "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} {NULL, NULL, 0, NULL}
}; };
@ -76,6 +186,10 @@ PyMODINIT_FUNC
initwpd(void) { initwpd(void) {
PyObject *m; 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."); m = Py_InitModule3("wpd", wpd_methods, "Interface to the WPD windows service.");
if (m == NULL) return; if (m == NULL) return;
@ -84,6 +198,10 @@ initwpd(void) {
NoWPD = PyErr_NewException("wpd.NoWPD", NULL, NULL); NoWPD = PyErr_NewException("wpd.NoWPD", NULL, NULL);
if (NoWPD == NULL) return; if (NoWPD == NULL) return;
Py_INCREF(&DeviceType);
PyModule_AddObject(m, "Device", (PyObject *)&DeviceType);
} }

View File

@ -193,7 +193,11 @@ class PRST1(USBMS):
time_offsets = {} time_offsets = {}
for i, row in enumerate(cursor): for i, row in enumerate(cursor):
try:
comp_date = int(os.path.getmtime(self.normalize_path(prefix + row[0])) * 1000); 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]); device_date = int(row[1]);
offset = device_date - comp_date offset = device_date - comp_date
time_offsets.setdefault(offset, 0) time_offsets.setdefault(offset, 0)

View File

@ -10,7 +10,8 @@ from threading import RLock
from collections import namedtuple from collections import namedtuple
from calibre import prints, as_unicode 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 osx_scanner = win_scanner = linux_scanner = None
@ -253,13 +254,18 @@ freebsd_scanner = None
if isfreebsd: if isfreebsd:
freebsd_scanner = FreeBSDScanner() freebsd_scanner = FreeBSDScanner()
netbsd_scanner = None
''' NetBSD support currently not written yet '''
if isnetbsd:
netbsd_scanner = None
class DeviceScanner(object): class DeviceScanner(object):
def __init__(self, *args): def __init__(self, *args):
if isosx and osx_scanner is None: if isosx and osx_scanner is None:
raise RuntimeError('The Python extension usbobserver must be available on OS X.') 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 = [] self.devices = []
def scan(self): def scan(self):

View File

@ -11,11 +11,12 @@ import socket, select, json, inspect, os, traceback, time, sys, random
import hashlib, threading import hashlib, threading
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from functools import wraps from functools import wraps
from errno import EAGAIN, EINTR
from calibre import prints from calibre import prints
from calibre.constants import numeric_version, DEBUG from calibre.constants import numeric_version, DEBUG
from calibre.devices.errors import (OpenFailed, ControlError, TimeoutError, from calibre.devices.errors import (OpenFailed, ControlError, TimeoutError,
InitialConnectionError) InitialConnectionError, PacketError)
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.devices.usbms.books import Book, CollectionsBookList from calibre.devices.usbms.books import Book, CollectionsBookList
from calibre.devices.usbms.deviceconfig import DeviceConfig 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_CLIENT_COMM_TIMEOUT = 60.0 # Wait at most N seconds for an answer
MAX_UNSUCCESSFUL_CONNECTS = 5 MAX_UNSUCCESSFUL_CONNECTS = 5
SEND_NOOP_EVERY_NTH_PROBE = 5
DISCONNECT_AFTER_N_SECONDS = 30*60 # 30 minutes
opcodes = { opcodes = {
'NOOP' : 12, 'NOOP' : 12,
'OK' : 0, 'OK' : 0,
@ -120,7 +124,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
_('Use fixed network port') + ':::<p>' + _('Use fixed network port') + ':::<p>' +
_('If checked, use the port number in the "Port" box, otherwise ' _('If checked, use the port number in the "Port" box, otherwise '
'the driver will pick a random port') + '</p>', 'the driver will pick a random port') + '</p>',
_('Port') + ':::<p>' + _('Port number: ') + ':::<p>' +
_('Enter the port number the driver is to use if the "fixed port" box is checked') + '</p>', _('Enter the port number the driver is to use if the "fixed port" box is checked') + '</p>',
_('Print extra debug information') + ':::<p>' + _('Print extra debug information') + ':::<p>' +
_('Check this box if requested when reporting problems') + '</p>', _('Check this box if requested when reporting problems') + '</p>',
@ -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 ' _('. 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 ' 'these values to the list to enable them. The collections will be '
'given the name provided after the ":" character.')%dict( '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') + ':::<p>' +
_('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,) + '</p>',
] ]
EXTRA_CUSTOMIZATION_DEFAULT = [ EXTRA_CUSTOMIZATION_DEFAULT = [
False, False,
@ -141,7 +151,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
False, '9090', False, '9090',
False, False,
'', '',
'' '',
'',
True,
] ]
OPT_AUTOSTART = 0 OPT_AUTOSTART = 0
OPT_PASSWORD = 2 OPT_PASSWORD = 2
@ -149,6 +161,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
OPT_PORT_NUMBER = 5 OPT_PORT_NUMBER = 5
OPT_EXTRA_DEBUG = 6 OPT_EXTRA_DEBUG = 6
OPT_COLLECTIONS = 8 OPT_COLLECTIONS = 8
OPT_AUTODISCONNECT = 10
def __init__(self, path): def __init__(self, path):
self.sync_lock = threading.RLock() self.sync_lock = threading.RLock()
@ -165,6 +178,15 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
inspect.stack()[1][3]), end='') inspect.stack()[1][3]), end='')
for a in args: for a in args:
try: try:
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='') prints('', a, end='')
except: except:
prints('', 'value too long', end='') prints('', 'value too long', end='')
@ -339,6 +361,27 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
pos += len(v) pos += len(v)
return data 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): def _call_client(self, op, arg, print_debug_info=True):
if op != 'NOOP': if op != 'NOOP':
self.noop_counter = 0 self.noop_counter = 0
@ -355,9 +398,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if print_debug_info and extra_debug: if print_debug_info and extra_debug:
self._debug('send string', s) self._debug('send string', s)
self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT) self.device_socket.settimeout(self.MAX_CLIENT_COMM_TIMEOUT)
self.device_socket.sendall(('%d' % len(s))+s) self._send_byte_string((b'%d' % len(s))+s)
self.device_socket.settimeout(None)
v = self._read_string_from_net() v = self._read_string_from_net()
self.device_socket.settimeout(None)
if print_debug_info and extra_debug: if print_debug_info and extra_debug:
self._debug('received string', v) self._debug('received string', v)
if v: if v:
@ -373,13 +416,13 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
except socket.error: except socket.error:
self._debug('device went away') self._debug('device went away')
self._close_device_socket() self._close_device_socket()
raise ControlError('Device closed the network connection') raise ControlError(desc='Device closed the network connection')
except: except:
self._debug('other exception') self._debug('other exception')
traceback.print_exc() traceback.print_exc()
self._close_device_socket() self._close_device_socket()
raise 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. # Write a file as a series of base64-encoded strings.
def _put_file(self, infile, lpath, book_metadata, this_book, total_books): 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 self.is_connected = False
if self.is_connected: if self.is_connected:
self.noop_counter += 1 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: try:
ans = select.select((self.device_socket,), (), (), 0) ans = select.select((self.device_socket,), (), (), 0)
if len(ans[0]) == 0: if len(ans[0]) == 0:
@ -486,6 +530,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
# This will usually toss an exception if the socket is gone. # This will usually toss an exception if the socket is gone.
except: except:
pass pass
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: try:
if self._call_client('NOOP', dict())[0] is None: if self._call_client('NOOP', dict())[0] is None:
self._close_device_socket() self._close_device_socket()
@ -533,7 +582,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug() self._debug()
if not self.is_connected: if not self.is_connected:
# We have been called to retry the connection. Give up immediately # 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_uuid = library_uuid
self.current_library_name = current_library_name() self.current_library_name = current_library_name()
try: try:
@ -569,6 +618,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._debug('Protocol error - bogus book packet length') self._debug('Protocol error - bogus book packet length')
self._close_device_socket() self._close_device_socket()
return False return False
self._debug('CC version #:', result.get('ccVersionNumber', 'unknown'))
self.max_book_packet_len = result.get('maxBookContentPacketLen', self.max_book_packet_len = result.get('maxBookContentPacketLen',
self.BASE_PACKET_LEN) self.BASE_PACKET_LEN)
exts = result.get('acceptedExtensions', None) exts = result.get('acceptedExtensions', None)
@ -689,7 +739,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self._set_known_metadata(book) self._set_known_metadata(book)
bl.add_book(book, replace_metadata=True) bl.add_book(book, replace_metadata=True)
else: else:
raise ControlError('book metadata not returned') raise ControlError(desc='book metadata not returned')
return bl return bl
@synchronous('sync_lock') @synchronous('sync_lock')
@ -720,7 +770,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
print_debug_info=False) print_debug_info=False)
if opcode != 'OK': if opcode != 'OK':
self._debug('protocol error', opcode, i) self._debug('protocol error', opcode, i)
raise ControlError('sync_booklists') raise ControlError(desc='sync_booklists')
@synchronous('sync_lock') @synchronous('sync_lock')
def eject(self): def eject(self):
@ -748,7 +798,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
book = Book(self.PREFIX, lpath, other=mdata) book = Book(self.PREFIX, lpath, other=mdata)
length = self._put_file(infile, lpath, book, i, len(files)) length = self._put_file(infile, lpath, book, i, len(files))
if length < 0: 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)) paths.append((lpath, length))
# No need to deal with covers. The client will get the thumbnails # No need to deal with covers. The client will get the thumbnails
# in the mi structure # in the mi structure
@ -789,7 +839,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
if opcode == 'OK': if opcode == 'OK':
self._debug('removed book with UUID', result['uuid']) self._debug('removed book with UUID', result['uuid'])
else: else:
raise ControlError('Protocol error - delete books') raise ControlError(desc='Protocol error - delete books')
@synchronous('sync_lock') @synchronous('sync_lock')
def remove_books_from_metadata(self, paths, booklists): def remove_books_from_metadata(self, paths, booklists):
@ -825,7 +875,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
else: else:
eof = True eof = True
else: else:
raise ControlError('request for book data failed') raise ControlError(desc='request for book data failed')
@synchronous('sync_lock') @synchronous('sync_lock')
def set_plugboards(self, plugboards, pb_func): def set_plugboards(self, plugboards, pb_func):

View File

@ -31,7 +31,7 @@ BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'txtz', 'text', 'ht
'epub', 'fb2', 'djv', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', 'epub', 'fb2', 'djv', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'pmlz', 'mbp', 'tan', 'snb', 'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'pmlz', 'mbp', 'tan', 'snb',
'xps', 'oxps', 'azw4', 'book', 'zbf', 'pobi', 'docx', 'md', 'xps', 'oxps', 'azw4', 'book', 'zbf', 'pobi', 'docx', 'md',
'textile', 'markdown', 'ibook', 'iba', 'azw3'] 'textile', 'markdown', 'ibook', 'iba', 'azw3', 'ps']
class HTMLRenderer(object): class HTMLRenderer(object):

View File

@ -88,6 +88,15 @@ class MOBIOutput(OutputFormatPlugin):
'formats. This option tells calibre not to do this. ' 'formats. This option tells calibre not to do this. '
'Useful if your document contains lots of GIF/PNG images that ' 'Useful if your document contains lots of GIF/PNG images that '
'become very large when converted to JPEG.')), '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): def check_for_periodical(self):
@ -165,11 +174,10 @@ class MOBIOutput(OutputFormatPlugin):
toc.nodes[0].href = toc.nodes[0].nodes[0].href toc.nodes[0].href = toc.nodes[0].nodes[0].href
def convert(self, oeb, output_path, input_plugin, opts, log): def convert(self, oeb, output_path, input_plugin, opts, log):
from calibre.utils.config import tweaks
from calibre.ebooks.mobi.writer2.resources import Resources from calibre.ebooks.mobi.writer2.resources import Resources
self.log, self.opts, self.oeb = log, opts, oeb 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: if self.is_periodical:
mobi_type = 'old' # Amazon does not support KF8 periodicals mobi_type = 'old' # Amazon does not support KF8 periodicals
create_kf8 = mobi_type in ('new', 'both') create_kf8 = mobi_type in ('new', 'both')

View File

@ -11,6 +11,7 @@ from collections import defaultdict
from lxml import etree from lxml import etree
import cssutils import cssutils
from cssutils.css import Property
from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES, from calibre.ebooks.oeb.base import (XHTML, XHTML_NS, CSS_MIME, OEB_STYLES,
namespace, barename, XPath) namespace, barename, XPath)
@ -276,10 +277,16 @@ class CSSFlattener(object):
cssdict['font-family'] = node.attrib['face'] cssdict['font-family'] = node.attrib['face']
del node.attrib['face'] del node.attrib['face']
if 'color' in node.attrib: 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'] del node.attrib['color']
if 'bgcolor' in node.attrib: 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'] del node.attrib['bgcolor']
if cssdict.get('font-weight', '').lower() == 'medium': if cssdict.get('font-weight', '').lower() == 'medium':
cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium

View File

@ -25,7 +25,7 @@ class PluginWidget(Widget, Ui_Form):
'mobi_keep_original_images', 'mobi_keep_original_images',
'mobi_ignore_margins', 'mobi_toc_at_start', 'mobi_ignore_margins', 'mobi_toc_at_start',
'dont_compress', 'no_inline_toc', 'share_not_sync', '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 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.font_family_model = font_family_model
self.opt_masthead_font.setModel(self.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) self.initialize_options(get_option, get_help, db, book_id)

View File

@ -14,80 +14,10 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="8" column="0" colspan="2"> <item row="0" column="0">
<widget class="QGroupBox" name="groupBox"> <widget class="QCheckBox" name="opt_no_inline_toc">
<property name="title">
<string>Kindle options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Personal Doc tag:</string> <string>Do not add Table of Contents to book</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="opt_personal_doc"/>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="opt_share_not_sync">
<property name="text">
<string>Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="9" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_mobi_toc_at_start">
<property name="text">
<string>Put generated Table of Contents at &amp;start of book instead of end</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_mobi_ignore_margins">
<property name="text">
<string>Ignore &amp;margins</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_prefer_author_sort">
<property name="text">
<string>Use author &amp;sort for author</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -104,17 +34,24 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="opt_toc_title"/> <widget class="QLineEdit" name="opt_toc_title"/>
</item> </item>
<item row="6" column="0"> <item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_dont_compress"> <widget class="QCheckBox" name="opt_mobi_toc_at_start">
<property name="text"> <property name="text">
<string>Disable compression of the file contents</string> <string>Put generated Table of Contents at &amp;start of book instead of end</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="3" column="0">
<widget class="QCheckBox" name="opt_no_inline_toc"> <widget class="QCheckBox" name="opt_mobi_ignore_margins">
<property name="text"> <property name="text">
<string>Do not add Table of Contents to book</string> <string>Ignore &amp;margins</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="opt_prefer_author_sort">
<property name="text">
<string>Use author &amp;sort for author</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -125,6 +62,55 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0">
<widget class="QCheckBox" name="opt_dont_compress">
<property name="text">
<string>Disable compression of the file contents</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Kindle options</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>MOBI file &amp;type:</string>
</property>
<property name="buddy">
<cstring>opt_mobi_file_type</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_mobi_file_type"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Personal Doc tag:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="opt_personal_doc"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_share_not_sync">
<property name="text">
<string>Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -529,6 +529,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.remove_button.clicked.connect(self.s_r_remove_query) self.remove_button.clicked.connect(self.s_r_remove_query)
self.queries = JSONConfig("search_replace_queries") self.queries = JSONConfig("search_replace_queries")
self.saved_search_name = ''
self.query_field.addItem("") self.query_field.addItem("")
self.query_field_values = sorted([q for q in self.queries], key=sort_key) self.query_field_values = sorted([q for q in self.queries], key=sort_key)
self.query_field.addItems(self.query_field_values) self.query_field.addItems(self.query_field_values)
@ -1034,11 +1035,16 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.queries.commit() self.queries.commit()
def s_r_save_query(self, *args): 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 = '' name = ''
while not name: while not name:
name, ok = QInputDialog.getItem(self, _('Save search/replace'), 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: if not ok:
return return
if not name: if not name:
@ -1086,6 +1092,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
def s_r_query_change(self, item_name): def s_r_query_change(self, item_name):
if not item_name: if not item_name:
self.s_r_reset_query_fields() self.s_r_reset_query_fields()
self.saved_search_name = ''
return return
item = self.queries.get(unicode(item_name), None) item = self.queries.get(unicode(item_name), None)
if item is None: if item is None:

View File

@ -1241,17 +1241,18 @@ not multiple and the destination field is multiple</string>
<tabstop>search_mode</tabstop> <tabstop>search_mode</tabstop>
<tabstop>s_r_src_ident</tabstop> <tabstop>s_r_src_ident</tabstop>
<tabstop>s_r_template</tabstop> <tabstop>s_r_template</tabstop>
<tabstop>search_for</tabstop>
<tabstop>case_sensitive</tabstop>
<tabstop>replace_with</tabstop> <tabstop>replace_with</tabstop>
<tabstop>replace_func</tabstop> <tabstop>replace_func</tabstop>
<tabstop>destination_field</tabstop>
<tabstop>replace_mode</tabstop> <tabstop>replace_mode</tabstop>
<tabstop>comma_separated</tabstop> <tabstop>comma_separated</tabstop>
<tabstop>s_r_dst_ident</tabstop> <tabstop>s_r_dst_ident</tabstop>
<tabstop>results_count</tabstop> <tabstop>results_count</tabstop>
<tabstop>scrollArea11</tabstop>
<tabstop>destination_field</tabstop>
<tabstop>search_for</tabstop>
<tabstop>case_sensitive</tabstop>
<tabstop>starting_from</tabstop> <tabstop>starting_from</tabstop>
<tabstop>multiple_separator</tabstop>
<tabstop>scrollArea11</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>

View File

@ -310,8 +310,18 @@ class MetadataSingleDialogBase(ResizableDialog):
self.update_from_mi(mi) self.update_from_mi(mi)
def cover_from_format(self, *args): def cover_from_format(self, *args):
try:
mi, ext = self.formats_manager.get_selected_format_metadata(self.db, mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id) 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: if mi is None:
return return
cdata = None cdata = None

View File

@ -410,6 +410,7 @@ class TagsModel(QAbstractItemModel): # {{{
# first letter can actually be more than one letter long. # first letter can actually be more than one letter long.
cl_list = [None] * len(data[key]) cl_list = [None] * len(data[key])
last_ordnum = 0 last_ordnum = 0
last_c = ' '
for idx,tag in enumerate(data[key]): for idx,tag in enumerate(data[key]):
if not tag.sort: if not tag.sort:
c = ' ' c = ' '

View File

@ -417,7 +417,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
vprefs.set('viewer_splitter_state', vprefs.set('viewer_splitter_state',
bytearray(self.splitter.saveState())) bytearray(self.splitter.saveState()))
vprefs['multiplier'] = self.view.multiplier 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): def restore_state(self):
state = vprefs.get('viewer_toolbar_state', None) state = vprefs.get('viewer_toolbar_state', None)
@ -434,8 +434,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
# specific location, ensure they are visible. # specific location, ensure they are visible.
self.tool_bar.setVisible(True) self.tool_bar.setVisible(True)
self.tool_bar2.setVisible(True) self.tool_bar2.setVisible(True)
self.action_toggle_paged_mode.setChecked(not vprefs.get('in_paged_mode1', self.action_toggle_paged_mode.setChecked(not vprefs.get('in_paged_mode',
False)) True))
self.toggle_paged_mode(self.action_toggle_paged_mode.isChecked(), self.toggle_paged_mode(self.action_toggle_paged_mode.isChecked(),
at_start=True) at_start=True)

View File

@ -440,8 +440,7 @@ class KindlePage(QWizardPage, KindleUI):
x = unicode(self.to_address.text()).strip() x = unicode(self.to_address.text()).strip()
parts = x.split('@') parts = x.split('@')
if (self.send_email_widget.set_email_settings(True) and len(parts) >= 2 if (len(parts) >= 2 and parts[0] and self.send_email_widget.set_email_settings(True)):
and parts[0]):
conf = smtp_prefs() conf = smtp_prefs()
accounts = conf.parse().accounts accounts = conf.parse().accounts
if not accounts: accounts = {} if not accounts: accounts = {}
@ -676,8 +675,9 @@ class LibraryPage(QWizardPage, LibraryUI):
self.language.blockSignals(True) self.language.blockSignals(True)
self.language.clear() self.language.clear()
from calibre.utils.localization import (available_translations, from calibre.utils.localization import (available_translations,
get_language, get_lang) get_language, get_lang, get_lc_messages_path)
lang = get_lang() lang = get_lang()
lang = get_lc_messages_path(lang) if lang else lang
if lang is None or lang not in available_translations(): if lang is None or lang not in available_translations():
lang = 'en' lang = 'en'
def get_esc_lang(l): def get_esc_lang(l):

View File

@ -11,7 +11,7 @@ import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, \
from collections import defaultdict from collections import defaultdict
import threading, random import threading, random
from itertools import repeat from itertools import repeat
from math import ceil from math import ceil, floor
from calibre import prints, force_unicode from calibre import prints, force_unicode
from calibre.ebooks.metadata import (title_sort, author_to_author_sort, 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: if name and name != fname:
changed = True changed = True
break break
if path == current_path and not changed:
return
tpath = os.path.join(self.library_path, *path.split('/')) tpath = os.path.join(self.library_path, *path.split('/'))
if not os.path.exists(tpath): if not os.path.exists(tpath):
os.makedirs(tpath) os.makedirs(tpath)
if path == current_path and not changed:
return
spath = os.path.join(self.library_path, *current_path.split('/')) spath = os.path.join(self.library_path, *current_path.split('/'))
if current_path and os.path.exists(spath): # Migrate existing files 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 `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)): if callable(getattr(data, 'save', None)):
data.save(path) data.save(path)
else: else:
@ -2080,7 +2089,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return 1.0 return 1.0
series_indices = [x[0] for x in series_indices] series_indices = [x[0] for x in series_indices]
if tweaks['series_index_auto_increment'] == 'next': 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': if tweaks['series_index_auto_increment'] == 'first_free':
for i in range(1, 10000): for i in range(1, 10000):
if i not in series_indices: if i not in series_indices:

View File

@ -42,7 +42,7 @@ class Restore(Thread):
self.src_library_path = os.path.abspath(library_path) self.src_library_path = os.path.abspath(library_path)
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$') 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): if not callable(self.progress_callback):
self.progress_callback = lambda x, y: x self.progress_callback = lambda x, y: x
self.dirs = [] self.dirs = []

View File

@ -22,13 +22,18 @@ def available_translations():
_available_translations = [x for x in stats if stats[x] > 0.1] _available_translations = [x for x in stats if stats[x] > 0.1]
return _available_translations return _available_translations
def get_lang(): def get_system_locale():
'Try to figure out what language to display the interface in' from calibre.constants import iswindows
from calibre.utils.config_base import prefs lang = None
lang = prefs['language'] if iswindows:
lang = os.environ.get('CALIBRE_OVERRIDE_LANG', lang) from calibre.constants import get_windows_user_locale_name
if lang: try:
return lang 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: try:
lang = locale.getdefaultlocale(['LANGUAGE', 'LC_ALL', 'LC_CTYPE', lang = locale.getdefaultlocale(['LANGUAGE', 'LC_ALL', 'LC_CTYPE',
'LC_MESSAGES', 'LANG'])[0] 'LC_MESSAGES', 'LANG'])[0]
@ -39,6 +44,25 @@ def get_lang():
lang = os.environ['LANG'] lang = os.environ['LANG']
except: except:
pass 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
lang = prefs['language']
lang = os.environ.get('CALIBRE_OVERRIDE_LANG', lang)
if lang:
return lang
try:
lang = get_system_locale()
except:
import traceback
traceback.print_exc()
lang = None
if lang: if lang:
match = re.match('[a-z]{2,3}(_[A-Z]{2}){0,1}', lang) match = re.match('[a-z]{2,3}(_[A-Z]{2}){0,1}', lang)
if match: if match:
@ -55,7 +79,7 @@ def get_lc_messages_path(lang):
if lang in available_translations(): if lang in available_translations():
hlang = lang hlang = lang
else: else:
xlang = lang.split('_')[0] xlang = lang.split('_')[0].lower()
if xlang in available_translations(): if xlang in available_translations():
hlang = xlang hlang = xlang
return hlang return hlang