Импорт снимков с карты памяти с сортировкой по дате/времени

В наше время процесс импортирования фотографий с карты памяти фотокамеры на жесткий диск задача достаточно тривиальная. Учитывая количество десктопного софта для этих нужд под любую ОСь. Платного и не совсем.

И тем не менее существует не мало индивидуумов, коих ни одно из оных приложений не устраивает по тем или иным причинам. Среди подобных и автор данных строк.

Конкретно мои капризы на данный счет весьма скромны и ожидаемы: отсутствие лишнего функционала и наличие нужного. Парадоксально, но мне среди имеющихся решений до сих пор не попадалось ничего подходящего. Точнее почти ничего. Когда-то под Win XP была у меня в фаворитах глючная и заброшенная своими авторами софтинка. Сейчас уже и название не вспомню. Несмотря на нестабильность своей работы софтинка по функционалу приближалась впритык к моему идеалу.  Тупо указывался шаблон для упорядочивания по директориям и именования конечных файлов в конфиг-файле, пара настроек в "гувых" опциях и полетели. Сказка, хоть и вылетала в реальность через раз.

Но вот прошло уж много лун.  К Linux`у приучены и жена и дети и собака. Лучшим интерфейсом для меня стала командная строка. Любимым языком программирования, внезапно, - Python. Ну и основным кузнецом своего цифрового счастья... вы поняли кто. На сим от вступительного бла-бла переходим к заявленной сути: пишем консольный скрипт на Python 3.

Определяемся с задачей

Что мы(я) хотим получить на выходе.

  • Хотим просто указать скрипту исходную директорию. Дальше пусть сам все разруливает в соответствии с настройками. Для пущего фарша мы интегрируем скрипт с гуевым файловым менеджером(на примере Thunar из xfce4).
  • Перед началом импорта хотим еще раз убедиться, что все цели и задачи поставлены верно.
  • Импортируемые снимки должны быть разложены в соответствии с иерархией: ДИРЕКТОРИЯ_С_ФОТОАРХИВОМ / ГОД_СЪЕМКИ / МЕСЯЦ_СЪЕМКИ / ДЕНЬ_СЪЕМКИ
  • Хотим наблюдать воочию процесс импорта и прогресс выполнения.
  • Желаем по завершению иметь статистику по успешным и фейловым процессам при обработке каждого файла.

Понеслась... Пишем код

Для того, что бы работать с мета-данными фотоснимка нужна будет дополнительная библиотека. Я в качестве таковой использую ExifRead. Ставим:

$ pip install exifread

Теперь все готово для написания нашего скрипта.

Импортируем нужные модули:


#!/usr/bin/env python3
  
import sys
import os
# На всякий случай подстрахуемся от отсутствия ExifRead`а.
try:
    import exifread
