# Copyright (C) 2017 Damon Lynch # This file is part of Rapid Photo Downloader. # # Rapid Photo Downloader is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Rapid Photo Downloader is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Rapid Photo Downloader. If not, # see . """ Display backup preferences """ __author__ = 'Damon Lynch' __copyright__ = "Copyright 2017, Damon Lynch" from typing import Optional, Dict, Tuple, Union, Set, List, DefaultDict import logging import os from collections import namedtuple, defaultdict from gettext import gettext as _ from PyQt5.QtCore import (Qt, pyqtSlot, QAbstractListModel, QModelIndex, QSize) from PyQt5.QtWidgets import ( QWidget, QSizePolicy, QVBoxLayout, QLabel, QLineEdit, QCheckBox, QScrollArea, QFrame, QStyledItemDelegate, QStyleOptionViewItem, QStyle, QGroupBox, QHBoxLayout, QGridLayout ) from PyQt5.QtGui import (QPainter, QFontMetrics, QFont, QColor, QPalette, QIcon) from raphodo.constants import ( StandardFileLocations, ThumbnailBackgroundName, FileType, Roles, ViewRowType, BackupLocationType ) from raphodo.viewutils import (QFramedWidget, RowTracker) from raphodo.rpdfile import FileTypeCounter, Photo, Video from raphodo.panelview import QPanelView from raphodo.preferences import Preferences from raphodo.foldercombo import FolderCombo import raphodo.qrc_resources as qrc_resources from raphodo.storage import (ValidMounts, get_media_dir, StorageSpace, get_path_display_name) from raphodo.devices import (BackupDeviceCollection, BackupVolumeDetails) from raphodo.devicedisplay import (DeviceDisplay, BodyDetails, icon_size, DeviceView) from raphodo.destinationdisplay import make_body_details, adjusted_download_size from raphodo.storage import get_mount_size BackupVolumeUse = namedtuple('BackupVolumeUse', 'bytes_total bytes_free backup_type marked ' 'photos_size_to_download videos_size_to_download') BackupViewRow = namedtuple('BackupViewRow', 'mount display_name backup_type os_stat_device') class BackupDeviceModel(QAbstractListModel): """ Stores 'devices' used for backing up photos and videos. Want to display: (1) destination on local files systems (2) external devices, e.g. external hard drives Need to account for when download destination is same file system as backup destination. """ def __init__(self, parent) -> None: super().__init__(parent) self.raidApp = parent.rapidApp self.prefs = parent.prefs size = icon_size() self.removableIcon = QIcon(':icons/drive-removable-media.svg').pixmap(size) self.folderIcon = QIcon(':/icons/folder.svg').pixmap(size) self._initValues() def _initValues(self): self.rows = RowTracker() # type: RowTracker self.row_id_counter = 0 # type: int # {row_id} self.headers = set() # type: Set[int] # path: BackupViewRow self.backup_devices = dict() # type: Dict[str, BackupViewRow] self.path_to_row_ids = defaultdict(list) # type: Dict[str, List[int]] self.row_id_to_path = dict() # type: Dict[int, str] self.marked = FileTypeCounter() self.photos_size_to_download = self.videos_size_to_download = 0 # os_stat_device: Set[FileType] self._downloading_to = defaultdict(list) # type: DefaultDict[int, Set[FileType]] @property def downloading_to(self): return self._downloading_to @downloading_to.setter def downloading_to(self, downloading_to: DefaultDict[int, Set[FileType]]): self._downloading_to = downloading_to self.downloadSizeChanged() def reset(self) -> None: self.beginResetModel() self._initValues() self.endResetModel() def columnCount(self, parent=QModelIndex()): return 1 def rowCount(self, parent=QModelIndex()): return max(len(self.rows), 1) def insertRows(self, position, rows=2, index=QModelIndex()): self.beginInsertRows(QModelIndex(), position, position + rows - 1) self.endInsertRows() return True def removeRows(self, position, rows=2, index=QModelIndex()): self.beginRemoveRows(QModelIndex(), position, position + rows - 1) self.endRemoveRows() return True def addBackupVolume(self, mount_details: BackupVolumeDetails) -> None: mount = mount_details.mount display_name = mount_details.name path = mount_details.path backup_type = mount_details.backup_type os_stat_device = mount_details.os_stat_device assert mount is not None assert display_name assert path assert backup_type # two rows per device: header row, and detail row row = len(self.rows) self.insertRows(position=row) logging.debug("Adding %s to backup device display with root path %s at rows %s - %s", display_name, mount.rootPath(), row, row+1) for row_id in range(self.row_id_counter, self.row_id_counter + 2): self.row_id_to_path[row_id] = path self.rows[row] = row_id row += 1 self.path_to_row_ids[path].append(row_id) header_row_id = self.row_id_counter self.headers.add(header_row_id) self.row_id_counter += 2 self.backup_devices[path] = BackupViewRow(mount=mount, display_name=display_name, backup_type=backup_type, os_stat_device=os_stat_device) def removeBackupVolume(self, path: str) -> None: """ :param path: the value of the volume (mount's path), NOT a manually specified path! """ row_ids = self.path_to_row_ids[path] header_row_id = row_ids[0] row = self.rows.row(header_row_id) logging.debug("Removing 2 rows from backup view, starting at row %s", row) self.rows.remove_rows(row, 2) self.headers.remove(header_row_id) del self.path_to_row_ids[path] del self.backup_devices[path] for row_id in row_ids: del self.row_id_to_path[row_id] self.removeRows(row, 2) def setDownloadAttributes(self, marked: FileTypeCounter, photos_size: int, videos_size: int, merge: bool) -> None: """ Set the attributes used to generate the visual display of the files marked to be downloaded :param marked: number and type of files marked for download :param photos_size: size in bytes of photos marked for download :param videos_size: size in bytes of videos marked for download :param merge: whether to replace or add to the current values """ if not merge: self.marked = marked self.photos_size_to_download = photos_size self.videos_size_to_download = videos_size else: self.marked.update(marked) self.photos_size_to_download += photos_size self.videos_size_to_download += videos_size self.downloadSizeChanged() def downloadSizeChanged(self) -> None: # TODO possibly optimize for photo vs video rows for row in range(1, len(self.rows), 2): self.dataChanged.emit(self.index(row, 0), self.index(row, 0)) def _download_size_by_backup_type(self, backup_type: BackupLocationType) -> Tuple[int, int]: """ Include photos or videos in download size only if those file types are being backed up to this backup device :param backup_type: which file types are being backed up to this device :return: photos_size_to_download, videos_size_to_download """ photos_size_to_download = videos_size_to_download = 0 if backup_type != BackupLocationType.videos: photos_size_to_download = self.photos_size_to_download if backup_type != BackupLocationType.photos: videos_size_to_download = self.videos_size_to_download return photos_size_to_download, videos_size_to_download def data(self, index: QModelIndex, role=Qt.DisplayRole): if not index.isValid(): return None row = index.row() # check for special case where no backup devices are active if len(self.rows) == 0: if role == Qt.DisplayRole: return ViewRowType.header elif role == Roles.device_details: if not self.prefs.backup_files: return (_('Backups are not configured'), self.removableIcon) elif self.prefs.backup_device_autodetection: return (_('No backup devices detected'), self.removableIcon) else: return (_('Valid backup locations not yet specified'), self.folderIcon) # at least one device / location is being used if row >= len(self.rows) or row < 0: return None if row not in self.rows: return None row_id = self.rows[row] path = self.row_id_to_path[row_id] if role == Qt.DisplayRole: if row_id in self.headers: return ViewRowType.header else: return ViewRowType.content else: device = self.backup_devices[path] mount = device.mount if role == Qt.ToolTipRole: return path elif role == Roles.device_details: if self.prefs.backup_device_autodetection: icon = self.removableIcon else: icon = self.folderIcon return (device.display_name, icon) elif role == Roles.storage: photos_size_to_download, videos_size_to_download = \ self._download_size_by_backup_type(backup_type=device.backup_type) photos_size_to_download, videos_size_to_download = adjusted_download_size( photos_size_to_download=photos_size_to_download, videos_size_to_download=videos_size_to_download, os_stat_device=device.os_stat_device, downloading_to=self._downloading_to) bytes_total, bytes_free = get_mount_size(mount=mount) return BackupVolumeUse( bytes_total=bytes_total, bytes_free=bytes_free, backup_type=device.backup_type, marked = self.marked, photos_size_to_download=photos_size_to_download, videos_size_to_download=videos_size_to_download ) return None def sufficientSpaceAvailable(self) -> bool: """ Detect if each backup device has sufficient space for backing up, taking into accoutn situations where downloads and backups are going to the same partition. :return: False if any backup device has insufficient space, else True. True if there are no backup devices. """ for device in self.backup_devices.values(): photos_size_to_download, videos_size_to_download = \ self._download_size_by_backup_type(backup_type=device.backup_type) photos_size_to_download, videos_size_to_download = adjusted_download_size( photos_size_to_download=photos_size_to_download, videos_size_to_download=videos_size_to_download, os_stat_device=device.os_stat_device, downloading_to=self._downloading_to ) bytes_total, bytes_free = get_mount_size(mount=device.mount) if photos_size_to_download + videos_size_to_download >= bytes_free: return False return True class BackupDeviceView(DeviceView): def __init__(self, rapidApp, parent=None) -> None: super().__init__(rapidApp, parent) self.setMouseTracking(False) self.entered.disconnect() class BackupDeviceDelegate(QStyledItemDelegate): def __init__(self, rapidApp, parent=None) -> None: super().__init__(parent) self.rapidApp = rapidApp self.deviceDisplay = DeviceDisplay() def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None: painter.save() x = option.rect.x() y = option.rect.y() width = option.rect.width() view_type = index.data(Qt.DisplayRole) # type: ViewRowType if view_type == ViewRowType.header: display_name, icon = index.data(Roles.device_details) self.deviceDisplay.paint_header(painter=painter, x=x, y=y, width=width, icon=icon, display_name=display_name, ) else: assert view_type == ViewRowType.content data = index.data(Roles.storage) # type: BackupVolumeUse details = make_body_details(bytes_total=data.bytes_total, bytes_free=data.bytes_free, files_to_display=data.backup_type, marked=data.marked, photos_size_to_download=data.photos_size_to_download, videos_size_to_download=data.videos_size_to_download) self.deviceDisplay.paint_body(painter=painter, x=x, y=y, width=width, details=details) painter.restore() def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: view_type = index.data(Qt.DisplayRole) # type: ViewRowType if view_type == ViewRowType.header: height = self.deviceDisplay.device_name_height else: storage_space = index.data(Roles.storage) if storage_space is None: height = self.deviceDisplay.base_height else: height = self.deviceDisplay.storage_height return QSize(self.deviceDisplay.view_width, height) class BackupOptionsWidget(QFramedWidget): """ Display and allow editing of preference values for Downloads today and Stored Sequence Number and associated options, as well as the strip incompatible characters option. """ def __init__(self, prefs: Preferences, parent, rapidApp) -> None: super().__init__(parent) self.rapidApp = rapidApp self.prefs = prefs self.media_dir = get_media_dir() self.setBackgroundRole(QPalette.Base) self.setAutoFillBackground(True) backupLayout = QGridLayout() layout = QVBoxLayout() layout.addLayout(backupLayout) self.setLayout(layout) self.backupExplanation = QLabel( _( 'You can have your photos and videos backed up to ' 'multiple locations as they are downloaded, e.g. ' 'external hard drives.' ) ) self.backupExplanation.setWordWrap(True) self.backupExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) self.backup = QCheckBox(_('Back up photos and videos when downloading')) self.backup.setChecked(self.prefs.backup_files) self.backup.stateChanged.connect(self.backupChanged) checkbox_width = self.backup.style().pixelMetric(QStyle.PM_IndicatorWidth) self.autoBackup = QCheckBox(_('Automatically detect backup devices')) self.autoBackup.setChecked(self.prefs.backup_device_autodetection) self.autoBackup.stateChanged.connect(self.autoBackupChanged) self.folderExplanation = QLabel( _( 'Specify the folder in which backups are stored on the ' 'device.

