mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
0.8.64+
This commit is contained in:
commit
31af65d019
@ -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'),
|
||||
]
|
||||
|
||||
|
@ -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,6 +299,7 @@ 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)
|
||||
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:
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:")
|
||||
|
@ -1,3 +1,11 @@
|
||||
/*
|
||||
* libmtp.c
|
||||
* Copyright (C) 2012 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
|
||||
#define UNICODE
|
||||
#include <Python.h>
|
||||
|
||||
|
11
src/calibre/devices/mtp/windows/__init__.py
Normal file
11
src/calibre/devices/mtp/windows/__init__.py
Normal 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'
|
||||
|
||||
|
||||
|
137
src/calibre/devices/mtp/windows/device.cpp
Normal file
137
src/calibre/devices/mtp/windows/device.cpp
Normal 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 */
|
||||
}; // }}}
|
||||
|
349
src/calibre/devices/mtp/windows/device_enumeration.cpp
Normal file
349
src/calibre/devices/mtp/windows/device_enumeration.cpp
Normal 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
|
200
src/calibre/devices/mtp/windows/driver.py
Normal file
200
src/calibre/devices/mtp/windows/driver.py
Normal 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)
|
||||
|
||||
|
||||
|
58
src/calibre/devices/mtp/windows/global.h
Normal file
58
src/calibre/devices/mtp/windows/global.h
Normal 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);
|
||||
|
||||
}
|
||||
|
77
src/calibre/devices/mtp/windows/remote.py
Normal file
77
src/calibre/devices/mtp/windows/remote.py
Normal 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()
|
||||
|
44
src/calibre/devices/mtp/windows/utils.cpp
Normal file
44
src/calibre/devices/mtp/windows/utils.cpp
Normal 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;
|
||||
}
|
||||
|
@ -2,35 +2,50 @@
|
||||
* mtp.c
|
||||
* 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
|
||||
#include <Windows.h>
|
||||
#include <Python.h>
|
||||
using namespace wpd;
|
||||
|
||||
#include <Objbase.h>
|
||||
#include <PortableDeviceApi.h>
|
||||
// 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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -193,7 +193,11 @@ class PRST1(USBMS):
|
||||
|
||||
time_offsets = {}
|
||||
for i, row in enumerate(cursor):
|
||||
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)
|
||||
|
@ -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):
|
||||
|
@ -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') + ':::<p>' +
|
||||
_('If checked, use the port number in the "Port" box, otherwise '
|
||||
'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>',
|
||||
_('Print extra debug information') + ':::<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 '
|
||||
'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') + ':::<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 = [
|
||||
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,6 +178,15 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
||||
inspect.stack()[1][3]), end='')
|
||||
for a in args:
|
||||
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='')
|
||||
except:
|
||||
prints('', 'value too long', end='')
|
||||
@ -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,6 +530,11 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
||||
# This will usually toss an exception if the socket is gone.
|
||||
except:
|
||||
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:
|
||||
if self._call_client('NOOP', dict())[0] is None:
|
||||
self._close_device_socket()
|
||||
@ -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):
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -14,80 +14,10 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="8" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<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">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="opt_no_inline_toc">
|
||||
<property name="text">
|
||||
<string>Personal Doc tag:</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 &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 &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 &sort for author</string>
|
||||
<string>Do not add Table of Contents to book</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -104,17 +34,24 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="opt_toc_title"/>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="opt_dont_compress">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_mobi_toc_at_start">
|
||||
<property name="text">
|
||||
<string>Disable compression of the file contents</string>
|
||||
<string>Put generated Table of Contents at &start of book instead of end</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="opt_no_inline_toc">
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="opt_mobi_ignore_margins">
|
||||
<property name="text">
|
||||
<string>Do not add Table of Contents to book</string>
|
||||
<string>Ignore &margins</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="opt_prefer_author_sort">
|
||||
<property name="text">
|
||||
<string>Use author &sort for author</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -125,6 +62,55 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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 &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>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
@ -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:
|
||||
|
@ -1241,17 +1241,18 @@ not multiple and the destination field is multiple</string>
|
||||
<tabstop>search_mode</tabstop>
|
||||
<tabstop>s_r_src_ident</tabstop>
|
||||
<tabstop>s_r_template</tabstop>
|
||||
<tabstop>search_for</tabstop>
|
||||
<tabstop>case_sensitive</tabstop>
|
||||
<tabstop>replace_with</tabstop>
|
||||
<tabstop>replace_func</tabstop>
|
||||
<tabstop>destination_field</tabstop>
|
||||
<tabstop>replace_mode</tabstop>
|
||||
<tabstop>comma_separated</tabstop>
|
||||
<tabstop>s_r_dst_ident</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>multiple_separator</tabstop>
|
||||
<tabstop>scrollArea11</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
|
@ -310,8 +310,18 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.update_from_mi(mi)
|
||||
|
||||
def cover_from_format(self, *args):
|
||||
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
|
||||
|
@ -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 = ' '
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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 = []
|
||||
|
@ -22,13 +22,18 @@ def available_translations():
|
||||
_available_translations = [x for x in stats if stats[x] > 0.1]
|
||||
return _available_translations
|
||||
|
||||
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
|
||||
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]
|
||||
@ -39,6 +44,25 @@ def get_lang():
|
||||
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
|
||||
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:
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user