def __init__(self, upload_config_widget, image_list, *arg, **kw): super(PhotiniUploader, self).__init__(*arg, **kw) self.app = QtWidgets.QApplication.instance() self.app.aboutToQuit.connect(self.shutdown) logger.debug('using %s', keyring.get_keyring().__module__) self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) self.session = self.session_factory() self.session.connection_changed.connect(self.connection_changed) self.upload_worker = None # user details self.user = {} user_group = QtWidgets.QGroupBox(translate('UploaderTabsAll', 'User')) user_group.setLayout(QtWidgets.QVBoxLayout()) self.user_photo = QtWidgets.QLabel() self.user_photo.setAlignment(Qt.AlignHCenter | Qt.AlignTop) user_group.layout().addWidget(self.user_photo) self.user_name = QtWidgets.QLabel() self.user_name.setWordWrap(True) self.user_name.setFixedWidth(80) user_group.layout().addWidget(self.user_name) user_group.layout().addStretch(1) self.layout().addWidget(user_group, 0, 0, 1, 2) # connect / disconnect button self.user_connect = StartStopButton( translate('UploaderTabsAll', 'Log in'), translate('UploaderTabsAll', 'Log out')) self.user_connect.click_start.connect(self.log_in) self.user_connect.click_stop.connect(self.session.log_out) self.layout().addWidget(self.user_connect, 1, 0, 1, 2) # 'service' specific widget self.layout().addWidget(upload_config_widget, 0, 2, 2, 2) # upload button self.upload_button = StartStopButton( translate('UploaderTabsAll', 'Start upload'), translate('UploaderTabsAll', 'Stop upload')) self.upload_button.setEnabled(False) self.upload_button.click_start.connect(self.start_upload) self.upload_button.click_stop.connect(self.stop_upload) self.layout().addWidget(self.upload_button, 2, 3) # progress bar self.layout().addWidget( QtWidgets.QLabel(translate('UploaderTabsAll', 'Progress')), 2, 0) self.total_progress = QtWidgets.QProgressBar() self.layout().addWidget(self.total_progress, 2, 1, 1, 2) # adjust spacing self.layout().setColumnStretch(2, 1) self.layout().setRowStretch(0, 1) # initialise as not connected self.connection_changed(False)
def __init__(self, upload_config_widget, image_list, *arg, **kw): super(PhotiniUploader, self).__init__(*arg, **kw) QtWidgets.QApplication.instance().aboutToQuit.connect(self.shutdown) self.logger = logging.getLogger(self.__class__.__name__) self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) self.session = self.session_factory() self.upload_worker = None self.connected = False # user details self.user = {} user_group = QtWidgets.QGroupBox(self.tr('User')) user_group.setLayout(QtWidgets.QVBoxLayout()) self.user_photo = QtWidgets.QLabel() self.user_photo.setAlignment(Qt.AlignHCenter | Qt.AlignTop) user_group.layout().addWidget(self.user_photo) self.user_name = QtWidgets.QLabel() self.user_name.setWordWrap(True) self.user_name.setFixedWidth(80) user_group.layout().addWidget(self.user_name) user_group.layout().addStretch(1) self.layout().addWidget(user_group, 0, 0, 1, 2) # connect / disconnect button self.user_connect = QtWidgets.QPushButton() self.user_connect.setCheckable(True) self.user_connect.clicked.connect(self.connect_user) self.layout().addWidget(self.user_connect, 1, 0, 1, 2) # 'service' specific widget self.layout().addWidget(upload_config_widget, 0, 2, 2, 2) # upload button self.upload_button = StartStopButton(self.tr('Start upload'), self.tr('Stop upload')) self.upload_button.setEnabled(False) self.upload_button.click_start.connect(self.start_upload) self.upload_button.click_stop.connect(self.stop_upload) self.layout().addWidget(self.upload_button, 2, 3) # progress bar self.layout().addWidget(QtWidgets.QLabel(self.tr('Progress')), 2, 0) self.total_progress = QtWidgets.QProgressBar() self.layout().addWidget(self.total_progress, 2, 1, 1, 2) # adjust spacing self.layout().setColumnStretch(2, 1) self.layout().setRowStretch(0, 1)
def __init__(self, image_list, parent=None): super(TabWidget, self).__init__(parent) app = QtWidgets.QApplication.instance() app.aboutToQuit.connect(self.shutdown) if gp and app.test_mode: self.gp_log = gp.check_result(gp.use_python_logging()) self.config_store = app.config_store self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) form = QtWidgets.QFormLayout() form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) self.nm = NameMangler() self.file_data = {} self.file_list = [] self.source = None self.file_reader = None self.file_writer = None # source selector box = QtWidgets.QHBoxLayout() box.setContentsMargins(0, 0, 0, 0) self.source_selector = QtWidgets.QComboBox() self.source_selector.currentIndexChanged.connect(self.new_source) box.addWidget(self.source_selector) refresh_button = QtWidgets.QPushButton(self.tr('refresh')) refresh_button.clicked.connect(self.refresh) box.addWidget(refresh_button) box.setStretch(0, 1) form.addRow(self.tr('Source'), box) # path format self.path_format = QtWidgets.QLineEdit() self.path_format.setValidator(PathFormatValidator()) self.path_format.textChanged.connect(self.nm.new_format) self.path_format.editingFinished.connect(self.path_format_finished) form.addRow(self.tr('Target format'), self.path_format) # path example self.path_example = QtWidgets.QLabel() self.nm.new_example.connect(self.path_example.setText) form.addRow('=>', self.path_example) self.layout().addLayout(form, 0, 0) # file list self.file_list_widget = QtWidgets.QListWidget() self.file_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.file_list_widget.itemSelectionChanged.connect(self.selection_changed) self.layout().addWidget(self.file_list_widget, 1, 0) # selection buttons buttons = QtWidgets.QVBoxLayout() buttons.addStretch(1) self.selected_count = QtWidgets.QLabel() self.selection_changed() buttons.addWidget(self.selected_count) select_all = QtWidgets.QPushButton(self.tr('Select\nall')) select_all.clicked.connect(self.select_all) buttons.addWidget(select_all) select_new = QtWidgets.QPushButton(self.tr('Select\nnew')) select_new.clicked.connect(self.select_new) buttons.addWidget(select_new) self.copy_button = StartStopButton(self.tr('Copy\nphotos'), self.tr('Stop\nimport')) self.copy_button.click_start.connect(self.copy_selected) self.copy_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.copy_button) self.layout().addLayout(buttons, 0, 1, 2, 1) # final initialisation self.image_list.sort_order_changed.connect(self.sort_file_list) path = os.path.expanduser('~/Pictures') if not os.path.isdir(path) and sys.platform == 'win32': try: import win32com.shell as ws path = ws.shell.SHGetFolderPath( 0, ws.shellcon.CSIDL_MYPICTURES, None, 0) except ImportError: pass self.path_format.setText( os.path.join(path, '%Y', '%Y_%m_%d', '{name}')) self.refresh() self.list_files()
class TabWidget(QtWidgets.QWidget): @staticmethod def tab_name(): return QtCore.QCoreApplication.translate('TabWidget', '&Import photos') def __init__(self, image_list, parent=None): super(TabWidget, self).__init__(parent) app = QtWidgets.QApplication.instance() app.aboutToQuit.connect(self.shutdown) if gp and app.test_mode: self.gp_log = gp.check_result(gp.use_python_logging()) self.config_store = app.config_store self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) form = QtWidgets.QFormLayout() form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) self.nm = NameMangler() self.file_data = {} self.file_list = [] self.source = None self.file_reader = None self.file_writer = None # source selector box = QtWidgets.QHBoxLayout() box.setContentsMargins(0, 0, 0, 0) self.source_selector = QtWidgets.QComboBox() self.source_selector.currentIndexChanged.connect(self.new_source) box.addWidget(self.source_selector) refresh_button = QtWidgets.QPushButton(self.tr('refresh')) refresh_button.clicked.connect(self.refresh) box.addWidget(refresh_button) box.setStretch(0, 1) form.addRow(self.tr('Source'), box) # path format self.path_format = QtWidgets.QLineEdit() self.path_format.setValidator(PathFormatValidator()) self.path_format.textChanged.connect(self.nm.new_format) self.path_format.editingFinished.connect(self.path_format_finished) form.addRow(self.tr('Target format'), self.path_format) # path example self.path_example = QtWidgets.QLabel() self.nm.new_example.connect(self.path_example.setText) form.addRow('=>', self.path_example) self.layout().addLayout(form, 0, 0) # file list self.file_list_widget = QtWidgets.QListWidget() self.file_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.file_list_widget.itemSelectionChanged.connect(self.selection_changed) self.layout().addWidget(self.file_list_widget, 1, 0) # selection buttons buttons = QtWidgets.QVBoxLayout() buttons.addStretch(1) self.selected_count = QtWidgets.QLabel() self.selection_changed() buttons.addWidget(self.selected_count) select_all = QtWidgets.QPushButton(self.tr('Select\nall')) select_all.clicked.connect(self.select_all) buttons.addWidget(select_all) select_new = QtWidgets.QPushButton(self.tr('Select\nnew')) select_new.clicked.connect(self.select_new) buttons.addWidget(select_new) self.copy_button = StartStopButton(self.tr('Copy\nphotos'), self.tr('Stop\nimport')) self.copy_button.click_start.connect(self.copy_selected) self.copy_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.copy_button) self.layout().addLayout(buttons, 0, 1, 2, 1) # final initialisation self.image_list.sort_order_changed.connect(self.sort_file_list) path = os.path.expanduser('~/Pictures') if not os.path.isdir(path) and sys.platform == 'win32': try: import win32com.shell as ws path = ws.shell.SHGetFolderPath( 0, ws.shellcon.CSIDL_MYPICTURES, None, 0) except ImportError: pass self.path_format.setText( os.path.join(path, '%Y', '%Y_%m_%d', '{name}')) self.refresh() self.list_files() @QtCore.pyqtSlot(int) @catch_all def new_source(self, idx): self.source = None item_data = self.source_selector.itemData(idx) if callable(item_data): # a special 'source' that's actually a method to call (item_data)() return # select new source self.source, self.config_section = item_data path_format = self.path_format.text() path_format = self.config_store.get( self.config_section, 'path_format', path_format) path_format = path_format.replace('(', '{').replace(')', '}') self.path_format.setText(path_format) self.file_list_widget.clear() # allow 100ms for display to update before getting file list QtCore.QTimer.singleShot(100, self.list_files) def add_folder(self): folders = eval(self.config_store.get('importer', 'folders', '[]')) if folders: directory = folders[0] else: directory = '' root = QtWidgets.QFileDialog.getExistingDirectory( self, self.tr("Select root folder"), directory) if not root: self._fail() return if root in folders: folders.remove(root) folders.insert(0, root) if len(folders) > 5: del folders[-1] self.config_store.set('importer', 'folders', repr(folders)) self.refresh() idx = self.source_selector.count() - (1 + len(folders)) self.source_selector.setCurrentIndex(idx) @QtCore.pyqtSlot() @catch_all def path_format_finished(self): if self.source: self.config_store.set( self.config_section, 'path_format', self.nm.format_string) self.show_file_list() @QtCore.pyqtSlot() @catch_all def refresh(self): was_blocked = self.source_selector.blockSignals(True) # save current selection idx = self.source_selector.currentIndex() if idx >= 0: old_item_data = self.source_selector.itemData(idx) else: old_item_data = None # rebuild list self.source_selector.clear() self.source_selector.addItem( self.tr('<select source>'), self._new_file_list) for model, port_name in get_camera_list(): self.source_selector.addItem( self.tr('camera: {0}').format(model), (CameraSource(model, port_name), 'importer ' + model)) for root in eval(self.config_store.get('importer', 'folders', '[]')): if os.path.isdir(root): self.source_selector.addItem( self.tr('folder: {0}').format(root), (FolderSource(root), 'importer folder ' + root)) self.source_selector.addItem(self.tr('<add a folder>'), self.add_folder) # restore saved selection new_idx = -1 for idx in range(self.source_selector.count()): item_data = self.source_selector.itemData(idx) if item_data == old_item_data: new_idx = idx self.source_selector.setCurrentIndex(idx) break self.source_selector.blockSignals(was_blocked) if new_idx < 0: self.source_selector.setCurrentIndex(0) def do_not_close(self): if not self.file_reader: return False dialog = QtWidgets.QMessageBox() dialog.setWindowTitle(self.tr('Photini: import in progress')) dialog.setText(self.tr('<h3>Importing photos has not finished.</h3>')) dialog.setInformativeText( self.tr('Closing now will terminate the import.')) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons( QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel) dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel) result = dialog.exec_() return result == QtWidgets.QMessageBox.Cancel @QtCore.pyqtSlot(list) def new_selection(self, selection): pass def list_files(self): file_data = {} if self.source: with Busy(): file_data = self.source.get_file_data() if file_data is None: self._fail() return self._new_file_list(file_data) def _fail(self): self.source_selector.setCurrentIndex(0) self.refresh() def _new_file_list(self, file_data={}): self.file_list = list(file_data.keys()) self.file_data = file_data self.sort_file_list() @QtCore.pyqtSlot() @catch_all def sort_file_list(self): if eval(self.config_store.get('controls', 'sort_date', 'False')): self.file_list.sort(key=lambda x: self.file_data[x]['timestamp']) else: self.file_list.sort() self.show_file_list() if self.file_list: example = self.file_data[self.file_list[-1]] else: example = { 'camera' : None, 'name' : 'IMG_9999.JPG', 'timestamp' : datetime.now(), } self.nm.set_example(example) def show_file_list(self): self.file_list_widget.clear() first_active = None item = None for name in self.file_list: file_data = self.file_data[name] dest_path = self.nm.transform(file_data) file_data['dest_path'] = dest_path item = QtWidgets.QListWidgetItem(name + ' -> ' + dest_path) item.setData(Qt.UserRole, name) if os.path.exists(dest_path): item.setFlags(Qt.NoItemFlags) else: if not first_active: first_active = item item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.file_list_widget.addItem(item) if not first_active: first_active = item self.file_list_widget.scrollToItem( first_active, QtWidgets.QAbstractItemView.PositionAtTop) @QtCore.pyqtSlot() @catch_all def selection_changed(self): count = len(self.file_list_widget.selectedItems()) self.selected_count.setText(self.tr('%n file(s)\nselected', '', count)) @QtCore.pyqtSlot() @catch_all def select_all(self): self.select_files(datetime.min) @QtCore.pyqtSlot() @catch_all def select_new(self): since = datetime.min if self.source: since = self.config_store.get( self.config_section, 'last_transfer', since.isoformat(' ')) if len(since) > 19: since = datetime.strptime(since, '%Y-%m-%d %H:%M:%S.%f') else: since = datetime.strptime(since, '%Y-%m-%d %H:%M:%S') self.select_files(since) def select_files(self, since): count = self.file_list_widget.count() if not count: return self.file_list_widget.clearSelection() first_active = None for row in range(count): item = self.file_list_widget.item(row) if not (item.flags() & Qt.ItemIsSelectable): continue name = item.data(Qt.UserRole) timestamp = self.file_data[name]['timestamp'] if timestamp > since: if not first_active: first_active = item item.setSelected(True) if not first_active: first_active = item self.file_list_widget.scrollToItem( first_active, QtWidgets.QAbstractItemView.PositionAtTop) @QtCore.pyqtSlot() @catch_all def copy_selected(self): copy_list = [] for item in self.file_list_widget.selectedItems(): name = item.data(Qt.UserRole) copy_list.append(self.file_data[name]) if not copy_list: return self.copy_button.set_checked(True) self.last_file_copied = None, datetime.min # start file writer in a thread self.file_writer = FileWriter() self.file_writer_thread = QtCore.QThread(self) self.file_writer.moveToThread(self.file_writer_thread) self.file_writer.output.connect(self.file_copied) self.file_writer_thread.start() # start file reader in another thread self.file_reader = FileReader(self.source, copy_list) self.file_reader_thread = QtCore.QThread(self) self.file_reader.moveToThread(self.file_reader_thread) self.file_reader.output.connect(self.file_writer.input) self.file_reader_thread.started.connect(self.file_reader.start) self.file_reader_thread.start() @QtCore.pyqtSlot(dict, object, six.text_type) @catch_all def file_copied(self, info, camera_file, status): if info: self.image_list.open_file(info['dest_path']) if self.last_file_copied[1] < info['timestamp']: self.last_file_copied = info['dest_path'], info['timestamp'] return self.copy_button.set_checked(False) self.file_reader = None self.file_writer = None self.file_reader_thread.quit() self.file_writer_thread.quit() if self.last_file_copied[0]: self.config_store.set(self.config_section, 'last_transfer', self.last_file_copied[1].isoformat(' ')) self.image_list.done_opening(self.last_file_copied[0]) if status != 'ok': self._fail() self.show_file_list() @QtCore.pyqtSlot() @catch_all def stop_copy(self): if self.file_reader: self.file_reader.running = False @QtCore.pyqtSlot() @catch_all def shutdown(self): if self.file_reader: self.file_reader.running = False self.file_reader_thread.quit() self.file_writer_thread.quit() self.file_reader_thread.wait() self.file_writer_thread.wait()
class PhotiniUploader(QtWidgets.QWidget): upload_file = QtCore.pyqtSignal(object, object) def __init__(self, upload_config_widget, image_list, *arg, **kw): super(PhotiniUploader, self).__init__(*arg, **kw) QtWidgets.QApplication.instance().aboutToQuit.connect(self.shutdown) self.logger = logging.getLogger(self.__class__.__name__) self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) self.session = self.session_factory() self.upload_worker = None self.connected = False # user details self.user = {} user_group = QtWidgets.QGroupBox(self.tr('User')) user_group.setLayout(QtWidgets.QVBoxLayout()) self.user_photo = QtWidgets.QLabel() self.user_photo.setAlignment(Qt.AlignHCenter | Qt.AlignTop) user_group.layout().addWidget(self.user_photo) self.user_name = QtWidgets.QLabel() self.user_name.setWordWrap(True) self.user_name.setFixedWidth(80) user_group.layout().addWidget(self.user_name) user_group.layout().addStretch(1) self.layout().addWidget(user_group, 0, 0, 1, 2) # connect / disconnect button self.user_connect = QtWidgets.QPushButton() self.user_connect.setCheckable(True) self.user_connect.clicked.connect(self.connect_user) self.layout().addWidget(self.user_connect, 1, 0, 1, 2) # 'service' specific widget self.layout().addWidget(upload_config_widget, 0, 2, 2, 2) # upload button self.upload_button = StartStopButton(self.tr('Start upload'), self.tr('Stop upload')) self.upload_button.setEnabled(False) self.upload_button.click_start.connect(self.start_upload) self.upload_button.click_stop.connect(self.stop_upload) self.layout().addWidget(self.upload_button, 2, 3) # progress bar self.layout().addWidget(QtWidgets.QLabel(self.tr('Progress')), 2, 0) self.total_progress = QtWidgets.QProgressBar() self.layout().addWidget(self.total_progress, 2, 1, 1, 2) # adjust spacing self.layout().setColumnStretch(2, 1) self.layout().setRowStretch(0, 1) @QtCore.pyqtSlot() def shutdown(self): if self.upload_worker: self.upload_worker.abort_upload() self.upload_worker.thread.quit() self.upload_worker.thread.wait() def refresh(self, force=False): with Busy(): self.connected = (self.user_connect.isChecked() and self.session.permitted('read')) if self.connected: self.user_connect.setText(self.tr('Log out')) if force: # load_user_data can be slow, so only do it when forced try: self.load_user_data() except Exception as ex: self.logger.error(ex) self.connected = False if not self.connected: self.user_connect.setText(self.tr('Connect')) # clearing user data is quick so do it anyway self.load_user_data() self.user_connect.setChecked(self.connected) self.upload_config.setEnabled(self.connected and not self.upload_worker) self.user_connect.setEnabled(not self.upload_worker) # enable or disable upload button self.new_selection(self.image_list.get_selected_images()) @QtCore.pyqtSlot(bool) def connect_user(self, connect): if connect: self.authorise('read') else: self.session.log_out() self.refresh(force=True) def do_not_close(self): if not self.upload_worker: return False dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle(self.tr('Photini: upload in progress')) dialog.setText( self.tr('<h3>Upload to {} has not finished.</h3>').format( self.service_name)) dialog.setInformativeText( self.tr('Closing now will terminate the upload.')) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel) dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel) result = dialog.exec_() return result == QtWidgets.QMessageBox.Cancel def show_user(self, name, picture): if name: self.user_name.setText( self.tr('Connected to {0} on {1}').format( name, self.service_name)) else: self.user_name.setText( self.tr('Not connected to {}').format(self.service_name)) pixmap = QtGui.QPixmap() if picture: pixmap.loadFromData(picture) self.user_photo.setPixmap(pixmap) def get_temp_filename(self, image, ext='.jpg'): temp_dir = appdirs.user_cache_dir('photini') if not os.path.isdir(temp_dir): os.makedirs(temp_dir) return os.path.join(temp_dir, os.path.basename(image.path) + ext) def copy_metadata(self, image, path): # copy metadata, forcing IPTC creation md = Metadata(path, None) md.copy(image.metadata) md.save(True, 'none', True) def convert_to_jpeg(self, image): im = QtGui.QImage(image.path) path = self.get_temp_filename(image) im.save(path, format='jpeg', quality=95) self.copy_metadata(image, path) return path def copy_file_and_metadata(self, image): path = self.get_temp_filename(image, ext='') shutil.copyfile(image.path, path) self.copy_metadata(image, path) return path def is_convertible(self, image): file_type = image.file_type.split('/') if file_type[0] != 'image': # can only convert images return False if 'raw' in file_type[1]: # can't convert raw files return False if image.pixmap.isNull(): # if Qt can't read it, we can't convert it return False return True def get_conversion_function(self, image): if image.file_type in self.image_types['accepted']: if image.metadata._sc or not image.metadata.has_iptc(): # need to create file without sidecar and with IPTC return self.copy_file_and_metadata return None if not self.is_convertible(image): msg = self.tr('File "{0}" is of type "{1}", which {2} does not' + ' accept and Photini cannot convert.') buttons = QtWidgets.QMessageBox.Ignore elif (self.image_types['rejected'] == '*' or image.file_type in self.image_types['rejected']): msg = self.tr('File "{0}" is of type "{1}", which {2} does not' + ' accept. Would you like to convert it to JPEG?') buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Ignore else: msg = self.tr( 'File "{0}" is of type "{1}", which {2} may not' + ' handle correctly. Would you like to convert it to JPEG?') buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle(self.tr('Photini: incompatible type')) dialog.setText(self.tr('<h3>Incompatible image type.</h3>')) dialog.setInformativeText( msg.format(os.path.basename(image.path), image.file_type, self.service_name)) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(buttons) dialog.setDefaultButton(QtWidgets.QMessageBox.Yes) result = dialog.exec_() if result == QtWidgets.QMessageBox.Ignore: return 'omit' if result == QtWidgets.QMessageBox.Yes: return self.convert_to_jpeg return None @QtCore.pyqtSlot() def stop_upload(self): if self.upload_worker: # invoke worker method in this thread as worker thread is busy self.upload_worker.abort_upload() # reset GUI self.upload_file_done(None, '') @QtCore.pyqtSlot() def start_upload(self): if not self.image_list.unsaved_files_dialog(with_discard=False): self.upload_button.setChecked(False) return # make list of items to upload self.upload_list = [] for image in self.image_list.get_selected_images(): convert = self.get_conversion_function(image) if convert == 'omit': continue self.upload_list.append((image, convert)) if not self.upload_list: self.upload_button.setChecked(False) return if not self.authorise('write'): self.refresh(force=True) self.upload_button.setChecked(False) return # start uploading in separate thread, so GUI can continue self.upload_worker = UploadWorker(self.session_factory, self.get_upload_params()) self.upload_file.connect(self.upload_worker.upload_file) self.upload_worker.upload_progress.connect( self.total_progress.setValue) self.upload_worker.upload_file_done.connect(self.upload_file_done) self.upload_worker.thread.start() self.upload_config.setEnabled(False) self.user_connect.setEnabled(False) self.uploads_done = 0 self.next_upload() def next_upload(self): image, convert = self.upload_list[self.uploads_done] self.total_progress.setFormat('{} ({}/{}) %p%'.format( os.path.basename(image.path), 1 + self.uploads_done, len(self.upload_list))) self.total_progress.setValue(0) QtWidgets.QApplication.processEvents() self.upload_file.emit(image, convert) @QtCore.pyqtSlot(object, str) def upload_file_done(self, image, error): if error: dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle(self.tr('Photini: upload error')) dialog.setText( self.tr('<h3>File "{}" upload failed.</h3>').format( os.path.basename(image.path))) dialog.setInformativeText(error) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Retry) dialog.setDefaultButton(QtWidgets.QMessageBox.Retry) if dialog.exec_() == QtWidgets.QMessageBox.Abort: self.upload_button.setChecked(False) else: self.uploads_done += 1 if (self.upload_button.isChecked() and self.uploads_done < len(self.upload_list)): # start uploading next file (or retry same file) self.next_upload() return self.upload_button.setChecked(False) self.total_progress.setValue(0) self.total_progress.setFormat('%p%') self.upload_config.setEnabled(True) self.user_connect.setEnabled(True) self.upload_finished() self.upload_file.disconnect() self.upload_worker.upload_progress.disconnect() self.upload_worker.upload_file_done.disconnect() self.upload_worker.thread.quit() self.upload_worker.thread.wait() self.upload_worker = None # enable or disable upload button self.new_selection(self.image_list.get_selected_images()) def auth_dialog(self, auth_url): if webbrowser.open(auth_url, new=2, autoraise=0): info_text = self.tr('use your web browser') else: info_text = self.tr('open "{0}" in a web browser').format(auth_url) auth_code, OK = QtWidgets.QInputDialog.getText( self, self.tr('Photini: authorise {}').format(self.service_name), self.tr("""Please {0} to grant access to Photini, then enter the verification code:""").format(info_text)) if OK: return six.text_type(auth_code).strip() return None def authorise(self, level): with Busy(): if self.session.permitted(level): return True # do full authentication procedure auth_url = self.session.get_auth_url(level) auth_code = self.auth_dialog(auth_url) if not auth_code: return False with Busy(): self.session.get_access_token(auth_code) return self.session.permitted(level) @QtCore.pyqtSlot(list) def new_selection(self, selection): self.upload_button.setEnabled(self.upload_button.isChecked() or (len(selection) > 0 and self.connected))
def __init__(self, image_list, parent=None): super(TabWidget, self).__init__(parent) self.app = QtWidgets.QApplication.instance() self.app.aboutToQuit.connect(self.stop_copy) if gp and self.app.options.test: self.gp_log = gp.check_result(gp.use_python_logging()) self.config_store = self.app.config_store self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) form = QtWidgets.QFormLayout() form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) self.nm = NameMangler() self.file_data = {} self.file_list = [] self.source = None self.file_copier = None self.updating = QtCore.QMutex() # source selector box = QtWidgets.QHBoxLayout() box.setContentsMargins(0, 0, 0, 0) self.source_selector = QtWidgets.QComboBox() self.source_selector.currentIndexChanged.connect(self.new_source) self.source_selector.setContextMenuPolicy(Qt.CustomContextMenu) self.source_selector.customContextMenuRequested.connect( self.remove_folder) box.addWidget(self.source_selector) refresh_button = QtWidgets.QPushButton( translate('ImporterTab', 'refresh')) refresh_button.clicked.connect(self.refresh) box.addWidget(refresh_button) box.setStretch(0, 1) form.addRow(translate('ImporterTab', 'Source'), box) # update config self.config_store.delete('importer', 'folders') for section in self.config_store.config.sections(): if not section.startswith('importer'): continue path_format = self.config_store.get(section, 'path_format') if not (path_format and '(' in path_format): continue path_format = path_format.replace('(', '{').replace(')', '}') self.config_store.set(section, 'path_format', path_format) # path format self.path_format = QtWidgets.QLineEdit() self.path_format.setValidator(PathFormatValidator()) self.path_format.textChanged.connect(self.nm.new_format) self.path_format.editingFinished.connect(self.path_format_finished) form.addRow(translate('ImporterTab', 'Target format'), self.path_format) # path example self.path_example = QtWidgets.QLabel() self.nm.new_example.connect(self.path_example.setText) form.addRow('=>', self.path_example) self.layout().addLayout(form, 0, 0) # file list self.file_list_widget = QtWidgets.QListWidget() self.file_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.file_list_widget.itemSelectionChanged.connect( self.selection_changed) self.layout().addWidget(self.file_list_widget, 1, 0) # selection buttons buttons = QtWidgets.QVBoxLayout() buttons.addStretch(1) self.selected_count = QtWidgets.QLabel() buttons.addWidget(self.selected_count) select_all = QtWidgets.QPushButton( translate('ImporterTab', 'Select\nall')) select_all.clicked.connect(self.select_all) buttons.addWidget(select_all) select_new = QtWidgets.QPushButton( translate('ImporterTab', 'Select\nnew')) select_new.clicked.connect(self.select_new) buttons.addWidget(select_new) # copy buttons self.move_button = StartStopButton( translate('ImporterTab', 'Move\nphotos'), translate('ImporterTab', 'Stop\nmove')) self.move_button.click_start.connect(self.move_selected) self.move_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.move_button) self.copy_button = StartStopButton( translate('ImporterTab', 'Copy\nphotos'), translate('ImporterTab', 'Stop\ncopy')) self.copy_button.click_start.connect(self.copy_selected) self.copy_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.copy_button) self.layout().addLayout(buttons, 0, 1, 2, 1) self.selection_changed() # final initialisation self.image_list.sort_order_changed.connect(self.sort_file_list) path = QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.PicturesLocation) self.path_format.setText(os.path.join(path, '%Y', '%Y_%m_%d', '{name}'))
class TabWidget(QtWidgets.QWidget): @staticmethod def tab_name(): return translate('ImporterTab', '&Import photos') def __init__(self, image_list, parent=None): super(TabWidget, self).__init__(parent) self.app = QtWidgets.QApplication.instance() self.app.aboutToQuit.connect(self.stop_copy) if gp and self.app.options.test: self.gp_log = gp.check_result(gp.use_python_logging()) self.config_store = self.app.config_store self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) form = QtWidgets.QFormLayout() form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) self.nm = NameMangler() self.file_data = {} self.file_list = [] self.source = None self.file_copier = None self.updating = QtCore.QMutex() # source selector box = QtWidgets.QHBoxLayout() box.setContentsMargins(0, 0, 0, 0) self.source_selector = QtWidgets.QComboBox() self.source_selector.currentIndexChanged.connect(self.new_source) self.source_selector.setContextMenuPolicy(Qt.CustomContextMenu) self.source_selector.customContextMenuRequested.connect( self.remove_folder) box.addWidget(self.source_selector) refresh_button = QtWidgets.QPushButton( translate('ImporterTab', 'refresh')) refresh_button.clicked.connect(self.refresh) box.addWidget(refresh_button) box.setStretch(0, 1) form.addRow(translate('ImporterTab', 'Source'), box) # update config self.config_store.delete('importer', 'folders') for section in self.config_store.config.sections(): if not section.startswith('importer'): continue path_format = self.config_store.get(section, 'path_format') if not (path_format and '(' in path_format): continue path_format = path_format.replace('(', '{').replace(')', '}') self.config_store.set(section, 'path_format', path_format) # path format self.path_format = QtWidgets.QLineEdit() self.path_format.setValidator(PathFormatValidator()) self.path_format.textChanged.connect(self.nm.new_format) self.path_format.editingFinished.connect(self.path_format_finished) form.addRow(translate('ImporterTab', 'Target format'), self.path_format) # path example self.path_example = QtWidgets.QLabel() self.nm.new_example.connect(self.path_example.setText) form.addRow('=>', self.path_example) self.layout().addLayout(form, 0, 0) # file list self.file_list_widget = QtWidgets.QListWidget() self.file_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.file_list_widget.itemSelectionChanged.connect( self.selection_changed) self.layout().addWidget(self.file_list_widget, 1, 0) # selection buttons buttons = QtWidgets.QVBoxLayout() buttons.addStretch(1) self.selected_count = QtWidgets.QLabel() buttons.addWidget(self.selected_count) select_all = QtWidgets.QPushButton( translate('ImporterTab', 'Select\nall')) select_all.clicked.connect(self.select_all) buttons.addWidget(select_all) select_new = QtWidgets.QPushButton( translate('ImporterTab', 'Select\nnew')) select_new.clicked.connect(self.select_new) buttons.addWidget(select_new) # copy buttons self.move_button = StartStopButton( translate('ImporterTab', 'Move\nphotos'), translate('ImporterTab', 'Stop\nmove')) self.move_button.click_start.connect(self.move_selected) self.move_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.move_button) self.copy_button = StartStopButton( translate('ImporterTab', 'Copy\nphotos'), translate('ImporterTab', 'Stop\ncopy')) self.copy_button.click_start.connect(self.copy_selected) self.copy_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.copy_button) self.layout().addLayout(buttons, 0, 1, 2, 1) self.selection_changed() # final initialisation self.image_list.sort_order_changed.connect(self.sort_file_list) path = QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.PicturesLocation) self.path_format.setText(os.path.join(path, '%Y', '%Y_%m_%d', '{name}')) @QtSlot(int) @catch_all def new_source(self, idx): self.source = None item_data = self.source_selector.itemData(idx) if not item_data: return if callable(item_data): # a special 'source' that's actually a method to call (item_data)() return # select new source self.source, self.config_section = item_data path_format = self.path_format.text() path_format = self.config_store.get(self.config_section, 'path_format', path_format) self.path_format.setText(path_format) self.file_list_widget.clear() # allow 100ms for display to update before getting file list QtCore.QTimer.singleShot(100, self.list_files) @QtSlot(QtCore.QPoint) @catch_all def remove_folder(self, pos): menu = QtWidgets.QMenu() roots = [] for section in self.config_store.config.sections(): if not section.startswith('importer folder '): continue roots.append((section[16:], self.config_store.get(section, 'last_transfer', datetime.min.isoformat(' ')))) roots.sort(key=lambda x: x[1], reverse=True) for root, last_transfer in roots: name = translate('ImporterTab', 'folder: {0}').format(root) action = QtGui2.QAction(translate('TechnicalTab', 'Remove "{}"').format(name), parent=self) action.setData('importer folder ' + root) menu.addAction(action) if menu.isEmpty(): return action = menu.exec_(self.mapToGlobal(pos)) if not action: return self.config_store.remove_section(action.data()) self.refresh() def add_folder(self): directory = '' for idx in range(self.source_selector.count()): item_data = self.source_selector.itemData(idx) if callable(item_data): continue source, section = item_data if section.startswith('importer folder '): directory = section[16:] break root = QtWidgets.QFileDialog.getExistingDirectory( self, translate('ImporterTab', "Select root folder"), directory) if not root: self._fail() return section = 'importer folder ' + root self.config_store.set(section, 'last_transfer', datetime.min.isoformat(' ')) self.source_selector.addItem( translate('ImporterTab', 'folder: {0}').format(root), (FolderSource(root), section)) idx = self.source_selector.count() - 1 self.source_selector.setCurrentIndex(idx) self.refresh() @QtSlot() @catch_all def path_format_finished(self): if self.source: self.config_store.set(self.config_section, 'path_format', self.nm.format_string) self.show_file_list() @QtSlot() @catch_all def refresh(self): was_blocked = self.source_selector.blockSignals(True) # save current selection idx = self.source_selector.currentIndex() if idx >= 0: old_item_text = self.source_selector.itemText(idx) else: old_item_text = None # rebuild list self.source_selector.clear() self.source_selector.addItem( translate('ImporterTab', '<select source>'), self._new_file_list) for model, port_name in get_camera_list(): self.source_selector.addItem( translate('ImporterTab', 'camera: {0}').format(model), (CameraSource(model, port_name), 'importer ' + model)) roots = [] for section in self.config_store.config.sections(): if not section.startswith('importer folder '): continue roots.append((section[16:], self.config_store.get(section, 'last_transfer', datetime.min.isoformat(' ')))) roots.sort(key=lambda x: x[1], reverse=True) for root, last_transfer in roots: if os.path.isdir(root): self.source_selector.addItem( translate('ImporterTab', 'folder: {0}').format(root), (FolderSource(root), 'importer folder ' + root)) self.source_selector.addItem( translate('ImporterTab', '<add a folder>'), self.add_folder) # restore saved selection new_idx = -1 for idx in range(self.source_selector.count()): item_text = self.source_selector.itemText(idx) if item_text == old_item_text: new_idx = idx self.source_selector.setCurrentIndex(idx) break self.source_selector.blockSignals(was_blocked) if new_idx < 0: self.source_selector.setCurrentIndex(0) self.new_source(0) def do_not_close(self): if not self.file_copier: return False dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle( translate('ImporterTab', 'Photini: import in progress')) dialog.setText( translate('ImporterTab', '<h3>Importing photos has not finished.</h3>')) dialog.setInformativeText( translate('ImporterTab', 'Closing now will terminate the import.')) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel) dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel) result = dialog.exec_() if result == QtWidgets.QMessageBox.Close: self.stop_copy() return result == QtWidgets.QMessageBox.Cancel def new_selection(self, selection): pass @QtSlot() @catch_all def list_files(self): file_data = {} if self.source: with Busy(): file_data = self.source.get_file_data() if file_data is None: self._fail() return self._new_file_list(file_data) def _fail(self): self.source_selector.setCurrentIndex(0) self.refresh() def _new_file_list(self, file_data={}): self.file_list = list(file_data.keys()) self.file_data = file_data self.sort_file_list() @QtSlot() @catch_all def sort_file_list(self): if self.config_store.get('controls', 'sort_date', False): self.file_list.sort(key=lambda x: self.file_data[x]['timestamp']) else: self.file_list.sort() self.show_file_list() if self.file_list: example = self.file_data[self.file_list[-1]] else: example = { 'camera': None, 'name': 'IMG_9999.JPG', 'timestamp': datetime.now(), } self.nm.set_example(example) def show_file_list(self): self.file_list_widget.clear() first_active = None item = None for name in self.file_list: file_data = self.file_data[name] dest_path = self.nm.transform(file_data) file_data['dest_path'] = dest_path item = QtWidgets.QListWidgetItem(name + ' -> ' + dest_path) item.setData(Qt.UserRole, name) if os.path.exists(dest_path): item.setFlags(Qt.NoItemFlags) else: if not first_active: first_active = item item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.file_list_widget.addItem(item) if not first_active: first_active = item self.file_list_widget.scrollToItem( first_active, QtWidgets.QAbstractItemView.PositionAtTop) @QtSlot() @catch_all def selection_changed(self): count = len(self.file_list_widget.selectedItems()) self.selected_count.setText( translate('ImporterTab', '%n file(s)\nselected', '', count)) if not self.file_copier: self.move_button.setEnabled(count > 0) self.copy_button.setEnabled(count > 0) @QtSlot() @catch_all def select_all(self): self.select_files(datetime.min) @QtSlot() @catch_all def select_new(self): since = datetime.min if self.source: since = self.config_store.get(self.config_section, 'last_transfer', since.isoformat(' ')) if len(since) > 19: since = datetime.strptime(since, '%Y-%m-%d %H:%M:%S.%f') else: since = datetime.strptime(since, '%Y-%m-%d %H:%M:%S') self.select_files(since) def select_files(self, since): count = self.file_list_widget.count() if not count: return self.file_list_widget.clearSelection() first_active = None for row in range(count): item = self.file_list_widget.item(row) if not (item.flags() & Qt.ItemIsSelectable): continue name = item.data(Qt.UserRole) timestamp = self.file_data[name]['timestamp'] if timestamp > since: if not first_active: first_active = item item.setSelected(True) if not first_active: first_active = item self.file_list_widget.scrollToItem( first_active, QtWidgets.QAbstractItemView.PositionAtTop) @QtSlot() @catch_all def move_selected(self): self.copy_selected(move=True) @QtSlot() @catch_all def copy_selected(self, move=False): copy_list = [] for item in self.file_list_widget.selectedItems(): name = item.data(Qt.UserRole) info = self.file_data[name] if (move and 'path' in info and self.image_list.get_image(info['path'])): # don't rename an open file logger.warning('Please close image %s before moving it', info['name']) else: copy_list.append(info) if not copy_list: return if move: self.move_button.set_checked(True) self.copy_button.setEnabled(False) else: self.copy_button.set_checked(True) self.move_button.setEnabled(False) last_file_copied = None, datetime.min copier_result = deque() # start file copier in a separate thread self.file_copier = FileCopier(self.source, copy_list, move, copier_result) copier_thread = QtCore.QThread(self) self.file_copier.moveToThread(copier_thread) copier_thread.started.connect(self.file_copier.start) copier_thread.start() # show files as they're copied while self.file_copier.running: if copier_result: info, status = copier_result.popleft() if not info: # copier thread has finished break if status != 'ok': self._fail() break if last_file_copied[1] < info['timestamp']: last_file_copied = info['dest_path'], info['timestamp'] for n in range(self.file_list_widget.count()): item = self.file_list_widget.item(n) if item.data(Qt.UserRole) == info['name']: item.setFlags(Qt.NoItemFlags) self.file_list_widget.scrollToItem( item, QtWidgets.QAbstractItemView.PositionAtTop) self.selection_changed() break self.image_list.open_file(info['dest_path']) else: # wait for copier result self.app.processEvents() self.move_button.set_checked(False) self.copy_button.set_checked(False) self.file_copier = None copier_thread.quit() copier_thread.wait() if last_file_copied[0]: self.config_store.set(self.config_section, 'last_transfer', last_file_copied[1].isoformat(' ')) self.image_list.done_opening(last_file_copied[0]) self.list_files() @QtSlot() @catch_all def stop_copy(self): if self.file_copier: self.file_copier.running = False
class Importer(QtWidgets.QWidget): def __init__(self, image_list, parent=None): super(Importer, self).__init__(parent) app = QtWidgets.QApplication.instance() self.config_store = app.config_store self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) form = QtWidgets.QFormLayout() form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) self.nm = NameMangler() self.file_data = {} self.file_list = [] self.session_factory = None self.import_in_progress = False # source selector box = QtWidgets.QHBoxLayout() box.setContentsMargins(0, 0, 0, 0) self.source_selector = QtWidgets.QComboBox() self.source_selector.currentIndexChanged.connect(self.new_source) box.addWidget(self.source_selector) refresh_button = QtWidgets.QPushButton(self.tr('refresh')) refresh_button.clicked.connect(self.refresh) box.addWidget(refresh_button) box.setStretch(0, 1) form.addRow(self.tr('Source'), box) # path format self.path_format = QtWidgets.QLineEdit() self.path_format.setValidator(PathFormatValidator()) self.path_format.textChanged.connect(self.nm.new_format) self.path_format.editingFinished.connect(self.path_format_finished) form.addRow(self.tr('Target format'), self.path_format) # path example self.path_example = QtWidgets.QLabel() self.nm.new_example.connect(self.path_example.setText) form.addRow('=>', self.path_example) self.layout().addLayout(form, 0, 0) # file list self.file_list_widget = QtWidgets.QListWidget() self.file_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.file_list_widget.itemSelectionChanged.connect( self.selection_changed) self.layout().addWidget(self.file_list_widget, 1, 0) # selection buttons buttons = QtWidgets.QVBoxLayout() buttons.addStretch(1) self.selected_count = QtWidgets.QLabel() self.selection_changed() buttons.addWidget(self.selected_count) select_all = QtWidgets.QPushButton(self.tr('Select\nall')) select_all.clicked.connect(self.select_all) buttons.addWidget(select_all) select_new = QtWidgets.QPushButton(self.tr('Select\nnew')) select_new.clicked.connect(self.select_new) buttons.addWidget(select_new) self.copy_button = StartStopButton(self.tr('Copy\nphotos'), self.tr('Stop\nimport')) self.copy_button.click_start.connect(self.copy_selected) buttons.addWidget(self.copy_button) self.layout().addLayout(buttons, 0, 1, 2, 1) # final initialisation self.image_list.sort_order_changed.connect(self.sort_file_list) if sys.platform == 'win32': import win32com.shell as ws path = ws.shell.SHGetFolderPath(0, ws.shellcon.CSIDL_MYPICTURES, None, 0) else: path = os.path.expanduser('~/Pictures') self.path_format.setText(os.path.join(path, '%Y', '%Y_%m_%d', '(name)')) self.refresh() self.list_files() @QtCore.pyqtSlot(int) def new_source(self, idx): self.session_factory = None item_data = self.source_selector.itemData(idx) if callable(item_data): # a special 'source' that's actually a method to call (item_data)() return # select new source (self.session_factory, self.session_params, self.config_section) = item_data path_format = self.path_format.text() path_format = self.config_store.get(self.config_section, 'path_format', path_format) self.path_format.setText(path_format) self.file_list_widget.clear() # allow 100ms for display to update before getting file list QtCore.QTimer.singleShot(100, self.list_files) def add_folder(self): folders = eval(self.config_store.get('importer', 'folders', '[]')) if folders: directory = folders[0] else: directory = '' root = QtWidgets.QFileDialog.getExistingDirectory( self, self.tr("Select root folder"), directory) if not root: self._fail() return if root in folders: folders.remove(root) folders.insert(0, root) if len(folders) > 5: del folders[-1] self.config_store.set('importer', 'folders', repr(folders)) self.refresh() idx = self.source_selector.count() - (1 + len(folders)) self.source_selector.setCurrentIndex(idx) @QtCore.pyqtSlot() def path_format_finished(self): if self.session_factory: self.config_store.set(self.config_section, 'path_format', self.nm.format_string) self.show_file_list() @QtCore.pyqtSlot() def refresh(self): was_blocked = self.source_selector.blockSignals(True) # save current selection idx = self.source_selector.currentIndex() if idx >= 0: old_item_data = self.source_selector.itemData(idx) else: old_item_data = None # rebuild list self.source_selector.clear() self.source_selector.addItem(self.tr('<select source>'), self._new_file_list) for model, port_name in get_camera_list(): self.source_selector.addItem( self.tr('camera: {0}').format(model), (CameraSource, (model, port_name), 'importer ' + model)) for root in eval(self.config_store.get('importer', 'folders', '[]')): if os.path.isdir(root): self.source_selector.addItem( self.tr('folder: {0}').format(root), (FolderSource, (root, ), 'importer folder ' + root)) self.source_selector.addItem(self.tr('<add a folder>'), self.add_folder) # restore saved selection new_idx = -1 for idx in range(self.source_selector.count()): item_data = self.source_selector.itemData(idx) if item_data == old_item_data: new_idx = idx self.source_selector.setCurrentIndex(idx) break self.source_selector.blockSignals(was_blocked) if new_idx < 0: self.source_selector.setCurrentIndex(0) def do_not_close(self): if not self.import_in_progress: return False dialog = QtWidgets.QMessageBox() dialog.setWindowTitle(self.tr('Photini: import in progress')) dialog.setText(self.tr('<h3>Importing photos has not finished.</h3>')) dialog.setInformativeText( self.tr('Closing now will terminate the import.')) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel) dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel) result = dialog.exec_() return result == QtWidgets.QMessageBox.Cancel @QtCore.pyqtSlot(list) def new_selection(self, selection): pass @contextmanager def session(self): session = (self.session_factory)(*self.session_params) yield session session.close() def list_files(self): file_data = {} if self.session_factory: with self.session() as session: with Busy(): try: file_list = session.list_files() except gp.GPhoto2Error: # camera is no longer visible self._fail() return for path in file_list: try: info = session.get_file_info(path) except gp.GPhoto2Error: self._fail() return file_data[info['name']] = info self._new_file_list(file_data) def _fail(self): self.source_selector.setCurrentIndex(0) self.refresh() def _new_file_list(self, file_data={}): self.file_list = list(file_data.keys()) self.file_data = file_data self.sort_file_list() @QtCore.pyqtSlot() def sort_file_list(self): if eval(self.config_store.get('controls', 'sort_date', 'False')): self.file_list.sort(key=lambda x: self.file_data[x]['timestamp']) else: self.file_list.sort() self.show_file_list() if self.file_list: example = self.file_data[self.file_list[-1]] else: example = { 'camera': None, 'name': 'IMG_9999.JPG', 'timestamp': datetime.now(), } self.nm.set_example(example) def show_file_list(self): self.file_list_widget.clear() first_active = None item = None for name in self.file_list: file_data = self.file_data[name] dest_path = self.nm.transform(file_data) file_data['dest_path'] = dest_path item = QtWidgets.QListWidgetItem(name + ' -> ' + dest_path) if os.path.exists(dest_path): item.setFlags(Qt.NoItemFlags) else: if not first_active: first_active = item item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self.file_list_widget.addItem(item) if not first_active: first_active = item self.file_list_widget.scrollToItem( first_active, QtWidgets.QAbstractItemView.PositionAtTop) @QtCore.pyqtSlot() def selection_changed(self): count = len(self.file_list_widget.selectedItems()) self.selected_count.setText(self.tr('%n file(s)\nselected', '', count)) @QtCore.pyqtSlot() def select_all(self): self.select_files(datetime.min) @QtCore.pyqtSlot() def select_new(self): since = datetime.min if self.session_factory: since = self.config_store.get(self.config_section, 'last_transfer', since.isoformat(' ')) if len(since) > 19: since = datetime.strptime(since, '%Y-%m-%d %H:%M:%S.%f') else: since = datetime.strptime(since, '%Y-%m-%d %H:%M:%S') self.select_files(since) def select_files(self, since): count = self.file_list_widget.count() if not count: return self.file_list_widget.clearSelection() first_active = None for row in range(count): item = self.file_list_widget.item(row) if not (item.flags() & Qt.ItemIsSelectable): continue name = item.text().split()[0] timestamp = self.file_data[name]['timestamp'] if timestamp > since: if not first_active: first_active = item item.setSelected(True) if not first_active: first_active = item self.file_list_widget.scrollToItem( first_active, QtWidgets.QAbstractItemView.PositionAtTop) @QtCore.pyqtSlot() def copy_selected(self): if self.import_in_progress: # user has clicked while import is still cancelling self.copy_button.setChecked(False) return self.import_in_progress = True copy_list = [] for item in self.file_list_widget.selectedItems(): name = item.text().split()[0] copy_list.append(self.file_data[name]) last_item = None, datetime.min with self.session() as session: with Busy(): for item in copy_list: dest_path = item['dest_path'] dest_dir = os.path.dirname(dest_path) if self.abort_copy(): break if not os.path.isdir(dest_dir): os.makedirs(dest_dir) try: camera_file = session.copy_file(item, dest_path) if self.abort_copy(): break if camera_file: camera_file.save(dest_path) except gp.GPhoto2Error as ex: self.logger.error(str(ex)) self._fail() break timestamp = item['timestamp'] if last_item[1] < timestamp: last_item = dest_path, timestamp if self.abort_copy(): break self.image_list.open_file(dest_path) if self.abort_copy(): break QtCore.QCoreApplication.flush() if last_item[0]: self.config_store.set(self.config_section, 'last_transfer', last_item[1].isoformat(' ')) self.image_list.done_opening(last_item[0]) self.show_file_list() self.copy_button.setChecked(False) self.import_in_progress = False def abort_copy(self): # test if user has stopped copy or quit program QtCore.QCoreApplication.processEvents() return not (self.copy_button.isChecked() and self.isVisible())
class PhotiniUploader(QtWidgets.QWidget): abort_upload = QtCore.Signal(bool) def __init__(self, upload_config_widget, image_list, *arg, **kw): super(PhotiniUploader, self).__init__(*arg, **kw) self.app = QtWidgets.QApplication.instance() self.app.aboutToQuit.connect(self.shutdown) logger.debug('using %s', keyring.get_keyring().__module__) self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) self.session = self.session_factory() self.session.connection_changed.connect(self.connection_changed) self.upload_worker = None # user details self.user = {} user_group = QtWidgets.QGroupBox(translate('UploaderTabsAll', 'User')) user_group.setLayout(QtWidgets.QVBoxLayout()) self.user_photo = QtWidgets.QLabel() self.user_photo.setAlignment(Qt.AlignHCenter | Qt.AlignTop) user_group.layout().addWidget(self.user_photo) self.user_name = QtWidgets.QLabel() self.user_name.setWordWrap(True) self.user_name.setFixedWidth(80) user_group.layout().addWidget(self.user_name) user_group.layout().addStretch(1) self.layout().addWidget(user_group, 0, 0, 1, 2) # connect / disconnect button self.user_connect = StartStopButton( translate('UploaderTabsAll', 'Log in'), translate('UploaderTabsAll', 'Log out')) self.user_connect.click_start.connect(self.log_in) self.user_connect.click_stop.connect(self.session.log_out) self.layout().addWidget(self.user_connect, 1, 0, 1, 2) # 'service' specific widget self.layout().addWidget(upload_config_widget, 0, 2, 2, 2) # upload button self.upload_button = StartStopButton( translate('UploaderTabsAll', 'Start upload'), translate('UploaderTabsAll', 'Stop upload')) self.upload_button.setEnabled(False) self.upload_button.click_start.connect(self.start_upload) self.upload_button.click_stop.connect(self.stop_upload) self.layout().addWidget(self.upload_button, 2, 3) # progress bar self.layout().addWidget( QtWidgets.QLabel(translate('UploaderTabsAll', 'Progress')), 2, 0) self.total_progress = QtWidgets.QProgressBar() self.layout().addWidget(self.total_progress, 2, 1, 1, 2) # adjust spacing self.layout().setColumnStretch(2, 1) self.layout().setRowStretch(0, 1) # initialise as not connected self.connection_changed(False) def tr(self, *arg, **kw): return QtCore.QCoreApplication.translate('UploaderTabsAll', *arg, **kw) @QtCore.Slot() @catch_all def shutdown(self): self.session.disconnect() @QtCore.Slot(bool) @catch_all def connection_changed(self, connected): if connected: with Busy(): self.show_user(*self.session.get_user()) self.show_album_list(self.session.get_albums()) else: self.show_user(None, None) self.show_album_list([]) self.user_connect.set_checked(connected) self.upload_config.setEnabled(connected and not self.upload_worker) self.user_connect.setEnabled(not self.upload_worker) self.enable_upload_button() def refresh(self): if not self.user_connect.is_checked(): self.log_in(do_auth=False) self.enable_upload_button() def do_not_close(self): if not self.upload_worker: return False dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle(translate( 'UploaderTabsAll', 'Photini: upload in progress')) dialog.setText(translate( 'UploaderTabsAll', '<h3>Upload to {} has not finished.</h3>').format(self.service_name)) dialog.setInformativeText( translate('UploaderTabsAll', 'Closing now will terminate the upload.')) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons( QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel) dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel) result = dialog.exec_() return result == QtWidgets.QMessageBox.Cancel def show_user(self, name, picture): if name: self.user_name.setText(translate( 'UploaderTabsAll', 'Logged in as {0} on {1}').format(name, self.service_name)) else: self.user_name.setText(translate( 'UploaderTabsAll', 'Not logged in to {}').format(self.service_name)) pixmap = QtGui.QPixmap() if picture: pixmap.loadFromData(picture) self.user_photo.setPixmap(pixmap) def get_temp_filename(self, image, ext='.jpg'): temp_dir = appdirs.user_cache_dir('photini') if not os.path.isdir(temp_dir): os.makedirs(temp_dir) return os.path.join(temp_dir, os.path.basename(image.path) + ext) def copy_metadata(self, image, path): # copy metadata md = Metadata.clone(path, image.metadata) # save metedata, forcing IPTC creation md.dirty = True md.save(if_mode=True, sc_mode='none', force_iptc=True) def convert_to_jpeg(self, image): im = QtGui.QImage(image.path) path = self.get_temp_filename(image) im.save(path, format='jpeg', quality=95) self.copy_metadata(image, path) return path def copy_file_and_metadata(self, image): path = self.get_temp_filename(image, ext='') shutil.copyfile(image.path, path) self.copy_metadata(image, path) return path def is_convertible(self, image): if not image.file_type.startswith('image'): # can only convert images return False return QtGui.QImageReader(image.path).canRead() def get_conversion_function(self, image, params): if image.file_type in self.image_types['accepted']: if image.file_type.startswith('video'): # don't try to write metadata to videos return None if image.metadata._sc or not image.metadata._if.has_iptc(): # need to create file without sidecar and with IPTC return self.copy_file_and_metadata return None if not self.is_convertible(image): msg = translate( 'UploaderTabsAll', 'File "{0}" is of type "{1}", which {2} does not' + ' accept and Photini cannot convert.') buttons = QtWidgets.QMessageBox.Ignore elif (self.image_types['rejected'] == '*' or image.file_type in self.image_types['rejected']): msg = translate( 'UploaderTabsAll', 'File "{0}" is of type "{1}", which {2} does not' + ' accept. Would you like to convert it to JPEG?') buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Ignore else: msg = translate( 'UploaderTabsAll', 'File "{0}" is of type "{1}", which {2} may not' + ' handle correctly. Would you like to convert it to JPEG?') buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle( translate('UploaderTabsAll', 'Photini: incompatible type')) dialog.setText( translate('UploaderTabsAll', '<h3>Incompatible image type.</h3>')) dialog.setInformativeText(msg.format(os.path.basename(image.path), image.file_type, self.service_name)) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(buttons) dialog.setDefaultButton(QtWidgets.QMessageBox.Yes) result = dialog.exec_() if result == QtWidgets.QMessageBox.Ignore: return 'omit' if result == QtWidgets.QMessageBox.Yes: return self.convert_to_jpeg return None @QtCore.Slot() @catch_all def stop_upload(self): self.abort_upload.emit(False) @QtCore.Slot() @catch_all def start_upload(self): if not self.image_list.unsaved_files_dialog(with_discard=False): return # make list of items to upload upload_list = [] for image in self.image_list.get_selected_images(): params = self.get_upload_params(image) if not params: continue convert = self.get_conversion_function(image, params) if convert == 'omit': continue upload_list.append((image, convert, params)) if not upload_list: self.upload_button.setChecked(False) return self.upload_button.set_checked(True) self.upload_config.setEnabled(False) self.user_connect.setEnabled(False) # do uploading in separate thread, so GUI can continue self.upload_worker = UploadWorker(self.session_factory, upload_list) thread = QtCore.QThread(self) self.upload_worker.moveToThread(thread) self.upload_worker.upload_error.connect( self.upload_error, Qt.BlockingQueuedConnection) self.abort_upload.connect( self.upload_worker.abort_upload, Qt.DirectConnection) self.upload_worker.upload_progress.connect(self.upload_progress) thread.started.connect(self.upload_worker.start) self.upload_worker.finished.connect(self.uploader_finished) self.upload_worker.finished.connect(thread.quit) self.upload_worker.finished.connect(self.upload_worker.deleteLater) thread.finished.connect(thread.deleteLater) thread.start() @QtCore.Slot(float, six.text_type) @catch_all def upload_progress(self, value, format_): self.total_progress.setValue(value) if format_: self.total_progress.setFormat(format_) @QtCore.Slot(six.text_type, six.text_type) @catch_all def upload_error(self, name, error): dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle(translate( 'UploaderTabsAll', 'Photini: upload error')) dialog.setText(translate( 'UploaderTabsAll', '<h3>File "{}" upload failed.</h3>').format( name)) dialog.setInformativeText(error) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Retry) dialog.setDefaultButton(QtWidgets.QMessageBox.Retry) self.abort_upload.emit(dialog.exec_() == QtWidgets.QMessageBox.Retry) @QtCore.Slot() @catch_all def uploader_finished(self): self.upload_button.set_checked(False) self.upload_config.setEnabled(True) self.user_connect.setEnabled(True) self.upload_worker = None self.enable_upload_button() @QtCore.Slot() @catch_all def log_in(self, do_auth=True): with DisableWidget(self.user_connect): with Busy(): connect = self.session.connect() if connect is None: # can't reach server return if do_auth and not connect: self.authorise() def authorise(self): with Busy(): # do full authentication procedure http_server = HTTPServer(('127.0.0.1', 0), AuthRequestHandler) redirect_uri = 'http://127.0.0.1:' + str(http_server.server_port) auth_url = self.session.get_auth_url(redirect_uri) if not auth_url: logger.error('Failed to get auth URL') http_server.server_close() return server = AuthServer() thread = QtCore.QThread(self) server.moveToThread(thread) server.server = http_server server.response.connect(self.auth_response) thread.started.connect(server.handle_requests) server.finished.connect(thread.quit) server.finished.connect(server.deleteLater) thread.finished.connect(thread.deleteLater) thread.start() if QtGui.QDesktopServices.openUrl(QtCore.QUrl(auth_url)): return logger.error('Failed to open web browser') @QtCore.Slot(dict) @catch_all def auth_response(self, result): with Busy(): self.session.get_access_token(result) @QtCore.Slot(list) @catch_all def new_selection(self, selection): self.enable_upload_button(selection=selection) def enable_upload_button(self, selection=None): if self.upload_button.is_checked(): # can always cancel upload in progress self.upload_button.setEnabled(True) return if not self.user_connect.is_checked(): # can't upload if not logged in self.upload_button.setEnabled(False) return if selection is None: selection = self.image_list.get_selected_images() self.upload_button.setEnabled(len(selection) > 0)
def __init__(self, image_list, parent=None): super(TabWidget, self).__init__(parent) app = QtWidgets.QApplication.instance() app.aboutToQuit.connect(self.shutdown) if gp and app.test_mode: self.gp_log = gp.check_result(gp.use_python_logging()) self.config_store = app.config_store self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) form = QtWidgets.QFormLayout() form.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) self.nm = NameMangler() self.file_data = {} self.file_list = [] self.source = None self.file_copier = None # source selector box = QtWidgets.QHBoxLayout() box.setContentsMargins(0, 0, 0, 0) self.source_selector = QtWidgets.QComboBox() self.source_selector.currentIndexChanged.connect(self.new_source) box.addWidget(self.source_selector) refresh_button = QtWidgets.QPushButton(self.tr('refresh')) refresh_button.clicked.connect(self.refresh) box.addWidget(refresh_button) box.setStretch(0, 1) form.addRow(self.tr('Source'), box) # path format self.path_format = QtWidgets.QLineEdit() self.path_format.setValidator(PathFormatValidator()) self.path_format.textChanged.connect(self.nm.new_format) self.path_format.editingFinished.connect(self.path_format_finished) form.addRow(self.tr('Target format'), self.path_format) # path example self.path_example = QtWidgets.QLabel() self.nm.new_example.connect(self.path_example.setText) form.addRow('=>', self.path_example) self.layout().addLayout(form, 0, 0) # file list self.file_list_widget = QtWidgets.QListWidget() self.file_list_widget.setSelectionMode( QtWidgets.QAbstractItemView.ExtendedSelection) self.file_list_widget.itemSelectionChanged.connect( self.selection_changed) self.layout().addWidget(self.file_list_widget, 1, 0) # selection buttons buttons = QtWidgets.QVBoxLayout() buttons.addStretch(1) self.selected_count = QtWidgets.QLabel() buttons.addWidget(self.selected_count) select_all = QtWidgets.QPushButton(self.tr('Select\nall')) select_all.clicked.connect(self.select_all) buttons.addWidget(select_all) select_new = QtWidgets.QPushButton(self.tr('Select\nnew')) select_new.clicked.connect(self.select_new) buttons.addWidget(select_new) # copy buttons self.move_button = StartStopButton(self.tr('Move\nphotos'), self.tr('Stop\nmove')) self.move_button.click_start.connect(self.move_selected) self.move_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.move_button) self.copy_button = StartStopButton(self.tr('Copy\nphotos'), self.tr('Stop\ncopy')) self.copy_button.click_start.connect(self.copy_selected) self.copy_button.click_stop.connect(self.stop_copy) buttons.addWidget(self.copy_button) self.layout().addLayout(buttons, 0, 1, 2, 1) self.selection_changed() # final initialisation self.image_list.sort_order_changed.connect(self.sort_file_list) if qt_version_info >= (5, 0): path = QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.PicturesLocation) else: path = QtGui.QDesktopServices.storageLocation( QtGui.QDesktopServices.PicturesLocation) self.path_format.setText(os.path.join(path, '%Y', '%Y_%m_%d', '{name}')) self.refresh() self.list_files()