Example #1
0
def load_metadata(
    rpd_file: Union[Photo, Video],
    et_process: exiftool.ExifTool,
    problems: RenamingProblems,
) -> bool:
    """
    Loads the metadata for the file.

    :param rpd_file: photo or video
    :param et_process: the daemon ExifTool process
    :param problems: problems encountered renaming the file
    :return True if operation succeeded, false otherwise
    """
    if rpd_file.metadata is None:
        if not rpd_file.load_metadata(
            full_file_name=rpd_file.temp_full_file_name, et_process=et_process
        ):
            # Error in reading metadata

            problems.append(
                FileMetadataLoadProblem(
                    name=rpd_file.name, uri=rpd_file.get_uri(), file_type=rpd_file.title
                )
            )
            return False
    return True
Example #2
0
    def __init__(self) -> None:
        super().__init__("Rename and Move")

        self.prefs = Preferences()

        self.sync_raw_jpeg = SyncRawJpeg()
        self.downloaded = DownloadedSQL()

        logging.debug("Start of day is set to %s", self.prefs.day_start)

        self.platform_c_maxint = platform_c_maxint()

        # This will be assigned again in run(), but initializing it here
        # clarifies any problems with type checking in an IDE
        self.problems = RenamingProblems()
Example #3
0
    def run(self) -> None:
        """
        Generate subfolder and filename, and attempt to move the file
        from its temporary directory.

        Move video THM and/or audio file if there is one.

        If successful, increment sequence values.

        Report any success or failure.
        """
        i = 0

        # Dict of filename keys and int values used to track ints to add as
        # 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:
                    if i:
                        logging.debug("Finished %s. Getting next task.", i)

                    # rename file and move to generated subfolder
                    directive, content = self.receiver.recv_multipart()

                    self.check_for_command(directive, content)

                    data = pickle.loads(content)  # type: RenameAndMoveFileData
                    if data.message == RenameAndMoveStatus.download_started:

                        # 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)

                        self.initialise_sequence_number_usage()

                        self.must_synchronize_raw_jpg = (
                            self.prefs.must_synchronize_raw_jpg()
                        )

                        self.problems = RenamingProblems()

                    elif data.message == RenameAndMoveStatus.download_completed:
                        if len(self.problems):
                            self.content = pickle.dumps(
                                RenameAndMoveFileResults(problems=self.problems),
                                pickle.HIGHEST_PROTOCOL,
                            )
                            self.send_message_to_sink()

                        # Ask main application process to update prefs with stored
                        # sequence number and downloads today values. But first sync
                        # the prefs here, to write out the dirty values so they are not
                        # saved when a sync is done at download start, overwriting
                        # the values that may have been changed in the main process
                        logging.debug(
                            "Rename and move process syncing preferences to the file "
                            "system"
                        )
                        self.prefs.sync()
                        self.content = pickle.dumps(
                            RenameAndMoveFileResults(
                                stored_sequence_no=self.sequences.stored_sequence_no,
                                downloads_today=self.downloads_today_tracker.downloads_today,
                            ),
                            pickle.HIGHEST_PROTOCOL,
                        )
                        dl_today = (
                            self.downloads_today_tracker.get_or_reset_downloads_today()
                        )
                        logging.debug("Downloads today: %s", dl_today)
                        self.send_message_to_sink()
                    else:
                        rpd_file = data.rpd_file
                        download_count = data.download_count

                        if data.download_succeeded:
                            move_succeeded = self.process_file(rpd_file, download_count)
                            if not move_succeeded:
                                self.process_rename_failure(rpd_file)
                            else:
                                # Record file as downloaded in SQLite database
                                try:
                                    self.downloaded.add_downloaded_file(
                                        name=rpd_file.name,
                                        size=rpd_file.size,
                                        modification_time=rpd_file.modification_time,
                                        download_full_file_name=rpd_file.download_full_file_name,
                                    )
                                except sqlite3.OperationalError as e:
                                    # This should never happen because this is the only
                                    # process writing to the database..... but just in
                                    # case
                                    logging.error(
                                        "Database error adding download file %s: %s. "
                                        "Will not retry.",
                                        rpd_file.download_full_file_name,
                                        e,
                                    )
                        else:
                            move_succeeded = False

                        rpd_file.metadata = None
                        self.content = pickle.dumps(
                            RenameAndMoveFileResults(
                                move_succeeded=move_succeeded,
                                rpd_file=rpd_file,
                                download_count=download_count,
                            ),
                            pickle.HIGHEST_PROTOCOL,
                        )
                        self.send_message_to_sink()

                        i += 1
