1
1
from __future__ import with_statement
2
2
__license__ = 'GPL v3'
3
3
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
4
import os, traceback, Queue, time, socket
4
import os, traceback, Queue, time, socket, cStringIO
5
5
from threading import Thread, RLock
6
6
from itertools import repeat
7
7
from functools import partial
10
10
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
13
from calibre.devices import devices
13
from calibre.customize.ui import available_input_formats, available_output_formats, \
15
from calibre.devices.interface import DevicePlugin
14
16
from calibre.constants import iswindows
15
17
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
16
from calibre.parallel import Job
18
from calibre.utils.ipc.job import BaseJob
17
19
from calibre.devices.scanner import DeviceScanner
18
20
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
21
pixmap_to_data, warning_dialog, \
20
23
from calibre.ebooks.metadata import authors_to_string
21
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
22
from calibre.devices.interface import Device
23
from calibre import sanitize_file_name, preferred_encoding
24
from calibre import preferred_encoding
24
25
from calibre.utils.filenames import ascii_filename
25
26
from calibre.devices.errors import FreeSpaceError
26
27
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
27
28
config as email_config
29
def warning(title, msg, details, parent):
30
from calibre.gui2.widgets import WarningDialog
31
WarningDialog(title, msg, details, parent).exec_()
36
def __init__(self, func, *args, **kwargs):
37
Job.__init__(self, *args, **kwargs)
30
class DeviceJob(BaseJob):
32
def __init__(self, func, done, job_manager, args=[], kwargs={},
34
BaseJob.__init__(self, description, done=done)
36
self.args, self.kwargs = args, kwargs
38
self.job_manager = job_manager
39
self._details = _('No details available.')
42
self.start_time = time.time()
43
self.job_manager.changed_queue.put(self)
46
self.duration = time.time() - self.start_time
48
self.job_manager.changed_queue.put(self)
50
def report_progress(self, percent, msg=''):
51
self.notifications.put((percent, msg))
52
self.job_manager.changed_queue.put(self)
43
57
self.result = self.func(*self.args, **self.kwargs)
44
58
except (Exception, SystemExit), err:
60
self._details = unicode(err) + '\n\n' + \
61
traceback.format_exc()
45
62
self.exception = err
46
self.traceback = traceback.format_exc()
68
return cStringIO.StringIO(self._details.encode('utf-8'))
51
71
class DeviceManager(Thread):
151
189
def _books(self):
152
190
'''Get metadata from device'''
153
mainlist = self.device.books(oncard=False, end_session=False)
154
cardlist = self.device.books(oncard=True)
155
return (mainlist, cardlist)
191
mainlist = self.device.books(oncard=None, end_session=False)
192
cardalist = self.device.books(oncard='carda')
193
cardblist = self.device.books(oncard='cardb')
194
return (mainlist, cardalist, cardblist)
157
196
def books(self, done):
158
197
'''Return callable that returns the list of books on device as two booklists'''
167
206
return self.create_job(self._sync_booklists, done, args=[booklists],
168
207
description=_('Send metadata to device'))
170
def _upload_books(self, files, names, on_card=False, metadata=None):
209
def _upload_books(self, files, names, on_card=None, metadata=None):
171
210
'''Upload books to device: '''
172
211
return self.device.upload_books(files, names, on_card,
173
212
metadata=metadata, end_session=False)
175
def upload_books(self, done, files, names, on_card=False, titles=None,
214
def upload_books(self, done, files, names, on_card=None, titles=None,
177
216
desc = _('Upload %d books to device')%len(names)
198
237
'''Copy books from device to disk'''
199
238
for path in paths:
200
239
name = path.rpartition('/')[2]
201
f = open(os.path.join(target, name), 'wb')
202
self.device.get_file(path, f)
240
dest = os.path.join(target, name)
241
if os.path.abspath(dest) != os.path.abspath(path):
243
self.device.get_file(path, f)
205
246
def save_books(self, done, paths, target):
206
247
return self.create_job(self._save_books, done, args=[paths, target],
267
308
self.connect(action2, SIGNAL('a_s(QAction)'),
268
309
self.action_triggered)
274
312
('main:', False, False, ':/images/reader.svg',
275
313
_('Send to main memory')),
276
('card:0', False, False, ':/images/sd.svg',
277
_('Send to storage card')),
314
('carda:0', False, False, ':/images/sd.svg',
315
_('Send to storage card A')),
316
('cardb:0', False, False, ':/images/sd.svg',
317
_('Send to storage card B')),
279
319
('main:', True, False, ':/images/reader.svg',
280
320
_('Send to main memory')),
281
('card:0', True, False, ':/images/sd.svg',
282
_('Send to storage card')),
321
('carda:0', True, False, ':/images/sd.svg',
322
_('Send to storage card A')),
323
('cardb:0', True, False, ':/images/sd.svg',
324
_('Send to storage card B')),
284
326
('main:', False, True, ':/images/reader.svg',
285
327
_('Send specific format to main memory')),
286
('card:0', False, True, ':/images/sd.svg',
287
_('Send specific format to storage card')),
328
('carda:0', False, True, ':/images/sd.svg',
329
_('Send specific format to storage card A')),
330
('cardb:0', False, True, ':/images/sd.svg',
331
_('Send specific format to storage card B')),
290
334
if default_account is not None:
344
388
self.action_triggered(action)
347
def enable_device_actions(self, enable):
391
def enable_device_actions(self, enable, card_prefix=(None, None)):
348
392
for action in self.actions:
349
if action.dest[:4] in ('main', 'card'):
350
action.setEnabled(enable)
393
if action.dest in ('main:', 'carda:0', 'cardb:0'):
395
action.setEnabled(False)
397
if action.dest == 'main:':
398
action.setEnabled(True)
399
elif action.dest == 'carda:0':
400
if card_prefix and card_prefix[0] != None:
401
action.setEnabled(True)
403
action.setEnabled(False)
404
elif action.dest == 'cardb:0':
405
if card_prefix and card_prefix[1] != None:
406
action.setEnabled(True)
408
action.setEnabled(False)
352
411
class Emailer(Thread):
421
480
d = ChooseFormatDialog(self, _('Choose format to send to device'),
422
self.device_manager.device_class.FORMATS)
481
self.device_manager.device_class.settings().format_map)
424
483
fmt = d.format().lower()
425
484
dest, sub_dest = dest.split(':')
426
if dest in ('main', 'card'):
485
if dest in ('main', 'carda', 'cardb'):
427
486
if not self.device_connected or not self.device_manager:
428
487
error_dialog(self, _('No device'),
429
488
_('Cannot send: No device is connected')).exec_()
431
on_card = dest == 'card'
432
if on_card and not self.device_manager.has_card():
433
error_dialog(self, _('No card'),
434
_('Cannot send: Device has no storage card')).exec_()
490
if dest == 'carda' and not self.device_manager.has_card():
491
error_dialog(self, _('No card'),
492
_('Cannot send: Device has no storage card')).exec_()
494
if dest == 'cardb' and not self.device_manager.has_card():
495
error_dialog(self, _('No card'),
496
_('Cannot send: Device has no storage card')).exec_()
436
502
self.sync_to_device(on_card, delete, fmt)
437
503
elif dest == 'mail':
438
504
to, fmts = sub_dest.split(';')
439
505
fmts = [x.strip().lower() for x in fmts.split(',')]
440
506
self.send_by_mail(to, fmts, delete)
442
def send_by_mail(self, to, fmts, delete_from_library):
443
rows = self.library_view.selectionModel().selectedRows()
444
if not rows or len(rows) == 0:
508
def send_by_mail(self, to, fmts, delete_from_library, send_ids=None,
509
do_auto_convert=True, specific_format=None):
510
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
511
if not ids or len(ids) == 0:
446
ids = iter(self.library_view.model().id(r) for r in rows)
513
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
514
fmts, paths=True, set_metadata=True,
515
specific_format=specific_format,
516
exclude_auto=do_auto_convert)
518
ids = list(set(ids).difference(_auto_ids))
447
522
full_metadata = self.library_view.model().get_metadata(
448
rows, full_metadata=True)[-1]
449
files = self.library_view.model().get_preferred_formats(rows,
450
fmts, paths=True, set_metadata=True)
523
ids, full_metadata=True, rows_are_ids=True)[-1]
451
524
files = [getattr(f, 'name', None) for f in files]
453
526
bad, remove_ids, jobnames = [], [], []
482
555
attachments, to_s, subjects, texts, attachment_names)
483
556
self.status_bar.showMessage(_('Sending email to')+' '+to, 3000)
561
if specific_format == None:
562
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
563
formats = formats if formats != None else []
564
if list(set(formats).intersection(available_input_formats())) != [] and list(set(fmts).intersection(available_output_formats())) != []:
567
bad.append(self.library_view.model().db.title(id, index_is_id=True))
569
if specific_format in list(set(fmts).intersection(set(available_output_formats()))):
572
bad.append(self.library_view.model().db.title(id, index_is_id=True))
575
format = specific_format if specific_format in list(set(fmts).intersection(set(available_output_formats()))) else None
578
if fmt in list(set(fmts).intersection(set(available_output_formats()))):
584
autos = [self.library_view.model().db.title(id, index_is_id=True) for id in auto]
585
autos = '\n'.join('%s'%i for i in autos)
586
if question_dialog(self, _('No suitable formats'),
587
_('Auto convert the following books before sending via '
588
'email?'), det_msg=autos):
589
self.auto_convert_mail(to, fmts, delete_from_library, auto, format)
486
bad = u'\n'.join(u'<li>%s</li>'%(i,) for i in bad)
487
details = u'<p><ul>%s</ul></p>'%bad
488
warning(_('No suitable formats'),
592
bad = '\n'.join('%s'%(i,) for i in bad)
593
d = warning_dialog(self, _('No suitable formats'),
489
594
_('Could not email the following books '
490
'as no suitable formats were found:'), details, self)
595
'as no suitable formats were found:'), bad)
492
598
def emails_sent(self, results, remove=[]):
493
599
errors, good = [], []
500
606
good.append(title)
502
608
errors = '\n'.join([
503
'<li><b>%s</b><br>%s<br>%s<br></li>' %
504
(title, e, tb.replace('\n', '<br>')) for \
505
611
title, e, tb in errors
507
ConversionErrorDialog(self, _('Failed to email books'),
508
'<p>'+_('Failed to email the following books:')+\
509
'<ul>%s</ul>'%errors,
613
error_dialog(self, _('Failed to email books'),
614
_('Failed to email the following books:'),
512
618
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
552
658
', '.join(sent_mails), 3000)
661
def sync_news(self, send_ids=None, do_auto_convert=True):
556
662
if self.device_connected:
557
ids = list(dynamic.get('news_to_be_synced', set([])))
663
ids = list(dynamic.get('news_to_be_synced', set([]))) if send_ids is None else send_ids
558
664
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
559
files = self.library_view.model().get_preferred_formats_from_ids(
560
ids, self.device_manager.device_class.FORMATS)
665
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(
666
ids, self.device_manager.device_class.settings().format_map,
667
exclude_auto=do_auto_convert)
669
if do_auto_convert and _auto_ids:
671
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
672
formats = formats if formats != None else []
673
if list(set(formats).intersection(available_input_formats())) != [] and list(set(self.device_manager.device_class.settings().format_map).intersection(available_output_formats())) != []:
677
for fmt in self.device_manager.device_class.settings().format_map:
678
if fmt in list(set(self.device_manager.device_class.settings().format_map).intersection(set(available_output_formats()))):
681
if format is not None:
682
autos = [self.library_view.model().db.title(id, index_is_id=True) for id in auto]
683
autos = '\n'.join('%s'%i for i in autos)
684
if question_dialog(self, _('No suitable formats'),
685
_('Auto convert the following books before uploading to '
686
'the device?'), det_msg=autos):
687
self.auto_convert_news(auto, format)
561
688
files = [f for f in files if f is not None]
563
690
dynamic.set('news_to_be_synced', set([]))
579
706
if config['upload_news_to_device'] and files:
580
707
remove = ids if \
581
708
config['delete_news_from_library_on_upload'] else []
582
on_card = self.location_view.model().free[0] < \
583
self.location_view.model().free[1]
709
space = { self.location_view.model().free[0] : None,
710
self.location_view.model().free[1] : 'carda',
711
self.location_view.model().free[2] : 'cardb' }
712
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
584
713
self.upload_books(files, names, metadata,
586
715
memory=[[f.name for f in files], remove])
590
719
def sync_to_device(self, on_card, delete_from_library,
591
specific_format=None):
592
rows = self.library_view.selectionModel().selectedRows()
593
if not self.device_manager or not rows or len(rows) == 0:
720
specific_format=None, send_ids=None, do_auto_convert=True):
721
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
722
if not self.device_manager or not ids or len(ids) == 0:
595
ids = iter(self.library_view.model().id(r) for r in rows)
596
metadata = self.library_view.model().get_metadata(rows)
725
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
726
self.device_manager.device_class.settings().format_map,
727
paths=True, set_metadata=True,
728
specific_format=specific_format,
729
exclude_auto=do_auto_convert)
731
ok_ids = list(set(ids).difference(_auto_ids))
732
ids = [i for i in ids if i in ok_ids]
736
metadata = self.library_view.model().get_metadata(ids, True)
597
738
for mi in metadata:
598
739
cdata = mi['cover']
600
741
mi['cover'] = self.cover_to_thumbnail(cdata)
601
742
metadata = iter(metadata)
602
_files = self.library_view.model().get_preferred_formats(rows,
603
self.device_manager.device_class.FORMATS,
604
paths=True, set_metadata=True,
605
specific_format=specific_format)
606
744
files = [getattr(f, 'name', None) for f in _files]
607
745
bad, good, gf, names, remove_ids = [], [], [], [], []
628
766
remove = remove_ids if delete_from_library else []
629
767
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
630
768
self.status_bar.showMessage(_('Sending books to device.'), 5000)
773
if specific_format == None:
774
formats = [f.lower() for f in self.library_view.model().db.formats(id, index_is_id=True).split(',')]
775
formats = formats if formats != None else []
776
if list(set(formats).intersection(available_input_formats())) != [] and list(set(self.device_manager.device_class.settings().format_map).intersection(available_output_formats())) != []:
779
bad.append(self.library_view.model().db.title(id, index_is_id=True))
781
if specific_format in list(set(self.device_manager.device_class.settings().format_map).intersection(set(available_output_formats()))):
784
bad.append(self.library_view.model().db.title(id, index_is_id=True))
787
format = specific_format if specific_format in list(set(self.device_manager.device_class.settings().format_map).intersection(set(available_output_formats()))) else None
789
for fmt in self.device_manager.device_class.settings().format_map:
790
if fmt in list(set(self.device_manager.device_class.settings().format_map).intersection(set(available_output_formats()))):
796
autos = [self.library_view.model().db.title(id, index_is_id=True) for id in auto]
797
autos = '\n'.join('%s'%i for i in autos)
798
if question_dialog(self, _('No suitable formats'),
799
_('Auto convert the following books before uploading to '
800
'the device?'), det_msg=autos):
801
self.auto_convert(auto, on_card, format)
632
bad = u'\n'.join(u'<li>%s</li>'%(i,) for i in bad)
633
details = u'<p><ul>%s</ul></p>'%bad
634
warning(_('No suitable formats'),
804
bad = '\n'.join('%s'%(i,) for i in bad)
805
d = warning_dialog(self, _('No suitable formats'),
635
806
_('Could not upload the following books to the device, '
636
807
'as no suitable formats were found. Try changing the output '
637
808
'format in the upper right corner next to the red heart and '
638
're-converting.'), details, self)
809
're-converting.'), bad)
640
812
def upload_booklists(self):
649
821
Called once metadata has been uploaded.
651
if job.exception is not None:
652
824
self.device_job_exception(job)
654
826
cp, fs = job.result
655
827
self.location_view.model().update_devices(cp, fs)
657
def upload_books(self, files, names, metadata, on_card=False, memory=None):
829
def upload_books(self, files, names, metadata, on_card=None, memory=None):
659
831
Upload books to device.
660
832
:param files: List of either paths to files or file like objects