From 7214c96fd3a5f50fc513a704a558093dd9c1bbf8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Aug 2012 20:42:25 +0530 Subject: [PATCH 01/12] Add driver for Samsung Galaxy Ace S5839i --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 12e7f40301..9d36c30fc0 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -214,7 +214,7 @@ 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'] 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', From 3c8450f620ceebd8e21667c82bd82f53f15da0a3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Aug 2012 22:29:51 +0530 Subject: [PATCH 02/12] ... --- src/calibre/devices/mtp/windows/remote.py | 51 +++++++++++++++++++---- src/calibre/devices/mtp/windows/utils.cpp | 5 +-- src/calibre/devices/mtp/windows/wpd.cpp | 1 + 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py index 5bf971ce8f..7238110532 100644 --- a/src/calibre/devices/mtp/windows/remote.py +++ b/src/calibre/devices/mtp/windows/remote.py @@ -7,9 +7,9 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import subprocess, sys, os +import subprocess, sys, os, pprint -def build(): +def build(mod='wpd'): builder = subprocess.Popen('ssh xp_build ~/build-wpd'.split()) syncer = subprocess.Popen('ssh getafix ~/test-wpd'.split()) if builder.wait() != 0: @@ -17,9 +17,9 @@ def build(): if syncer.wait() != 0: raise Exception('Failed to rsync to getafix') subprocess.check_call( - 'scp xp_build:build/calibre/src/calibre/plugins/wpd.pyd /tmp'.split()) + ('scp xp_build:build/calibre/src/calibre/plugins/%s.pyd /tmp'%mod).split()) subprocess.check_call( - 'scp /tmp/wpd.pyd getafix:calibre/src/calibre/devices/mtp/windows'.split()) + ('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() @@ -27,7 +27,6 @@ def build(): def main(): - import pprint sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import wpd from calibre.constants import plugins @@ -41,6 +40,44 @@ def main(): finally: wpd.uninit() -if __name__ == '__main__': - main() +def winutil(): + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + del sys.modules['winutil'] + import winutil + from calibre.constants import plugins + plugins._plugins['winutil'] = (winutil, '') + sys.path.pop(0) + print (winutil.serial_number_from_drive('F')) + +def get_subkeys(key): + import _winreg + index = -1 + while True: + index += 1 + try: + yield _winreg.EnumKey(key, index) + except OSError: + break + +def get_values(key): + import _winreg + index = -1 + while True: + index +=1 + try: + yield _winreg.EnumValue(key, index) + except OSError: + break + +def test(): + vid, pid = 0x1949, 0x4 + import _winreg as r + usb = r.OpenKey(r.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Enum\\USB') + q = ('vid_%4.4x&pid_%4.4x'%(vid, pid)) + dev = r.OpenKey(usb, q) + print (list(get_subkeys(dev))) + +if __name__ == '__main__': + # main() + winutil() diff --git a/src/calibre/devices/mtp/windows/utils.cpp b/src/calibre/devices/mtp/windows/utils.cpp index 424dae0852..7cf2757dc8 100644 --- a/src/calibre/devices/mtp/windows/utils.cpp +++ b/src/calibre/devices/mtp/windows/utils.cpp @@ -33,13 +33,12 @@ PyObject *wpd::hresult_set_exc(const char *msg, HRESULT hr) { wchar_t *wpd::unicode_to_wchar(PyObject *o) { wchar_t *buf; Py_ssize_t len; - if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The pnp id must be a unicode object"); return NULL;} + if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The python object must be a unicode object"); return NULL;} len = PyUnicode_GET_SIZE(o); - if (len < 1) {PyErr_Format(PyExc_TypeError, "The pnp id must not be empty."); return NULL;} 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 pnp id."); return NULL; } + if (len == -1) { free(buf); PyErr_Format(PyExc_TypeError, "Invalid python unicode object."); return NULL; } return buf; } diff --git a/src/calibre/devices/mtp/windows/wpd.cpp b/src/calibre/devices/mtp/windows/wpd.cpp index 76ecea65b8..73b1767732 100644 --- a/src/calibre/devices/mtp/windows/wpd.cpp +++ b/src/calibre/devices/mtp/windows/wpd.cpp @@ -148,6 +148,7 @@ wpd_device_info(PyObject *self, PyObject *args) { 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(); From 4e3740db20bdcf58a54060e38912599c7246753f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Aug 2012 23:04:05 +0530 Subject: [PATCH 03/12] ... --- src/calibre/devices/mtp/windows/remote.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py index 7238110532..cc35e36e64 100644 --- a/src/calibre/devices/mtp/windows/remote.py +++ b/src/calibre/devices/mtp/windows/remote.py @@ -78,6 +78,6 @@ def test(): print (list(get_subkeys(dev))) if __name__ == '__main__': - # main() - winutil() + main() + # winutil() From 8138a17a719c2b99192ada07fdea36f217b7cc74 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Aug 2012 23:18:47 +0530 Subject: [PATCH 04/12] Fix #1035902 (Add Driver for COBY kYROS MID7042 Android Tablet) --- src/calibre/devices/android/driver.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 9d36c30fc0..f9efe3354e 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -87,7 +87,7 @@ class ANDROID(USBMS): # Google 0x18d1 : { - 0x0001 : [0x0223, 0x9999], + 0x0001 : [0x0223, 0x230, 0x9999], 0x0003 : [0x0230], 0x4e11 : [0x0100, 0x226, 0x227], 0x4e12 : [0x0100, 0x226, 0x227], @@ -196,7 +196,7 @@ class ANDROID(USBMS): 'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON', 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', - 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI'] + 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', @@ -214,7 +214,8 @@ class ANDROID(USBMS): 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', - 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', 'S5830I_CARD'] + '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' From 37eb99304470ccdee3917faeb97404f419640568 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 07:31:54 +0530 Subject: [PATCH 05/12] WPD: Get information about storage on WPD devices --- .../mtp/windows/device_enumeration.cpp | 122 +++++++++++++++++- src/calibre/devices/mtp/windows/remote.py | 1 + 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/windows/device_enumeration.cpp b/src/calibre/devices/mtp/windows/device_enumeration.cpp index 43adc35625..3135a50032 100644 --- a/src/calibre/devices/mtp/windows/device_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/device_enumeration.cpp @@ -72,6 +72,119 @@ IPortableDevice *open_device(const wchar_t *pnp_id, IPortableDeviceValues *clien } // }}} +PyObject* get_storage_info(IPortableDevice *device) { + HRESULT hr; + 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++) { + if SUCCEEDED(properties->GetValues(object_ids[i], storage_properties, &values)) { + 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; @@ -83,7 +196,7 @@ PyObject* get_device_information(IPortableDevice *device) { // {{{ DWORD num_of_categories, i; LPWSTR temp; ULONG ti; - PyObject *t, *ans = NULL; + PyObject *t, *ans = NULL, *storage = NULL; char *type; Py_BEGIN_ALLOW_THREADS; @@ -216,6 +329,13 @@ PyObject* get_device_information(IPortableDevice *device) { // {{{ } 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(); diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py index cc35e36e64..7c16a87ed1 100644 --- a/src/calibre/devices/mtp/windows/remote.py +++ b/src/calibre/devices/mtp/windows/remote.py @@ -8,6 +8,7 @@ __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' import subprocess, sys, os, pprint +pprint def build(mod='wpd'): builder = subprocess.Popen('ssh xp_build ~/build-wpd'.split()) From 43e8d66dfc5dbe9d4c2228b76643dc4f34374314 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 07:37:27 +0530 Subject: [PATCH 06/12] BSD patches --- src/calibre/constants.py | 3 ++- src/calibre/devices/scanner.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index c4d2675bda..5b99346ef1 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -28,7 +28,8 @@ isosx = 'darwin' in _plat isnewosx = isosx and getattr(sys, 'new_app_bundle', False) isfreebsd = 'freebsd' in _plat isnetbsd = 'netbsd' in _plat -isbsd = isfreebsd or isnetbsd +isdragonflybsd = 'dragonfly' in _plat +isbsd = isfreebsd or isnetbsd or isdragonflybsd islinux = not(iswindows or isosx or isbsd) isfrozen = hasattr(sys, 'frozen') isunix = isosx or islinux diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index 89bd4a0cec..49273dd8bc 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -10,7 +10,8 @@ from threading import RLock from collections import namedtuple from calibre import prints, as_unicode -from calibre.constants import iswindows, isosx, plugins, islinux, isfreebsd +from calibre.constants import (iswindows, isosx, plugins, islinux, isfreebsd, + isnetbsd) osx_scanner = win_scanner = linux_scanner = None @@ -253,13 +254,18 @@ freebsd_scanner = None if isfreebsd: freebsd_scanner = FreeBSDScanner() +netbsd_scanner = None + +''' NetBSD support currently not written yet ''' +if isnetbsd: + netbsd_scanner = None class DeviceScanner(object): def __init__(self, *args): if isosx and osx_scanner is None: raise RuntimeError('The Python extension usbobserver must be available on OS X.') - self.scanner = win_scanner if iswindows else osx_scanner if isosx else freebsd_scanner if isfreebsd else linux_scanner + self.scanner = win_scanner if iswindows else osx_scanner if isosx else freebsd_scanner if isfreebsd else netbsd_scanner if isnetbsd else linux_scanner self.devices = [] def scan(self): From 18222e1ce6fa7408d50548823dc6810816b08222 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 08:02:59 +0530 Subject: [PATCH 07/12] Do not error out when setting the cover for a book that has no folders in the library. Fixes #1035935 (Adding cover to Empty book: error) --- src/calibre/library/database2.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index c210993936..f3e0510750 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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: From 2b32f1fd25e68ebcdbd0c203ebb29e7b7db2925c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 10:47:46 +0530 Subject: [PATCH 08/12] ... --- src/calibre/devices/mtp/windows/device_enumeration.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/mtp/windows/device_enumeration.cpp b/src/calibre/devices/mtp/windows/device_enumeration.cpp index 3135a50032..1476690d3a 100644 --- a/src/calibre/devices/mtp/windows/device_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/device_enumeration.cpp @@ -73,7 +73,7 @@ IPortableDevice *open_device(const wchar_t *pnp_id, IPortableDeviceValues *clien } // }}} PyObject* get_storage_info(IPortableDevice *device) { - HRESULT hr; + HRESULT hr, hr2; IPortableDeviceContent *content = NULL; IEnumPortableDeviceObjectIDs *objects = NULL; IPortableDeviceProperties *properties = NULL; @@ -132,7 +132,10 @@ PyObject* get_storage_info(IPortableDevice *device) { Py_END_ALLOW_THREADS; if (SUCCEEDED(hr)) { for(i = 0; i < fetched; i++) { - if SUCCEEDED(properties->GetValues(object_ids[i], storage_properties, &values)) { + 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) From 20774544ac30c1bf85fd5b166bacd30a00ef9de6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 11:19:21 +0530 Subject: [PATCH 09/12] ... --- src/calibre/devices/mtp/windows/device_enumeration.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/mtp/windows/device_enumeration.cpp b/src/calibre/devices/mtp/windows/device_enumeration.cpp index 1476690d3a..745023b4b7 100644 --- a/src/calibre/devices/mtp/windows/device_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/device_enumeration.cpp @@ -154,20 +154,17 @@ PyObject* get_storage_info(IPortableDevice *device) { 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); + 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); + 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); + if (desc != NULL) { PyDict_SetItemString(so, "filesystem", desc); Py_DECREF(desc);} CoTaskMemFree(storage_desc); storage_desc = NULL; } PyList_Append(storage, so); From 3ecdd2d2690ae4f9ca94e11497cf289d610330bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 12:26:17 +0530 Subject: [PATCH 10/12] ... --- src/calibre/devices/mtp/unix/libmtp.c | 8 ++++++++ src/calibre/devices/mtp/windows/device_enumeration.cpp | 6 +++--- src/calibre/devices/mtp/windows/global.h | 2 +- src/calibre/devices/mtp/windows/utils.cpp | 2 +- src/calibre/devices/mtp/windows/wpd.cpp | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index 58e8df6686..7ea987782a 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -1,3 +1,11 @@ +/* + * libmtp.c + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + + #define UNICODE #include diff --git a/src/calibre/devices/mtp/windows/device_enumeration.cpp b/src/calibre/devices/mtp/windows/device_enumeration.cpp index 745023b4b7..775464d4c8 100644 --- a/src/calibre/devices/mtp/windows/device_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/device_enumeration.cpp @@ -2,7 +2,7 @@ * device_enumeration.cpp * Copyright (C) 2012 Kovid Goyal * - * Distributed under terms of the MIT license. + * Distributed under terms of the GPL3 license. */ #include "global.h" @@ -72,7 +72,7 @@ IPortableDevice *open_device(const wchar_t *pnp_id, IPortableDeviceValues *clien } // }}} -PyObject* get_storage_info(IPortableDevice *device) { +PyObject* get_storage_info(IPortableDevice *device) { // {{{ HRESULT hr, hr2; IPortableDeviceContent *content = NULL; IEnumPortableDeviceObjectIDs *objects = NULL; @@ -183,7 +183,7 @@ end: if (storage_properties != NULL) storage_properties->Release(); if (values != NULL) values->Release(); return ans; -} +} // }}} PyObject* get_device_information(IPortableDevice *device) { // {{{ IPortableDeviceContent *content = NULL; diff --git a/src/calibre/devices/mtp/windows/global.h b/src/calibre/devices/mtp/windows/global.h index 65a3d53718..d4f8eed3a9 100644 --- a/src/calibre/devices/mtp/windows/global.h +++ b/src/calibre/devices/mtp/windows/global.h @@ -2,7 +2,7 @@ * global.h * Copyright (C) 2012 Kovid Goyal * - * Distributed under terms of the MIT license. + * Distributed under terms of the GPL3 license. */ #pragma once diff --git a/src/calibre/devices/mtp/windows/utils.cpp b/src/calibre/devices/mtp/windows/utils.cpp index 7cf2757dc8..527885a6f5 100644 --- a/src/calibre/devices/mtp/windows/utils.cpp +++ b/src/calibre/devices/mtp/windows/utils.cpp @@ -2,7 +2,7 @@ * utils.cpp * Copyright (C) 2012 Kovid Goyal * - * Distributed under terms of the MIT license. + * Distributed under terms of the GPL3 license. */ #include "global.h" diff --git a/src/calibre/devices/mtp/windows/wpd.cpp b/src/calibre/devices/mtp/windows/wpd.cpp index 73b1767732..e739826fbb 100644 --- a/src/calibre/devices/mtp/windows/wpd.cpp +++ b/src/calibre/devices/mtp/windows/wpd.cpp @@ -2,7 +2,7 @@ * mtp.c * Copyright (C) 2012 Kovid Goyal * - * Distributed under terms of the MIT license. + * Distributed under terms of the GPL3 license. */ #include "global.h" From a7cbda6f660e55963cabb8face4d5102c36b3ce9 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 13 Aug 2012 04:21:46 -0600 Subject: [PATCH 11/12] Improved behavior of GenericRulesTable when focus is lost, fixed some cross-referencing bugs in HTML, made anchors XHTML compliant. --- resources/catalog/stylesheet.css | 4 +- src/calibre/gui2/catalog/catalog_epub_mobi.py | 179 +++++++++++------- .../library/catalogs/epub_mobi_builder.py | 64 +++---- 3 files changed, 142 insertions(+), 105 deletions(-) diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index f14b1aaa29..48edce76c8 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -74,7 +74,7 @@ p.date_read { p.author { font-size:large; margin-top:0em; - margin-bottom:0em; + margin-bottom:0.1em; text-align: center; text-indent: 0em; } @@ -122,7 +122,7 @@ p.genres { p.series { font-style:italic; - margin-top:0.25em; + margin-top:0.10em; margin-bottom:0em; margin-left:2em; text-align:left; diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 42143eb506..185b6ab668 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -15,8 +15,9 @@ from calibre.utils.icu import sort_key from catalog_epub_mobi_ui import Ui_Form from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, - QDoubleSpinBox, QIcon, QLineEdit, QRadioButton, QSize, QSizePolicy, - QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget) + QDoubleSpinBox, QIcon, QLineEdit, QObject, QRadioButton, QSize, QSizePolicy, + QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget, + SIGNAL) class PluginWidget(QWidget,Ui_Form): @@ -454,6 +455,8 @@ class GenericRulesTable(QTableWidget): Add QTableWidget, controls to parent QGroupBox placeholders for basic methods to be overriden ''' + FOCUS_SWITCHING = True + DEBUG = False def __init__(self, parent_gb, object_name, rules, eligible_custom_fields, db): self.rules = rules @@ -476,11 +479,15 @@ class GenericRulesTable(QTableWidget): self.setRowCount(0) self.layout.addWidget(self) - self.last_row_selected = self.currentRow() - self.last_rows_selected = self.selectionModel().selectedRows() + if self.FOCUS_SWITCHING: + self.last_row_selected = self.currentRow() + self.last_rows_selected = self.selectionModel().selectedRows() self._init_controls() + # Hook check_box changes. Everything else is already hooked + QObject.connect(self, SIGNAL('cellChanged(int,int)'), self.enabled_state_changed) + def _init_controls(self): # Add the control set vbl = QVBoxLayout() @@ -516,7 +523,12 @@ class GenericRulesTable(QTableWidget): def add_row(self): self.setFocus() - row = self.last_row_selected + 1 + if self.FOCUS_SWITCHING: + row = self.last_row_selected + 1 + else: + row = self.currentRow() + 1 + if self.DEBUG and self.FOCUS_SWITCHING: + print("%s:add_row(): last_row_selected: %d, row: %d" % (self.objectName(), self.last_row_selected, row)) self.insertRow(row) self.populate_table_row(row, self.create_blank_row_data()) self.select_and_scroll_to_row(row) @@ -538,7 +550,10 @@ class GenericRulesTable(QTableWidget): def delete_row(self): self.setFocus() - rows = self.last_rows_selected + if self.FOCUS_SWITCHING: + rows = self.last_rows_selected + else: + rows = self.selectionModel().selectedRows() if len(rows) == 0: return @@ -558,11 +573,18 @@ class GenericRulesTable(QTableWidget): elif self.rowCount() > 0: self.select_and_scroll_to_row(first_sel_row - 1) + def focusInEvent(self,e): + if self.DEBUG: + print("%s:focusInEvent()" % self.objectName()) + def focusOutEvent(self,e): # Override of QTableWidget method - clear selection when table loses focus - self.last_row_selected = self.currentRow() - self.last_rows_selected = self.selectionModel().selectedRows() - self.clearSelection() + if self.FOCUS_SWITCHING: + self.last_row_selected = self.currentRow() + self.last_rows_selected = self.selectionModel().selectedRows() + self.clearSelection() + if self.DEBUG: + print("%s:focusOutEvent(): self.last_row_selected: %d" % (self.objectName(),self.last_row_selected)) def get_data(self): ''' @@ -572,7 +594,10 @@ class GenericRulesTable(QTableWidget): def move_row_down(self): self.setFocus() - rows = self.last_rows_selected + if self.FOCUS_SWITCHING: + rows = self.last_rows_selected + else: + rows = self.selectionModel().selectedRows() if len(rows) == 0: return last_sel_row = rows[-1].row() @@ -598,13 +623,16 @@ class GenericRulesTable(QTableWidget): self.blockSignals(False) scroll_to_row = last_sel_row + 1 - if scroll_to_row < self.rowCount() - 1: - scroll_to_row = scroll_to_row + 1 + #if scroll_to_row < self.rowCount() - 1: + # scroll_to_row = scroll_to_row + 1 self.select_and_scroll_to_row(scroll_to_row) def move_row_up(self): self.setFocus() - rows = self.last_rows_selected + if self.FOCUS_SWITCHING: + rows = self.last_rows_selected + else: + rows = self.selectionModel().selectedRows() if len(rows) == 0: return first_sel_row = rows[0].row() @@ -623,10 +651,14 @@ class GenericRulesTable(QTableWidget): self.removeRow(selrow.row() - 1) self.blockSignals(False) - scroll_to_row = first_sel_row - 1 + scroll_to_row = first_sel_row if scroll_to_row > 0: scroll_to_row = scroll_to_row - 1 self.select_and_scroll_to_row(scroll_to_row) + if self.DEBUG: + print("%s:move_row_up(): first_sel_row: %d" % (self.objectName(), first_sel_row)) + print("%s:move_row_up(): scroll_to_row: %d" % (self.objectName(), scroll_to_row)) + print("%s move_row_down(): current_row: %d" % (self.objectName(), self.currentRow())) def populate_table_row(self): ''' @@ -642,13 +674,69 @@ class GenericRulesTable(QTableWidget): def rule_name_edited(self): current_row = self.currentRow() self.cellWidget(current_row,1).home(False) - self.setFocus() self.select_and_scroll_to_row(current_row) def select_and_scroll_to_row(self, row): + self.setFocus() self.selectRow(row) self.scrollToItem(self.currentItem()) + def _source_index_changed(self, combo): + # Figure out which row we're in + for row in range(self.rowCount()): + if self.cellWidget(row, self.COLUMNS['FIELD']['ordinal']) is combo: + break + + if self.DEBUG: + print("%s:_source_index_changed(): calling source_index_changed with row: %d " % + (self.objectName(), row)) + + self.source_index_changed(combo, row) + + def source_index_changed(self, combo, row, pattern=''): + # Populate the Pattern field based upon the Source field + + source_field = str(combo.currentText()) + if source_field == '': + values = [] + elif source_field == 'Tags': + values = sorted(self.db.all_tags(), key=sort_key) + else: + if self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['enumeration', 'text']: + values = self.db.all_custom(self.db.field_metadata.key_to_label( + self.eligible_custom_fields[unicode(source_field)]['field'])) + values = sorted(values, key=sort_key) + elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['bool']: + values = [_('True'),_('False'),_('unspecified')] + elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['composite']: + values = [_('any value'),_('unspecified')] + elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['datetime']: + values = [_('any date'),_('unspecified')] + + values_combo = ComboBox(self, values, pattern) + values_combo.currentIndexChanged.connect(partial(self.values_index_changed, values_combo)) + self.setCellWidget(row, self.COLUMNS['PATTERN']['ordinal'], values_combo) + self.select_and_scroll_to_row(row) + + def values_index_changed(self, combo): + # After edit, select row + for row in range(self.rowCount()): + if self.cellWidget(row, self.COLUMNS['PATTERN']['ordinal']) is combo: + self.select_and_scroll_to_row(row) + break + + if self.DEBUG: + print("%s:values_index_changed(): row %d " % + (self.objectName(), row)) + + def enabled_state_changed(self, row, col): + # After state change, select row + if col in [self.COLUMNS['ENABLED']['ordinal']]: + self.select_and_scroll_to_row(row) + if self.DEBUG: + print("%s:enabled_state_changed(): row %d col %d" % + (self.objectName(), row, col)) + class ExclusionRules(GenericRulesTable): COLUMNS = { 'ENABLED':{'ordinal': 0, 'name': ''}, @@ -658,6 +746,7 @@ class ExclusionRules(GenericRulesTable): def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): super(ExclusionRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db) + self.setObjectName("exclusion_rules_table") self._init_table_widget() self._initialize() @@ -730,7 +819,7 @@ class ExclusionRules(GenericRulesTable): def set_source_field_in_row(row, col, field=''): source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) - source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) + source_combo.currentIndexChanged.connect(partial(self._source_index_changed, source_combo)) self.setCellWidget(row, col, source_combo) return source_combo @@ -738,7 +827,8 @@ class ExclusionRules(GenericRulesTable): self.blockSignals(True) # Enabled - self.setItem(row, self.COLUMNS['ENABLED']['ordinal'], CheckableTableWidgetItem(data['enabled'])) + check_box = CheckableTableWidgetItem(data['enabled']) + self.setItem(row, self.COLUMNS['ENABLED']['ordinal'], check_box) # Rule name set_rule_name_in_row(row, self.COLUMNS['NAME']['ordinal'], name=data['name']) @@ -748,32 +838,10 @@ class ExclusionRules(GenericRulesTable): # Pattern # The contents of the Pattern field is driven by the Source field - self.source_index_changed(source_combo, row, self.COLUMNS['PATTERN']['ordinal'], pattern=data['pattern']) + self.source_index_changed(source_combo, row, pattern=data['pattern']) self.blockSignals(False) - def source_index_changed(self, combo, row, col, pattern=''): - # Populate the Pattern field based upon the Source field - source_field = str(combo.currentText()) - if source_field == '': - values = [] - elif source_field == 'Tags': - values = sorted(self.db.all_tags(), key=sort_key) - else: - if self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['enumeration', 'text']: - values = self.db.all_custom(self.db.field_metadata.key_to_label( - self.eligible_custom_fields[unicode(source_field)]['field'])) - values = sorted(values, key=sort_key) - elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['bool']: - values = ['True','False','unspecified'] - elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['composite']: - values = ['any value','unspecified'] - elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['datetime']: - values = ['any date','unspecified'] - - values_combo = ComboBox(self, values, pattern) - self.setCellWidget(row, self.COLUMNS['PATTERN']['ordinal'], values_combo) - class PrefixRules(GenericRulesTable): COLUMNS = { 'ENABLED':{'ordinal': 0, 'name': ''}, @@ -784,6 +852,7 @@ class PrefixRules(GenericRulesTable): def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): super(PrefixRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db) + self.setObjectName("prefix_rules_table") self._init_table_widget() self._initialize() @@ -998,14 +1067,12 @@ class PrefixRules(GenericRulesTable): def set_source_field_in_row(row, col, field=''): source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) - source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) + source_combo.currentIndexChanged.connect(partial(self._source_index_changed, source_combo)) self.setCellWidget(row, col, source_combo) return source_combo - # Entry point self.blockSignals(True) - #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) # Enabled self.setItem(row, self.COLUMNS['ENABLED']['ordinal'], CheckableTableWidgetItem(data['enabled'])) @@ -1021,31 +1088,7 @@ class PrefixRules(GenericRulesTable): # Pattern # The contents of the Pattern field is driven by the Source field - self.source_index_changed(source_combo, row, self.COLUMNS['PATTERN']['ordinal'], pattern=data['pattern']) + self.source_index_changed(source_combo, row, pattern=data['pattern']) self.blockSignals(False) - def source_index_changed(self, combo, row, col, pattern=''): - # Populate the Pattern field based upon the Source field - # row, col are the control that changed - - source_field = str(combo.currentText()) - if source_field == '': - values = [] - elif source_field == 'Tags': - values = sorted(self.db.all_tags(), key=sort_key) - else: - if self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['enumeration', 'text']: - values = self.db.all_custom(self.db.field_metadata.key_to_label( - self.eligible_custom_fields[unicode(source_field)]['field'])) - values = sorted(values, key=sort_key) - elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['bool']: - values = ['True','False','unspecified'] - elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['composite']: - values = ['any value','unspecified'] - elif self.eligible_custom_fields[unicode(source_field)]['datatype'] in ['datetime']: - values = ['any date','unspecified'] - - values_combo = ComboBox(self, values, pattern) - self.setCellWidget(row, self.COLUMNS['PATTERN']['ordinal'], values_combo) - diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index c196db3ca5..1ebe710993 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -1191,8 +1191,7 @@ Author '{0}': if self.opts.generate_series: aTag = Tag(soup,'a') - aTag['href'] = "%s.html#%s_series" % ('BySeries', - re.sub('\s','',book['series']).lower()) + aTag['href'] = "%s.html#%s" % ('BySeries',self.generateSeriesAnchor(book['series'])) aTag.insert(0, book['series']) pSeriesTag.insert(0, aTag) else: @@ -1333,8 +1332,9 @@ Author '{0}': pSeriesTag['class'] = "series" if self.opts.generate_series: aTag = Tag(soup,'a') - aTag['href'] = "%s.html#%s_series" % ('BySeries', - re.sub('\W','',new_entry['series']).lower()) + + if self.letter_or_symbol(new_entry['series']) == self.SYMBOLS: + aTag['href'] = "%s.html#%s" % ('BySeries',self.generateSeriesAnchor(new_entry['series'])) aTag.insert(0, new_entry['series']) pSeriesTag.insert(0, aTag) else: @@ -1741,17 +1741,6 @@ Author '{0}': body = soup.find('body') btc = 0 - - pTag = Tag(soup, "p") - pTag['style'] = 'display:none' - ptc = 0 - aTag = Tag(soup,'a') - aTag['id'] = 'section_start' - pTag.insert(ptc, aTag) - ptc += 1 - body.insert(btc, pTag) - btc += 1 - divTag = Tag(soup, "div") dtc = 0 current_letter = "" @@ -1788,10 +1777,7 @@ Author '{0}': pSeriesTag = Tag(soup,'p') pSeriesTag['class'] = "series" aTag = Tag(soup, 'a') - if self.letter_or_symbol(book['series']): - aTag['id'] = "symbol_%s_series" % re.sub('\W','',book['series']).lower() - else: - aTag['id'] = "%s_series" % re.sub('\W','',book['series']).lower() + aTag['id'] = self.generateSeriesAnchor(book['series']) pSeriesTag.insert(0,aTag) pSeriesTag.insert(1,NavigableString('%s' % book['series'])) divTag.insert(dtc,pSeriesTag) @@ -1847,19 +1833,23 @@ Author '{0}': divTag.insert(dtc, pBookTag) dtc += 1 + pTag = Tag(soup, "p") + pTag['class'] = 'title' + ptc = 0 + aTag = Tag(soup,'a') + aTag['id'] = 'section_start' + pTag.insert(ptc, aTag) + ptc += 1 + if not self.__generateForKindle: # Insert the

