diff options
Diffstat (limited to 'raphodo')
-rw-r--r-- | raphodo/__about__.py | 6 | ||||
-rw-r--r-- | raphodo/aboutdialog.py | 6 | ||||
-rw-r--r-- | raphodo/constants.py | 14 | ||||
-rw-r--r-- | raphodo/destinationdisplay.py | 5 | ||||
-rw-r--r-- | raphodo/didyouknow.py | 1 | ||||
-rw-r--r-- | raphodo/errorlog.py | 10 | ||||
-rw-r--r-- | raphodo/filebrowse.py | 30 | ||||
-rw-r--r-- | raphodo/generatename.py | 7 | ||||
-rwxr-xr-x | raphodo/nameeditor.py | 143 | ||||
-rw-r--r-- | raphodo/preferences.py | 102 | ||||
-rw-r--r-- | raphodo/proximity.py | 4 | ||||
-rwxr-xr-x | raphodo/rapid.py | 43 | ||||
-rwxr-xr-x | raphodo/renameandmovefile.py | 101 | ||||
-rwxr-xr-x | raphodo/scan.py | 5 | ||||
-rw-r--r-- | raphodo/storage.py | 63 | ||||
-rw-r--r-- | raphodo/thumbnaildisplay.py | 44 |
16 files changed, 387 insertions, 197 deletions
diff --git a/raphodo/__about__.py b/raphodo/__about__.py index d0dce5f..2657dd2 100644 --- a/raphodo/__about__.py +++ b/raphodo/__about__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2017 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2016-2018 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -29,10 +29,10 @@ __summary__ = 'Downloads, renames and backs up photos and videos from cameras, p 'memory cards and other devices' __uri__ = 'http://www.damonlynch.net/rapid' -__version__ = '0.9.6' +__version__ = '0.9.7' __author__ = 'Damon Lynch' __email__ = 'damonlynch@gmail.com' __license__ = 'GPL' -__copyright__ = 'Copyright 2007-2017 {}'.format(__author__) +__copyright__ = 'Copyright 2007-2018 {}'.format(__author__) diff --git a/raphodo/aboutdialog.py b/raphodo/aboutdialog.py index 0a80296..280228b 100644 --- a/raphodo/aboutdialog.py +++ b/raphodo/aboutdialog.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2017 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2016-2018 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -21,7 +21,7 @@ Display an About window """ __author__ = 'Damon Lynch' -__copyright__ = "Copyright 2016-2017, Damon Lynch" +__copyright__ = "Copyright 2016-2018, Damon Lynch" from gettext import gettext as _ @@ -108,7 +108,7 @@ class AboutDialog(QDialog): # Credits view credits_text = """ - Copyright © 2007-2017 Damon Lynch. + Copyright © 2007-2018 Damon Lynch. Portions copyright © 2008-2015 Canonical Ltd. Portions copyright © 2013 Bernard Baeyens. Portions copyright © 2012-2015 Jim Easterbrook. diff --git a/raphodo/constants.py b/raphodo/constants.py index dbb8aa8..f2d39d4 100644 --- a/raphodo/constants.py +++ b/raphodo/constants.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007-2017 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2007-2018 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -18,7 +18,7 @@ # see <http://www.gnu.org/licenses/>. __author__ = 'Damon Lynch' -__copyright__ = "Copyright 2007-2017, Damon Lynch" +__copyright__ = "Copyright 2007-2018, Damon Lynch" from enum import (Enum, IntEnum) from PyQt5.QtCore import Qt @@ -411,7 +411,7 @@ proximity_time_steps = [5, 10, 15, 30, 45, 60, 90, 120, 180, 240, 480, 960, 1440 class TemporalProximityState(Enum): empty = 1 - pending = 2 + pending = 2 # e.g. 2 devices scanning, only 1 scan finished generating = 3 regenerate = 4 generated = 5 @@ -430,6 +430,11 @@ class StandardFileLocations(Enum): downloads = 8 +class FileManagerType(Enum): + regular = 1 + select = 2 + + max_remembered_destinations = 10 ThumbnailBackgroundName = MediumGray @@ -489,7 +494,8 @@ class Desktop(Enum): lxde = 7 lxqt = 8 ubuntugnome = 9 - unknown = 10 + popgnome = 10 + unknown = 11 class Distro(Enum): diff --git a/raphodo/destinationdisplay.py b/raphodo/destinationdisplay.py index 87b0a99..5a59fab 100644 --- a/raphodo/destinationdisplay.py +++ b/raphodo/destinationdisplay.py @@ -444,8 +444,9 @@ class DestinationDisplay(QWidget): pref_list = self.prefs.video_subfolder generation_type = NameGenerationType.video_subfolder - prefDialog = PrefDialog(pref_defn, pref_list, generation_type, self.prefs, - self.sample_rpd_file) + prefDialog = PrefDialog( + pref_defn, pref_list, generation_type, self.prefs, self.sample_rpd_file + ) if prefDialog.exec(): user_pref_list = prefDialog.getPrefList() if not user_pref_list: diff --git a/raphodo/didyouknow.py b/raphodo/didyouknow.py index b0e6780..140c4ea 100644 --- a/raphodo/didyouknow.py +++ b/raphodo/didyouknow.py @@ -428,7 +428,6 @@ href="http://damonlynch.net/rapid/documentation/#caches">online documentation</a # Miscellaneous Preferences - class Tips: def __getitem__(self, item) -> str: if 0 > item >= len(tips): diff --git a/raphodo/errorlog.py b/raphodo/errorlog.py index 763d094..b59399b 100644 --- a/raphodo/errorlog.py +++ b/raphodo/errorlog.py @@ -50,6 +50,7 @@ from raphodo.constants import ErrorType from raphodo.rpdfile import RPDFile from raphodo.problemnotification import Problem, Problems from raphodo.viewutils import translateButtons +from raphodo.storage import open_in_file_manager # ErrorLogMessage = namedtuple('ErrorLogMessage', 'title body name uri') @@ -405,10 +406,11 @@ class ErrorReport(QDialog): index = int(fake_uri[fake_uri.find('///') + 3:]) uri = self.uris[index] - cmd = '{} {}'.format(self.rapidApp.file_manager, uri) - logging.debug("Launching: %s", cmd) - args = shlex.split(cmd) - subprocess.Popen(args) + open_in_file_manager( + file_manager=self.rapidApp.file_manager, + file_manager_type=self.rapidApp.file_manager_type, + uri=uri + ) def _saveUrls(self, text: str) -> str: """ diff --git a/raphodo/filebrowse.py b/raphodo/filebrowse.py index 4508d7c..6f17b29 100644 --- a/raphodo/filebrowse.py +++ b/raphodo/filebrowse.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2016-2017 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -21,7 +21,7 @@ Display file system folders and allow the user to select one """ __author__ = 'Damon Lynch' -__copyright__ = "Copyright 2016, Damon Lynch" +__copyright__ = "Copyright 2016-2017, Damon Lynch" import os import pathlib @@ -32,14 +32,19 @@ import subprocess from gettext import gettext as _ -from PyQt5.QtCore import (QDir, Qt, QModelIndex, QItemSelectionModel, QSortFilterProxyModel, QPoint) -from PyQt5.QtWidgets import (QTreeView, QAbstractItemView, QFileSystemModel, QSizePolicy, - QStyledItemDelegate, QStyleOptionViewItem, QMenu) +from PyQt5.QtCore import ( + QDir, Qt, QModelIndex, QItemSelectionModel, QSortFilterProxyModel, QPoint +) +from PyQt5.QtWidgets import ( + QTreeView, QAbstractItemView, QFileSystemModel, QSizePolicy, QStyledItemDelegate, + QStyleOptionViewItem, QMenu +) from PyQt5.QtGui import QIcon -from PyQt5.QtGui import (QPainter, QFont) +from PyQt5.QtGui import QPainter, QFont import raphodo.qrc_resources as qrc_resources -from raphodo.constants import (minPanelWidth, minFileSystemViewHeight, Roles) +from raphodo.constants import minPanelWidth, minFileSystemViewHeight, Roles +from raphodo.storage import gvfs_gphoto2_path class FileSystemModel(QFileSystemModel): @@ -130,7 +135,7 @@ class FileSystemView(QTreeView): """ Call only after the model has been initialized """ - for i in (1,2,3): + for i in (1, 2, 3): self.hideColumn(i) def goToPath(self, path: str, scrollTo: bool=True) -> None: @@ -205,10 +210,16 @@ class FileSystemFilter(QSortFilterProxyModel): self.invalidateFilter() def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex=None) -> bool: + index = self.sourceModel().index(sourceRow, 0, sourceParent) # type: QModelIndex + path = index.data(QFileSystemModel.FilePathRole) # type: str + + if gvfs_gphoto2_path(path): + logging.debug("Rejecting browsing path %s", path) + return False + if not self.filtered_dir_names: return True - index = self.sourceModel().index(sourceRow, 0, sourceParent) # type: QModelIndex file_name = index.data(QFileSystemModel.FileNameRole) return file_name not in self.filtered_dir_names @@ -217,6 +228,7 @@ class FileSystemDelegate(QStyledItemDelegate): """ Italicize provisional download folders that were not already created """ + def __init__(self, parent=None): super().__init__(parent) diff --git a/raphodo/generatename.py b/raphodo/generatename.py index bf28a7e..2057485 100644 --- a/raphodo/generatename.py +++ b/raphodo/generatename.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2007-2016 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2007-2017 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -20,7 +20,7 @@ ### USA __author__ = 'Damon Lynch' -__copyright__ = "Copyright 2007-2016, Damon Lynch" +__copyright__ = "Copyright 2007-2017, Damon Lynch" import re from datetime import datetime, timedelta @@ -35,7 +35,6 @@ locale.setlocale(locale.LC_ALL, '') from gettext import gettext as _ from raphodo.preferences import DownloadsTodayTracker -import raphodo.problemnotification as pn from raphodo.problemnotification import ( RenamingProblems, FilenameNotFullyGeneratedProblem, make_href, FolderNotFullyGeneratedProblemProblem, Problem @@ -746,7 +745,7 @@ class Sequences: """ def __init__(self, downloads_today_tracker: DownloadsTodayTracker, - stored_sequence_no: int): + stored_sequence_no: int) -> None: self.session_sequence_no = 0 self.sequence_letter = -1 self.downloads_today_tracker = downloads_today_tracker diff --git a/raphodo/nameeditor.py b/raphodo/nameeditor.py index 5bb570e..83b16b4 100755 --- a/raphodo/nameeditor.py +++ b/raphodo/nameeditor.py @@ -275,10 +275,12 @@ class PrefEditor(QTextEdit): cursor.insertText('<{}>'.format(pref_value)) def _setHighlighter(self) -> None: - self.highlighter = PrefHighlighter(list(self.string_to_pref_mapper.keys()), - self.pref_color, - self.document()) + self.highlighter = PrefHighlighter( + list(self.string_to_pref_mapper.keys()), self.pref_color, self.document() + ) + # when color coding of text in the editor is complete, + # generate the preference list self.highlighter.blockHighlighted.connect(self.generatePrefList) def setPrefMapper(self, pref_mapper: Dict[Tuple[str, str, str], str], @@ -289,7 +291,7 @@ class PrefEditor(QTextEdit): self.pref_color = pref_color self._setHighlighter() - def _parseTextFragment(self, text_fragment) -> List[str]: + def _parseTextFragment(self, text_fragment) -> None: if self.subfolder: text_fragments = text_fragment.split(os.sep) for index, text_fragment in enumerate(text_fragments): @@ -421,7 +423,8 @@ def make_subfolder_menu_entry(prefs: Tuple[str]) -> str: desc = prefs[0] elements = prefs[1:] return _("%(description)s - %(elements)s") % dict( - description=desc, elements=os.sep.join(elements)) + description=desc, elements=os.sep.join(elements) + ) def make_rename_menu_entry(prefs: Tuple[str]) -> str: @@ -603,20 +606,26 @@ class PresetComboBox(QComboBox): self.removeItem(index) self.preset_edited = self.new_preset = False - def setRemoveAllCustomEnabled(self, enabled: bool) -> None: + def _setRowEnabled(self, enabled: bool, offset: int) -> None: assert self.edit_mode # Our big assumption here is that the model is a QStandardItemModel model = self.model() count = self.count() if self.preset_edited: - row = count - 2 + row = count - offset - 1 else: - row = count - 1 + row = count - offset item = model.item(row, 0) # type: QStandardItem if not enabled: item.setFlags(Qt.NoItemFlags) else: - item.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + + def setRemoveAllCustomEnabled(self, enabled: bool) -> None: + self._setRowEnabled(enabled=enabled, offset=1) + + def setSaveNewCustomPresetEnabled(self, enabled: bool) -> None: + self._setRowEnabled(enabled=enabled, offset=2) def getComboBoxIndex(self, preset_index: int) -> int: """ @@ -636,11 +645,10 @@ class PresetComboBox(QComboBox): assert self.preset_separator return preset_index + 1 - def getPresetIndex(self, combobox_index: int) -> int: """ Opposite of getComboBoxIndex: calculates the preset index based on the - given combox box index (which includes separators etc.) + given combo box index (which includes separators etc.) :param combobox_index: the index into the combobox entries the user sees :return: the index into the presets (built-in & custom) """ @@ -726,9 +734,9 @@ def make_sample_rpd_file(sample_job_code: str, downloads_today_tracker = DownloadsTodayTracker( day_start=prefs.day_start, - downloads_today=prefs.downloads_today) - sequences = gn.Sequences(downloads_today_tracker, - prefs.stored_sequence_no) + downloads_today=prefs.downloads_today + ) + sequences = gn.Sequences(downloads_today_tracker, prefs.stored_sequence_no) if sample_rpd_file is not None: if sample_rpd_file.metadata is None: logging.debug('Sample file is missing its metadata') @@ -754,6 +762,7 @@ def make_sample_rpd_file(sample_job_code: str, return sample_rpd_file + class EditorCombobox(QComboBox): """ Regular combobox, but ignores the mouse wheel @@ -762,6 +771,7 @@ class EditorCombobox(QComboBox): def wheelEvent(self, event: QWheelEvent) -> None: event.ignore() + class PrefDialog(QDialog): """ Dialog window to allow editing of file renaming and subfolder generation @@ -840,7 +850,8 @@ class PrefDialog(QDialog): # <b>. These are used to format the text the users sees warning_msg = _( '<b><font color="red">Warning:</font></b> <i>There is insufficient data to fully ' - 'generate the name. Please use other renaming options.</i>') + 'generate the name. Please use other renaming options.</i>' + ) self.is_subfolder = generation_type in ( NameGenerationType.photo_subfolder, NameGenerationType.video_subfolder @@ -850,14 +861,16 @@ class PrefDialog(QDialog): # Translators: please do not modify, change the order of or leave out html formatting # tags like <i> and <b>. These are used to format the text the users sees. # In this case, the </i> really is supposed to come before the <i>. - subfolder_msg = _("The character</i> %(separator)s <i>creates a new subfolder " - "level.") % dict(separator=os.sep) + subfolder_msg = _( + "The character</i> %(separator)s <i>creates a new subfolder level." + ) % dict(separator=os.sep) # Translators: please do not modify, change the order of or leave out html formatting # tags like <i> and <b>. These are used to format the text the users sees # In this case, the </i> really is supposed to come before the <i>. - subfolder_first_char_msg = _("There is no need start or end with the folder " - "separator </i> %(separator)s<i>, because it is added " - "automatically.") % dict(separator=os.sep) + subfolder_first_char_msg = _( + "There is no need start or end with the folder separator </i> %(separator)s<i>, " + "because it is added automatically." + ) % dict(separator=os.sep) messages = (warning_msg, subfolder_msg, subfolder_first_char_msg) else: # Translators: please do not modify or leave out html formatting tags like <i> and @@ -893,7 +906,7 @@ class PrefDialog(QDialog): self.setLayout(layout) layout.addLayout(flayout) - layout.addSpacing(QFontMetrics(QFont()).height() / 2) + layout.addSpacing(int(QFontMetrics(QFont()).height() / 2)) layout.addWidget(self.editor) layout.addWidget(self.messageWidget) @@ -1136,20 +1149,27 @@ class PrefDialog(QDialog): self.updateComboBoxCurrentIndex() def updateComboBoxCurrentIndex(self) -> None: + """ + Sets the combo value to match the current preference value + """ + combobox_index, pref_list_index = self.getPresetMatch() if pref_list_index >= 0: + # the editor contains an existing preset self.preset.setCurrentIndex(combobox_index) if self.preset.preset_edited or self.preset.new_preset: self.preset.resetPresetList() + self.preset.setSaveNewCustomPresetEnabled(enabled=False) if pref_list_index >= len(self.builtin_pref_names): self.current_custom_name = self.preset.currentText() else: self.current_custom_name = None elif not (self.preset.new_preset or self.preset.preset_edited): - if self.current_custom_name is None: - self.preset.setPresetNew() - else: - self.preset.setPresetEdited(self.current_custom_name) + if self.current_custom_name is None: + self.preset.setPresetNew() + else: + self.preset.setPresetEdited(self.current_custom_name) + self.preset.setSaveNewCustomPresetEnabled(enabled=True) else: self.preset.setCurrentIndex(0) @@ -1228,6 +1248,7 @@ class PrefDialog(QDialog): self.saveNewPreset(preset_name=preset_name) if len(self.preset_names) == 1: self.preset.setRemoveAllCustomEnabled(True) + self.preset.setSaveNewCustomPresetEnabled(enabled=False) else: # User cancelled creating a new preset self.updateComboBoxCurrentIndex() @@ -1264,8 +1285,10 @@ class PrefDialog(QDialog): self.movePresetToFront(index=index) else: self._updateCombinedPrefs() - self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names, - preset_pref_lists=self.preset_pref_lists) + self.prefs.set_preset( + preset_type=self.preset_type, preset_names=self.preset_names, + preset_pref_lists=self.preset_pref_lists + ) def movePresetToFront(self, index: int) -> None: """ @@ -1287,8 +1310,10 @@ class PrefDialog(QDialog): self.preset_names.insert(0, preset_name) self.preset_pref_lists.insert(0, pref_list) self._updateCombinedPrefs() - self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names, - preset_pref_lists=self.preset_pref_lists) + self.prefs.set_preset( + preset_type=self.preset_type, preset_names=self.preset_names, + preset_pref_lists=self.preset_pref_lists + ) def saveNewPreset(self, preset_name: str) -> None: """ @@ -1306,8 +1331,10 @@ class PrefDialog(QDialog): self.preset_names.insert(0, preset_name) self.preset_pref_lists.insert(0, user_pref_list) self._updateCombinedPrefs() - self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names, - preset_pref_lists=self.preset_pref_lists) + self.prefs.set_preset( + preset_type=self.preset_type, preset_names=self.preset_names, + preset_pref_lists=self.preset_pref_lists + ) def clearCustomPresets(self) -> None: """ @@ -1321,8 +1348,10 @@ class PrefDialog(QDialog): self.preset_pref_lists = [] self.current_custom_name = None self._updateCombinedPrefs() - self.prefs.set_preset(preset_type=self.preset_type, preset_names=self.preset_names, - preset_pref_lists=self.preset_pref_lists) + self.prefs.set_preset( + preset_type=self.preset_type, preset_names=self.preset_names, + preset_pref_lists=self.preset_pref_lists + ) def udpateCachedPrefLists(self) -> None: self.preset_names, self.preset_pref_lists = self.prefs.get_preset( @@ -1339,8 +1368,9 @@ class PrefDialog(QDialog): if the current user pref list matches an entry in it. Else Tuple of (-1, -1). """ - index = match_pref_list(pref_lists=self.combined_pref_lists, - user_pref_list=self.editor.user_pref_list) + index = match_pref_list( + pref_lists=self.combined_pref_lists, user_pref_list=self.editor.user_pref_list + ) if index >= 0: combobox_name = self.combined_pref_names[index] return self.preset.findText(combobox_name), index @@ -1362,22 +1392,27 @@ class PrefDialog(QDialog): msgBox.setIcon(QMessageBox.Question) msgBox.setWindowTitle(title) if self.preset.new_preset: - message = _("<b>Do you want to save the changes in a new custom preset?</b><br><br>" - "Creating a custom preset is not required, but can help you keep " - "organized.<br><br>" - "The changes to the preferences will still be applied regardless of " - "whether you create a new custom preset or not.") + message = _( + "<b>Do you want to save the changes in a new custom preset?</b><br><br>" + "Creating a custom preset is not required, but can help you keep " + "organized.<br><br>" + "The changes to the preferences will still be applied regardless of " + "whether you create a new custom preset or not." + ) msgBox.setStandardButtons(QMessageBox.Yes|QMessageBox.No) updateButton = newButton = None else: assert self.preset.preset_edited - message = _("<b>Do you want to save the changes in a custom preset?</b><br><br>" - "If you like, you can create a new custom preset or update the " - "existing custom preset.<br><br>" - "The changes to the preferences will still be applied regardless of " - "whether you save a custom preset or not.") - updateButton = msgBox.addButton(_('Update Custom Preset "%s"') % - self.current_custom_name, QMessageBox.YesRole) + message = _( + "<b>Do you want to save the changes in a custom preset?</b><br><br>" + "If you like, you can create a new custom preset or update the " + "existing custom preset.<br><br>" + "The changes to the preferences will still be applied regardless of " + "whether you save a custom preset or not." + ) + updateButton = msgBox.addButton( + _('Update Custom Preset "%s"') % self.current_custom_name, QMessageBox.YesRole + ) newButton = msgBox.addButton(_('Save New Custom Preset'), QMessageBox.YesRole) msgBox.addButton(QMessageBox.No) @@ -1418,12 +1453,18 @@ if __name__ == '__main__': prefs = Preferences() - prefDialog = PrefDialog(DICT_IMAGE_RENAME_L0, PHOTO_RENAME_MENU_DEFAULTS_CONV[1], - NameGenerationType.photo_name, prefs) + # prefDialog = PrefDialog(DICT_IMAGE_RENAME_L0, PHOTO_RENAME_MENU_DEFAULTS_CONV[1], + # NameGenerationType.photo_name, prefs) # prefDialog = PrefDialog(DICT_VIDEO_RENAME_L0, VIDEO_RENAME_MENU_DEFAULTS_CONV[1], # NameGenerationType.video_name, prefs) - # prefDialog = PrefDialog(DICT_SUBFOLDER_L0, PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[2], - # NameGenerationType.photo_subfolder, prefs) + prefDialog = PrefDialog( + DICT_SUBFOLDER_L0, PHOTO_SUBFOLDER_MENU_DEFAULTS_CONV[2], + NameGenerationType.photo_subfolder, prefs + ) + # prefDialog = PrefDialog( + # DICT_VIDEO_SUBFOLDER_L0, VIDEO_SUBFOLDER_MENU_DEFAULTS_CONV[2], + # NameGenerationType.video_subfolder, prefs + # ) prefDialog.show() app.exec_() diff --git a/raphodo/preferences.py b/raphodo/preferences.py index 832b7ac..001c147 100644 --- a/raphodo/preferences.py +++ b/raphodo/preferences.py @@ -32,11 +32,13 @@ from PyQt5.QtCore import QSettings, QTime, Qt from gettext import gettext as _ -from raphodo.storage import (xdg_photos_directory, xdg_videos_directory, xdg_photos_identifier, - xdg_videos_identifier) +from raphodo.storage import ( + xdg_photos_directory, xdg_videos_directory, xdg_photos_identifier, xdg_videos_identifier +) from raphodo.generatenameconfig import * import raphodo.constants as constants -from raphodo.utilities import available_cpu_count +from raphodo.constants import PresetPrefType +from raphodo.utilities import available_cpu_count, make_internationalized_list import raphodo.__about__ from raphodo.rpdfile import ALL_KNOWN_EXTENSIONS @@ -444,7 +446,7 @@ class Preferences: def restore(self, key: str) -> None: self[key] = self.defaults[key] - def get_preset(self, preset_type: constants.PresetPrefType) -> Tuple[List[str], + def get_preset(self, preset_type: PresetPrefType) -> Tuple[List[str], List[List[str]]]: """ Returns the custom presets for the particular type. @@ -473,7 +475,7 @@ class Preferences: return preset_names, preset_pref_lists - def set_preset(self, preset_type: constants.PresetPrefType, + def set_preset(self, preset_type: PresetPrefType, preset_names: List[str], preset_pref_lists: List[str]) -> None: """ @@ -491,15 +493,17 @@ class Preferences: preset = preset_type.name - if not preset_names: - self.settings.remove(preset) - else: - self.settings.beginWriteArray(preset) - for i in range(len(preset_names)): - self.settings.setArrayIndex(i) - self.settings.setValue('name', preset_names[i]) - self.settings.setValue('pref_list', preset_pref_lists[i]) - self.settings.endArray() + # Clear all the existing presets with that name. + # If we don't do this, when the array shrinks, old values can hang around, + # even though the array size is set correctly. + self.settings.remove(preset) + + self.settings.beginWriteArray(preset) + for i in range(len(preset_names)): + self.settings.setArrayIndex(i) + self.settings.setValue('name', preset_names[i]) + self.settings.setValue('pref_list', preset_pref_lists[i]) + self.settings.endArray() self.settings.endGroup() @@ -601,10 +605,12 @@ class Preferences: msg = '' valid = True - tests = ((self.photo_rename, DICT_IMAGE_RENAME_L0), - (self.video_rename, DICT_VIDEO_RENAME_L0), - (self.photo_subfolder, DICT_SUBFOLDER_L0), - (self.video_subfolder, DICT_VIDEO_SUBFOLDER_L0)) + tests = ( + (self.photo_rename, DICT_IMAGE_RENAME_L0), + (self.video_rename, DICT_VIDEO_RENAME_L0), + (self.photo_subfolder, DICT_SUBFOLDER_L0), + (self.video_subfolder, DICT_VIDEO_SUBFOLDER_L0) + ) # test file renaming for pref, pref_defn in tests[:2]: @@ -622,23 +628,63 @@ class Preferences: L1s = [pref[i] for i in range(0, len(pref), 3)] if L1s[0] == SEPARATOR: - raise PrefValueKeyComboError(_( - "Subfolder preferences should not start with a %s") % os.sep) + raise PrefValueKeyComboError( + _("Subfolder preferences should not start with a %s") % os.sep + ) elif L1s[-1] == SEPARATOR: - raise PrefValueKeyComboError(_( - "Subfolder preferences should not end with a %s") % os.sep) + raise PrefValueKeyComboError( + _("Subfolder preferences should not end with a %s") % os.sep + ) else: for i in range(len(L1s) - 1): if L1s[i] == SEPARATOR and L1s[i + 1] == SEPARATOR: - raise PrefValueKeyComboError(_( - "Subfolder preferences should not contain " - "two %s one after the other") % os.sep) + raise PrefValueKeyComboError( + _( + "Subfolder preferences should not contain two %s one after " + "the other" + ) % os.sep + ) except PrefError as e: valid = False msg += e.msg + "\n" - return (valid, msg) + return valid, msg + + def _filter_duplicate_generation_prefs(self, preset_type: PresetPrefType) -> None: + preset_names, preset_pref_lists = self.get_preset(preset_type=preset_type) + seen = set() + filtered_names = [] + filtered_pref_lists = [] + duplicates = [] + for name, pref_list in zip(preset_names, preset_pref_lists): + value = tuple(pref_list) + if value in seen: + duplicates.append(name) + else: + seen.add(value) + filtered_names.append(name) + filtered_pref_lists.append(pref_list) + + if duplicates: + human_readable = preset_type.name[len('preset_'):].replace('_', ' ') + logging.warning( + 'Removed %s duplicate(s) from %s presets: %s', + len(duplicates), human_readable, make_internationalized_list(duplicates) + ) + self.set_preset( + preset_type=preset_type, preset_names=filtered_names, + preset_pref_lists=filtered_pref_lists + ) + + def filter_duplicate_generation_prefs(self) -> None: + """ + Remove any duplicate subfolder generation or file renaming custom presets + """ + + logging.info("Checking for duplicate name generation preference values") + for preset_type in PresetPrefType: + self._filter_duplicate_generation_prefs(preset_type) def must_synchronize_raw_jpg(self) -> bool: """ @@ -676,7 +722,7 @@ class Preferences: :return: a tuple of the photo & video rename and subfolder generation preferences """ - return (self.photo_rename, self.photo_subfolder, self.video_rename, self.video_subfolder) + return self.photo_rename, self.photo_subfolder, self.video_rename, self.video_subfolder def get_day_start_qtime(self) -> QTime: """ @@ -981,4 +1027,4 @@ def match_pref_list(pref_lists: List[List[str]], user_pref_list: List[str]) -> i try: return pref_lists.index(user_pref_list) except ValueError: - return -1
\ No newline at end of file + return -1 diff --git a/raphodo/proximity.py b/raphodo/proximity.py index 6bedf0c..b3875d3 100644 --- a/raphodo/proximity.py +++ b/raphodo/proximity.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2017 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2015-2018 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -17,7 +17,7 @@ # see <http://www.gnu.org/licenses/>. __author__ = 'Damon Lynch' -__copyright__ = "Copyright 2015-2017, Damon Lynch" +__copyright__ = "Copyright 2015-2018, Damon Lynch" from collections import (namedtuple, defaultdict, deque, Counter) from operator import attrgetter diff --git a/raphodo/rapid.py b/raphodo/rapid.py index 6c1254a..683520b 100755 --- a/raphodo/rapid.py +++ b/raphodo/rapid.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2011-2017 Damon Lynch <damonlynch@gmail.com> +# Copyright (C) 2011-2018 Damon Lynch <damonlynch@gmail.com> # This file is part of Rapid Photo Downloader. # @@ -29,7 +29,7 @@ Project line length: 100 characters (i.e. word wrap at 99) """ __author__ = 'Damon Lynch' -__copyright__ = "Copyright 2011-2017, Damon Lynch" +__copyright__ = "Copyright 2011-2018, Damon Lynch" import sys import logging @@ -94,7 +94,7 @@ from raphodo.storage import ( has_one_or_more_folders, mountPaths, get_desktop_environment, get_desktop, gvfs_controls_mounts, get_default_file_manager, validate_download_folder, validate_source_folder, get_fdo_cache_thumb_base_directory, WatchDownloadDirs, get_media_dir, - StorageSpace + StorageSpace, gvfs_gphoto2_path ) from raphodo.interprocess import ( ScanArguments, CopyFilesArguments, RenameAndMoveFileData, BackupArguments, @@ -640,6 +640,9 @@ class RapidWindow(QMainWindow): "Version downgrade detected, from %s to %s", __about__.__version__, previous_version ) + if pv < pkg_resources.parse_version('0.9.7b1'): + # Remove any duplicate subfolder generation or file renaming custom presets + self.prefs.filter_duplicate_generation_prefs() def startThreadControlSockets(self) -> None: """ @@ -809,7 +812,7 @@ class RapidWindow(QMainWindow): self.updateThumbnailModelAfterProximityChange ) - self.file_manager = get_default_file_manager() + self.file_manager, self.file_manager_type = get_default_file_manager() if self.file_manager: logging.info("Default file manager: %s", self.file_manager) else: @@ -2574,7 +2577,7 @@ class RapidWindow(QMainWindow): """ Respond to This Computer Toggle Switch - :param on: whether swich is on or off + :param on: whether switch is on or off """ if on: @@ -2594,14 +2597,15 @@ class RapidWindow(QMainWindow): """ Respond to Devices Toggle Switch - :param on: whether swich is on or off + :param on: whether switch is on or off """ self.prefs.device_autodetection = on if not on: for scan_id in list(self.devices.volumes_and_cameras): self.removeDevice(scan_id=scan_id, adjust_temporal_proximity=False) - if len(self.devices) == 0: + state = self.proximityStatePostDeviceRemoval() + if state == TemporalProximityState.empty: self.temporalProximity.setState(TemporalProximityState.empty) else: self.generateTemporalProximityTableData("devices were removed as a download source") @@ -2611,6 +2615,19 @@ class RapidWindow(QMainWindow): QTimer.singleShot(100, self.devicesViewToggledOn) self.adjustLeftPanelSliderHandles() + def proximityStatePostDeviceRemoval(self) -> TemporalProximityState: + """ + :return: set correct proximity state after a device is removed + """ + + # ignore devices that are scanning - we don't care about them, because the scan + # could take a long time, especially with phones + if len(self.devices) - len(self.devices.scanning) > 0: + # Other already scanned devices are present + return TemporalProximityState.regenerate + else: + return TemporalProximityState.empty + @pyqtSlot() def devicesViewToggledOn(self) -> None: self.searchForCameras() @@ -4446,13 +4463,6 @@ Do you want to proceed with the download? Initiate Timeline generation if it's right to do so """ - if len(self.devices.scanning): - logging.info( - "Was tasked to generate Timeline because %s, but ignoring request " - "because a scan is occurring", reason - ) - return - if self.temporalProximity.state == TemporalProximityState.ctime_rebuild: logging.info( "Was tasked to generate Timeline because %s, but ignoring request " @@ -5106,7 +5116,8 @@ Do you want to proceed with the download? self.setDownloadCapabilities() if adjust_temporal_proximity: - if len(self.devices) == 0: + state = self.proximityStatePostDeviceRemoval() + if state == TemporalProximityState.empty: self.temporalProximity.setState(TemporalProximityState.empty) elif files_removed: self.generateTemporalProximityTableData("a download source was removed") @@ -6098,7 +6109,7 @@ def main(): sys.exit(1) media_dir = get_media_dir() - auto_detect = args.path.startswith(media_dir) + auto_detect = args.path.startswith(media_dir) or gvfs_gphoto2_path(args.path) if auto_detect: this_computer_source = False this_computer_location = None diff --git a/raphodo/renameandmovefile.py b/raphodo/renameandmovefile.py index 1405cc5..ae2c5e5 100755 --- a/raphodo/renameandmovefile.py +++ b/raphodo/renameandmovefile.py @@ -44,7 +44,6 @@ from gettext import gettext as _ import raphodo.exiftool as exiftool import raphodo.generatename as gn -import raphodo.problemnotification as pn from raphodo.preferences import DownloadsTodayTracker, Preferences from raphodo.constants import (ConflictResolution, FileType, DownloadStatus, RenameAndMoveStatus) from raphodo.interprocess import (RenameAndMoveFileData, RenameAndMoveFileResults, DaemonProcess) @@ -153,10 +152,9 @@ def load_metadata(rpd_file: Union[Photo, Video], """ Loads the metadata for the file. + :param rpd_file: photo or video :param et_process: the daemon ExifTool process - :param temp_file: If true, the the metadata from the temporary file - rather than the original source file is used. This is important, - because the metadata can be modified by the filemodify process + :param problems: problems encountered renaming the file :return True if operation succeeded, false otherwise """ if rpd_file.metadata is None: @@ -205,6 +203,7 @@ def generate_subfolder(rpd_file: Union[Photo, Video], :param rpd_file: file to work on :param et_process: the daemon ExifTool process + :param problems: problems encountered renaming the file """ if rpd_file.file_type == FileType.photo: @@ -223,6 +222,7 @@ def generate_name(rpd_file: Union[Photo, Video], :param rpd_file: file to work on :param et_process: the daemon ExifTool process + :param problems: problems encountered renaming the file """ if rpd_file.file_type == FileType.photo: @@ -268,9 +268,11 @@ class RenameMoveFileWorker(DaemonProcess): dt = datetime.fromtimestamp(modification_time) date = dt.strftime("%x") time = dt.strftime("%X") - except: - logging.error("Could not determine the file modification time of %s", - rpd_file.download_full_file_name) + except Exception: + logging.error( + "Could not determine the file modification time of %s", + rpd_file.download_full_file_name + ) date = time = '' source = rpd_file.get_souce_href() @@ -312,7 +314,7 @@ class RenameMoveFileWorker(DaemonProcess): """ Handle cases where file failed to download """ - uri=get_uri( + uri = get_uri( full_file_name=rpd_file.full_file_name, camera_details=rpd_file.camera_details ) device = make_href(name=rpd_file.device_display_name, uri=rpd_file.device_uri) @@ -365,7 +367,7 @@ class RenameMoveFileWorker(DaemonProcess): assert isinstance(i1_date_time, datetime) i1_date = i1_date_time.strftime("%x") i1_time = i1_date_time.strftime("%X") - assert isinstance(image2_date_time,datetime) + assert isinstance(image2_date_time, datetime) image2_date = image2_date_time.strftime("%x") image2_time = image2_date_time.strftime("%X") @@ -383,7 +385,7 @@ class RenameMoveFileWorker(DaemonProcess): def _move_associate_file(self, extension: str, full_base_name: str, - temp_associate_file: str) -> str: + temp_associate_file: str) -> str: """ Move (rename) the associate file using the pre-generated name. @@ -628,7 +630,7 @@ class RenameMoveFileWorker(DaemonProcess): DuplicateFileWhenSyncingProblem( name=rpd_file.name, uri=rpd_file.get_uri(), - file_type = rpd_file.title, + file_type=rpd_file.title, ) ) @@ -678,12 +680,15 @@ class RenameMoveFileWorker(DaemonProcess): generate_name(rpd_file, self.exiftool_process, self.problems) if rpd_file.name_generation_problem: - logging.warning("Encountered a problem generating file name for file %s", - rpd_file.name) + logging.warning( + "Encountered a problem generating file name for file %s", + rpd_file.name + ) rpd_file.status = DownloadStatus.downloaded_with_warning else: - logging.debug("Generated file name %s for file %s", rpd_file.download_name, - rpd_file.name) + logging.debug( + "Generated file name %s for file %s", rpd_file.download_name, rpd_file.name + ) else: logging.error("Failed to generate subfolder name for file: %s", rpd_file.name) @@ -700,8 +705,9 @@ class RenameMoveFileWorker(DaemonProcess): move_succeeded = False rpd_file.download_path = os.path.join(rpd_file.download_folder, rpd_file.download_subfolder) - rpd_file.download_full_file_name = os.path.join(rpd_file.download_path, - rpd_file.download_name) + rpd_file.download_full_file_name = os.path.join( + rpd_file.download_path, rpd_file.download_name + ) rpd_file.download_full_base_name = os.path.splitext(rpd_file.download_full_file_name)[0] if not os.path.isdir(rpd_file.download_path): @@ -727,8 +733,10 @@ class RenameMoveFileWorker(DaemonProcess): try: if os.path.exists(rpd_file.download_full_file_name): raise OSError(errno.EEXIST, "File exists: %s" % rpd_file.download_full_file_name) - logging.debug("Renaming %s to %s .....", - rpd_file.temp_full_file_name, rpd_file.download_full_file_name) + logging.debug( + "Renaming %s to %s .....", + rpd_file.temp_full_file_name, rpd_file.download_full_file_name + ) os.rename(rpd_file.temp_full_file_name, rpd_file.download_full_file_name) logging.debug("....successfully renamed file") move_succeeded = True @@ -753,8 +761,9 @@ class RenameMoveFileWorker(DaemonProcess): self.prepare_rpd_file(rpd_file) - synchronize_raw_jpg = (self.prefs.must_synchronize_raw_jpg() and - rpd_file.file_type == FileType.photo) + synchronize_raw_jpg = ( + self.prefs.must_synchronize_raw_jpg() and rpd_file.file_type == FileType.photo + ) if synchronize_raw_jpg: sync_result = self.sync_raw_jpg(rpd_file) @@ -780,8 +789,8 @@ class RenameMoveFileWorker(DaemonProcess): date_time=rpd_file.date_time(), sequence_number_used=sequence) - if not synchronize_raw_jpg or (synchronize_raw_jpg and - sync_result.sequence_to_use is None): + if not synchronize_raw_jpg or ( + synchronize_raw_jpg and sync_result.sequence_to_use is None): uses_sequence_session_no = self.prefs.any_pref_uses_session_sequence_no() uses_sequence_letter = self.prefs.any_pref_uses_sequence_letter_value() if uses_sequence_session_no or uses_sequence_letter: @@ -809,6 +818,23 @@ class RenameMoveFileWorker(DaemonProcess): return move_succeeded + def initialise_downloads_today_stored_number(self) -> None: + """ + Initialize (or reinitialize) Downloads Today and Stored No + sequence values from the program preferences. + """ + + # Synchronize QSettings instance in preferences class + self.prefs.sync() + + # Track downloads today, using a class whose purpose is to + # take the value in the user prefs, increment, and then + # finally used to update the prefs + self.downloads_today_tracker = DownloadsTodayTracker( + day_start=self.prefs.day_start, + downloads_today=self.prefs.downloads_today + ) + def run(self) -> None: """ Generate subfolder and filename, and attempt to move the file @@ -826,6 +852,12 @@ class RenameMoveFileWorker(DaemonProcess): # suffixes to duplicate files self.duplicate_files = {} + self.initialise_downloads_today_stored_number() + + self.sequences = gn.Sequences( + self.downloads_today_tracker, self.prefs.stored_sequence_no + ) + with stdchannel_redirected(sys.stderr, os.devnull): with exiftool.ExifTool() as self.exiftool_process: while True: @@ -839,18 +871,13 @@ class RenameMoveFileWorker(DaemonProcess): data = pickle.loads(content) # type: RenameAndMoveFileData if data.message == RenameAndMoveStatus.download_started: - # Synchronize QSettings instance in preferences class - self.prefs.sync() - - # Track downloads today, using a class whose purpose is to - # take the value in the user prefs, increment, and then - # finally used to update the prefs - self.downloads_today_tracker = DownloadsTodayTracker( - day_start=self.prefs.day_start, - downloads_today=self.prefs.downloads_today) - - self.sequences = gn.Sequences(self.downloads_today_tracker, - self.prefs.stored_sequence_no) + + # reinitialize downloads today and stored sequence number + # in case the user has updated them via the user interface + self.initialise_downloads_today_stored_number() + self.sequences.downloads_today_tracker = self.downloads_today_tracker + self.sequences.stored_sequence_no = self.prefs.stored_sequence_no + dl_today = self.downloads_today_tracker.get_or_reset_downloads_today() logging.debug("Completed downloads today: %s", dl_today) @@ -859,9 +886,7 @@ class RenameMoveFileWorker(DaemonProcess): elif data.message == RenameAndMoveStatus.download_completed: if len(self.problems): self.content = pickle.dumps( - RenameAndMoveFileResults( - problems=self.problems - ), + RenameAndMoveFileResults(problems=self.problems), pickle.HIGHEST_PROTOCOL ) self.send_message_to_sink() diff --git a/raphodo/scan.py b/raphodo/scan.py index abad59e..7b98941 100755 --- a/raphodo/scan.py +++ b/raphodo/scan.py @@ -90,7 +90,7 @@ from raphodo.problemnotification import ( CameraFileReadProblem, FileMetadataLoadProblem, FileWriteProblem, FsMetadataReadProblem, FileZeroLengthProblem ) -from raphodo.storage import get_uri, CameraDetails +from raphodo.storage import get_uri, CameraDetails, gvfs_gphoto2_path FileInfo = namedtuple('FileInfo', 'path modification_time size ext_lower base_name file_type') CameraFile = namedtuple('CameraFile', 'name size') @@ -393,6 +393,9 @@ class ScanWorker(WorkerInPublishPullPipeline): for dir_name, dir_list, file_list in walk(path_to_walk): if len(dir_list) > 0: + # Do not scan gvfs gphoto2 mount + dir_list[:] = (d for d in dir_list if not gvfs_gphoto2_path(dir_name + d)) + if self.scan_preferences.ignored_paths: # Don't inspect paths the user wants ignored # Altering subdirs in place controls the looping diff --git a/raphodo/storage.py b/raphodo/storage.py index 6856c38..f4ef551 100644 --- a/raphodo/storage.py +++ b/raphodo/storage.py @@ -75,7 +75,7 @@ from gi.repository import GUdev, UDisks, GLib from gettext import gettext as _ -from raphodo.constants import Desktop, Distro +from raphodo.constants import Desktop, Distro, FileManagerType from raphodo.utilities import ( process_running, log_os_release, remove_topmost_directory_from_path, find_mount_point ) @@ -170,6 +170,24 @@ def get_media_dir() -> str: raise ("Mounts.setValidMountPoints() not implemented on %s", sys.platform()) +_gvfs_gphoto2 = re.compile('gvfs.*gphoto2.*host') + + +def gvfs_gphoto2_path(path: str) -> bool: + """ + :return: True if the path appears to be a GVFS gphoto2 path + + >>> p = "/run/user/1000/gvfs/gphoto2:host=%5Busb%3A002%2C013%5D" + >>> gvfs_gphoto2_path(p) + True + >>> p = '/home/damon' + >>> gvfs_gphoto2_path(p) + False + """ + + return _gvfs_gphoto2.search(path) is not None + + class ValidMounts(): r""" Operations to find 'valid' mount points, i.e. the places in which @@ -359,6 +377,8 @@ def get_desktop() -> Desktop: env = 'cinnamon' elif env == 'ubuntu:gnome': env = 'ubuntugnome' + elif env == 'pop:gnome': + env = 'popgnome' try: return Desktop[env] except KeyError: @@ -546,7 +566,8 @@ def get_fdo_cache_thumb_base_directory() -> str: return os.path.join(BaseDirectory.xdg_cache_home, 'thumbnails') -def get_default_file_manager(remove_args: bool = True) -> Optional[str]: +def get_default_file_manager(remove_args: bool = True) -> Tuple[ + Optional[str], Optional[FileManagerType]]: """ Attempt to determine the default file manager for the system :param remove_args: if True, remove any arguments such as %U from @@ -558,7 +579,7 @@ def get_default_file_manager(remove_args: bool = True) -> Optional[str]: try: desktop_file = subprocess.check_output(cmd, universal_newlines=True) # type: str except: - return None + return None, None # Remove new line character from output desktop_file = desktop_file[:-1] if desktop_file.endswith(';'): @@ -570,21 +591,40 @@ def get_default_file_manager(remove_args: bool = True) -> Optional[str]: try: desktop_entry = DesktopEntry(path) except xdg.Exceptions.ParsingError: - return None + return None, None try: desktop_entry.parse(path) except: - return None + return None, None fm = desktop_entry.getExec() + if fm.startswith('dolphin'): + file_manager_type = FileManagerType.select + else: + file_manager_type = FileManagerType.regular if remove_args: - return fm.split()[0] + return fm.split()[0], file_manager_type else: - return fm + return fm, file_manager_type # Special case: LXQt if get_desktop() == Desktop.lxqt: if shutil.which('pcmanfm-qt'): - return 'pcmanfm-qt' + return 'pcmanfm-qt', FileManagerType.regular + + return None, None + +def open_in_file_manager(file_manager: str, + file_manager_type: FileManagerType, + uri: str) -> None: + if file_manager_type == FileManagerType.regular: + arg = '' + else: + arg = '--select ' + + cmd = '{} {}"{}"'.format(file_manager, arg, uri) + logging.debug("Launching: %s", cmd) + args = shlex.split(cmd) + subprocess.Popen(args) _desktop = get_desktop() @@ -612,7 +652,7 @@ def get_uri(full_file_name: Optional[str]=None, prefix = 'file://' if desktop_environment: desktop = get_desktop() - if full_file_name and desktop in (Desktop.mate, Desktop.kde): + if full_file_name and desktop == Desktop.mate: full_file_name = os.path.dirname(full_file_name) else: if not desktop_environment: @@ -637,11 +677,6 @@ def get_uri(full_file_name: Optional[str]=None, prefix = 'mtp:/' + pathname2url( '{}/{}'.format(camera_details.display_name, camera_details.storage_desc) ) - # Dolphin doesn't highlight the file if it's passed. - # Instead it tries to open it, but fails. - # So don't pass the file, just the directory it's in. - if full_file_name: - full_file_name = os.path.dirname(full_file_name) else: logging.error("Don't know how to generate MTP prefix for %s", _desktop.name) else: diff --git a/raphodo/thumbnaildisplay.py b/raphodo/thumbnaildisplay.py index cde71f8..e7a3f9b 100644 --- a/raphodo/thumbnaildisplay.py +++ b/raphodo/thumbnaildisplay.py @@ -59,7 +59,9 @@ from raphodo.constants import ( Desktop, DeviceState, extensionColor, FadeSteps, FadeMilliseconds, PaleGray, DarkGray, DoubleDarkGray, Plural, manually_marked_previously_downloaded, thumbnail_margin ) -from raphodo.storage import get_program_cache_directory, get_desktop, validate_download_folder +from raphodo.storage import ( + get_program_cache_directory, get_desktop, validate_download_folder, open_in_file_manager +) from raphodo.utilities import ( CacheDirs, make_internationalized_list, format_size_for_user, runs, arrow_locale ) @@ -1715,9 +1717,13 @@ class ThumbnailView(QListView): return else: uid = uids[0] - row = model.uid_to_row[uid] - index = model.index(row, 0) - self.scrollTo(index, QAbstractItemView.PositionAtTop) + try: + row = model.uid_to_row[uid] + except KeyError: + logging.debug("Ignoring scroll request to unknown thumbnail") + else: + index = model.index(row, 0) + self.scrollTo(index, QAbstractItemView.PositionAtTop) class ThumbnailDelegate(QStyledItemDelegate): @@ -1766,7 +1772,7 @@ class ThumbnailDelegate(QStyledItemDelegate): self.contextMenu = QMenu() self.openInFileBrowserAct = self.contextMenu.addAction(_('Open in File Browser...')) - self.openInFileBrowserAct.triggered.connect(self.doOpenInFileBrowserAct) + self.openInFileBrowserAct.triggered.connect(self.doOpenInFileManagerAct) self.copyPathAct = self.contextMenu.addAction(_('Copy Path')) self.copyPathAct.triggered.connect(self.doCopyPathAction) # Translators: 'File' here applies to a single file. The command allows users to instruct @@ -1844,14 +1850,15 @@ class ThumbnailDelegate(QStyledItemDelegate): QApplication.clipboard().setText(path) @pyqtSlot() - def doOpenInFileBrowserAct(self) -> None: + def doOpenInFileManagerAct(self) -> None: index = self.clickedIndex if index: uri = index.model().data(index, Roles.uri) - cmd = '{} "{}"'.format(self.rapidApp.file_manager, uri) - logging.debug("Launching: %s", cmd) - args = shlex.split(cmd) - subprocess.Popen(args) + open_in_file_manager( + file_manager=self.rapidApp.file_manager, + file_manager_type=self.rapidApp.file_manager_type, + uri=uri + ) @pyqtSlot() def doMarkFileDownloadedAct(self) -> None: @@ -2072,12 +2079,15 @@ class ThumbnailDelegate(QStyledItemDelegate): def oneOrMoreNotDownloaded(self) -> Tuple[int, Plural]: i = 0 selectedIndexes = self.selectedIndexes() - noSelected = len(selectedIndexes) - for index in selectedIndexes: - if not index.data(Roles.previously_downloaded): - i += 1 - if i == 2: - break + if selectedIndexes is None: + noSelected = 0 + else: + noSelected = len(selectedIndexes) + for index in selectedIndexes: + if not index.data(Roles.previously_downloaded): + i += 1 + if i == 2: + break if i == 0: return noSelected, Plural.zero @@ -2208,7 +2218,7 @@ class ThumbnailDelegate(QStyledItemDelegate): else: logging.debug("Not applying job code because no files selected") - def selectedIndexes(self): + def selectedIndexes(self) -> Optional[List[QModelIndex]]: selection = self.rapidApp.thumbnailView.selectionModel() # type: QItemSelectionModel if selection.hasSelection(): selected = selection.selection() # type: QItemSelection |