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