tag with book_count at the head - #

By Series

- pTag = Tag(soup, "p") - pTag['class'] = 'title' aTag = Tag(soup, "a") anchor_name = friendly_name.lower() aTag['id'] = anchor_name.replace(" ","") pTag.insert(0,aTag) - #h2Tag.insert(1,NavigableString('%s (%d)' % (friendly_name, series_count))) pTag.insert(1,NavigableString('%s' % friendly_name)) - body.insert(btc,pTag) - btc += 1 + body.insert(btc,pTag) + btc += 1 # Add the divTag to the body body.insert(btc, divTag) @@ -3353,15 +3343,17 @@ Author '{0}': return codeTag else: spanTag = Tag(soup, "span") + #spanTag['class'] = "prefix" if prefix_char is None: spanTag['style'] = "color:white" prefix_char = self.defaultPrefix + #prefix_char = " " spanTag.insert(0,NavigableString(prefix_char)) return spanTag def generateAuthorAnchor(self, author): - # Strip white space to '' - return re.sub("\W","", author) + # Generate a legal XHTML id/href string + return re.sub("\W","", ascii_text(author)) def generateFormatArgs(self, book): series_index = str(book['series_index']) @@ -3438,8 +3430,7 @@ Author '{0}': pSeriesTag['class'] = "series" if self.opts.generate_series: aTag = Tag(soup,'a') - aTag['href'] = "%s.html#%s_series" % ('BySeries', - re.sub('\W','',book['series']).lower()) + aTag['href'] = "%s.html#%s" % ('BySeries', self.generateSeriesAnchor(book['series'])) aTag.insert(0, book['series']) pSeriesTag.insert(0, aTag) else: @@ -3641,12 +3632,7 @@ Author '{0}': if aTag: if book['series']: if self.opts.generate_series: - if self.letter_or_symbol(book['series']): - aTag['href'] = "%s.html#symbol_%s_series" % ('BySeries', - re.sub('\W','',book['series']).lower()) - else: - aTag['href'] = "%s.html#%s_series" % ('BySeries', - re.sub('\s','',book['series']).lower()) + aTag['href'] = "%s.html#%s" % ('BySeries',self.generateSeriesAnchor(book['series'])) else: aTag.extract() @@ -3780,6 +3766,14 @@ Author '{0}': pass return rating + def generateSeriesAnchor(self, series): + # Generate a legal XHTML id/href string + if self.letter_or_symbol(series) == self.SYMBOLS: + return "symbol_%s_series" % re.sub('\W','',series).lower() + else: + return "%s_series" % re.sub('\W','',ascii_text(series)).lower() + + def generateShortDescription(self, description, dest=None): # Truncate the description, on word boundaries if necessary # Possible destinations: From 6c94cdb03c039ab8158ab83065f3708d669cb3a7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 13 Aug 2012 17:11:44 +0530 Subject: [PATCH 12/12] WPD: Device detection --- setup/extensions.py | 3 +- src/calibre/devices/mtp/windows/driver.py | 125 ++++++++++++++++++++++ src/calibre/devices/mtp/windows/remote.py | 109 +++++++++---------- src/calibre/devices/mtp/windows/wpd.cpp | 14 +-- 4 files changed, 181 insertions(+), 70 deletions(-) create mode 100644 src/calibre/devices/mtp/windows/driver.py diff --git a/setup/extensions.py b/setup/extensions.py index 44a07a34c6..b51c8c7aaa 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -298,7 +298,8 @@ class Build(Command): self.obj_dir = os.path.join(os.path.dirname(SRC), 'build', 'objects') if not os.path.exists(self.obj_dir): os.makedirs(self.obj_dir) - self.build_style(self.j(self.SRC, 'calibre', 'plugins')) + if not opts.only: + self.build_style(self.j(self.SRC, 'calibre', 'plugins')) for ext in extensions: if opts.only != 'all' and opts.only != ext.name: continue diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py new file mode 100644 index 0000000000..3d98155efe --- /dev/null +++ b/src/calibre/devices/mtp/windows/driver.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import time +from threading import RLock + +from calibre import as_unicode, prints +from calibre.constants import plugins, __appname__, numeric_version +from calibre.devices.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 + + @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): + 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 + self.ejected_devices = set(self.detected_devices).intersection(self.ejected_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 = 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 = None + + diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py index 7c16a87ed1..4830a8e4ec 100644 --- a/src/calibre/devices/mtp/windows/remote.py +++ b/src/calibre/devices/mtp/windows/remote.py @@ -7,78 +7,67 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import subprocess, sys, os, pprint +import subprocess, sys, os, pprint, signal, time, glob pprint def build(mod='wpd'): - builder = subprocess.Popen('ssh xp_build ~/build-wpd'.split()) - syncer = subprocess.Popen('ssh getafix ~/test-wpd'.split()) - if builder.wait() != 0: - raise Exception('Failed to build plugin') - 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() - + 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(): - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + 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) - wpd.init('calibre', 1, 0, 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: - for pnp_id in wpd.enumerate_devices(): - print (pnp_id) - pprint.pprint(wpd.device_info(pnp_id)) + devices = win_scanner() + pnp_id = dev.detect_managed_devices(devices) + # pprint.pprint(dev.detected_devices) + print ('Trying to connect to:', pnp_id) finally: - wpd.uninit() - -def winutil(): - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - del sys.modules['winutil'] - import winutil - from calibre.constants import plugins - plugins._plugins['winutil'] = (winutil, '') - sys.path.pop(0) - print (winutil.serial_number_from_drive('F')) - -def get_subkeys(key): - import _winreg - index = -1 - while True: - index += 1 - try: - yield _winreg.EnumKey(key, index) - except OSError: - break - -def get_values(key): - import _winreg - index = -1 - while True: - index +=1 - try: - yield _winreg.EnumValue(key, index) - except OSError: - break - -def test(): - vid, pid = 0x1949, 0x4 - import _winreg as r - usb = r.OpenKey(r.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Enum\\USB') - q = ('vid_%4.4x&pid_%4.4x'%(vid, pid)) - dev = r.OpenKey(usb, q) - print (list(get_subkeys(dev))) + dev.shutdown() if __name__ == '__main__': main() - # winutil() diff --git a/src/calibre/devices/mtp/windows/wpd.cpp b/src/calibre/devices/mtp/windows/wpd.cpp index e739826fbb..4d7cbc310d 100644 --- a/src/calibre/devices/mtp/windows/wpd.cpp +++ b/src/calibre/devices/mtp/windows/wpd.cpp @@ -92,14 +92,10 @@ wpd_enumerate_devices(PyObject *self, PyObject *args) { ENSURE_WPD(NULL); - if (!PyArg_ParseTuple(args, "|O", &refresh)) return NULL; - - if (refresh != NULL && PyObject_IsTrue(refresh)) { - 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); - } + 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 @@ -175,7 +171,7 @@ static PyMethodDef wpd_methods[] = { }, {"enumerate_devices", wpd_enumerate_devices, METH_VARARGS, - "enumerate_devices(refresh=False)\n\n Get the list of device PnP ids for all connected devices recognized by the WPD service. The result is cached, unless refresh=True. Do not call with refresh=True too often as it is resource intensive." + "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,