except ImportError:
    sys.exit("Для работы скрипта необходим модуль EXIFREAD, а он у вас, похоже, не установлен.
\nЗадание не выполнено.")

Задаем директорию с фотоархивом по-умолчанию.  Пути в примерах ниже приведены в контексте Unix-систем. Если вы на Windows - помните о том.

DEFAULT_OUTPUT = '/full/path/to/photoarchive_dir'

Вспомогательные функции

Функция подтверждения перед началом импорта:

def confirm():
    while True:
        answer = input('Приступаем: [Y]/n? ').lower()
        if answer in ['', 'y']:
            return True
        elif answer == 'n':
            return False

На входе скрипта ожидаем получить:

  • Первым аргументом - исходную директорию (источник импорта).
  • Вторым аргуметом - целевую директорию (куда импортировать).

Второй аргумент необязателен. У нас есть заданная выше константа с дефолтным каталогом.

Проверяем полученные аргументы-каталоги:

def getDirs(args, default = ''):
    while True:
        argsCount = len(args)
        if argsCount > 1:
            if os.path.isdir(args[1]):
                dirs = [args[1]]
            else:
                args[1] = input('Исходный путь для импорта не верен, задайте правильный: ')
                continue
            if argsCount >= 3:
                dirs.append(args[2])
            else:
                dirs.append(default)
                args.append(dirs[1])
            try:
                os.makedirs(dirs[1], exist_ok=True)
                break
            except PermissionError:
                args[2] = input('Целевой каталог не доступен на запись, задайте другой: ')
        else:
            # Если скрипт запущен без аргументов, то просим это исправить.
            args.append(input("Задайте исходный каталог: "))
    return dirs

Точка входа

Теперь приступаем с основному коду - точке входа нашего скрипта:

def main():
    # Получаем исходную и целевую директории.
    dirsList = getDirs(sys.argv, DEFAULT_OUTPUT)
    # Показываем куда и откуда будет импорт.
    print('Источник импорта: ', dirsList[0])
    print('Директория назначения: ', dirsList[1])
    # Просим подтверждение перед началом работы.
    if not confirm():
        sys.exit("Задание отменено")
    
    # Задаем счетчики "плохих" и "хороших" операций.
    fTotal = okOp = badOp = 0
    badFiles = []
    # Собственно, импорт.
    for root, dirs, files in os.walk(dirsList[0]):
        i = 0
        for name in files:
            i += 1
            fTotal += 1
            filePath = os.path.join(root, name)
            # Отображаем текущий процесс и прогресс выполнения.
            print('Обработка файла {} из {} (дир. -= {} =-)'
                  .format(i, len(files), os.path.basename(root).upper()))
            with open(filePath, 'rb') as f:
                tags = exifread.process_file(f, details=False)
                exifDateTime = tags.get('EXIF DateTimeOriginal')
    
                # При отсутствии у фото EXIF-данных - файл пропускаем,
                # занося его в список "плохих" файлов.
                if not exifDateTime:
                    print('Отсутствуют EXIF данные. Файл пропущен!!!', name)
                    badOp += 1
                    badFiles.append(filePath)
                    continue
                  
            # Вытаскиваем из EXIF снимка данные даты/времени
            exifDate, exifTime = str(exifDateTime).split(' ')
            year, month, day = exifDate.split(':')
            hour, minute, sec = exifTime.split(':')
  
            fileExtension = os.path.splitext(name)[1]
            # Задаем название импортированного файла.
            newFileName = '{}-{}-{}_{}{}'.format(hour,
                                                 minute, sec, i, fileExtension)
            path = dirsList[1] + \
                '/{}/{}/{}'.format(year, month, day).replace('//', '/')
  
            if not os.path.exists(path):
                print('Создаем новую папку')
                os.makedirs(path, exist_ok=True)
  
            newPath = os.path.join(path, newFileName)
            doCopy = os.system("cp {} {}".format(
                filePath.replace(' ', '\ '), newPath))
   
            if doCopy == 0:
                print('OK')
                okOp += 1
            else:
                print('BAD')
                badOp += 1
    # По завершении импорта выводим статистику сделанного.
    print('Обработано файлов всего:', fTotal)
    print('Успешных операций:', okOp)
    print('Завершенных с ошибками:', badOp)
    # И имена "плохих" файлов, если таковые есть.
    if len(badFiles):
        print('Необработанные файлы: ')
        for bf in badFiles:
            print(bf)

В завершение указываем нашу точку входа, как таковую при непосредственном запуске скрипта.

if __name__=='__main__':
    main()

Интеграция с файловым менеджером на примере Thunar

Как интегрировать скрипты в Windows не имею ни малейшего представления. Посему дальнейшее описание будет полезно лишь  пользователям Unix-систем. Где файловые менеджеры основных графических сред имеют схожий механизм интеграции с консольными приложениями.

В графическом файловом менеджере Thunar (Xfce) такой механизм в русском переводе именуется "особые действия". Все что от нас требуется для интеграции со свеженаписанным скриптом это добавить новое "действие", в коем задать команду вида:

xfce4-terminal --tab --drop-down --hold -x /path/do/scripta/nash_script_importa.py %f

Где:

  • xfce4-terminal --tab --drop-down --hold -x - вызов используемого вами эмулятора терминала;
  • /path/do/scripta/nash_script_importa.py - полный путь до скрипта импорта;
  • %f - метка-указатель на исходный каталог для импорта (из контекстного меню которого мы и будем запускать импорт);

Исходник на github.