Импорт фотографий из командной строки Linux

от автора

Собралось много фотографий на различных носителях, да еще в полном беспорядке. Где, когда было отснято, и как это все отсортировать и привести в порядок? Объемы большие, работы много. Сортировать все это вручную, так себе вариант. Использовать какие-то органайзеры и прочий софт желания не было. Тем более что простой просмотрщик уже был установлен. Но использовать его для сортировки занятие не только утомительное, но еще и затратное по времени и силам. Побродив немного по всемирной паутине в поисках решения, как это все сделать просто, да еще и в фоновом режиме и не найдя подходящего, для себя по крайней мере, решения, написал простенький скрипт на shell-е. Почему shell? Да все просто, его более чем достаточно для этой задачи. Плюс внешние утилиты командной строки, используемые в скрипте, в любом Linux идут по дефолту. Но это все предыстория, а нам пора переходить  к скрипту.

Что умеет скрипт

Представьте что у нас хранятся сотни тонн всевозможных снимков в совершенно разных местах, на разных носителях. Где-то они были разобраны по годам и месяцам, а где-то просто “свалены” в кучу и оставлены до лучших времен.

Мы запускаем программу указываем ей начальную точку, откуда начинать, и идем заниматься своими делами. Скрипт сканирует все директории какие встретит на своем пути, включая и вложенные, в поисках фотографий. После завершения работы программы получаем фотоальбом с отсортированными снимками по годам и месяцам в каждом году, т.е. в виде: год / месяц / снимки. Да еще и без дубликатов фотографий. А сами снимки будут переименованы из непонятных типа IMG_654372984.jpg или AB54645456.jpg во вполне читабельные и понятные имена вида YYYYMMDD_hhmmss.jpg. При этом не пострадает ни один оригинал фотографии.

