https://github.com/dmitriy-dorofeev/photo-sorter
https://github.com/dmitriy-dorofeev/photo-sorter
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/dmitriy-dorofeev/photo-sorter
- Owner: dmitriy-dorofeev
- Created: 2026-04-18T19:21:12.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-12T19:50:42.000Z (about 2 months ago)
- Last Synced: 2026-05-12T20:13:05.314Z (about 2 months ago)
- Language: Go
- Homepage:
- Size: 2.82 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Roadmap: roadmap.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# photo-sorter
Консольное TUI-приложение на Go для организации фотографий и видео с разных устройств.
## Что делает
- **Сканирует** исходные папки (iPhone, Android, компьютер и т.д.)
- **Инкрементальные запуски** — при повторном запуске обрабатывает только новые и изменённые файлы, пропуская уже отсортированные
- **Находит и пропускает дубликаты** — двухуровневая проверка (размер → xxhash), включая межзапусковую дедупликацию
- **Определяет дату съёмки** по приоритету:
1. **Фото:** EXIF (`DateTimeOriginal`)
2. **Видео:** метаданные через `exiftool` (`DateTimeOriginal` → `CreateDate` → `MediaCreateDate`)
3. **Парсинг имени файла** — 8 паттернов (iPhone, Android, Screenshot, Signal, WhatsApp, Pixel и др.)
4. **`mtime`** (опционально, включается флагом `--use-mtime`)
5. **Fallback:** файл попадает в папку `unsorted/`
- **Копирует** файлы в целевую папку по структуре `YYYY/MM/DD/`
- **Переименовывает файлы по шаблону** — дата, оригинальное имя, устройство-источник, порядковый номер
- **Поддерживает Live Photos** — `.HEIC` + `.MOV` с одним basename группируются рядом
- **Обратная синхронизация метаданных** — если дата определена только по имени или `mtime`, можно записать её в EXIF (`DateTimeOriginal`) через `exiftool`
- **Безопасно работает** — только копирование, никогда не перемещает
## Требования
- **Go 1.25+**
- **`exiftool`** (опционально, но рекомендуется для видео)
## Поддерживаемые платформы
- **macOS** — основная целевая платформа (Apple Silicon и Intel)
- **Linux** — полная поддержка
- **BSD** — совместимость через `unix.Statfs`
- **Windows** — требуется адаптация модуля проверки свободного места на диске (используется `golang.org/x/sys/unix`)
### Установка Go
```bash
# macOS через Homebrew
brew install go
# Проверь версию
go version
```
### Установка exiftool
```bash
# macOS через Homebrew
brew install exiftool
# Проверь версию
exiftool -ver
```
> Если `exiftool` не установлен, видео всё равно будут обработаны через парсинг имени файла или `mtime`.
## Сборка
### Быстрая сборка (Makefile)
```bash
git clone
cd photo-sorter
make build
```
Результат появится в `bin/photo-sorter`. Версия автоматически подставится из текущего git-тега или хеша коммита.
### Вручную через go build
```bash
go build -ldflags "-X main.version=$(git describe --tags --always --dirty)" -o photo-sorter ./cmd
```
> Флаг `-ldflags "-X main.version=..."` встраивает версию в бинарник. Без него приложение сообщит версию `dev`.
## macOS .app bundle
Для удобства на macOS можно собрать приложение как `.app` bundle с иконкой в Finder.
### Сборка .app
```bash
make build-mac-app
```
Результат:
- `bin/Photo Sorter.app/` — готовый bundle
- `bin/Photo Sorter.app.zip` — zip-архив для распространения
### Иконка
Чтобы у bundle была иконка, положите PNG размером **1024×1024** в `build/macos/icon.png` перед сборкой. Скрипт автоматически сконвертирует её в `.icns`.
### Как запускать
1. Распакуйте `Photo Sorter.app.zip` (двойной клик в Finder).
2. (Опционально) Перетащите `Photo Sorter.app` в папку `Applications`.
3. Двойной клик по иконке — откроется **Terminal** и в нём запустится TUI.
> Поскольку photo-sorter — консольное TUI-приложение, `.app` bundle не запускает графический интерфейс напрямую, а открывает Terminal. Это ожидаемое поведение.
### Обновление внутри .app bundle
Команда `update` работает и из `.app`, но с ограничениями:
- Обновляется только бинарник внутри `Photo Sorter.app/Contents/MacOS/photo-sorter`.
- Wrapper, `Info.plist` и иконка **останутся от первоначальной версии** ( updater скачивает сырой бинарник, а не целый `.app` bundle).
- Если приложение установлено в `/Applications/`, для обновления могут потребоваться права администратора (`sudo`).
При мажорных обновлениях рекомендуется скачивать новый `.app.zip` вручную.
## Версионирование и релизы
Проект следует [Semantic Versioning](https://semver.org/lang/ru/): `vMAJOR.MINOR.PATCH`.
### Как посмотреть версию
```bash
./photo-sorter --version
```
### Автоматическое обновление
Проверить, есть ли новая версия:
```bash
./photo-sorter --check-update
```
Установить последнюю версию прямо из GitHub Releases:
```bash
./photo-sorter update
```
> **Примечание:** при обновлении приложение скачивает архив под текущую платформу, распаковывает его и заменяет текущий бинарник. Старая версия сохраняется с суффиксом `.bak` на случай отката. Для сборок из исходников (`dev`) автообновление недоступно.
>
> Если приложение запущено из `.app` bundle на macOS, обновляется только бинарник внутри bundle. Wrapper, иконка и `Info.plist` остаются от версии, с которой был скачан `.app.zip`.
### Как выпустить новый релиз
Релизы создаются автоматически при пуше git-тега, начинающегося на `v`:
```bash
# 1. Обновите CHANGELOG или ROADMAP (по желанию)
# 2. Создайте и запушьте тег
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
```
GitHub Actions запустит [GoReleaser](https://goreleaser.com/), который:
- соберёт бинарники для **macOS** (Intel + Apple Silicon) и **Linux** (x86_64 + ARM64);
- упакует их в архивы `.tar.gz`;
- для **macOS** дополнительно создаст `.app.zip` с иконкой и bundle;
- сгенерирует файл контрольных сумм `checksums.txt`;
- создаст страницу Release на GitHub с changelog.
### Где скачать готовые артефакты
Готовые бинарники доступны на странице **Releases** репозитория:
```
https://github.com/dmitriy-dorofeev/photo-sorter/releases
```
Выберите последний релиз, скачайте архив под свою платформу и распакуйте:
```bash
# Пример для macOS Apple Silicon (сырой бинарник)
curl -LO https://github.com/dmitriy-dorofeev/photo-sorter/releases/download/v1.0.0/photo-sorter_v1.0.0_Darwin_arm64.tar.gz
tar -xzf photo-sorter_v1.0.0_Darwin_arm64.tar.gz
./photo-sorter --version
```
Для macOS также доступен `.app` bundle с иконкой:
```bash
# Скачайте .app.zip и распакуйте в Applications
curl -LO https://github.com/dmitriy-dorofeev/photo-sorter/releases/download/v1.0.0/photo-sorter_v1.0.0_macOS_arm64.app.zip
unzip photo-sorter_v1.0.0_macOS_arm64.app.zip -d /Applications/
```
> **Windows не поддерживается** в готовых сборках из-за использования системных вызовов `unix.Statfs` (можно собрать вручную с адаптацией).
### Локальная сборка snapshot (без публикации)
Если хотите проверить, как будет выглядеть релиз, не публикуя его:
```bash
make snapshot
```
Требуется установленный [GoReleaser](https://goreleaser.com/install/). Артефакты появятся в папке `dist/`.
## Тестирование
```bash
# Запуск всех тестов
go test ./...
# Запуск с подробным выводом
go test -v ./...
```
## Запуск
Приложение работает в двух режимах: интерактивном **TUI** и консольном **CLI**.
### TUI-режим (интерактивный)
Просто запустите бинарник без аргументов:
```bash
./photo-sorter
```
Откроется интерфейс с 6 экранами:
1. **Выбор источника** — браузер папок. Выберите папку с фотографиями (`Пробел`), затем `→`.
2. **Выбор цели** — выберите папку, куда скопировать отсортированные файлы.
3. **Настройки** — шаблон папок (`YYYY/MM/DD` и др.), шаблон имён файлов, опции Live Photos, видео, mtime, уведомления.
4. **Сканирование** — прогресс обработки файлов.
5. **Предпросмотр** — дерево целевой структуры, список дублей и файлов без даты.
6. **Копирование** — прогресс копирования файлов.
**Навигация:** `↑/↓` — курсор, `Enter` — открыть/подтвердить, `Backspace` — вверх по папкам, `←/→` — назад/вперёд по экранам, `Esc` — выход.
### CLI-режим (консольный)
Для запуска из терминала без интерактивного интерфейса укажите `--source` и `--target`:
```bash
# Пробный прогон
./photo-sorter --source ~/Photos/iPhone --target /Volumes/ExternalDisk --dry-run
# Несколько источников
./photo-sorter --source ~/Photos/iPhone --source ~/Photos/Android --target /Volumes/ExternalDisk
# Реальное копирование
./photo-sorter --source ~/Photos --target ~/Sorted --dry-run=false
# Записать дату в EXIF (если дата взята из имени/mtime)
./photo-sorter --source ~/Photos --target ~/Sorted --write-exif --dry-run=false
# JSON-отчёт вместо текстового
./photo-sorter --source ~/Photos --target ~/Sorted --format=json
# Свой шаблон имён файлов
./photo-sorter --source ~/Photos --target ~/Sorted --name-template "{YYYY}-{MM}-{DD}_{original}{ext}"
# Посмотреть все флаги и примеры
./photo-sorter --help
```
**Основные флаги:**
- `--source` — исходная папка (можно указать несколько раз)
- `--target` — целевая папка
- `--template` — шаблон папок (default: `2006-01-02`)
- `--name-template` — шаблон имён файлов (default: `{original}{ext}`)
- `--live-photos` — группировать Live Photos (default: `true`)
- `--include-video` — обрабатывать видео (default: `true`)
- `--dry-run` — пробный прогон (default: `true`)
- `--use-mtime` — fallback на дату изменения (default: `true`)
- `--write-exif` — записывать дату в EXIF, если она взята из имени/mtime (default: `false`)
- `--notify` — показать системное уведомление по завершении (default: `true`)
- `--full-check` — игнорировать state, пересортировать все файлы (default: `false`)
- `--reset-state` — удалить файл состояния перед запуском (default: `false`)
- `--format` — формат отчёта: `text` или `json` (default: `text`)
- `--version` — показать версию приложения и выйти
- `--check-update` — проверить наличие новой версии на GitHub
### Первый запуск на реальных данных
1. **Всегда начинайте с пробного прогона:**
```bash
./photo-sorter --source ~/Photos --target /Volumes/ExternalDisk --dry-run
```
2. **Проверьте отчёт:** убедитесь, что даты определены правильно, дубли корректны.
3. **Запустите реальное копирование:**
```bash
./photo-sorter --source ~/Photos --target /Volumes/ExternalDisk --dry-run=false
```
4. **Проверьте результат** — файлы скопированы, оригиналы на месте.
### Инкрементальные запуски
По умолчанию `photo-sorter` запоминает, какие файлы уже были обработаны, и при повторном запуске на те же `source → target` пропускает неизменившиеся файлы. Это ускоряет повторные прогоны на больших коллекциях.
- Состояние хранится в скрытой папке `/.photo-sorter/state.bolt` (BoltDB).
- Повторный запуск обрабатывает только новые и изменённые файлы (проверка по размеру и времени модификации).
- Если добавленный файл является дубликатом уже скопированного — он будет распознан и пропущен.
**Управление:**
```bash
# Обычный инкрементальный запуск (только новые/изменённые файлы)
./photo-sorter --source ~/Photos --target ~/Sorted --dry-run=false
# Полный пересорт всех файлов (игнорировать state, но обновить его после)
./photo-sorter --source ~/Photos --target ~/Sorted --full-check --dry-run=false
# Полный сброс — удалить state и пересортировать всё с нуля
./photo-sorter --source ~/Photos --target ~/Sorted --reset-state --dry-run=false
```
> **Примечание:** `--dry-run` не изменяет state-файл.
### Шаблоны имён файлов
По умолчанию файлы сохраняют оригинальные имена (`{original}{ext}`). Вы можете задать свой шаблон через `--name-template` или в TUI (шаг 3).
**Доступные плейсхолдеры:**
| Плейсхолдер | Описание | Пример |
|-------------|----------|--------|
| `{YYYY}` | Год (4 цифры) | `2024` |
| `{YY}` | Год (2 цифры) | `24` |
| `{MM}` | Месяц | `03` |
| `{DD}` | День | `15` |
| `{HH}` | Часы | `14` |
| `{mm}` | Минуты | `30` |
| `{SS}` | Секунды | `22` |
| `{original}` | Имя файла без расширения | `IMG_1234` |
| `{ext}` | Расширение с точкой (сохраняет регистр) | `.jpg` |
| `{device}` | Устройство-источник (iPhone, Pixel, Screenshot и др.) | `iPhone` |
| `{seq}` | Порядковый номер в папке | `1` |
| `{seq:03}` | Порядковый номер с ведущими нулями | `001` |
**Примеры шаблонов:**
```bash
# Оригинальное имя (по умолчанию)
--name-template "{original}{ext}"
# Дата + оригинальное имя
--name-template "{YYYY}-{MM}-{DD}_{original}{ext}"
# Дата-время + устройство
--name-template "{YYYY}-{MM}-{DD}_{HH}-{mm}-{SS}_{device}{ext}"
# Компактная дата-время
--name-template "{YYYY}{MM}{DD}_{HH}{mm}{SS}{ext}"
# Хронологический счётчик по папкам
--name-template "{YYYY}{MM}{DD}_{seq:03}{ext}"
```
> Файлы без даты (`unsorted/`) получают `0000-00-00_00-00-00` вместо плейсхолдеров даты.
## Безопасность
- Приложение **только копирует**, никогда не перемещает.
- В TUI сначала посмотрите предпросмотр (шаг 5), затем запускайте копирование.
- В CLI используйте `--dry-run` перед реальным запуском.
- Файлы без распознаваемой даты попадают в папку `unsorted/`.
- При потере диска во время копирования приложение прерывает работу и сохраняет лог прогресса.
## Поддерживаемые форматы
- **Фото:** `.jpg`, `.jpeg`, `.png`, `.heic`
- **Видео:** `.mov`, `.mp4`
- **Другие:** любые файлы, если включён `--include-video` или файл попадает под фильтр расширений
## Структура проекта
```
photo-sorter/
├── cmd/
│ └── main.go # точка входа (TUI по умолчанию, CLI через флаги)
├── internal/
│ ├── scanner/ # рекурсивный обход папок с фильтрацией
│ ├── dateresolver/ # движок дат: EXIF, видео-метаданные, парсинг имён, mtime
│ ├── deduper/ # поиск дубликатов (размер → xxhash)
│ ├── sorter/ # построение целевого дерева папок
│ ├── copier/ # безопасное копирование с проверкой диска
│ ├── logger/ # запись логов каждого запуска
│ ├── notify/ # системные уведомления (macOS / Linux)
│ ├── runner/ # единый pipeline для TUI и CLI
│ └── updater/ # проверка и установка обновлений
├── tui/ # интерфейс на bubbletea (6 экранов)
└── testdata/ # тестовые файлы
```
## Лицензия
MIT