Собралось много фотографий на различных носителях, да еще в полном беспорядке. Где, когда было отснято, и как это все отсортировать и привести в порядок? Объемы большие, работы много. Сортировать все это вручную, так себе вариант. Использовать какие-то органайзеры и прочий софт желания не было. Тем более что простой просмотрщик уже был установлен. Но использовать его для сортировки занятие не только утомительное, но еще и затратное по времени и силам. Побродив немного по всемирной паутине в поисках решения, как это все сделать просто, да еще и в фоновом режиме и не найдя подходящего, для себя по крайней мере, решения, написал простенький скрипт на 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 он присутствует по умолчанию. Да и задача не настолько тяжелая чтобы решать на других языках.
Единственное, что не прикрутил к нему конвертер снимков, для уменьшения размера файла. Да вряд ли такой функционал будет востребован. Но заготовку функции в скрипт вставил. А поскольку у него только одна функция импорта работает то и парсера командной строки тоже не стал добавлять. Также как нет и справки по работе с программой. Все настройки внутри скрипта в самом начале ровно как и небольшой туториал по нему.