сам скрипт import-photo
#! /usr/bin/env bash  # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= # Импорт фотографий в фотоальбом # author: VlaSard # github: https://github.com/VlaSard # date: 2022.09.05 # photo-import # # DESCRIPTION: #   Импортирует фотографии в фотоальбом и сортирует по директориям [YYYY / MM]. #   Переименовывает фотографии в формат YYYYMMDD_hhmmss.jpg. #   НЕ УДАЛЯЕТ оригиналы файлов. #   НЕ УВЕЛИЧИВАЕТ разрешение. # #   !!! convert - РАБОТАЕТ БЕЗ ПРОВЕРОК НА НАЛИЧИЕ ДУБЛИКАТОВ !!! # # REVIEWS: #   exiv2 #   convert # # USAGE: #   photo-import [dir_name] #   dir_name - необязательный параметр, откуда будем импортировать фотографии # # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=  # -=-=-=-=-=-=-= VARIABLES =-=-=-=-=-=-=- # если каталог с фотографиями не указан, используется текущий каталог SourceDir=${1:-${PWD}}  # каталог назначения DestDir="$HOME/Photo_album"  # настройка импорта фотографий: # import - импорт без изменения фотографий; # convert - импорт с изменением фотографий; ImportPhoto=import  # степень сжатия фотографии Quality=80  # максимальный размер фотографии Resize=1920x1080  # переменные colors NORMAL='\033[0m' GREEN='\033[32m' CYAN='\033[36m' # RED='\033[31m'  # -=-=-=-=-=-=-= MESSAGES =-=-=-=-=-=-=-=- msg_FileExists=("Файл" "уже есть в альбоме! ${CYAN}Пропускаем${NORMAL}.") msg_FileImport=("Импортируем фотографию" "в альбом.")  # -=-=-=-=-=-=-= FUNCTIONS =-=-=-=-=-=-=-  # -== вывод сообщений скрипта ==- # параметры: #   ${message[@]} - сообщение в виде массива #   ${file_name} - имя файла msgPrint() {     printf "%b\n" "${1} ${GREEN}${3}${NORMAL} ${2}" }  # -== проверка наличия фотографии в альбоме ==- # параметры: #   $1 - объект источник #   $2 - объект назначение # возвращает: $FileExists #   0 - объекта $2 нет #   1 - объект $2 есть и он идентичен объекту $1 #   2 - объект $2 есть и он отличается от объекта $1 file_exists() {     FileExists=0      if [ -f "${2}" ]; then          # если файлы одинаковые, пропускаем         if cmp -s "${1}" "${2}"; then             msgPrint "${msg_FileExists[@]}" "${1}"             FileExists=1             return         fi          FileExists=2         return      fi }  # -== импорт фотографий без изменений ==- # переменные: #   $1 - находится полное имя фотографии в альбоме #   FilePhoto - объект источник #   destPhoto - объект назначения #   baseName - имя файла без расширения #   count - счетчик дубликатов фотографии import() {     destPhoto="${1}"     local baseName="${destPhoto%.*}"     local COUNT=1      # проверить наличие фотографии в альбоме     file_exists "${FilePhoto}" "${destPhoto}"      # если фотография в альбоме есть - пропускаем     if [ "${FileExists}" = 1 ]; then         return 1      # если фотография в альбоме есть, но отличается от исходной     elif [ "${FileExists}" = 2 ]; then          # проверяем все похожие фотографии         while [ "${FileExists}" != 0 ]; do              # добавили к имени счетчик и ушли на проверку             NewName="${baseName}-${COUNT}.jpg"             COUNT=$((COUNT + 1))             file_exists "${FilePhoto}" "${NewName}"              if [ "${FileExists}" = 1 ]; then                 return 1             fi          done          destPhoto="${NewName}"      fi      # если фотографии в альбоме нет - импортируем     msgPrint "${msg_FileImport[@]}" "${destPhoto}"     cp -up "${FilePhoto}" "${destPhoto}" }  # импорт с изменением фотографий # переменные: #   $1 - находится полное имя фотографии в альбоме #   FilePhoto - объект источник #   destPhoto - объект назначения convert() {     destPhoto="${1}"      # если фотография есть в альбоме - пропускаем     # if ! file_exists "${FilePhoto}" "${destPhoto}"; then     #     return 1     # fi      # изменить качество и размер фотографии, переименовывать и копировать в альбом     msgPrint "${msg_FileImport[@]}" "${FilePhoto}"     convert -quality "${Quality}" -resize "${Resize}" "${FilePhoto}" "${destPhoto}"  }  # -=-=-=-=-=-=-=-= MAIN  =-=-=-=-=-=-=-=-  # перебираем все jpg-файлы в указанной директории и всех поддиректориях find "${SourceDir}" -iname "*.jpg" | sort |     while read -r FilePhoto; do          for PhotoDate in "Exif.Photo.DateTimeOriginal" "Exif.Image.DateTime"; do              # читаем дату из EXIF             PhotoDate=$(exiv2 -g "${PhotoDate}" -Pv "${FilePhoto}")              # если прочитали дату снимка, прекращаем поиск даты             [ -n "${PhotoDate}" ] && break          done          # если не нашли в EXIF ищем в названии файла         if [ -z "${PhotoDate}" ]; then              # ищем дату в названии файла и формируем в виде YYYY:MM:DD HH:MM:SS для корректного добавления в EXIF             PhotoDate=$(                 basename "${FilePhoto}" ".jpg" |                     perl -e 'if                     (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s)                     { print "$1:$2:$3 $4:$5:$6" }'             )              if [ -n "${PhotoDate}" ]; then                  # если нашли добавляем дату из названия файла в EXIF                 exiv2 -M "add Exif.Image.DateTime Ascii ${PhotoDate}" "${FilePhoto}"              else                  # если даты в названии не нашли, берем дату создания (изменения) файла                 PhotoDate=$(date +"%Y:%m:%d %T" -r "${FilePhoto}")              fi          fi          # формируем массив с переменными для фотоальбома         # AlbumParam[0] - адрес фотоальбома         # AlbumParam[1] - новое имя файла фотографии         # AlbumParam[2] - дата создания в соответствии с EXIF         IFS=" " read -r -a AlbumParam <<<"$(echo "$PhotoDate" |             perl -e 'if             (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s)             { print "$1/$2 $1$2$3_$4$5$6.jpg $1$2$3$4$5.$6" }')"          # создать структуру фотоальбома (YYYY / MM)         [ -d "${DestDir}/${AlbumParam[0]}" ] || mkdir -p "${DestDir}/${AlbumParam[0]}"          # если импортировали фотографию         if ${ImportPhoto} "${DestDir}/${AlbumParam[0]}/${AlbumParam[1]}"; then              # установить дату создания в соответствии с датой в EXIF             touch -t "${AlbumParam[2]}" "${destPhoto}"          fi      done 

