~ubuntu-branches/ubuntu/karmic/calibre/karmic

« back to all changes in this revision

Viewing changes to src/calibre/gui2/device.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-30 12:49:41 UTC
  • mfrom: (1.3.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090730124941-qjdsmri25zt8zocn
Tags: 0.6.3+dfsg-0ubuntu1
* New upstream release. Please see http://calibre.kovidgoyal.net/new_in_6/
  for the list of new features and changes.
* remove_postinstall.patch: Update for new version.
* build_debug.patch: Does not apply any more, disable for now. Might not be
  necessary any more.
* debian/copyright: Fix reference to versionless GPL.
* debian/rules: Drop obsolete dh_desktop call.
* debian/rules: Add workaround for weird Python 2.6 setuptools behaviour of
  putting compiled .so files into src/calibre/plugins/calibre/plugins
  instead of src/calibre/plugins.
* debian/rules: Drop hal fdi moving, new upstream version does not use hal
  any more. Drop hal dependency, too.
* debian/rules: Install udev rules into /lib/udev/rules.d.
* Add debian/calibre.preinst: Remove unmodified
  /etc/udev/rules.d/95-calibre.rules on upgrade.
* debian/control: Bump Python dependencies to 2.6, since upstream needs
  it now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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, \
11
11
                     Qt
12
12
 
13
 
from calibre.devices import devices
 
13
from calibre.customize.ui import available_input_formats, available_output_formats, \
 
14
    device_plugins
 
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, \
19
 
                                   pixmap_to_data
 
21
                                   pixmap_to_data, warning_dialog, \
 
22
                                   question_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
28
29
 
29
 
def warning(title, msg, details, parent):
30
 
    from calibre.gui2.widgets import WarningDialog
31
 
    WarningDialog(title, msg, details, parent).exec_()
32
 
 
33
 
 
34
 
class DeviceJob(Job):
35
 
 
36
 
    def __init__(self, func, *args, **kwargs):
37
 
        Job.__init__(self, *args, **kwargs)
 
30
class DeviceJob(BaseJob):
 
31
 
 
32
    def __init__(self, func, done, job_manager, args=[], kwargs={},
 
33
            description=''):
 
34
        BaseJob.__init__(self, description, done=done)
38
35
        self.func = func
 
36
        self.args, self.kwargs = args, kwargs
 
37
        self.exception = None
 
38
        self.job_manager = job_manager
 
39
        self._details = _('No details available.')
 
40
 
 
41
    def start_work(self):
 
42
        self.start_time = time.time()
 
43
        self.job_manager.changed_queue.put(self)
 
44
 
 
45
    def job_done(self):
 
46
        self.duration = time.time() - self.start_time
 
47
        self.percent = 1
 
48
        self.job_manager.changed_queue.put(self)
 
49
 
 
50
    def report_progress(self, percent, msg=''):
 
51
        self.notifications.put((percent, msg))
 
52
        self.job_manager.changed_queue.put(self)
39
53
 
40
54
    def run(self):
41
55
        self.start_work()
42
56
        try:
43
57
            self.result = self.func(*self.args, **self.kwargs)
44
58
        except (Exception, SystemExit), err:
 
59
            self.failed = True
 
60
            self._details = unicode(err) + '\n\n' + \
 
61
                traceback.format_exc()
45
62
            self.exception = err
46
 
            self.traceback = traceback.format_exc()
47
63
        finally:
48
64
            self.job_done()
49
65
 
 
66
    @property
 
67
    def log_file(self):
 
68
        return cStringIO.StringIO(self._details.encode('utf-8'))
 
69
 
50
70
 
51
71
class DeviceManager(Thread):
52
72
 
57
77
        '''
58
78
        Thread.__init__(self)
59
79
        self.setDaemon(True)
60
 
        self.devices        = [[d, False] for d in devices()]
 
80
        # [Device driver, Showing in GUI, Ejected]
 
81
        self.devices        = [[d, False, False] for d in device_plugins()]
61
82
        self.device         = None
62
83
        self.device_class   = None
63
84
        self.sleep_time     = sleep_time
73
94
        for device in self.devices:
74
95
            connected = self.scanner.is_device_connected(device[0])
75
96
            if connected and not device[1]:
 
97
                if device[2]:
 
98
                    continue
76
99
                try:
77
 
                    dev = device[0]()
 
100
                    dev = device[0]
 
101
                    dev.reset()
78
102
                    if iswindows:
79
103
                        import pythoncom
80
104
                        pythoncom.CoInitialize()
98
122
                        job.abort(Exception(_('Device no longer connected.')))
99
123
                    except Queue.Empty:
100
124
                        break
 
125
                device[2] = False
101
126
                self.device = None
102
127
                self.connected_slot(False)
103
128
                device[1] ^= True
104
129
 
 
130
    def umount_device(self):
 
131
        if self.device is not None:
 
132
            self.device.eject()
 
133
            dev = None
 
134
            for x in self.devices:
 
135
                if x[0] is self.device:
 
136
                    dev = x
 
137
                    break
 
138
            if dev is not None:
 
139
                dev[2] = True
 
140
            self.connected_slot(False)
 
141
 
 
142
 
105
143
    def next(self):
106
144
        if not self.jobs.empty():
107
145
            try:
116
154
                job = self.next()
117
155
                if job is not None:
118
156
                    self.current_job = job
119
 
                    self.device.set_progress_reporter(job.update_status)
 
157
                    self.device.set_progress_reporter(job.report_progress)
120
158
                    self.current_job.run()
121
159
                    self.current_job = None
122
160
                else:
150
188
 
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)
156
195
 
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'))
169
208
 
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)
174
213
 
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,
176
215
                     metadata=None):
177
216
        desc = _('Upload %d books to device')%len(names)
178
217
        if titles:
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)
203
 
            f.close()
 
240
            dest = os.path.join(target, name)
 
241
            if os.path.abspath(dest) != os.path.abspath(path):
 
242
                f = open(dest, 'wb')
 
243
                self.device.get_file(path, f)
 
244
                f.close()
204
245
 
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)
269
310
 
270
 
 
271
 
 
272
 
 
273
311
        _actions = [
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')),
278
318
                '-----',
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')),
283
325
                '-----',
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')),
288
332
 
289
333
                ]
290
334
        if default_account is not None:
344
388
                self.action_triggered(action)
345
389
                break
346
390
 
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'):
 
394
                if not enable:
 
395
                    action.setEnabled(False)
 
396
                else:
 
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)
 
402
                        else:
 
403
                            action.setEnabled(False)
 
404
                    elif action.dest == 'cardb:0':
 
405
                        if card_prefix and card_prefix[1] != None:
 
406
                            action.setEnabled(True)
 
407
                        else:
 
408
                            action.setEnabled(False)
 
409
 
351
410
 
352
411
class Emailer(Thread):
353
412
 
419
478
        fmt = None
420
479
        if specific:
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)
423
482
            d.exec_()
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_()
430
489
                return
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_()
435
 
                return
 
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_()
 
493
                return
 
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_()
 
497
                return
 
498
            if dest == 'main':
 
499
                on_card = None
 
500
            else:
 
501
                on_card = dest
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)
441
507
 
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:
445
512
            return
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)
 
517
        if do_auto_convert:
 
518
            ids = list(set(ids).difference(_auto_ids))
 
519
        else:
 
520
            _auto_ids = []
 
521
 
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]
452
525
 
453
526
        bad, remove_ids, jobnames = [], [], []
469
542
                        '\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
470
543
                        _('in the %s format.') %
471
544
                        os.path.splitext(f)[1][1:].upper())
472
 
                prefix = sanitize_file_name(t+' - '+a)
 
545
                prefix = ascii_filename(t+' - '+a)
473
546
                if not isinstance(prefix, unicode):
474
547
                    prefix = prefix.decode(preferred_encoding, 'replace')
475
548
                attachment_names.append(prefix + os.path.splitext(f)[1])
482
555
                    attachments, to_s, subjects, texts, attachment_names)
483
556
            self.status_bar.showMessage(_('Sending email to')+' '+to, 3000)
484
557
 
 
558
        auto = []
 
559
        if _auto_ids != []:
 
560
            for id in _auto_ids:
 
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())) != []:
 
565
                        auto.append(id)
 
566
                    else:
 
567
                        bad.append(self.library_view.model().db.title(id, index_is_id=True))
 
568
                else:
 
569
                    if specific_format in list(set(fmts).intersection(set(available_output_formats()))):
 
570
                        auto.append(id)
 
571
                    else:
 
572
                        bad.append(self.library_view.model().db.title(id, index_is_id=True))
 
573
 
 
574
        if auto != []:
 
575
            format = specific_format if specific_format in list(set(fmts).intersection(set(available_output_formats()))) else None
 
576
            if not format:
 
577
                for fmt in fmts:
 
578
                    if fmt in list(set(fmts).intersection(set(available_output_formats()))):
 
579
                        format = fmt
 
580
                        break
 
581
            if format is None:
 
582
                bad += auto
 
583
            else:
 
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)
 
590
 
485
591
        if bad:
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)
 
596
            d.exec_()
491
597
 
492
598
    def emails_sent(self, results, remove=[]):
493
599
        errors, good = [], []
500
606
                good.append(title)
501
607
        if errors:
502
608
            errors = '\n'.join([
503
 
                    '<li><b>%s</b><br>%s<br>%s<br></li>' %
504
 
                    (title, e, tb.replace('\n', '<br>')) for \
 
609
                    '%s\n\n%s\n%s\n' %
 
610
                    (title, e, tb) for \
505
611
                            title, e, tb in errors
506
612
                    ])
507
 
            ConversionErrorDialog(self, _('Failed to email books'),
508
 
                    '<p>'+_('Failed to email the following books:')+\
509
 
                            '<ul>%s</ul>'%errors,
510
 
                        show=True)
 
613
            error_dialog(self, _('Failed to email books'),
 
614
                    _('Failed to email the following books:'),
 
615
                            '%s'%errors
 
616
                        )
511
617
        else:
512
618
            self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
513
619
                    5000)
517
623
        p.loadFromData(data)
518
624
        if not p.isNull():
519
625
            ht = self.device_manager.device_class.THUMBNAIL_HEIGHT \
520
 
                    if self.device_manager else Device.THUMBNAIL_HEIGHT
 
626
                    if self.device_manager else DevicePlugin.THUMBNAIL_HEIGHT
521
627
            p = p.scaledToHeight(ht, Qt.SmoothTransformation)
522
628
            return (p.width(), p.height(), pixmap_to_data(p))
523
629
 
527
633
                for account, x in opts.accounts.items() if x[1]]
528
634
        sent_mails = []
529
635
        for account, fmts in accounts:
530
 
            files = self.library_view.model().\
 
636
            files, auto = self.library_view.model().\
531
637
                    get_preferred_formats_from_ids([id], fmts)
532
638
            files = [f.name for f in files if f is not None]
533
639
            if not files:
552
658
                    ', '.join(sent_mails),  3000)
553
659
 
554
660
 
555
 
    def sync_news(self):
 
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)
 
668
            auto = []
 
669
            if do_auto_convert and _auto_ids:
 
670
                for id in _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())) != []:
 
674
                        auto.append(id)
 
675
            if auto != []:
 
676
                format = None
 
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()))):
 
679
                        format = fmt
 
680
                        break
 
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]
562
689
            if not files:
563
690
                dynamic.set('news_to_be_synced', set([]))
566
693
                    rows_are_ids=True)
567
694
            names = []
568
695
            for mi in metadata:
569
 
                prefix = sanitize_file_name(mi['title'])
 
696
                prefix = ascii_filename(mi['title'])
570
697
                if not isinstance(prefix, unicode):
571
698
                    prefix = prefix.decode(preferred_encoding, 'replace')
572
699
                prefix = ascii_filename(prefix)
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,
585
714
                        on_card=on_card,
586
715
                        memory=[[f.name for f in files], remove])
588
717
 
589
718
 
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:
594
723
            return
595
 
        ids = iter(self.library_view.model().id(r) for r in rows)
596
 
        metadata = self.library_view.model().get_metadata(rows)
 
724
 
 
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)
 
730
        if do_auto_convert:
 
731
            ok_ids = list(set(ids).difference(_auto_ids))
 
732
            ids = [i for i in ids if i in ok_ids]
 
733
        else:
 
734
            _auto_ids = []
 
735
 
 
736
        metadata = self.library_view.model().get_metadata(ids, True)
 
737
        ids = iter(ids)
597
738
        for mi in metadata:
598
739
            cdata = mi['cover']
599
740
            if cdata:
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)
 
743
 
606
744
        files = [getattr(f, 'name', None) for f in _files]
607
745
        bad, good, gf, names, remove_ids = [], [], [], [], []
608
746
        for f in files:
620
758
                a = mi['authors']
621
759
                if not a:
622
760
                    a = _('Unknown')
623
 
                prefix = sanitize_file_name(t+' - '+a)
 
761
                prefix = ascii_filename(t+' - '+a)
624
762
                if not isinstance(prefix, unicode):
625
763
                    prefix = prefix.decode(preferred_encoding, 'replace')
626
764
                prefix = ascii_filename(prefix)
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)
 
769
 
 
770
        auto = []
 
771
        if _auto_ids != []:
 
772
            for id in _auto_ids:
 
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())) != []:
 
777
                        auto.append(id)
 
778
                    else:
 
779
                        bad.append(self.library_view.model().db.title(id, index_is_id=True))
 
780
                else:
 
781
                    if specific_format in list(set(self.device_manager.device_class.settings().format_map).intersection(set(available_output_formats()))):
 
782
                        auto.append(id)
 
783
                    else:
 
784
                        bad.append(self.library_view.model().db.title(id, index_is_id=True))
 
785
 
 
786
        if auto != []:
 
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
 
788
            if not format:
 
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()))):
 
791
                        format = fmt
 
792
                        break
 
793
            if not format:
 
794
                bad += auto
 
795
            else:
 
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)
 
802
 
631
803
        if bad:
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)
 
810
            d.exec_()
639
811
 
640
812
    def upload_booklists(self):
641
813
        '''
648
820
        '''
649
821
        Called once metadata has been uploaded.
650
822
        '''
651
 
        if job.exception is not None:
 
823
        if job.failed:
652
824
            self.device_job_exception(job)
653
825
            return
654
826
        cp, fs = job.result
655
827
        self.location_view.model().update_devices(cp, fs)
656
828
 
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):
658
830
        '''
659
831
        Upload books to device.
660
832
        :param files: List of either paths to files or file like objects
693
865
 
694
866
        self.upload_booklists()
695
867
 
696
 
        view = self.card_view if on_card else self.memory_view
 
868
        view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view
697
869
        view.model().resort(reset=False)
698
870
        view.model().research()
699
871
        for f in files:
700
872
            getattr(f, 'close', lambda : True)()
701
873
        if memory and memory[1]:
702
874
            self.library_view.model().delete_books_by_id(memory[1])
703
 
 
704