mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
5ad16d52c1
@ -95,6 +95,10 @@ class DevicePlugin(Plugin):
|
|||||||
#: call post_yank_cleanup().
|
#: call post_yank_cleanup().
|
||||||
MANAGES_DEVICE_PRESENCE = False
|
MANAGES_DEVICE_PRESENCE = False
|
||||||
|
|
||||||
|
#: If set the True, calibre will call the :method:`get_driveinfo()` method
|
||||||
|
#: after the books lists have been loaded to get the driveinfo.
|
||||||
|
SLOW_DRIVEINFO = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_gui_name(cls):
|
def get_gui_name(cls):
|
||||||
if hasattr(cls, 'gui_name'):
|
if hasattr(cls, 'gui_name'):
|
||||||
@ -352,6 +356,18 @@ class DevicePlugin(Plugin):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_driveinfo(self):
|
||||||
|
'''
|
||||||
|
Return the driveinfo dictionary. Usually called from
|
||||||
|
get_device_information(), but if loading the driveinfo is slow for this
|
||||||
|
driver, then it should set SLOW_DRIVEINFO. In this case, this method
|
||||||
|
will be called by calibre after the book lists have been loaded. Note
|
||||||
|
that it is not called on the device thread, so the driver should cache
|
||||||
|
the drive info in the books() method and this function should return
|
||||||
|
the cached data.
|
||||||
|
'''
|
||||||
|
return {}
|
||||||
|
|
||||||
def card_prefix(self, end_session=True):
|
def card_prefix(self, end_session=True):
|
||||||
'''
|
'''
|
||||||
Return a 2 element list of the prefix to paths on the cards.
|
Return a 2 element list of the prefix to paths on the cards.
|
||||||
|
@ -288,7 +288,7 @@ class KINDLE2(KINDLE):
|
|||||||
name = 'Kindle 2/3/4/Touch Device Interface'
|
name = 'Kindle 2/3/4/Touch Device Interface'
|
||||||
description = _('Communicate with the Kindle 2/3/4/Touch eBook reader.')
|
description = _('Communicate with the Kindle 2/3/4/Touch eBook reader.')
|
||||||
|
|
||||||
FORMATS = KINDLE.FORMATS + ['pdf', 'azw4', 'pobi']
|
FORMATS = ['azw3'] + KINDLE.FORMATS + ['pdf', 'azw4', 'pobi']
|
||||||
DELETE_EXTS = KINDLE.DELETE_EXTS + ['.mbp1', '.mbs', '.sdr']
|
DELETE_EXTS = KINDLE.DELETE_EXTS + ['.mbp1', '.mbs', '.sdr']
|
||||||
|
|
||||||
PRODUCT_ID = [0x0002, 0x0004]
|
PRODUCT_ID = [0x0002, 0x0004]
|
||||||
@ -449,7 +449,7 @@ class KINDLE_DX(KINDLE2):
|
|||||||
name = 'Kindle DX Device Interface'
|
name = 'Kindle DX Device Interface'
|
||||||
description = _('Communicate with the Kindle DX eBook reader.')
|
description = _('Communicate with the Kindle DX eBook reader.')
|
||||||
|
|
||||||
|
FORMATS = KINDLE2.FORMATS[1:]
|
||||||
PRODUCT_ID = [0x0003]
|
PRODUCT_ID = [0x0003]
|
||||||
BCD = [0x0100]
|
BCD = [0x0100]
|
||||||
|
|
||||||
@ -462,7 +462,6 @@ class KINDLE_FIRE(KINDLE2):
|
|||||||
description = _('Communicate with the Kindle Fire')
|
description = _('Communicate with the Kindle Fire')
|
||||||
gui_name = 'Fire'
|
gui_name = 'Fire'
|
||||||
FORMATS = list(KINDLE2.FORMATS)
|
FORMATS = list(KINDLE2.FORMATS)
|
||||||
FORMATS.insert(0, 'azw3')
|
|
||||||
|
|
||||||
PRODUCT_ID = [0x0006]
|
PRODUCT_ID = [0x0006]
|
||||||
BCD = [0x216, 0x100]
|
BCD = [0x216, 0x100]
|
||||||
|
@ -35,6 +35,7 @@ class MTP_DEVICE(BASE):
|
|||||||
MANAGES_DEVICE_PRESENCE = True
|
MANAGES_DEVICE_PRESENCE = True
|
||||||
FORMATS = ['epub', 'azw3', 'mobi', 'pdf']
|
FORMATS = ['epub', 'azw3', 'mobi', 'pdf']
|
||||||
DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE'
|
DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE'
|
||||||
|
SLOW_DRIVEINFO = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
BASE.__init__(self, *args, **kwargs)
|
BASE.__init__(self, *args, **kwargs)
|
||||||
@ -47,12 +48,13 @@ class MTP_DEVICE(BASE):
|
|||||||
from calibre.library.save_to_disk import config
|
from calibre.library.save_to_disk import config
|
||||||
self._prefs = p = JSONConfig('mtp_devices')
|
self._prefs = p = JSONConfig('mtp_devices')
|
||||||
p.defaults['format_map'] = self.FORMATS
|
p.defaults['format_map'] = self.FORMATS
|
||||||
p.defaults['send_to'] = ['Books', 'eBooks/import', 'eBooks',
|
p.defaults['send_to'] = ['Calibre_Companion', 'Books',
|
||||||
'wordplayer/calibretransfer', 'sdcard/ebooks',
|
'eBooks/import', 'eBooks', 'wordplayer/calibretransfer',
|
||||||
'kindle']
|
'sdcard/ebooks', 'kindle']
|
||||||
p.defaults['send_template'] = config().parse().send_template
|
p.defaults['send_template'] = config().parse().send_template
|
||||||
p.defaults['blacklist'] = []
|
p.defaults['blacklist'] = []
|
||||||
p.defaults['history'] = {}
|
p.defaults['history'] = {}
|
||||||
|
p.defaults['rules'] = []
|
||||||
|
|
||||||
return self._prefs
|
return self._prefs
|
||||||
|
|
||||||
@ -75,6 +77,7 @@ class MTP_DEVICE(BASE):
|
|||||||
def open(self, devices, library_uuid):
|
def open(self, devices, library_uuid):
|
||||||
self.current_library_uuid = library_uuid
|
self.current_library_uuid = library_uuid
|
||||||
self.location_paths = None
|
self.location_paths = None
|
||||||
|
self.driveinfo = {}
|
||||||
BASE.open(self, devices, library_uuid)
|
BASE.open(self, devices, library_uuid)
|
||||||
h = self.prefs['history']
|
h = self.prefs['history']
|
||||||
if self.current_serial_num:
|
if self.current_serial_num:
|
||||||
@ -106,15 +109,19 @@ class MTP_DEVICE(BASE):
|
|||||||
dinfo['mtp_prefix'] = storage.storage_prefix
|
dinfo['mtp_prefix'] = storage.storage_prefix
|
||||||
raw = json.dumps(dinfo, default=to_json)
|
raw = json.dumps(dinfo, default=to_json)
|
||||||
self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw))
|
self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw))
|
||||||
self.driveinfo = dinfo
|
self.driveinfo[location_code] = dinfo
|
||||||
|
|
||||||
|
def get_driveinfo(self):
|
||||||
|
if not self.driveinfo:
|
||||||
|
self.driveinfo = {}
|
||||||
|
for sid, location_code in ( (self._main_id, 'main'), (self._carda_id,
|
||||||
|
'A'), (self._cardb_id, 'B')):
|
||||||
|
if sid is None: continue
|
||||||
|
self._update_drive_info(self.filesystem_cache.storage(sid), location_code)
|
||||||
|
return self.driveinfo
|
||||||
|
|
||||||
def get_device_information(self, end_session=True):
|
def get_device_information(self, end_session=True):
|
||||||
self.report_progress(1.0, _('Get device information...'))
|
self.report_progress(1.0, _('Get device information...'))
|
||||||
self.driveinfo = {}
|
|
||||||
for sid, location_code in ( (self._main_id, 'main'), (self._carda_id,
|
|
||||||
'A'), (self._cardb_id, 'B')):
|
|
||||||
if sid is None: continue
|
|
||||||
self._update_drive_info(self.filesystem_cache.storage(sid), location_code)
|
|
||||||
dinfo = self.get_basic_device_information()
|
dinfo = self.get_basic_device_information()
|
||||||
return tuple( list(dinfo) + [self.driveinfo] )
|
return tuple( list(dinfo) + [self.driveinfo] )
|
||||||
|
|
||||||
@ -134,6 +141,7 @@ class MTP_DEVICE(BASE):
|
|||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
from calibre.devices.mtp.books import JSONCodec
|
from calibre.devices.mtp.books import JSONCodec
|
||||||
from calibre.devices.mtp.books import BookList, Book
|
from calibre.devices.mtp.books import BookList, Book
|
||||||
|
self.get_driveinfo() # Ensure driveinfo is loaded
|
||||||
sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard,
|
sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard,
|
||||||
self._main_id)
|
self._main_id)
|
||||||
if sid is None:
|
if sid is None:
|
||||||
@ -273,15 +281,18 @@ class MTP_DEVICE(BASE):
|
|||||||
self.plugboards = plugboards
|
self.plugboards = plugboards
|
||||||
self.plugboard_func = pb_func
|
self.plugboard_func = pb_func
|
||||||
|
|
||||||
def create_upload_path(self, path, mdata, fname):
|
def create_upload_path(self, path, mdata, fname, routing):
|
||||||
from calibre.devices.utils import create_upload_path
|
from calibre.devices.utils import create_upload_path
|
||||||
from calibre.utils.filenames import ascii_filename as sanitize
|
from calibre.utils.filenames import ascii_filename as sanitize
|
||||||
|
ext = fname.rpartition('.')[-1].lower()
|
||||||
|
path = routing.get(ext, path)
|
||||||
|
|
||||||
filepath = create_upload_path(mdata, fname, self.save_template, sanitize,
|
filepath = create_upload_path(mdata, fname, self.save_template, sanitize,
|
||||||
prefix_path=path,
|
prefix_path=path,
|
||||||
path_type=posixpath,
|
path_type=posixpath,
|
||||||
maxlen=self.MAX_PATH_LEN,
|
maxlen=self.MAX_PATH_LEN,
|
||||||
use_subdirs = True,
|
use_subdirs=True,
|
||||||
news_in_folder = self.NEWS_IN_FOLDER,
|
news_in_folder=self.NEWS_IN_FOLDER,
|
||||||
)
|
)
|
||||||
return tuple(x for x in filepath.split('/'))
|
return tuple(x for x in filepath.split('/'))
|
||||||
|
|
||||||
@ -329,8 +340,10 @@ class MTP_DEVICE(BASE):
|
|||||||
self.report_progress(0, _('Transferring books to device...'))
|
self.report_progress(0, _('Transferring books to device...'))
|
||||||
i, total = 0, len(files)
|
i, total = 0, len(files)
|
||||||
|
|
||||||
|
routing = {fmt:dest for fmt,dest in self.get_pref('rules')}
|
||||||
|
|
||||||
for infile, fname, mi in izip(files, names, metadata):
|
for infile, fname, mi in izip(files, names, metadata):
|
||||||
path = self.create_upload_path(prefix, mi, fname)
|
path = self.create_upload_path(prefix, mi, fname, routing)
|
||||||
parent = self.ensure_parent(storage, path)
|
parent = self.ensure_parent(storage, path)
|
||||||
if hasattr(infile, 'read'):
|
if hasattr(infile, 'read'):
|
||||||
pos = infile.tell()
|
pos = infile.tell()
|
||||||
|
@ -230,6 +230,9 @@ class FilesystemCache(object):
|
|||||||
continue # Ignore .txt files in the root
|
continue # Ignore .txt files in the root
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.id_map)
|
||||||
|
|
||||||
def resolve_mtp_id_path(self, path):
|
def resolve_mtp_id_path(self, path):
|
||||||
if not path.startswith('mtp:::'):
|
if not path.startswith('mtp:::'):
|
||||||
raise ValueError('%s is not a valid MTP path'%path)
|
raise ValueError('%s is not a valid MTP path'%path)
|
||||||
|
@ -7,7 +7,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import operator, traceback, pprint, sys
|
import operator, traceback, pprint, sys, time
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@ -16,7 +16,7 @@ from calibre import prints, as_unicode
|
|||||||
from calibre.constants import plugins
|
from calibre.constants import plugins
|
||||||
from calibre.ptempfile import SpooledTemporaryFile
|
from calibre.ptempfile import SpooledTemporaryFile
|
||||||
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
|
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
|
||||||
from calibre.devices.mtp.base import MTPDeviceBase, synchronous
|
from calibre.devices.mtp.base import MTPDeviceBase, synchronous, debug
|
||||||
|
|
||||||
MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id '
|
MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id '
|
||||||
'bcd serial manufacturer product')
|
'bcd serial manufacturer product')
|
||||||
@ -193,6 +193,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
|||||||
@property
|
@property
|
||||||
def filesystem_cache(self):
|
def filesystem_cache(self):
|
||||||
if self._filesystem_cache is None:
|
if self._filesystem_cache is None:
|
||||||
|
st = time.time()
|
||||||
|
debug('Loading filesystem metadata...')
|
||||||
from calibre.devices.mtp.filesystem_cache import FilesystemCache
|
from calibre.devices.mtp.filesystem_cache import FilesystemCache
|
||||||
with self.lock:
|
with self.lock:
|
||||||
storage, all_items, all_errs = [], [], []
|
storage, all_items, all_errs = [], [], []
|
||||||
@ -220,6 +222,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
|||||||
self.current_friendly_name,
|
self.current_friendly_name,
|
||||||
self.format_errorstack(all_errs)))
|
self.format_errorstack(all_errs)))
|
||||||
self._filesystem_cache = FilesystemCache(storage, all_items)
|
self._filesystem_cache = FilesystemCache(storage, all_items)
|
||||||
|
debug('Filesystem metadata loaded in %g seconds (%d objects)'%(
|
||||||
|
time.time()-st, len(self._filesystem_cache)))
|
||||||
return self._filesystem_cache
|
return self._filesystem_cache
|
||||||
|
|
||||||
@synchronous
|
@synchronous
|
||||||
|
@ -16,7 +16,7 @@ from calibre import as_unicode, prints
|
|||||||
from calibre.constants import plugins, __appname__, numeric_version
|
from calibre.constants import plugins, __appname__, numeric_version
|
||||||
from calibre.ptempfile import SpooledTemporaryFile
|
from calibre.ptempfile import SpooledTemporaryFile
|
||||||
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
|
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
|
||||||
from calibre.devices.mtp.base import MTPDeviceBase
|
from calibre.devices.mtp.base import MTPDeviceBase, debug
|
||||||
|
|
||||||
class ThreadingViolation(Exception):
|
class ThreadingViolation(Exception):
|
||||||
|
|
||||||
@ -199,6 +199,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
|||||||
@property
|
@property
|
||||||
def filesystem_cache(self):
|
def filesystem_cache(self):
|
||||||
if self._filesystem_cache is None:
|
if self._filesystem_cache is None:
|
||||||
|
debug('Loading filesystem metadata...')
|
||||||
|
st = time.time()
|
||||||
from calibre.devices.mtp.filesystem_cache import FilesystemCache
|
from calibre.devices.mtp.filesystem_cache import FilesystemCache
|
||||||
ts = self.total_space()
|
ts = self.total_space()
|
||||||
all_storage = []
|
all_storage = []
|
||||||
@ -218,6 +220,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
|||||||
all_storage.append(storage)
|
all_storage.append(storage)
|
||||||
items.append(id_map.itervalues())
|
items.append(id_map.itervalues())
|
||||||
self._filesystem_cache = FilesystemCache(all_storage, chain(*items))
|
self._filesystem_cache = FilesystemCache(all_storage, chain(*items))
|
||||||
|
debug('Filesystem metadata loaded in %g seconds (%d objects)'%(
|
||||||
|
time.time()-st, len(self._filesystem_cache)))
|
||||||
return self._filesystem_cache
|
return self._filesystem_cache
|
||||||
|
|
||||||
@same_thread
|
@same_thread
|
||||||
|
@ -499,6 +499,7 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
self.icons = {}
|
self.icons = {}
|
||||||
for key in self.__class__.ICONS.keys():
|
for key in self.__class__.ICONS.keys():
|
||||||
self.icons[key] = I('mimetypes/')+self.__class__.ICONS[key]+'.png'
|
self.icons[key] = I('mimetypes/')+self.__class__.ICONS[key]+'.png'
|
||||||
|
self.icons['calibre'] = I('lt.png')
|
||||||
for i in ('dir', 'default', 'zero'):
|
for i in ('dir', 'default', 'zero'):
|
||||||
self.icons[i] = QIcon(self.icons[i])
|
self.icons[i] = QIcon(self.icons[i])
|
||||||
|
|
||||||
|
@ -152,8 +152,16 @@ def render_data(mi, use_roman_numbers=True, all_fields=False):
|
|||||||
scheme = u'devpath' if isdevice else u'path'
|
scheme = u'devpath' if isdevice else u'path'
|
||||||
url = prepare_string_for_xml(path if isdevice else
|
url = prepare_string_for_xml(path if isdevice else
|
||||||
unicode(mi.id), True)
|
unicode(mi.id), True)
|
||||||
link = u'<a href="%s:%s" title="%s">%s</a>' % (scheme, url,
|
pathstr = _('Click to open')
|
||||||
prepare_string_for_xml(path, True), _('Click to open'))
|
extra = ''
|
||||||
|
if isdevice:
|
||||||
|
durl = url
|
||||||
|
if durl.startswith('mtp:::'):
|
||||||
|
durl = ':::'.join( (durl.split(':::'))[2:] )
|
||||||
|
extra = '<br><span style="font-size:smaller">%s</span>'%(
|
||||||
|
prepare_string_for_xml(durl))
|
||||||
|
link = u'<a href="%s:%s" title="%s">%s</a>%s' % (scheme, url,
|
||||||
|
prepare_string_for_xml(path, True), pathstr, extra)
|
||||||
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link)))
|
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link)))
|
||||||
elif field == 'formats':
|
elif field == 'formats':
|
||||||
if isdevice: continue
|
if isdevice: continue
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>588</width>
|
<width>588</width>
|
||||||
<height>342</height>
|
<height>416</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -91,23 +91,33 @@
|
|||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QComboBox" name="opt_mobi_file_type"/>
|
<widget class="QComboBox" name="opt_mobi_file_type"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Personal Doc tag:</string>
|
<string>Personal Doc tag:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QLineEdit" name="opt_personal_doc"/>
|
<widget class="QLineEdit" name="opt_personal_doc"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="4" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="opt_share_not_sync">
|
<widget class="QCheckBox" name="opt_share_not_sync">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing</string>
|
<string>Enable sharing of book content via Facebook, etc. WARNING: Disables last read syncing</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string><b>WARNING:</b> Various Kindle devices have trouble displaying the new or both MOBI filetypes. If you wish to use the new format on your device, convert to AZW3 instead of MOBI.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -433,6 +433,15 @@ class DeviceManager(Thread): # {{{
|
|||||||
return self.create_job_step(self._get_device_information, done,
|
return self.create_job_step(self._get_device_information, done,
|
||||||
description=_('Get device information'), to_job=add_as_step_to_job)
|
description=_('Get device information'), to_job=add_as_step_to_job)
|
||||||
|
|
||||||
|
def slow_driveinfo(self):
|
||||||
|
''' Update the stored device information with the driveinfo if the
|
||||||
|
device indicates that getting driveinfo is slow '''
|
||||||
|
info = self._device_information['info']
|
||||||
|
if (not info[4] and self.device.SLOW_DRIVEINFO):
|
||||||
|
info = list(info)
|
||||||
|
info[4] = self.device.get_driveinfo()
|
||||||
|
self._device_information['info'] = tuple(info)
|
||||||
|
|
||||||
def get_current_device_information(self):
|
def get_current_device_information(self):
|
||||||
return self._device_information
|
return self._device_information
|
||||||
|
|
||||||
@ -1023,6 +1032,7 @@ class DeviceMixin(object): # {{{
|
|||||||
if job.failed:
|
if job.failed:
|
||||||
self.device_job_exception(job)
|
self.device_job_exception(job)
|
||||||
return
|
return
|
||||||
|
self.device_manager.slow_driveinfo()
|
||||||
# set_books_in_library might schedule a sync_booklists job
|
# set_books_in_library might schedule a sync_booklists job
|
||||||
self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job)
|
self.set_books_in_library(job.result, reset=True, add_as_step_to_job=job)
|
||||||
mainlist, cardalist, cardblist = job.result
|
mainlist, cardalist, cardblist = job.result
|
||||||
|
@ -11,12 +11,14 @@ import weakref
|
|||||||
|
|
||||||
from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel,
|
from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel,
|
||||||
QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout,
|
QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout,
|
||||||
QPushButton)
|
QPushButton, QGroupBox, QScrollArea, QHBoxLayout, QComboBox,
|
||||||
|
pyqtSignal, QSizePolicy, QDialog, QDialogButtonBox)
|
||||||
|
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||||
from calibre.utils.date import parse_date
|
from calibre.utils.date import parse_date
|
||||||
|
from calibre.gui2.device_drivers.mtp_folder_browser import Browser
|
||||||
|
|
||||||
class FormatsConfig(QWidget): # {{{
|
class FormatsConfig(QWidget): # {{{
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ class TemplateConfig(QWidget): # {{{
|
|||||||
m.setBuddy(t)
|
m.setBuddy(t)
|
||||||
l.addWidget(m, 0, 0, 1, 2)
|
l.addWidget(m, 0, 0, 1, 2)
|
||||||
l.addWidget(t, 1, 0, 1, 1)
|
l.addWidget(t, 1, 0, 1, 1)
|
||||||
b = self.b = QPushButton(_('Template editor'))
|
b = self.b = QPushButton(_('&Template editor'))
|
||||||
l.addWidget(b, 1, 1, 1, 1)
|
l.addWidget(b, 1, 1, 1, 1)
|
||||||
b.clicked.connect(self.edit_template)
|
b.clicked.connect(self.edit_template)
|
||||||
|
|
||||||
@ -116,19 +118,36 @@ class TemplateConfig(QWidget): # {{{
|
|||||||
|
|
||||||
class SendToConfig(QWidget): # {{{
|
class SendToConfig(QWidget): # {{{
|
||||||
|
|
||||||
def __init__(self, val):
|
def __init__(self, val, device):
|
||||||
QWidget.__init__(self)
|
QWidget.__init__(self)
|
||||||
self.t = t = QLineEdit(self)
|
self.t = t = QLineEdit(self)
|
||||||
t.setText(', '.join(val or []))
|
t.setText(', '.join(val or []))
|
||||||
t.setCursorPosition(0)
|
t.setCursorPosition(0)
|
||||||
self.l = l = QVBoxLayout(self)
|
self.l = l = QGridLayout(self)
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
self.m = m = QLabel('<p>'+_('''A <b>list of &folders</b> on the device to
|
self.m = m = QLabel('<p>'+_('''A <b>list of &folders</b> on the device to
|
||||||
which to send ebooks. The first one that exists will be used:'''))
|
which to send ebooks. The first one that exists will be used:'''))
|
||||||
m.setWordWrap(True)
|
m.setWordWrap(True)
|
||||||
m.setBuddy(t)
|
m.setBuddy(t)
|
||||||
l.addWidget(m)
|
l.addWidget(m, 0, 0, 1, 2)
|
||||||
l.addWidget(t)
|
l.addWidget(t, 1, 0)
|
||||||
|
self.b = b = QToolButton()
|
||||||
|
l.addWidget(b, 1, 1)
|
||||||
|
b.setIcon(QIcon(I('document_open.png')))
|
||||||
|
b.clicked.connect(self.browse)
|
||||||
|
b.setToolTip(_('Browse for a folder on the device'))
|
||||||
|
self._device = weakref.ref(device)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
return self._device()
|
||||||
|
|
||||||
|
def browse(self):
|
||||||
|
b = Browser(self.device.filesystem_cache, show_files=False,
|
||||||
|
parent=self)
|
||||||
|
if b.exec_() == b.Accepted:
|
||||||
|
sid, path = b.current_item
|
||||||
|
self.t.setText('/'.join(path[1:]))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
@ -176,6 +195,135 @@ class IgnoredDevices(QWidget): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# Rules {{{
|
||||||
|
|
||||||
|
class Rule(QWidget):
|
||||||
|
|
||||||
|
remove = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, device, rule=None):
|
||||||
|
QWidget.__init__(self)
|
||||||
|
self._device = weakref.ref(device)
|
||||||
|
|
||||||
|
self.l = l = QHBoxLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
|
||||||
|
self.l1 = l1 = QLabel(_('Send the '))
|
||||||
|
l.addWidget(l1)
|
||||||
|
self.fmt = f = QComboBox(self)
|
||||||
|
l.addWidget(f)
|
||||||
|
self.l2 = l2 = QLabel(_(' format to the folder: '))
|
||||||
|
l.addWidget(l2)
|
||||||
|
self.folder = f = QLineEdit(self)
|
||||||
|
f.setPlaceholderText(_('Folder on the device'))
|
||||||
|
l.addWidget(f)
|
||||||
|
self.b = b = QToolButton()
|
||||||
|
l.addWidget(b)
|
||||||
|
b.setIcon(QIcon(I('document_open.png')))
|
||||||
|
b.clicked.connect(self.browse)
|
||||||
|
b.setToolTip(_('Browse for a folder on the device'))
|
||||||
|
self.rb = rb = QPushButton(QIcon(I('list_remove.png')),
|
||||||
|
_('&Remove rule'), self)
|
||||||
|
l.addWidget(rb)
|
||||||
|
rb.clicked.connect(self.removed)
|
||||||
|
|
||||||
|
for fmt in sorted(BOOK_EXTENSIONS):
|
||||||
|
self.fmt.addItem(fmt.upper(), fmt.lower())
|
||||||
|
|
||||||
|
self.fmt.setCurrentIndex(0)
|
||||||
|
|
||||||
|
if rule is not None:
|
||||||
|
fmt, folder = rule
|
||||||
|
idx = self.fmt.findText(fmt.upper())
|
||||||
|
if idx > -1:
|
||||||
|
self.fmt.setCurrentIndex(idx)
|
||||||
|
self.folder.setText(folder)
|
||||||
|
|
||||||
|
self.ignore = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
return self._device()
|
||||||
|
|
||||||
|
def browse(self):
|
||||||
|
b = Browser(self.device.filesystem_cache, show_files=False,
|
||||||
|
parent=self)
|
||||||
|
if b.exec_() == b.Accepted:
|
||||||
|
sid, path = b.current_item
|
||||||
|
self.folder.setText('/'.join(path[1:]))
|
||||||
|
|
||||||
|
def removed(self):
|
||||||
|
self.remove.emit(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rule(self):
|
||||||
|
folder = unicode(self.folder.text()).strip()
|
||||||
|
if folder:
|
||||||
|
return (
|
||||||
|
unicode(self.fmt.itemData(self.fmt.currentIndex()).toString()),
|
||||||
|
folder
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
class FormatRules(QGroupBox):
|
||||||
|
|
||||||
|
def __init__(self, device, rules):
|
||||||
|
QGroupBox.__init__(self, _('Format specific sending'))
|
||||||
|
self._device = weakref.ref(device)
|
||||||
|
self.l = l = QVBoxLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
self.la = la = QLabel('<p>'+_(
|
||||||
|
'''You can create rules that control where ebooks of a specific
|
||||||
|
format are sent to on the device. These will take precedence over
|
||||||
|
the folders specified above.'''))
|
||||||
|
la.setWordWrap(True)
|
||||||
|
l.addWidget(la)
|
||||||
|
self.sa = sa = QScrollArea(self)
|
||||||
|
sa.setWidgetResizable(True)
|
||||||
|
self.w = w = QWidget(self)
|
||||||
|
w.l = QVBoxLayout()
|
||||||
|
w.setLayout(w.l)
|
||||||
|
sa.setWidget(w)
|
||||||
|
l.addWidget(sa)
|
||||||
|
self.widgets = []
|
||||||
|
for rule in rules:
|
||||||
|
r = Rule(device, rule)
|
||||||
|
self.widgets.append(r)
|
||||||
|
w.l.addWidget(r)
|
||||||
|
r.remove.connect(self.remove_rule)
|
||||||
|
|
||||||
|
if not self.widgets:
|
||||||
|
self.add_rule()
|
||||||
|
|
||||||
|
self.b = b = QPushButton(QIcon(I('plus.png')), _('Add a &new rule'))
|
||||||
|
l.addWidget(b)
|
||||||
|
b.clicked.connect(self.add_rule)
|
||||||
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
return self._device()
|
||||||
|
|
||||||
|
def add_rule(self):
|
||||||
|
r = Rule(self.device)
|
||||||
|
self.widgets.append(r)
|
||||||
|
self.w.l.addWidget(r)
|
||||||
|
r.remove.connect(self.remove_rule)
|
||||||
|
self.sa.verticalScrollBar().setValue(self.sa.verticalScrollBar().maximum())
|
||||||
|
|
||||||
|
def remove_rule(self, rule):
|
||||||
|
rule.setVisible(False)
|
||||||
|
rule.ignore = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rules(self):
|
||||||
|
for w in self.widgets:
|
||||||
|
if not w.ignore:
|
||||||
|
r = w.rule
|
||||||
|
if r is not None:
|
||||||
|
yield r
|
||||||
|
# }}}
|
||||||
|
|
||||||
class MTPConfig(QTabWidget):
|
class MTPConfig(QTabWidget):
|
||||||
|
|
||||||
def __init__(self, device, parent=None):
|
def __init__(self, device, parent=None):
|
||||||
@ -185,8 +333,8 @@ class MTPConfig(QTabWidget):
|
|||||||
cd = msg = None
|
cd = msg = None
|
||||||
if device.current_friendly_name is not None:
|
if device.current_friendly_name is not None:
|
||||||
if device.current_serial_num is None:
|
if device.current_serial_num is None:
|
||||||
msg = '<p>' + _('The <b>%s</b> device has no serial number, '
|
msg = '<p>' + (_('The <b>%s</b> device has no serial number, '
|
||||||
'it cannot be configured'%device.current_friendly_name)
|
'it cannot be configured')%device.current_friendly_name)
|
||||||
else:
|
else:
|
||||||
cd = 'device-'+device.current_serial_num
|
cd = 'device-'+device.current_serial_num
|
||||||
else:
|
else:
|
||||||
@ -211,24 +359,28 @@ class MTPConfig(QTabWidget):
|
|||||||
l = self.base.l = QGridLayout(self.base)
|
l = self.base.l = QGridLayout(self.base)
|
||||||
self.base.setLayout(l)
|
self.base.setLayout(l)
|
||||||
|
|
||||||
|
self.rules = r = FormatRules(self.device, self.get_pref('rules'))
|
||||||
self.formats = FormatsConfig(set(BOOK_EXTENSIONS),
|
self.formats = FormatsConfig(set(BOOK_EXTENSIONS),
|
||||||
self.get_pref('format_map'))
|
self.get_pref('format_map'))
|
||||||
self.send_to = SendToConfig(self.get_pref('send_to'))
|
self.send_to = SendToConfig(self.get_pref('send_to'), self.device)
|
||||||
self.template = TemplateConfig(self.get_pref('send_template'))
|
self.template = TemplateConfig(self.get_pref('send_template'))
|
||||||
self.base.la = la = QLabel(_(
|
self.base.la = la = QLabel(_(
|
||||||
'Choose the formats to send to the %s')%self.device.current_friendly_name)
|
'Choose the formats to send to the %s')%self.device.current_friendly_name)
|
||||||
la.setWordWrap(True)
|
la.setWordWrap(True)
|
||||||
l.addWidget(la, 0, 0, 1, 1)
|
self.base.b = b = QPushButton(QIcon(I('list_remove.png')),
|
||||||
l.addWidget(self.formats, 1, 0, 2, 1)
|
_('&Ignore the %s in calibre')%device.current_friendly_name,
|
||||||
l.addWidget(self.send_to, 1, 1, 1, 1)
|
|
||||||
l.addWidget(self.template, 2, 1, 1, 1)
|
|
||||||
l.setRowStretch(2, 10)
|
|
||||||
self.base.b = b = QPushButton(QIcon(I('minus.png')),
|
|
||||||
_('Ignore the %s in calibre')%device.current_friendly_name,
|
|
||||||
self.base)
|
self.base)
|
||||||
l.addWidget(b, 3, 0, 1, 2)
|
|
||||||
b.clicked.connect(self.ignore_device)
|
b.clicked.connect(self.ignore_device)
|
||||||
|
|
||||||
|
l.addWidget(b, 0, 0, 1, 2)
|
||||||
|
l.addWidget(la, 1, 0, 1, 1)
|
||||||
|
l.addWidget(self.formats, 2, 0, 3, 1)
|
||||||
|
l.addWidget(self.send_to, 2, 1, 1, 1)
|
||||||
|
l.addWidget(self.template, 3, 1, 1, 1)
|
||||||
|
l.setRowStretch(4, 10)
|
||||||
|
l.addWidget(r, 5, 0, 1, 2)
|
||||||
|
l.setRowStretch(5, 100)
|
||||||
|
|
||||||
self.igntab = IgnoredDevices(self.device.prefs['history'],
|
self.igntab = IgnoredDevices(self.device.prefs['history'],
|
||||||
self.device.prefs['blacklist'])
|
self.device.prefs['blacklist'])
|
||||||
self.addTab(self.igntab, _('Ignored devices'))
|
self.addTab(self.igntab, _('Ignored devices'))
|
||||||
@ -280,6 +432,11 @@ class MTPConfig(QTabWidget):
|
|||||||
if s and s != self.device.prefs['send_to']:
|
if s and s != self.device.prefs['send_to']:
|
||||||
p['send_to'] = s
|
p['send_to'] = s
|
||||||
|
|
||||||
|
p.pop('rules', None)
|
||||||
|
r = list(self.rules.rules)
|
||||||
|
if r and r != self.device.prefs['rules']:
|
||||||
|
p['rules'] = r
|
||||||
|
|
||||||
self.device.prefs[self.current_device_key] = p
|
self.device.prefs[self.current_device_key] = p
|
||||||
|
|
||||||
self.device.prefs['blacklist'] = self.igntab.blacklist
|
self.device.prefs['blacklist'] = self.igntab.blacklist
|
||||||
@ -296,8 +453,16 @@ if __name__ == '__main__':
|
|||||||
cd = dev.detect_managed_devices(s.devices)
|
cd = dev.detect_managed_devices(s.devices)
|
||||||
dev.open(cd, 'test')
|
dev.open(cd, 'test')
|
||||||
cw = dev.config_widget()
|
cw = dev.config_widget()
|
||||||
cw.show()
|
d = QDialog()
|
||||||
app.exec_()
|
d.l = QVBoxLayout()
|
||||||
|
d.setLayout(d.l)
|
||||||
|
d.l.addWidget(cw)
|
||||||
|
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||||
|
d.l.addWidget(bb)
|
||||||
|
bb.accepted.connect(d.accept)
|
||||||
|
bb.rejected.connect(d.reject)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
cw.commit()
|
||||||
dev.shutdown()
|
dev.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
119
src/calibre/gui2/device_drivers/mtp_folder_browser.py
Normal file
119
src/calibre/gui2/device_drivers/mtp_folder_browser.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
#!/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'
|
||||||
|
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog,
|
||||||
|
QDialogButtonBox, QVBoxLayout, QSize, pyqtSignal, QIcon)
|
||||||
|
|
||||||
|
from calibre.gui2 import file_icon_provider
|
||||||
|
|
||||||
|
def item(f, parent):
|
||||||
|
name = f.name
|
||||||
|
if not f.is_folder:
|
||||||
|
name += ' [%s]'%f.last_mod_string
|
||||||
|
ans = QTreeWidgetItem(parent, [name])
|
||||||
|
ans.setData(0, Qt.UserRole, f.full_path)
|
||||||
|
if f.is_folder:
|
||||||
|
ext = 'dir'
|
||||||
|
else:
|
||||||
|
ext = f.name.rpartition('.')[-1]
|
||||||
|
ans.setData(0, Qt.DecorationRole, file_icon_provider().icon_from_ext(ext))
|
||||||
|
|
||||||
|
return ans
|
||||||
|
|
||||||
|
class Storage(QTreeWidget):
|
||||||
|
|
||||||
|
def __init__(self, storage, show_files):
|
||||||
|
QTreeWidget.__init__(self)
|
||||||
|
self.show_files = show_files
|
||||||
|
self.create_children(storage, self)
|
||||||
|
self.name = storage.name
|
||||||
|
self.object_id = storage.persistent_id
|
||||||
|
self.setMinimumHeight(350)
|
||||||
|
self.setHeaderHidden(True)
|
||||||
|
|
||||||
|
def create_children(self, f, parent):
|
||||||
|
for child in sorted(f.folders, key=attrgetter('name')):
|
||||||
|
i = item(child, parent)
|
||||||
|
self.create_children(child, i)
|
||||||
|
if self.show_files:
|
||||||
|
for child in sorted(f.files, key=attrgetter('name')):
|
||||||
|
i = item(child, parent)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_item(self):
|
||||||
|
item = self.currentItem()
|
||||||
|
if item is not None:
|
||||||
|
return (self.object_id, item.data(0, Qt.UserRole).toPyObject())
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Folders(QTabWidget):
|
||||||
|
|
||||||
|
selected = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, filesystem_cache, show_files=True):
|
||||||
|
QTabWidget.__init__(self)
|
||||||
|
self.fs = filesystem_cache
|
||||||
|
for storage in self.fs.entries:
|
||||||
|
w = Storage(storage, show_files)
|
||||||
|
self.addTab(w, w.name)
|
||||||
|
w.doubleClicked.connect(self.selected)
|
||||||
|
|
||||||
|
self.setCurrentIndex(0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_item(self):
|
||||||
|
w = self.currentWidget()
|
||||||
|
if w is not None:
|
||||||
|
return w.current_item
|
||||||
|
|
||||||
|
class Browser(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, filesystem_cache, show_files=True, parent=None):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.l = l = QVBoxLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
self.folders = cw = Folders(filesystem_cache, show_files=show_files)
|
||||||
|
l.addWidget(cw)
|
||||||
|
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||||
|
l.addWidget(bb)
|
||||||
|
bb.accepted.connect(self.accept)
|
||||||
|
bb.rejected.connect(self.reject)
|
||||||
|
self.setMinimumSize(QSize(500, 500))
|
||||||
|
self.folders.selected.connect(self.accept)
|
||||||
|
self.setWindowTitle(_('Choose folder on device'))
|
||||||
|
self.setWindowIcon(QIcon(I('devices/galaxy_s3.png')))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_item(self):
|
||||||
|
return self.folders.current_item
|
||||||
|
|
||||||
|
def browse():
|
||||||
|
from calibre.gui2 import Application
|
||||||
|
from calibre.devices.mtp.driver import MTP_DEVICE
|
||||||
|
from calibre.devices.scanner import DeviceScanner
|
||||||
|
s = DeviceScanner()
|
||||||
|
s.scan()
|
||||||
|
app = Application([])
|
||||||
|
app
|
||||||
|
dev = MTP_DEVICE(None)
|
||||||
|
dev.startup()
|
||||||
|
cd = dev.detect_managed_devices(s.devices)
|
||||||
|
if cd is None:
|
||||||
|
raise ValueError('No MTP device found')
|
||||||
|
dev.open(cd, 'test')
|
||||||
|
d = Browser(dev.filesystem_cache)
|
||||||
|
d.exec_()
|
||||||
|
dev.shutdown()
|
||||||
|
return d.current_item
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print (browse())
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user