Example #4
0
class RenameMoveFileWorker(DaemonProcess):
    """
    Generates names for files and folders, and renames (moves) files.

    Runs as a daemon process.
    """

    def __init__(self) -> None:
        super().__init__("Rename and Move")

        self.prefs = Preferences()

        self.sync_raw_jpeg = SyncRawJpeg()
        self.downloaded = DownloadedSQL()

        logging.debug("Start of day is set to %s", self.prefs.day_start)

        self.platform_c_maxint = platform_c_maxint()

        # This will be assigned again in run(), but initializing it here
        # clarifies any problems with type checking in an IDE
        self.problems = RenamingProblems()

    def notify_file_already_exists(
        self, rpd_file: Union[Photo, Video], identifier: Optional[str] = None
    ) -> None:
        """
        Notify user that the download file already exists
        """

        # get information on when the existing file was last modified
        try:
            modification_time = os.path.getmtime(rpd_file.download_full_file_name)
            dt = datetime.fromtimestamp(modification_time)
            date = dt.strftime("%x")
            time = dt.strftime("%X")
        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()

        device = make_href(name=rpd_file.device_display_name, uri=rpd_file.device_uri)

        if not identifier:
            problem = FileAlreadyExistsProblem(
                file_type_capitalized=rpd_file.title_capitalized,
                file_type=rpd_file.title,
                name=rpd_file.download_name,
                uri=get_uri(full_file_name=rpd_file.download_full_file_name),
                source=source,
                device=device,
                date=date,
                time=time,
            )

            rpd_file.status = DownloadStatus.download_failed
        else:
            problem = IdentifierAddedProblem(
                file_type_capitalized=rpd_file.title_capitalized,
                file_type=rpd_file.title,
                name=rpd_file.download_name,
                uri=get_uri(full_file_name=rpd_file.download_full_file_name),
                source=source,
                device=device,
                date=date,
                time=time,
                identifier=identifier,
            )

            rpd_file.status = DownloadStatus.downloaded_with_warning

        self.problems.append(problem)

    def notify_download_failure_file_error(
        self, rpd_file: Union[Photo, Video], inst: Exception
    ) -> None:
        """
        Handle cases where file failed to download
        """
        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)

        problem = RenamingFileProblem(
            file_type=rpd_file.title,
            destination=rpd_file.download_name,
            folder=rpd_file.download_path,
            name=rpd_file.name,
            uri=uri,
            device=device,
            exception=inst,
        )
        self.problems.append(problem)

        rpd_file.status = DownloadStatus.download_failed

        try:
            msg = "Failed to create file {}: {} {}".format(
                rpd_file.download_full_file_name, inst.errno, inst.strerror
            )
            logging.error(msg)
        except AttributeError:
            logging.error(
                "Failed to create file %s: %s ",
                rpd_file.download_full_file_name,
                str(inst),
            )

    def download_file_exists(self, rpd_file: Union[Photo, Video]) -> bool:
        """
        Check how to handle a download file already existing
        """

        if self.prefs.conflict_resolution == ConflictResolution.add_identifier:
            logging.debug(
                "Will add unique identifier to avoid duplicate filename for %s",
                rpd_file.full_file_name,
            )
            return True
        else:
            self.notify_file_already_exists(rpd_file)
            return False

    def same_name_different_exif(
        self, sync_photo_name: str, rpd_file: Union[Photo, Video]
    ) -> None:
        """
        Notify the user that a file was already downloaded with the same
        name, but the exif information was different
        """

        i1_ext, i1_date_time = self.sync_raw_jpeg.ext_exif_date_time(sync_photo_name)
        image2_date_time = rpd_file.date_time()
        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)
        image2_date = image2_date_time.strftime("%x")
        image2_time = image2_date_time.strftime("%X")

        self.problems.append(
            SameNameDifferentExif(
                image1="%s%s" % (sync_photo_name, i1_ext),
                image1_date=i1_date,
                image1_time=i1_time,
                image2=rpd_file.name,
                image2_date=image2_date,
                image2_time=image2_time,
            )
        )
        rpd_file.status = DownloadStatus.downloaded_with_warning

    def _move_associate_file(
        self, extension: str, full_base_name: str, temp_associate_file: str
    ) -> str:
        """
        Move (rename) the associate file using the pre-generated name.

        Exceptions are not caught.

        :return: full path and filename
        """

        download_full_name = full_base_name + extension

        # move (rename) associate file
        # don't check to see if it already exists
        os.rename(temp_associate_file, download_full_name)

        return download_full_name

    def move_thm_file(self, rpd_file: Union[Photo, Video]) -> None:
        """
        Move (rename) the THM thumbnail file using the pre-generated name
        """

        try:
            if rpd_file.thm_extension:
                ext = rpd_file.thm_extension
            else:
                ext = ".THM"
        except AttributeError:
            ext = ".THM"

        try:
            rpd_file.download_thm_full_name = self._move_associate_file(
                extension=ext,
                full_base_name=rpd_file.download_full_base_name,
                temp_associate_file=rpd_file.temp_thm_full_name,
            )
        except (OSError, FileNotFoundError) as e:
            self.problems.append(
                RenamingAssociateFileProblem(
                    source=make_href(
                        name=os.path.basename(rpd_file.download_thm_full_name),
                        uri=get_uri(
                            full_file_name=rpd_file.download_thm_full_name,
                            camera_details=rpd_file.camera_details,
                        ),
                    ),
                    exception=e,
                )
            )

            logging.error(
                "Failed to move video THM file %s", rpd_file.download_thm_full_name
            )

    def move_audio_file(self, rpd_file: Union[Photo, Video]) -> None:
        """
        Move (rename) the associate audio file using the pre-generated
        name
        """

        try:
            if rpd_file.audio_extension:
                ext = rpd_file.audio_extension
            else:
                ext = ".WAV"
        except AttributeError:
            ext = ".WAV"

        try:
            rpd_file.download_audio_full_name = self._move_associate_file(
                extension=ext,
                full_base_name=rpd_file.download_full_base_name,
                temp_associate_file=rpd_file.temp_audio_full_name,
            )
        except (OSError, FileNotFoundError) as e:
            self.problems.append(
                RenamingAssociateFileProblem(
                    source=make_href(
                        name=os.path.basename(rpd_file.download_audio_full_name),
                        uri=get_uri(
                            full_file_name=rpd_file.download_audio_full_name,
                            camera_details=rpd_file.camera_details,
                        ),
                    ),
                    exception=e,
                )
            )
            logging.error(
                "Failed to move file's associated audio file %s",
                rpd_file.download_audio_full_name,
            )

    def move_xmp_file(self, rpd_file: Union[Photo, Video]) -> None:
        """
        Move (rename) the associate XMP file using the pre-generated
        name
        """

        try:
            if rpd_file.xmp_extension:
                ext = rpd_file.xmp_extension
            else:
                ext = ".XMP"
        except AttributeError:
            ext = ".XMP"

        try:
            rpd_file.download_xmp_full_name = self._move_associate_file(
                extension=ext,
                full_base_name=rpd_file.download_full_base_name,
                temp_associate_file=rpd_file.temp_xmp_full_name,
            )
        except (OSError, FileNotFoundError) as e:
            self.problems.append(
                RenamingAssociateFileProblem(
                    source=make_href(
                        name=os.path.basename(rpd_file.download_xmp_full_name),
                        uri=get_uri(
                            full_file_name=rpd_file.download_xmp_full_name,
                            camera_details=rpd_file.camera_details,
                        ),
                    ),
                    exception=e,
                )
            )
            logging.error(
                "Failed to move file's associated XMP file %s",
                rpd_file.download_xmp_full_name,
            )

    def move_log_file(self, rpd_file: Union[Photo, Video]) -> None:
        """
        Move (rename) the associate XMP file using the pre-generated
        name
        """

        try:
            if rpd_file.log_extension:
                ext = rpd_file.log_extension
            else:
                ext = ".LOG"
        except AttributeError:
            ext = ".LOG"

        try:
            rpd_file.download_log_full_name = self._move_associate_file(
                extension=ext,
                full_base_name=rpd_file.download_full_base_name,
                temp_associate_file=rpd_file.temp_log_full_name,
            )
        except (OSError, FileNotFoundError) as e:
            self.problems.append(
                RenamingAssociateFileProblem(
                    source=make_href(
                        name=os.path.basename(rpd_file.download_log_full_name),
                        uri=get_uri(
                            full_file_name=rpd_file.download_log_full_name,
                            camera_details=rpd_file.camera_details,
                        ),
                    ),
                    exception=e,
                )
            )
            logging.error(
                "Failed to move file's associated LOG file %s",
                rpd_file.download_log_full_name,
            )

    def check_for_fatal_name_generation_errors(
        self, rpd_file: Union[Photo, Video]
    ) -> bool:
        """
        :return False if either the download subfolder or filename are
         blank, else returns True
        """

        if not rpd_file.download_subfolder or not rpd_file.download_name:
            if not rpd_file.download_subfolder and not rpd_file.download_name:
                area = _("subfolder and filename")
            elif not rpd_file.download_name:
                area = _("filename")
            else:
                area = _("subfolder")

            rpd_file.status = DownloadStatus.download_failed
            self.problems.append(
                NoDataToNameProblem(
                    name=rpd_file.name,
                    uri=rpd_file.get_uri(),
                    area=area,
                    file_type=rpd_file.title,
                )
            )
            return False
        else:
            return True

    def add_unique_identifier(self, rpd_file: Union[Photo, Video]) -> bool:
        """
        Adds a unique identifier like _1 to a filename, in ever
        incrementing values, until a unique filename is generated.

        :param rpd_file: the file being worked on
        :return: True if the operation was successful, else returns
         False
        """

        name = os.path.splitext(rpd_file.download_name)
        full_name = rpd_file.download_full_file_name
        while True:
            self.duplicate_files[full_name] = self.duplicate_files.get(full_name, 0) + 1
            identifier = "_%s" % self.duplicate_files[full_name]
            rpd_file.download_name = "{}{}{}".format(name[0], identifier, name[1])
            rpd_file.download_full_file_name = os.path.join(
                rpd_file.download_path, rpd_file.download_name
            )

            try:
                if os.path.exists(rpd_file.download_full_file_name):
                    raise OSError(
                        errno.EEXIST,
                        "File exists: %s" % rpd_file.download_full_file_name,
                    )
                os.rename(
                    rpd_file.temp_full_file_name, rpd_file.download_full_file_name
                )
                self.notify_file_already_exists(rpd_file, identifier)
                return True

            except OSError as inst:
                if inst.errno != errno.EEXIST:
                    self.notify_download_failure_file_error(rpd_file, inst)
                    return False

    def sync_raw_jpg(self, rpd_file: Union[Photo, Video]) -> SyncRawJpegResult:

        failed = False
        sequence_to_use = None
        photo_name, photo_ext = os.path.splitext(rpd_file.name)
        if not load_metadata(rpd_file, self.exiftool_process, self.problems):
            failed = True
            rpd_file.status = DownloadStatus.download_failed
            self.check_for_fatal_name_generation_errors(rpd_file)
        else:
            date_time = rpd_file.date_time()
            if not isinstance(date_time, datetime):
                failed = True
                rpd_file.status = DownloadStatus.download_failed
                self.check_for_fatal_name_generation_errors(rpd_file)
            else:
                matching_pair = self.sync_raw_jpeg.matching_pair(
                    name=photo_name, extension=photo_ext, date_time=date_time
                )  # type: SyncRawJpegMatch
                sequence_to_use = matching_pair.sequence_number
                if matching_pair.status == SyncRawJpegStatus.error_already_downloaded:
                    # this exact file has already been
                    # downloaded (same extension, same filename,
                    # and roughly the same exif date time  info)
                    if (
                        self.prefs.conflict_resolution
                        != ConflictResolution.add_identifier
                    ):
                        self.problems.append(
                            DuplicateFileWhenSyncingProblem(
                                name=rpd_file.name,
                                uri=rpd_file.get_uri(),
                                file_type=rpd_file.title,
                            )
                        )

                        rpd_file.status = DownloadStatus.download_failed
                        failed = True
                else:
                    self.sequences.matched_sequences = matching_pair.sequence_number
                    self.sequences.use_matched_sequences = (
                        self.sequences.matched_sequences is not None
                    )
                    if (
                        matching_pair.status
                        == SyncRawJpegStatus.error_datetime_mismatch
                    ):
                        self.same_name_different_exif(photo_name, rpd_file)

        return SyncRawJpegResult(sequence_to_use, failed, photo_name, photo_ext)

    def prepare_rpd_file(self, rpd_file: Union[Photo, Video]) -> None:
        """
        Populate the RPDFile with download values used in subfolder
        and filename generation
        """

        if rpd_file.file_type == FileType.photo:
            rpd_file.download_folder = self.prefs.photo_download_folder
            rpd_file.subfolder_pref_list = self.prefs.photo_subfolder
            rpd_file.name_pref_list = self.prefs.photo_rename
        else:
            rpd_file.download_folder = self.prefs.video_download_folder
            rpd_file.subfolder_pref_list = self.prefs.video_subfolder
            rpd_file.name_pref_list = self.prefs.video_rename

    def process_rename_failure(self, rpd_file: RPDFile) -> None:
        try:
            os.remove(rpd_file.temp_full_file_name)
        except OSError:
            logging.error(
                "Failed to delete temporary file %s", rpd_file.temp_full_file_name
            )

    def generate_names(
        self, rpd_file: Union[Photo, Video], synchronize_raw_jpg: bool
    ) -> bool:

        rpd_file.strip_characters = self.prefs.strip_characters

        generate_subfolder(rpd_file, self.exiftool_process, self.problems)

        if rpd_file.download_subfolder:
            logging.debug(
                "Generated subfolder name %s for file %s",
                rpd_file.download_subfolder,
                rpd_file.name,
            )

            self.sequences.stored_sequence_no = self.prefs.stored_sequence_no
            if self.must_synchronize_raw_jpg and rpd_file.file_type == FileType.video:
                self.sequences.use_matched_sequences = False

            rpd_file.sequences = self.sequences

            # generate the file name
            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,
                )
                rpd_file.status = DownloadStatus.downloaded_with_warning
            else:
                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
            )

        return self.check_for_fatal_name_generation_errors(rpd_file)

    def move_file(self, rpd_file: Union[Photo, Video]) -> bool:
        """
        Having generated the file name and subfolder names, move
        the file
        :param rpd_file: photo or video being worked on
        :return: True if move succeeded, False otherwise
        """

        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_base_name = os.path.splitext(
            rpd_file.download_full_file_name
        )[0]

        if not os.path.isdir(rpd_file.download_path):
            try:
                os.makedirs(rpd_file.download_path)
            except OSError as inst:
                if inst.errno != errno.EEXIST:
                    logging.error(
                        "Failed to create download subfolder: %s",
                        rpd_file.download_path,
                    )
                    logging.error(inst)

                    problem = SubfolderCreationProblem(
                        folder=make_href(
                            name=rpd_file.download_subfolder,
                            uri=get_uri(path=rpd_file.download_path),
                        ),
                        exception=inst,
                    )
                    self.problems.append(problem)

        # Move temp file to subfolder

        add_unique_identifier = False
        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,
            )
            os.rename(rpd_file.temp_full_file_name, rpd_file.download_full_file_name)
            logging.debug("....successfully renamed file")
            move_succeeded = True
            if rpd_file.status != DownloadStatus.downloaded_with_warning:
                rpd_file.status = DownloadStatus.downloaded
        except OSError as inst:
            if inst.errno == errno.EEXIST:
                add_unique_identifier = self.download_file_exists(rpd_file)
            else:
                self.notify_download_failure_file_error(rpd_file, inst)
        except Exception as inst:
            # all other errors, including PermissionError
            self.notify_download_failure_file_error(rpd_file, inst)

        if add_unique_identifier:
            move_succeeded = self.add_unique_identifier(rpd_file)

        return move_succeeded

    def process_file(self, rpd_file: Union[Photo, Video], download_count: int) -> bool:
        """
        Generate file & subfolder name, and move (rename) photo / video
        :param rpd_file: photo or video
        :param download_count: used to track the file being downloaded via a counter
        :return: success or otherwise of operation
        """

        move_succeeded = False

        self.prepare_rpd_file(rpd_file)

        synchronize_raw_jpg = (
            self.must_synchronize_raw_jpg and rpd_file.file_type == FileType.photo
        )

        if synchronize_raw_jpg:
            sync_result = self.sync_raw_jpg(rpd_file)

            if sync_result.failed:
                return False

        generation_succeeded = self.generate_names(rpd_file, synchronize_raw_jpg)

        if generation_succeeded:
            move_succeeded = self.move_file(rpd_file)

            logging.debug("Finished processing file: %s", download_count)

        if move_succeeded:
            if synchronize_raw_jpg:
                if sync_result.sequence_to_use is None:
                    sequence = self.sequences.create_matched_sequences()
                else:
                    sequence = sync_result.sequence_to_use
                self.sync_raw_jpeg.add_download(
                    name=sync_result.photo_name,
                    extension=sync_result.photo_ext,
                    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 self.uses_sequence_session_no or self.uses_sequence_letter:
                    self.sequences.increment(
                        self.uses_sequence_session_no, self.uses_sequence_letter
                    )
                if self.uses_stored_sequence_no:
                    if self.prefs.stored_sequence_no == self.platform_c_maxint:
                        # wrap value if it exceeds the maximum size value that Qt can
                        # display in its spinbox
                        self.prefs.stored_sequence_no = 0
                    else:
                        self.prefs.stored_sequence_no += 1
                self.downloads_today_tracker.increment_downloads_today()

            if rpd_file.temp_thm_full_name:
                self.move_thm_file(rpd_file)

            if rpd_file.temp_audio_full_name:
                self.move_audio_file(rpd_file)

            if rpd_file.temp_xmp_full_name:
                self.move_xmp_file(rpd_file)

            if rpd_file.temp_log_full_name:
                self.move_log_file(rpd_file)

        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()
        if self.prefs.any_pref_uses_stored_sequence_no():
            logging.info(
                "Stored number at download start: %s", self.prefs.stored_sequence_no + 1
            )

        # 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 initialise_sequence_number_usage(self) -> None:
        """
        Determine what type of sequence numbers are being used in file name generation
        """

        self.uses_sequence_session_no = self.prefs.any_pref_uses_session_sequence_no()
        self.uses_sequence_letter = self.prefs.any_pref_uses_sequence_letter_value()
        self.uses_stored_sequence_no = self.prefs.any_pref_uses_stored_sequence_no()

    def run(self) -> None:
        """
        Generate subfolder and filename, and attempt to move the file
        from its temporary directory.

        Move video THM and/or audio file if there is one.

        If successful, increment sequence values.

        Report any success or failure.
        """
        i = 0

        # Dict of filename keys and int values used to track ints to add as
        # 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:
                    if i:
                        logging.debug("Finished %s. Getting next task.", i)

                    # rename file and move to generated subfolder
                    directive, content = self.receiver.recv_multipart()

                    self.check_for_command(directive, content)

                    data = pickle.loads(content)  # type: RenameAndMoveFileData
                    if data.message == RenameAndMoveStatus.download_started:

                        # 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)

                        self.initialise_sequence_number_usage()

                        self.must_synchronize_raw_jpg = (
                            self.prefs.must_synchronize_raw_jpg()
                        )

                        self.problems = RenamingProblems()

                    elif data.message == RenameAndMoveStatus.download_completed:
                        if len(self.problems):
                            self.content = pickle.dumps(
                                RenameAndMoveFileResults(problems=self.problems),
                                pickle.HIGHEST_PROTOCOL,
                            )
                            self.send_message_to_sink()

                        # Ask main application process to update prefs with stored
                        # sequence number and downloads today values. But first sync
                        # the prefs here, to write out the dirty values so they are not
                        # saved when a sync is done at download start, overwriting
                        # the values that may have been changed in the main process
                        logging.debug(
                            "Rename and move process syncing preferences to the file "
                            "system"
                        )
                        self.prefs.sync()
                        self.content = pickle.dumps(
                            RenameAndMoveFileResults(
                                stored_sequence_no=self.sequences.stored_sequence_no,
                                downloads_today=self.downloads_today_tracker.downloads_today,
                            ),
                            pickle.HIGHEST_PROTOCOL,
                        )
                        dl_today = (
                            self.downloads_today_tracker.get_or_reset_downloads_today()
                        )
                        logging.debug("Downloads today: %s", dl_today)
                        self.send_message_to_sink()
                    else:
                        rpd_file = data.rpd_file
                        download_count = data.download_count

                        if data.download_succeeded:
                            move_succeeded = self.process_file(rpd_file, download_count)
                            if not move_succeeded:
                                self.process_rename_failure(rpd_file)
                            else:
                                # Record file as downloaded in SQLite database
                                try:
                                    self.downloaded.add_downloaded_file(
                                        name=rpd_file.name,
                                        size=rpd_file.size,
                                        modification_time=rpd_file.modification_time,
                                        download_full_file_name=rpd_file.download_full_file_name,
                                    )
                                except sqlite3.OperationalError as e:
                                    # This should never happen because this is the only
                                    # process writing to the database..... but just in
                                    # case
                                    logging.error(
                                        "Database error adding download file %s: %s. "
                                        "Will not retry.",
                                        rpd_file.download_full_file_name,
                                        e,
                                    )
                        else:
                            move_succeeded = False

                        rpd_file.metadata = None
                        self.content = pickle.dumps(
                            RenameAndMoveFileResults(
                                move_succeeded=move_succeeded,
                                rpd_file=rpd_file,
                                download_count=download_count,
                            ),
                            pickle.HIGHEST_PROTOCOL,
                        )
                        self.send_message_to_sink()

                        i += 1