2
2
__license__ = 'GPL v3'
3
3
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
5
import os, sys, textwrap, collections, traceback, time
5
import os, sys, textwrap, collections, traceback, time, socket
6
6
from xml.parsers.expat import ExpatError
7
from Queue import Queue, Empty
8
from threading import Thread
7
9
from functools import partial
8
10
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \
9
11
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
10
12
QToolButton, QDialog, QDesktopServices, QFileDialog, \
11
13
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
12
QProgressDialog, QMessageBox, QStackedLayout
14
QMessageBox, QStackedLayout
13
15
from PyQt4.QtSvg import QSvgRenderer
15
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
17
from calibre import __version__, __appname__, \
18
iswindows, isosx, prints, patheq
19
from calibre.utils.filenames import ascii_filename
17
20
from calibre.ptempfile import PersistentTemporaryFile
18
21
from calibre.utils.config import prefs, dynamic
22
from calibre.utils.ipc.server import Server
19
23
from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
20
24
initialize_file_icon_provider, question_dialog,\
21
25
pixmap_to_data, choose_dir, ORG_NAME, \
22
26
set_sidebar_directories, Dispatcher, \
23
SingleApplication, Application, available_height, \
27
Application, available_height, \
24
28
max_available_height, config, info_dialog, \
25
29
available_width, GetMetadata
26
30
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
27
from calibre.gui2.widgets import ProgressIndicator, WarningDialog
31
from calibre.gui2.widgets import ProgressIndicator
32
from calibre.gui2.wizard import move_library
28
33
from calibre.gui2.dialogs.scheduler import Scheduler
29
34
from calibre.gui2.update import CheckForUpdates
30
from calibre.gui2.dialogs.progress import ProgressDialog
31
35
from calibre.gui2.main_window import MainWindow, option_parser as _option_parser
32
36
from calibre.gui2.main_ui import Ui_MainWindow
33
37
from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceGUI, Emailer
34
38
from calibre.gui2.status import StatusBar
35
from calibre.gui2.jobs2 import JobManager
39
from calibre.gui2.jobs import JobManager, JobsDialog
36
40
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
37
41
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
38
from calibre.gui2.dialogs.jobs import JobsDialog
39
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
40
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebooks, \
41
set_conversion_defaults, fetch_scheduled_recipe
42
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
43
fetch_scheduled_recipe
42
44
from calibre.gui2.dialogs.config import ConfigDialog
43
45
from calibre.gui2.dialogs.search import SearchDialog
44
46
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
45
47
from calibre.gui2.dialogs.book_info import BookInfo
46
48
from calibre.ebooks import BOOK_EXTENSIONS
47
49
from calibre.library.database2 import LibraryDatabase2, CoverCache
48
from calibre.parallel import JobKilled
49
50
from calibre.gui2.dialogs.confirm_delete import confirm
52
ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \
53
os.path.expanduser('~/.calibre-gui.socket')
55
class SaveMenu(QMenu):
57
def __init__(self, parent):
58
QMenu.__init__(self, _('Save single format to disk...'), parent)
59
for ext in sorted(BOOK_EXTENSIONS):
60
action = self.addAction(ext.upper())
61
setattr(self, 'do_'+ext, partial(self.do, ext))
62
self.connect(action, SIGNAL('triggered(bool)'),
63
getattr(self, 'do_'+ext))
65
def do(self, ext, *args):
66
self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext)
68
class Listener(Thread):
70
def __init__(self, listener):
73
self.listener, self.queue = listener, Queue()
80
conn = self.listener.accept()
51
94
class Main(MainWindow, Ui_MainWindow, DeviceGUI):
61
104
self.default_thumbnail = (pixmap.width(), pixmap.height(),
62
105
pixmap_to_data(pixmap))
64
def __init__(self, single_instance, opts, actions, parent=None):
107
def __init__(self, listener, opts, actions, parent=None):
65
108
self.preferences_action, self.quit_action = actions
109
self.spare_servers = []
66
110
MainWindow.__init__(self, opts, parent)
67
111
# Initialize fontconfig in a separate thread as this can be a lengthy
68
112
# process if run for the first time on this machine
69
self.fc = __import__('calibre.utils.fontconfig', fromlist=1)
70
self.single_instance = single_instance
71
if self.single_instance is not None:
72
self.connect(self.single_instance,
73
SIGNAL('message_received(PyQt_PyObject)'),
74
self.another_instance_wants_to_talk)
113
from calibre.utils.fonts import fontconfig
115
self.listener = Listener(listener)
116
self.check_messages_timer = QTimer()
117
self.connect(self.check_messages_timer, SIGNAL('timeout()'),
118
self.another_instance_wants_to_talk)
119
self.check_messages_timer.start(1000)
76
121
Ui_MainWindow.__init__(self)
77
122
self.setupUi(self)
132
178
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
133
179
self.system_tray_icon_activated)
134
180
self.tool_bar.contextMenuEvent = self.no_op
182
####################### Start spare job server ########################
183
QTimer.singleShot(1000, self.add_spare_server)
185
####################### Setup device detection ########################
186
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
188
self.device_manager.start()
135
191
####################### Location View ########################
136
192
QObject.connect(self.location_view,
137
193
SIGNAL('location_selected(PyQt_PyObject)'),
138
194
self.location_selected)
140
self.output_formats = sorted(['EPUB', 'MOBI', 'LRF'])
141
for f in self.output_formats:
142
self.output_format.addItem(f)
143
self.output_format.setCurrentIndex(self.output_formats.index(
144
prefs['output_format']))
145
self.connect(self.output_format, SIGNAL('currentIndexChanged(QString)'),
146
self.change_output_format, Qt.QueuedConnection)
195
QObject.connect(self.location_view,
196
SIGNAL('umount_device()'),
197
self.device_manager.umount_device)
148
199
####################### Vanity ########################
149
200
self.vanity_template = _('<p>For help visit <a href="http://%s.'
194
247
self.add_recursive_single)
195
248
QObject.connect(self.add_menu.actions()[2], SIGNAL("triggered(bool)"),
196
249
self.add_recursive_multiple)
250
QObject.connect(self.add_menu.actions()[3], SIGNAL('triggered(bool)'),
197
252
QObject.connect(self.action_del, SIGNAL("triggered(bool)"),
198
253
self.delete_books)
199
254
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"),
200
255
self.edit_metadata)
256
self.__em1__ = partial(self.edit_metadata, bulk=False)
201
257
QObject.connect(md.actions()[0], SIGNAL('triggered(bool)'),
202
partial(self.edit_metadata, bulk=False))
259
self.__em2__ = partial(self.edit_metadata, bulk=True)
203
260
QObject.connect(md.actions()[2], SIGNAL('triggered(bool)'),
204
partial(self.edit_metadata, bulk=True))
262
self.__em3__ = partial(self.download_metadata, covers=True)
205
263
QObject.connect(md.actions()[4], SIGNAL('triggered(bool)'),
206
partial(self.download_metadata, covers=True))
265
self.__em4__ = partial(self.download_metadata, covers=False)
207
266
QObject.connect(md.actions()[5], SIGNAL('triggered(bool)'),
208
partial(self.download_metadata, covers=False))
268
self.__em5__ = partial(self.download_metadata, covers=True,
209
270
QObject.connect(md.actions()[6], SIGNAL('triggered(bool)'),
210
partial(self.download_metadata, covers=True,
215
275
self.save_menu = QMenu()
216
276
self.save_menu.addAction(_('Save to disk'))
217
277
self.save_menu.addAction(_('Save to disk in a single directory'))
218
self.save_menu.addAction(_('Save only %s format to disk')%\
219
config.get('save_to_disk_single_format').upper())
278
self.save_menu.addAction(_('Save only %s format to disk')%
279
prefs['output_format'].upper())
280
self.save_sub_menu = SaveMenu(self)
281
self.save_menu.addMenu(self.save_sub_menu)
282
self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
283
self.save_specific_format_disk)
221
285
self.view_menu = QMenu()
222
286
self.view_menu.addAction(_('View'))
249
313
cm.addAction(_('Convert individually'))
250
314
cm.addAction(_('Bulk convert'))
252
cm.addAction(_('Set defaults for conversion'))
253
cm.addAction(_('Set defaults for conversion of comics'))
254
315
self.action_convert.setMenu(cm)
316
self._convert_single_hook = partial(self.convert_ebook, bulk=False)
255
317
QObject.connect(cm.actions()[0],
256
SIGNAL('triggered(bool)'), self.convert_single)
318
SIGNAL('triggered(bool)'), self._convert_single_hook)
319
self._convert_bulk_hook = partial(self.convert_ebook, bulk=True)
257
320
QObject.connect(cm.actions()[1],
258
SIGNAL('triggered(bool)'), self.convert_bulk)
259
QObject.connect(cm.actions()[3],
260
SIGNAL('triggered(bool)'), self.set_conversion_defaults)
261
QObject.connect(cm.actions()[4],
262
SIGNAL('triggered(bool)'), self.set_comic_conversion_defaults)
321
SIGNAL('triggered(bool)'), self._convert_bulk_hook)
263
322
QObject.connect(self.action_convert,
264
SIGNAL('triggered(bool)'), self.convert_single)
323
SIGNAL('triggered(bool)'), self.convert_ebook)
265
324
self.convert_menu = cm
327
ap = self.action_preferences
328
pm.addAction(ap.icon(), ap.text())
329
pm.addAction(_('Run welcome wizard'))
330
self.connect(pm.actions()[0], SIGNAL('triggered(bool)'),
332
self.connect(pm.actions()[1], SIGNAL('triggered(bool)'),
334
self.action_preferences.setMenu(pm)
335
self.preferences_menu = pm
266
337
self.tool_bar.widgetForAction(self.action_news).\
267
338
setPopupMode(QToolButton.MenuButtonPopup)
268
339
self.tool_bar.widgetForAction(self.action_edit).\
320
391
similar_menu=similar_menu)
321
392
self.memory_view.set_context_menu(None, None, None,
322
393
self.action_view, self.action_save, None, None)
323
self.card_view.set_context_menu(None, None, None,
394
self.card_a_view.set_context_menu(None, None, None,
395
self.action_view, self.action_save, None, None)
396
self.card_b_view.set_context_menu(None, None, None,
324
397
self.action_view, self.action_save, None, None)
325
398
QObject.connect(self.library_view,
326
399
SIGNAL('files_dropped(PyQt_PyObject)'),
328
for func, target in [
329
('connect_to_search_box', self.search),
400
self.files_dropped, Qt.QueuedConnection)
402
('connect_to_search_box', (self.search,
330
404
('connect_to_book_display',
331
self.status_bar.book_info.show_data),
405
(self.status_bar.book_info.show_data,)),
333
for view in (self.library_view, self.memory_view, self.card_view):
334
getattr(view, func)(target)
407
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
408
getattr(view, func)(*args)
336
410
self.memory_view.connect_dirtied_signal(self.upload_booklists)
337
self.card_view.connect_dirtied_signal(self.upload_booklists)
411
self.card_a_view.connect_dirtied_signal(self.upload_booklists)
412
self.card_b_view.connect_dirtied_signal(self.upload_booklists)
340
415
if self.system_tray_icon.isVisible() and opts.start_in_tray:
342
417
self.stack.setCurrentIndex(0)
344
419
db = LibraryDatabase2(self.library_path)
345
420
except Exception, err:
346
422
error_dialog(self, _('Bad database location'),
347
unicode(err)).exec_()
423
_('Bad database location')+':'+self.library_path,
424
det_msg=traceback.format_exc()).exec_()
348
425
dir = unicode(QFileDialog.getExistingDirectory(self,
349
426
_('Choose a location for your ebook library.'),
350
427
os.path.expanduser('~')))
352
429
QCoreApplication.exit(1)
354
432
self.library_path = dir
355
433
db = LibraryDatabase2(self.library_path)
451
540
def system_tray_icon_activated(self, r):
452
541
if r == QSystemTrayIcon.Trigger:
453
542
if self.isVisible():
454
for window in QApplication.topLevelWidgets():
455
if isinstance(window, (MainWindow, QDialog)) and \
458
setattr(window, '__systray_minimized', True)
460
for window in QApplication.topLevelWidgets():
461
if getattr(window, '__systray_minimized', False):
463
setattr(window, '__systray_minimized', False)
466
def change_output_format(self, x):
467
of = unicode(x).strip()
468
if of != prefs['output_format']:
469
if of not in ('LRF', 'EPUB', 'MOBI'):
470
warning_dialog(self, 'Warning',
471
('<p>%s support is still in beta. If you find bugs, '
472
'please report them by opening a <a href="http://cal'
473
'ibre.kovidgoyal.net">ticket</a>.')%of).exec_()
474
prefs.set('output_format', of)
547
def hide_windows(self):
548
for window in QApplication.topLevelWidgets():
549
if isinstance(window, (MainWindow, QDialog)) and \
552
setattr(window, '__systray_minimized', True)
554
def show_windows(self):
555
for window in QApplication.topLevelWidgets():
556
if getattr(window, '__systray_minimized', False):
558
setattr(window, '__systray_minimized', False)
477
560
def test_server(self, *args):
478
561
if self.content_server.exception is not None:
685
781
'Select root folder')
688
from calibre.gui2.add import AddRecursive
689
self._add_recursive_thread = AddRecursive(root,
690
self.library_view.model().db, self.get_metadata,
692
self.connect(self._add_recursive_thread, SIGNAL('finished()'),
693
self._recursive_files_added)
694
self._add_recursive_thread.start()
696
def _recursive_files_added(self):
697
self._add_recursive_thread.process_duplicates()
698
if self._add_recursive_thread.number_of_books_added > 0:
699
self.library_view.model().resort(reset=False)
700
self.library_view.model().research()
701
self.library_view.model().count_changed()
702
self._add_recursive_thread = None
784
from calibre.gui2.add import Adder
785
self._adder = Adder(self,
786
self.library_view.model().db,
787
Dispatcher(self._files_added), spare_server=self.spare_server)
788
self._adder.add_recursive(root, single)
704
790
def add_recursive_single(self, checked):
740
835
(_('LRF Books'), ['lrf']),
741
836
(_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
742
837
(_('LIT Books'), ['lit']),
743
(_('MOBI Books'), ['mobi', 'prc']),
838
(_('MOBI Books'), ['mobi', 'prc', 'azw']),
744
839
(_('Text books'), ['txt', 'rtf']),
745
840
(_('PDF Books'), ['pdf']),
746
(_('Comics'), ['cbz', 'cbr']),
841
(_('Comics'), ['cbz', 'cbr', 'cbc']),
747
842
(_('Archives'), ['zip', 'rar']),
755
850
def _add_books(self, paths, to_device, on_card=None):
756
851
if on_card is None:
757
on_card = self.stack.currentIndex() == 2
852
on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None
760
from calibre.gui2.add import AddFiles
761
self._add_files_thread = AddFiles(paths, self.default_thumbnail,
763
None if to_device else \
764
self.library_view.model().db
766
self._add_files_thread.send_to_device = to_device
767
self._add_files_thread.on_card = on_card
768
self._add_files_thread.create_progress_dialog(_('Adding books...'),
769
_('Reading metadata...'), self)
770
self.connect(self._add_files_thread, SIGNAL('finished()'),
772
self._add_files_thread.start()
855
from calibre.gui2.add import Adder
856
self.__adder_func = partial(self._files_added, on_card=on_card)
857
self._adder = Adder(self,
858
None if to_device else self.library_view.model().db,
859
Dispatcher(self.__adder_func), spare_server=self.spare_server)
860
self._adder.add(paths)
774
def _files_added(self):
775
t = self._add_files_thread
776
self._add_files_thread = None
779
self.upload_books(t.paths,
780
list(map(sanitize_file_name, t.names)),
781
t.infos, on_card=t.on_card)
782
self.status_bar.showMessage(
783
_('Uploading books to device.'), 2000)
785
t.process_duplicates()
786
if t.number_of_books_added > 0:
787
self.library_view.model().books_added(t.number_of_books_added)
862
def _files_added(self, paths=[], names=[], infos=[], on_card=None):
864
self.upload_books(paths,
865
list(map(ascii_filename, names)),
866
infos, on_card=on_card)
867
self.status_bar.showMessage(
868
_('Uploading books to device.'), 2000)
869
if self._adder.number_of_books_added > 0:
870
self.library_view.model().books_added(self._adder.number_of_books_added)
788
871
if hasattr(self, 'db_images'):
789
872
self.db_images.reset()
873
if self._adder.critical:
875
for name, log in self._adder.critical.items():
876
det_msg.append(name+'\n'+log)
877
warning_dialog(self, _('Failed to read metadata'),
878
_('Failed to read metadata from the following')+':',
879
det_msg='\n\n'.join(det_msg), show=True)
881
self._adder.cleanup()
792
885
############################################################################
875
982
db = self.library_view.model().refresh_ids(
878
details = ['<li><b>%s:</b> %s</li>'%(title, reason) for title,
985
details = ['%s: %s'%(title, reason) for title,
879
986
reason in x.failures.values()]
880
details = '<p><ul>%s</ul></p>'%(''.join(details))
881
WarningDialog(_('Failed to download some metadata'),
987
details = '%s\n'%('\n'.join(details))
988
warning_dialog(self, _('Failed to download some metadata'),
882
989
_('Failed to download metadata for the following:'),
883
details, self).exec_()
990
det_msg=details).exec_()
885
err = _('<b>Failed to download metadata:')+\
886
'</b><br><pre>'+x.tb+'</pre>'
887
ConversionErrorDialog(self, _('Error'), err,
992
err = _('Failed to download metadata:')
993
error_dialog(self, _('Error'), err, det_msg=x.tb).exec_()
893
996
def edit_metadata(self, checked, bulk=None):
939
1045
############################## Save to disk ################################
940
1046
def save_single_format_to_disk(self, checked):
941
self.save_to_disk(checked, True, config['save_to_disk_single_format'])
1047
self.save_to_disk(checked, True, prefs['output_format'])
1049
def save_specific_format_disk(self, fmt):
1050
self.save_to_disk(False, True, fmt)
943
1052
def save_to_single_dir(self, checked):
944
1053
self.save_to_disk(checked, True)
946
1055
def save_to_disk(self, checked, single_dir=False, single_format=None):
948
1056
rows = self.current_view().selectionModel().selectedRows()
949
1057
if not rows or len(rows) == 0:
950
d = error_dialog(self, _('Cannot save to disk'),
951
_('No books selected'))
955
progress = ProgressDialog(_('Saving to disk...'), min=0, max=len(rows),
958
def callback(count, msg):
959
progress.set_value(count)
960
progress.set_msg(_('Saved')+' '+msg)
961
QApplication.processEvents()
962
QApplication.sendPostedEvents()
964
return not progress.canceled
966
dir = choose_dir(self, 'save to disk dialog',
1058
return error_dialog(self, _('Cannot save to disk'),
1059
_('No books selected'), show=True)
1060
path = choose_dir(self, 'save to disk dialog',
967
1061
_('Choose destination directory'))
972
QApplication.processEvents()
973
QApplication.sendPostedEvents()
976
if self.current_view() == self.library_view:
977
failures = self.current_view().model().save_to_disk(rows, dir,
978
single_dir=single_dir,
980
single_format=single_format)
981
if failures and single_format is not None:
982
msg = _('<p>Could not save the following books to disk, '
983
'because the %s format is not available for them:<ul>')\
984
%single_format.upper()
986
msg += '<li>%s</li>'%f[1]
988
warning_dialog(self, _('Could not save some ebooks'),
990
QDesktopServices.openUrl(QUrl('file:'+dir))
992
paths = self.current_view().model().paths(rows)
993
self.device_manager.save_books(
994
Dispatcher(self.books_saved), paths, dir)
1065
if self.current_view() is self.library_view:
1066
from calibre.gui2.add import Saver
1067
self._saver = Saver(self, self.library_view.model().db,
1068
Dispatcher(self._books_saved), rows, path,
1069
by_author=self.library_view.model().by_author,
1070
single_dir=single_dir,
1071
single_format=single_format,
1072
spare_server=self.spare_server)
1075
paths = self.current_view().model().paths(rows)
1076
self.device_manager.save_books(
1077
Dispatcher(self.books_saved), paths, path)
1080
def _books_saved(self, path, failures, error):
1081
single_format = self._saver.worker.single_format
1084
return error_dialog(self, _('Error while saving'),
1085
_('There was an error while saving.'),
1087
if failures and single_format:
1088
single_format = single_format.upper()
1089
warning_dialog(self, _('Could not save some books'),
1090
_('Could not save some books') + ', ' +
1091
(_('as the %s format is not available for them.')%single_format) +
1092
_('Click the show details button to see which ones.'),
1093
'\n'.join(failures), show=True)
1094
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
998
1096
def books_saved(self, job):
999
if job.exception is not None:
1000
self.device_job_exception(job)
1098
return self.device_job_exception(job)
1003
1100
############################################################################
1034
1130
############################### Convert ####################################
1132
def auto_convert(self, book_ids, on_card, format):
1133
previous = self.library_view.currentIndex()
1134
rows = [x.row() for x in \
1135
self.library_view.selectionModel().selectedRows()]
1136
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
1137
if jobs == []: return
1138
for func, args, desc, fmt, id, temp_files in jobs:
1140
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted),
1141
func, args=args, description=desc)
1142
self.conversion_jobs[job] = (temp_files, fmt, id, on_card)
1145
self.library_view.model().refresh_rows(rows)
1146
current = self.library_view.currentIndex()
1147
self.library_view.model().current_changed(current, previous)
1149
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format):
1150
previous = self.library_view.currentIndex()
1151
rows = [x.row() for x in \
1152
self.library_view.selectionModel().selectedRows()]
1153
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
1154
if jobs == []: return
1155
for func, args, desc, fmt, id, temp_files in jobs:
1157
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted_mail),
1158
func, args=args, description=desc)
1159
self.conversion_jobs[job] = (temp_files, fmt, id,
1160
delete_from_library, to, fmts)
1163
self.library_view.model().refresh_rows(rows)
1164
current = self.library_view.currentIndex()
1165
self.library_view.model().current_changed(current, previous)
1167
def auto_convert_news(self, book_ids, format):
1168
previous = self.library_view.currentIndex()
1169
rows = [x.row() for x in \
1170
self.library_view.selectionModel().selectedRows()]
1171
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
1172
if jobs == []: return
1173
for func, args, desc, fmt, id, temp_files in jobs:
1175
job = self.job_manager.run_job(Dispatcher(self.book_auto_converted_news),
1176
func, args=args, description=desc)
1177
self.conversion_jobs[job] = (temp_files, fmt, id)
1180
self.library_view.model().refresh_rows(rows)
1181
current = self.library_view.currentIndex()
1182
self.library_view.model().current_changed(current, previous)
1036
1185
def get_books_for_conversion(self):
1037
1186
rows = [r.row() for r in \
1038
1187
self.library_view.selectionModel().selectedRows()]
1040
1189
d = error_dialog(self, _('Cannot convert'),
1041
1190
_('No books selected'))
1044
comics, others = [], []
1045
db = self.library_view.model().db
1047
formats = db.formats(r)
1048
if not formats: continue
1049
formats = formats.lower().split(',')
1050
if 'cbr' in formats or 'cbz' in formats:
1054
return comics, others
1057
def convert_bulk(self, checked):
1058
r = self.get_books_for_conversion()
1063
res = convert_bulk_ebooks(self,
1064
self.library_view.model().db, comics, others)
1068
for func, args, desc, fmt, id, temp_files in jobs:
1069
job = self.job_manager.run_job(Dispatcher(self.book_converted),
1070
func, args=args, description=desc)
1071
self.conversion_jobs[job] = (temp_files, fmt, id)
1074
self.library_view.model().resort(reset=False)
1075
self.library_view.model().research()
1077
def set_conversion_defaults(self, checked):
1078
set_conversion_defaults(False, self, self.library_view.model().db)
1080
def set_comic_conversion_defaults(self, checked):
1081
set_conversion_defaults(True, self, self.library_view.model().db)
1083
def convert_single(self, checked):
1084
r = self.get_books_for_conversion()
1085
if r is None: return
1193
return [self.library_view.model().db.id(r) for r in rows]
1195
def convert_ebook(self, checked, bulk=None):
1196
book_ids = self.get_books_for_conversion()
1197
if book_ids is None: return
1086
1198
previous = self.library_view.currentIndex()
1087
1199
rows = [x.row() for x in \
1088
1200
self.library_view.selectionModel().selectedRows()]
1090
jobs, changed = convert_single_ebook(self,
1091
self.library_view.model().db, comics, others)
1201
if bulk or (bulk is None and len(book_ids) > 1):
1202
jobs, changed, bad = convert_bulk_ebook(self,
1203
self.library_view.model().db, book_ids, out_format=prefs['output_format'])
1205
jobs, changed, bad = convert_single_ebook(self,
1206
self.library_view.model().db, book_ids, out_format=prefs['output_format'])
1092
1207
for func, args, desc, fmt, id, temp_files in jobs:
1093
job = self.job_manager.run_job(Dispatcher(self.book_converted),
1209
job = self.job_manager.run_job(Dispatcher(self.book_converted),
1094
1210
func, args=args, description=desc)
1095
self.conversion_jobs[job] = (temp_files, fmt, id)
1211
self.conversion_jobs[job] = (temp_files, fmt, id)
1098
1214
self.library_view.model().refresh_rows(rows)
1099
1215
current = self.library_view.currentIndex()
1100
1216
self.library_view.model().current_changed(current, previous)
1218
def book_auto_converted(self, job):
1219
temp_files, fmt, book_id, on_card = self.conversion_jobs.pop(job)
1222
return self.job_exception(job)
1223
data = open(temp_files[-1].name, 'rb')
1224
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
1226
self.status_bar.showMessage(job.description + (' completed'), 2000)
1228
for f in temp_files:
1230
if os.path.exists(f.name):
1234
self.tags_view.recount()
1235
if self.current_view() is self.library_view:
1236
current = self.library_view.currentIndex()
1237
self.library_view.model().current_changed(current, QModelIndex())
1239
self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
1241
def book_auto_converted_mail(self, job):
1242
temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs.pop(job)
1245
self.job_exception(job)
1247
data = open(temp_files[-1].name, 'rb')
1248
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
1250
self.status_bar.showMessage(job.description + (' completed'), 2000)
1252
for f in temp_files:
1254
if os.path.exists(f.name):
1258
self.tags_view.recount()
1259
if self.current_view() is self.library_view:
1260
current = self.library_view.currentIndex()
1261
self.library_view.model().current_changed(current, QModelIndex())
1263
self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
1265
def book_auto_converted_news(self, job):
1266
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
1269
return self.job_exception(job)
1270
data = open(temp_files[-1].name, 'rb')
1271
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
1273
self.status_bar.showMessage(job.description + (' completed'), 2000)
1275
for f in temp_files:
1277
if os.path.exists(f.name):
1281
self.tags_view.recount()
1282
if self.current_view() is self.library_view:
1283
current = self.library_view.currentIndex()
1284
self.library_view.model().current_changed(current, QModelIndex())
1286
self.sync_news(send_ids=[book_id], do_auto_convert=False)
1102
1288
def book_converted(self, job):
1103
1289
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
1105
if job.exception is not None:
1106
1292
self.job_exception(job)
1108
1294
data = open(temp_files[-1].name, 'rb')
1193
1386
def view_book(self, triggered):
1194
1387
rows = self.current_view().selectionModel().selectedRows()
1195
if self.current_view() is self.library_view:
1196
if not rows or len(rows) == 0:
1197
self._launch_viewer()
1201
formats = self.library_view.model().db.formats(row).upper()
1202
formats = formats.split(',')
1203
title = self.library_view.model().db.title(row)
1204
id = self.library_view.model().db.id(row)
1206
if len(formats) == 1:
1208
if 'LRF' in formats:
1210
if 'EPUB' in formats:
1212
if 'MOBI' in formats:
1215
d = error_dialog(self, _('Cannot view'),
1216
_('%s has no available formats.')%(title,))
1220
d = ChooseFormatDialog(self, _('Choose the format to view'),
1223
if d.result() == QDialog.Accepted:
1389
if not rows or len(rows) == 0:
1390
self._launch_viewer()
1394
if not question_dialog(self, _('Multiple Books Selected'),
1395
_('You are attempting to open %d books. Opening too many '
1396
'books at once can be slow and have a negative effect on the '
1397
'responsiveness of your computer. Once started the process '
1398
'cannot be stopped until complete. Do you wish to continue?'
1228
self.view_format(row, format)
1402
if self.current_view() is self.library_view:
1406
formats = self.library_view.model().db.formats(row).upper()
1407
formats = formats.split(',')
1408
title = self.library_view.model().db.title(row)
1411
error_dialog(self, _('Cannot view'),
1412
_('%s has no available formats.')%(title,), show=True)
1416
for format in prefs['input_format_order']:
1417
if format in formats:
1419
self.view_format(row, format)
1422
self.view_format(row, formats[0])
1230
1424
paths = self.current_view().model().paths(rows)
1232
1426
pt = PersistentTemporaryFile('_viewer_'+\
1233
os.path.splitext(paths[0])[1])
1427
os.path.splitext(path)[1])
1234
1428
self.persistent_files.append(pt)
1236
1430
self.device_manager.view_book(\
1237
1431
Dispatcher(self.book_downloaded_for_viewing),
1242
1435
############################################################################
1264
1457
self.content_server = d.server
1265
1458
if d.result() == d.Accepted:
1266
1459
self.tool_bar.setIconSize(config['toolbar_icon_size'])
1460
self.search.search_as_you_type(config['search_as_you_type'])
1267
1461
self.tool_bar.setToolButtonStyle(
1268
1462
Qt.ToolButtonTextUnderIcon if \
1269
1463
config['show_text_in_toolbar'] else \
1270
1464
Qt.ToolButtonIconOnly)
1271
1465
self.save_menu.actions()[2].setText(
1272
_('Save only %s format to disk')%config.get(
1273
'save_to_disk_single_format').upper())
1274
if self.library_path != d.database_location:
1276
newloc = d.database_location
1277
if not os.path.exists(os.path.join(newloc, 'metadata.db')):
1278
if os.access(self.library_path, os.R_OK):
1279
pd = QProgressDialog('', '', 0, 100, self)
1280
pd.setWindowModality(Qt.ApplicationModal)
1281
pd.setCancelButton(None)
1282
pd.setWindowTitle(_('Copying database'))
1284
self.status_bar.showMessage(
1285
_('Copying library to ')+newloc)
1286
self.setCursor(Qt.BusyCursor)
1287
self.library_view.setEnabled(False)
1288
self.library_view.model().db.move_library_to(
1292
db = LibraryDatabase2(newloc)
1293
self.library_view.set_database(db)
1294
except Exception, err:
1295
traceback.print_exc()
1296
d = error_dialog(self, _('Invalid database'),
1297
_('<p>An invalid database already exists at '
1298
'%s, delete it before trying to move the '
1299
'existing database.<br>Error: %s')%(newloc,
1302
self.library_path = \
1303
self.library_view.model().db.library_path
1304
prefs['library_path'] = self.library_path
1305
except Exception, err:
1306
traceback.print_exc()
1307
d = error_dialog(self, _('Could not move database'),
1312
self.library_view.setEnabled(True)
1313
self.status_bar.clearMessage()
1314
self.search.clear_to_help()
1315
self.status_bar.reset_info()
1316
self.library_view.sortByColumn(3, Qt.DescendingOrder)
1317
self.library_view.resizeRowsToContents()
1466
_('Save only %s format to disk')%
1467
prefs['output_format'].upper())
1318
1468
if hasattr(d, 'directories'):
1319
1469
set_sidebar_directories(d.directories)
1320
1470
self.library_view.model().read_config()
1321
1471
self.create_device_menu()
1474
if not patheq(self.library_path, d.database_location):
1475
newloc = d.database_location
1476
move_library(self.library_path, newloc, self,
1480
def library_moved(self, newloc):
1481
if newloc is None: return
1482
db = LibraryDatabase2(newloc)
1483
self.library_view.set_database(db)
1484
self.status_bar.clearMessage()
1485
self.search.clear_to_help()
1486
self.status_bar.reset_info()
1487
self.library_view.sortByColumn(3, Qt.DescendingOrder)
1324
1489
############################################################################
1326
1491
################################ Book info #################################
1611
1784
help=_('Log debugging information to console'))
1788
parser = option_parser()
1789
opts, args = parser.parse_args(args)
1790
if opts.with_library is not None and os.path.isdir(opts.with_library):
1791
prefs.set('library_path', opts.with_library)
1792
print 'Using library at', prefs['library_path']
1793
app = Application(args)
1794
actions = tuple(Main.create_application_menubar())
1795
app.setWindowIcon(QIcon(':/library'))
1796
QCoreApplication.setOrganizationName(ORG_NAME)
1797
QCoreApplication.setApplicationName(APP_UID)
1798
return app, opts, args, actions
1800
def run_gui(opts, args, actions, listener, app):
1801
initialize_file_icon_provider()
1802
if not dynamic.get('welcome_wizard_was_run', False):
1803
from calibre.gui2.wizard import wizard
1805
dynamic.set('welcome_wizard_was_run', True)
1806
main = Main(listener, opts, actions)
1807
sys.excepthook = main.unhandled_exception
1809
args[1] = os.path.abspath(args[1])
1810
main.add_filesystem_book(args[1])
1812
if getattr(main, 'run_wizard_b4_shutdown', False):
1813
from calibre.gui2.wizard import wizard
1815
if getattr(main, 'restart_after_quit', False):
1816
e = sys.executable if getattr(sys, 'froze', False) else sys.argv[0]
1817
print 'Restarting with:', e, sys.argv
1818
if hasattr(sys, 'frameworks_dir'):
1819
app = os.path.dirname(os.path.dirname(sys.frameworks_dir))
1821
subprocess.Popen('sleep 3s; open '+app, shell=True)
1823
os.execvp(e, sys.argv)
1827
main.system_tray_icon.hide()
1832
def cant_start(msg=_('If you are sure it is not running')+', ',
1834
d = QMessageBox(QMessageBox.Critical, _('Cannot Start ')+__appname__,
1835
'<p>'+(_('%s is already running.')%__appname__)+'</p>',
1837
base = '<p>%s</p><p>%s %s'
1838
where = __appname__ + ' '+_('may be running in the system tray, in the')+' '
1840
where += _('upper right region of the screen.')
1842
where += _('lower right region of the screen.')
1845
what = _('try rebooting your computer.')
1847
what = _('try deleting the file')+': '+ADDRESS
1849
d.setInformativeText(base%(where, msg, what))
1856
from multiprocessing.connection import Client
1858
self.conn = Client(ADDRESS)
1861
def communicate(args):
1866
f = os.path.expanduser('~/.calibre_calibre GUI.lock')
1867
cant_start(what=_('try deleting the file')+': '+f)
1871
args[1] = os.path.abspath(args[1])
1872
t.conn.send('launched:'+repr(args))
1614
1877
def main(args=sys.argv):
1878
app, opts, args, actions = init_qt(args)
1615
1879
from calibre.utils.lock import singleinstance
1617
pid = os.fork() if False and islinux else -1
1619
parser = option_parser()
1620
opts, args = parser.parse_args(args)
1621
if opts.with_library is not None and os.path.isdir(opts.with_library):
1622
prefs.set('library_path', opts.with_library)
1623
print 'Using library at', prefs['library_path']
1624
app = Application(args)
1625
actions = tuple(Main.create_application_menubar())
1626
app.setWindowIcon(QIcon(':/library'))
1627
QCoreApplication.setOrganizationName(ORG_NAME)
1628
QCoreApplication.setApplicationName(APP_UID)
1629
single_instance = None if SingleApplication is None else \
1630
SingleApplication('calibre GUI')
1631
if not singleinstance('calibre GUI'):
1633
args[1] = os.path.abspath(args[1])
1634
if single_instance is not None and \
1635
single_instance.is_running() and \
1636
single_instance.send_message('launched:'+repr(args)):
1638
extra = '' if iswindows else \
1639
('If you\'re sure it is not running, delete the file '
1640
'%s.'%os.path.expanduser('~/.calibre_calibre GUI.lock'))
1641
QMessageBox.critical(None, _('Cannot Start ')+__appname__,
1642
_('<p>%s is already running. %s</p>')%(__appname__, extra))
1644
initialize_file_icon_provider()
1645
main = Main(single_instance, opts, actions)
1646
sys.excepthook = main.unhandled_exception
1648
main.add_filesystem_book(args[1])
1650
if getattr(main, 'restart_after_quit', False):
1651
e = sys.executable if getattr(sys, 'froze', False) else sys.argv[0]
1652
print 'Restarting with:', e, sys.argv
1653
os.execvp(e, sys.argv)
1880
from multiprocessing.connection import Listener
1881
si = singleinstance('calibre GUI')
1884
listener = Listener(address=ADDRESS)
1885
except socket.error:
1890
listener = Listener(address=ADDRESS)
1891
except socket.error:
1894
return run_gui(opts, args, actions, listener, app)
1657
main.system_tray_icon.hide()
1896
return run_gui(opts, args, actions, listener, app)
1897
otherinstance = False
1899
listener = Listener(address=ADDRESS)
1900
except socket.error: # Good si is correct (on UNIX)
1901
otherinstance = True
1903
# On windows only singleinstance can be trusted
1904
otherinstance = True if iswindows else False
1905
if not otherinstance:
1906
return run_gui(opts, args, actions, listener, app)