An open API service indexing awesome lists of open source software.

https://github.com/dmitriy-dorofeev/photo-sorter


https://github.com/dmitriy-dorofeev/photo-sorter

Last synced: about 1 month ago
JSON representation

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