Под капотом

Подготовительные операции

Как уже упоминалось ранее при запуске мы указываем скрипту откуда начинать импортировать фотографии, начальную точку. Если этого не сделать, что допустимо, он сам определит в какой директории он был запущен и начнет оттуда. Директория фотоальбома задается только в теле скрипта, вряд ли она так часто меняется, чтобы передавать ее через командную строку. При желании нужно изменить всего пару строк. За все это отвечают строки приведенные ниже. В SourceDir присваивается или директория переданная через командную строку или директория в которой был запущен скрипт.

SourceDir=${1:-${PWD}} DstDir="$HOME/Phot_album" 

А дальше начинаем читать файл за файлом и раскладывать по директориям в альбоме, попутно создавая их если не было.

Ищем и читаем только jpg файлы. Поскольку это основной формат для хранения фотографий. Строки отвечающие за поиск и чтение файлов приведены ниже:

find "${SourceDir}" -iname "*.jpg" | sort | while read -r FilePhoto; do  ...  done 

Прочитав файл, пытаемся определить дату снимка из параметров EXIF. Циклом перебираем два тега и пытаемся прочитать из них информацию. Нашли, отлично. Пропускаем оставшиеся варианты поиска даты, проверив наличие параметра в переменной PhotoDate. Если их там не нашлось ищем в названии файла. При этом используется регулярка Perl т.к. она лучше документирована, позволяет сразу сформировать нужный результат, да и занимает всего одну строчку кода. В отличие от shell-овского варианта где пришлось бы использовать grep для поиска и sed для формирования результата. Если же в названии даты не нашлось возьмем дату создания (изменения) файла. Код выполняющий все описанные действия:

чтение даты
for PhotoDate in "Exif.Photo.DateTimeOriginal" "Exif.Image.DateTime"; do  # читаем дату из EXIF PhotoDate=$(exiv2 -g "${PhotoDate}" -Pv "${FilePhoto}")  # если прочитали дату снимка, прекращаем поиск даты [ -n "${PhotoDate}" ] && break  done  # если не нашли в EXIF ищем в названии файла if [ -z "${PhotoDate}" ]; then  # ищем дату в названии файла и формируем в виде YYYY:MM:DD HH:MM:SS для корректного добавления в EXIF PhotoDate=$( basename "${FilePhoto}" ".jpg" | perl -e 'if  (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s) { print "$1:$2:$3 $4:$5:$6" }' )  if [ -n "${PhotoDate}" ]; then  # если нашли добавляем дату из названия файла в EXIF exiv2 -M "add Exif.Image.DateTime Ascii ${PhotoDate}" "${FilePhoto}"  else  # если даты в названии не нашли, берем дату создания (изменения) файла PhotoDate=$(date +"%Y:%m:%d %T" -r "${FilePhoto}")  fi  fi 

Отлично. Дату нашли. Теперь формируем необходимые переменные для создания структуры фотоальбома. Этим также займется perl-однострочник. Сформируем сразу все переменные, у нас их три, и сложим в массив. Bash умеет неплохо работать с массивами.