' 'Note: the presence of a folder with this name ' 'is used to determine if the device is used for backups. ' 'For each device you wish to use for backing up to, ' 'create a folder in it with one of these folder names. ' 'By adding both folders, the same device can be used ' 'to back up both photos and videos.' ) ) self.folderExplanation.setWordWrap(True) self.folderExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) # Unless this next call is made, for some reason the widget is too high! :-( self.folderExplanation.setContentsMargins(0, 0, 1, 0) self.photoFolderNameLabel = QLabel(_('Photo folder name:')) self.photoFolderName = QLineEdit() self.photoFolderName.setText(self.prefs.photo_backup_identifier) self.photoFolderName.editingFinished.connect(self.photoFolderIdentifierChanged) self.videoFolderNameLabel = QLabel(_('Video folder name:')) self.videoFolderName = QLineEdit() self.videoFolderName.setText(self.prefs.video_backup_identifier) self.videoFolderName.editingFinished.connect(self.videoFolderIdentifierChanged) self.autoBackupExampleBox = QGroupBox(_('Example:')) self.autoBackupExample = QLabel() autoBackupExampleBoxLayout = QHBoxLayout() autoBackupExampleBoxLayout.addWidget(self.autoBackupExample) self.autoBackupExampleBox.setLayout(autoBackupExampleBoxLayout) valid_mounts = ValidMounts(onlyExternalMounts=True) self.manualLocationExplanation = QLabel( _('If you disable automatic detection, choose the exact backup locations.') ) self.manualLocationExplanation.setWordWrap(True) self.manualLocationExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) # Translators: the word 'location' is optional in your translation. The left # side of the folder chooser combo box will always line up with the left side of the # the text entry boxes where the user can enter the photo folder name and the video # folder name. See http://damonlynch.net/rapid/documentation/thumbnails/backup.png self.photoLocationLabel = QLabel(_('Photo backup location:')) self.photoLocationLabel.setWordWrap(True) self.photoLocation = FolderCombo( self, prefs=self.prefs, file_type=FileType.photo, file_chooser_title=_('Select Photo Backup Location'), special_dirs=(StandardFileLocations.pictures,), valid_mounts=valid_mounts ) self.photoLocation.setPath(self.prefs.backup_photo_location) self.photoLocation.pathChosen.connect(self.photoPathChosen) # Translators: the word 'location' is optional in your translation. The left # side of the folder chooser combo box will always line up with the left side of the # the text entry boxes where the user can enter the photo folder name and the video # folder name. See http://damonlynch.net/rapid/documentation/thumbnails/backup.png self.videoLocationLabel = QLabel(_('Video backup location:')) self.videoLocationLabel.setWordWrap(True) self.videoLocation = FolderCombo( self, prefs=self.prefs, file_type=FileType.video, file_chooser_title=_('Select Video Backup Location'), special_dirs=(StandardFileLocations.videos, ), valid_mounts=valid_mounts ) self.videoLocation.setPath(self.prefs.backup_video_location) self.videoLocation.pathChosen.connect(self.videoPathChosen) backupLayout.addWidget(self.backupExplanation, 0, 0, 1, 4) backupLayout.addWidget(self.backup, 1, 0, 1, 4) backupLayout.addWidget(self.autoBackup, 2, 1, 1, 3) backupLayout.addWidget(self.folderExplanation, 3, 2, 1, 2) backupLayout.addWidget(self.photoFolderNameLabel, 4, 2, 1, 1) backupLayout.addWidget(self.photoFolderName, 4, 3, 1, 1) backupLayout.addWidget(self.videoFolderNameLabel, 5, 2, 1, 1) backupLayout.addWidget(self.videoFolderName, 5, 3, 1, 1) backupLayout.addWidget(self.autoBackupExampleBox, 6, 2, 1, 2) backupLayout.addWidget(self.manualLocationExplanation, 7, 1, 1, 3, Qt.AlignBottom) backupLayout.addWidget(self.photoLocationLabel, 8, 1, 1, 2) backupLayout.addWidget(self.photoLocation, 8, 3, 1, 1) backupLayout.addWidget(self.videoLocationLabel, 9, 1, 1, 2) backupLayout.addWidget(self.videoLocation, 9, 3, 1, 1) backupLayout.setColumnMinimumWidth(0, checkbox_width) backupLayout.setColumnMinimumWidth(1, checkbox_width) backupLayout.setRowMinimumHeight(7, checkbox_width * 2) layout.addStretch() self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) self.setBackupButtonHighlight() # Group controls to enable / disable sets of them self._backup_controls_type = (self.autoBackup, ) self._backup_controls_auto = ( self.folderExplanation, self.photoFolderNameLabel, self.photoFolderName, self.videoFolderNameLabel, self.videoFolderName, self.autoBackupExampleBox ) self._backup_controls_manual = ( self.manualLocationExplanation, self.photoLocationLabel, self.photoLocation, self.videoLocationLabel, self.videoLocation, ) self.updateExample() self.enableControlsByBackupType() @pyqtSlot(int) def backupChanged(self, state: int) -> None: backup = state == Qt.Checked logging.info("Setting backup while downloading to %s", backup) self.prefs.backup_files = backup self.setBackupButtonHighlight() self.enableControlsByBackupType() self.rapidApp.resetupBackupDevices() @pyqtSlot(int) def autoBackupChanged(self, state: int) -> None: autoBackup = state == Qt.Checked logging.info("Setting automatically detect backup devices to %s", autoBackup) self.prefs.backup_device_autodetection = autoBackup self.setBackupButtonHighlight() self.enableControlsByBackupType() self.rapidApp.resetupBackupDevices() @pyqtSlot(str) def photoPathChosen(self, path: str) -> None: logging.info("Setting backup photo location to %s", path) self.prefs.backup_photo_location = path self.setBackupButtonHighlight() self.rapidApp.resetupBackupDevices() @pyqtSlot(str) def videoPathChosen(self, path: str) -> None: logging.info("Setting backup video location to %s", path) self.prefs.backup_video_location = path self.setBackupButtonHighlight() self.rapidApp.resetupBackupDevices() @pyqtSlot() def photoFolderIdentifierChanged(self) -> None: name = self.photoFolderName.text() logging.info("Setting backup photo folder name to %s", name) self.prefs.photo_backup_identifier = name self.setBackupButtonHighlight() self.rapidApp.resetupBackupDevices() @pyqtSlot() def videoFolderIdentifierChanged(self) -> None: name = self.videoFolderName.text() logging.info("Setting backup video folder name to %s", name) self.prefs.video_backup_identifier = name self.setBackupButtonHighlight() self.rapidApp.resetupBackupDevices() def updateExample(self) -> None: """ Update the example paths in the backup panel """ if self.autoBackup.isChecked() and hasattr(self.rapidApp, 'backup_devices') and len( self.rapidApp.backup_devices): drives = self.rapidApp.backup_devices.sample_device_paths() else: # Translators: this value is used as an example device when automatic backup device # detection is enabled. You should translate this. drive1 = os.path.join(self.media_dir, _("drive1")) # Translators: this value is used as an example device when automatic backup device # detection is enabled. You should translate this. drive2 = os.path.join(self.media_dir, _("drive2")) drives = ( os.path.join(path, identifier) for path, identifier in ( (drive1, self.prefs.photo_backup_identifier), (drive2, self.prefs.photo_backup_identifier), (drive2, self.prefs.video_backup_identifier) ) ) paths = '\n'.join(drives) self.autoBackupExample.setText(paths) def setBackupButtonHighlight(self) -> None: """ Indicate error status in GUI by highlighting Backup button. Do so only if doing manual backups and there is a problem with one of the paths """ self.rapidApp.backupButton.setHighlighted( self.prefs.backup_files and not self.prefs.backup_device_autodetection and ( self.photoLocation.invalid_path or self.videoLocation.invalid_path)) def enableControlsByBackupType(self) -> None: """ Enable or disable backup controls depending on what the user has enabled. """ backupsEnabled = self.backup.isChecked() autoEnabled = backupsEnabled and self.autoBackup.isChecked() manualEnabled = not autoEnabled and backupsEnabled for widget in self._backup_controls_type: widget.setEnabled(backupsEnabled) for widget in self._backup_controls_manual: widget.setEnabled(manualEnabled) for widget in self._backup_controls_auto: widget.setEnabled(autoEnabled) def updateLocationCombos(self) -> None: """ Update backup locatation comboboxes in case directory status has changed. """ for combo in self.photoLocation, self.videoLocation: combo.refreshFolderList() class BackupPanel(QScrollArea): """ Backup preferences widget, for photos and video backups while downloading. """ def __init__(self, parent) -> None: super().__init__(parent) assert parent is not None self.rapidApp = parent self.prefs = self.rapidApp.prefs # type: Preferences self.backupDevices = BackupDeviceModel(parent=self) self.setFrameShape(QFrame.NoFrame) self.backupStoragePanel = QPanelView( label=_('Projected Backup Storage Use'), headerColor=QColor(ThumbnailBackgroundName), headerFontColor=QColor(Qt.white) ) self.backupOptionsPanel = QPanelView( label=_('Backup Options'), headerColor=QColor(ThumbnailBackgroundName), headerFontColor=QColor(Qt.white) ) self.backupDevicesView = BackupDeviceView(rapidApp=self.rapidApp, parent=self) self.backupStoragePanel.addWidget(self.backupDevicesView) self.backupDevicesView.setModel(self.backupDevices) self.backupDevicesView.setItemDelegate(BackupDeviceDelegate(rapidApp=self.rapidApp)) self.backupDevicesView.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.Fixed ) self.backupOptionsPanel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding) self.backupOptions = BackupOptionsWidget( prefs=self.prefs, parent=self, rapidApp=self.rapidApp ) self.backupOptionsPanel.addWidget(self.backupOptions) widget = QWidget() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) widget.setLayout(layout) layout.addWidget(self.backupStoragePanel) layout.addWidget(self.backupOptionsPanel) # layout.addStretch() self.setWidget(widget) self.setWidgetResizable(True) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) def updateExample(self) -> None: """ Update the example paths in the backup panel """ self.backupOptions.updateExample() def updateLocationCombos(self) -> None: """ Update backup locatation comboboxes in case directory status has changed. """ self.backupOptions.updateLocationCombos() def addBackupVolume(self, mount_details: BackupVolumeDetails) -> None: self.backupDevices.addBackupVolume(mount_details=mount_details) self.backupDevicesView.updateGeometry() def removeBackupVolume(self, path: str) -> None: self.backupDevices.removeBackupVolume(path=path) self.backupDevicesView.updateGeometry() def resetBackupDisplay(self) -> None: self.backupDevices.reset() self.backupDevicesView.updateGeometry() def setupBackupDisplay(self) -> None: """ Sets up the backup view list regardless of whether backups are manual specified by the user, or auto-detection is on """ if not self.prefs.backup_files: logging.debug("No backups configured: no backup destinations to display") return backup_devices = self.rapidApp.backup_devices # type: BackupDeviceCollection if self.prefs.backup_device_autodetection: for path in backup_devices: self.backupDevices.addBackupVolume( mount_details=backup_devices.get_backup_volume_details(path=path)) else: # manually specified backup paths try: mounts = backup_devices.get_manual_mounts() if mounts is None: return self.backupDevices.addBackupVolume(mount_details=mounts[0]) if len(mounts) > 1: self.backupDevices.addBackupVolume(mount_details=mounts[1]) except Exception: logging.exception( 'An unexpected error occurred when adding backup destinations. Exception:' ) self.backupDevicesView.updateGeometry() def setDownloadAttributes(self, marked: FileTypeCounter, photos_size: int, videos_size: int, merge: bool) -> None: """ Set the attributes used to generate the visual display of the files marked to be downloaded :param marked: number and type of files marked for download :param photos_size: size in bytes of photos marked for download :param videos_size: size in bytes of videos marked for download :param merge: whether to replace or add to the current values """ self.backupDevices.setDownloadAttributes( marked=marked, photos_size=photos_size, videos_size=videos_size, merge=merge ) def sufficientSpaceAvailable(self) -> bool: """ Check to see that there is sufficient space with which to perform a download. :return: True or False value if sufficient space. Will always return True if backups are disabled or there are no backup devices. """ if self.prefs.backup_files: return self.backupDevices.sufficientSpaceAvailable() else: return True def setDownloadingTo(self, downloading_to: DefaultDict[int, Set[FileType]]) -> None: self.backupDevices.downloading_to = downloading_to