IFS=" " read -r -a AlbumParam <<<"$(echo "$PhotoDate" | perl -e 'if (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s) { print "$1/$2 $1$2$3_$4$5$6.jpg $1$2$3$4$5.$6" }')"

В shell-е пришлось бы написать подобную конструкцию в четыре строки кода. Разобрать дату на составляющие. А уже потом в каждой строке формировать три переменные. Здесь же одна строка кода.

В строках:

if ${ImportPhoto} "${DestDir}/${AlbumParam[0]}/${AlbumParam[1]}"; then touch -t "${AlbumParam[2]}" "${destPhoto}" fi 

запускаем процедуру импорта фотографии и установки даты создания файла в соответствии с датой в EXIF. Если ничего не импортировали, то ничего и не меняем.

Импорт снимка

За импорт фотографии отвечает функция import, код функции ниже:

функция import()
import() {     destPhoto="${1}"     local baseName="${destPhoto%.*}"     local COUNT=1      # проверить наличие фотографии в альбоме     file_exists "${FilePhoto}" "${destPhoto}"      # если фотография в альбоме есть - пропускаем     if [ "${FileExists}" = 1 ]; then         return 1      # если фотография в альбоме есть, но отличается от исходной     elif [ "${FileExists}" = 2 ]; then          # проверяем все похожие фотографии         while [ "${FileExists}" != 0 ]; do              # добавили к имени счетчик и ушли на проверку             NewName="${baseName}-${COUNT}.jpg"             COUNT=$((COUNT + 1))             file_exists "${FilePhoto}" "${NewName}"              if [ "${FileExists}" = 1 ]; then                 return 1             fi          done          destPhoto="${NewName}"      fi      # если фотографии в альбоме нет - импортируем     msgPrint "${msg_FileImport[@]}" "${destPhoto}"     cp -up "${FilePhoto}" "${destPhoto}" } 

Здесь все достаточно просто. В функцию передается только имя файла назначения. Прежде всего нужно проверить существует или нет снимок в альбоме, поручим это самостоятельной функции file_exists. Функция будет возвращать три значения проверки:

  • 0 — файла нет;

  • 1 — файл есть и он идентичен файлу источнику

  • 2 — файл есть но отличается от источника

С первыми двумя вариантами (0, 1) все просто или пропускаем или импортируем. В случае возврата в результатах проверки 2, просто перебираем имена, добавляя индекс к имени файла и отправляя на повторную проверку. Как только определим, что снимка с таким именем в альбоме нет производим импорт фотографии.

функция file_exists()
file_exists() {     FileExists=0      if [ -f "${2}" ]; then          # если файлы одинаковые, пропускаем         if cmp -s "${1}" "${2}"; then             msgPrint "${msg_FileExists[@]}" "${1}"             FileExists=1             return         fi          FileExists=2         return      fi } 

Проверили наличие файла и если файл существует сравнили по содержимому файл источник и файл назначения и вернули соответствующее значение.

Полный текст скрипта приведен выше. Также он доступен на GitHub по ссылке.

Эпилог

Скрипт появился давно, но только в последнее время появилась возможность и желание навести немного порядка в библиотеке приложений.

Почему скрипт на bash, а не python к примеру? Хотя бы потому, что пришлось бы установить пакет для работы с тегами фотографий. В bash он присутствует по умолчанию. Да и задача не настолько тяжелая чтобы решать на других языках.

Единственное, что не прикрутил к нему конвертер снимков, для уменьшения размера файла. Да вряд ли такой функционал будет востребован. Но заготовку функции в скрипт вставил. А поскольку у него только одна функция импорта работает то и парсера командной строки тоже не стал добавлять. Также как нет и справки по работе с программой. Все настройки внутри скрипта в самом начале ровно как и небольшой туториал по нему.


ссылка на оригинал статьи https://habr.com/ru/articles/688650/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *