From cc61d51c13f63735dbe4561aee3c6f422cd0b2c1 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 28 Jun 2026 16:04:08 +0200 Subject: [PATCH] =?UTF-8?q?init:=20Raskidator-3000=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=84?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=20=D0=BF=D0=BE=20EXIF=20=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 +++++++++ organize.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++ 3 files changed, 164 insertions(+) create mode 100644 README.md create mode 100644 organize.py create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..a379f0d --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Raskidator-3000 🗂️ + +Сортировка фотографий по EXIF дате. + +**Логика:** сканирует папку, читает EXIF (`DateTimeOriginal`), создаёт подпапки `Год/Месяц/День` и перемещает файлы туда. Файлы без EXIF даты остаются на месте. + +## Поддерживаемые форматы + +`.jpg` `.jpeg` `.png` `.heic` `.tiff` `.tif` `.webp` `.bmp` + +## Установка + +```bash +pip install -r requirements.txt +``` + +## Использование + +```bash +# Текущая папка +python organize.py + +# Указанная папка +python organize.py "D:\Фото\Скопировать" +``` diff --git a/organize.py b/organize.py new file mode 100644 index 0000000..96b199a --- /dev/null +++ b/organize.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Raskidator-3000 — сортировка фотографий по EXIF дате. + +Сканирует указанную папку (по умолчанию текущую) на изображения, +читает EXIF дату съёмки (DateTimeOriginal) и перемещает файлы +в подпапки Год/Месяц/День. + +Файлы без EXIF даты остаются на месте. + +Поддерживаемые форматы: .jpg, .jpeg, .png, .heic, .tiff, .tif, .webp, .bmp + +Использование: + python organize.py [путь_к_папке] +""" + +import os +import sys +import shutil +from datetime import datetime +from pathlib import Path + +EXTENSIONS = {'.jpg', '.jpeg', '.png', '.heic', '.tiff', '.tif', '.webp', '.bmp'} + + +def get_exif_date(filepath: Path) -> str | None: + """Извлекает дату съёмки из EXIF (DateTimeOriginal).""" + try: + # HEIC требует pillow-heif + if filepath.suffix.lower() == '.heic': + try: + from pillow_heif import open_heif + heif_file = open_heif(str(filepath)) + exif_data = heif_file.info.get('exif', b'') + if not exif_data: + return None + import piexif + exif_dict = piexif.load(exif_data) + if piexif.ExifIFD.DateTimeOriginal in exif_dict.get('Exif', {}): + return exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal].decode() + except Exception: + return None + else: + from PIL import Image + from PIL.ExifTags import TAGS + + img = Image.open(filepath) + exif_data = img._getexif() + if not exif_data: + return None + + tag_map = {v: k for k, v in TAGS.items()} + datetime_original_tag = tag_map.get('DateTimeOriginal') + if datetime_original_tag and datetime_original_tag in exif_data: + return exif_data[datetime_original_tag] + + # Fallback: DateTime (если DateTimeOriginal нет) + datetime_tag = tag_map.get('DateTime') + if datetime_tag and datetime_tag in exif_data: + return exif_data[datetime_tag] + + except Exception: + return None + + return None + + +def parse_date(date_str: str) -> datetime | None: + """Парсит EXIF дату в формате 'YYYY:MM:DD HH:MM:SS'.""" + try: + return datetime.strptime(date_str.strip(), '%Y:%m:%d %H:%M:%S') + except (ValueError, AttributeError): + return None + + +def organize(folder: Path) -> None: + """Сканирует папку и сортирует изображения.""" + if not folder.exists() or not folder.is_dir(): + print(f'Ошибка: папка не существует — {folder}') + sys.exit(1) + + files = [f for f in folder.iterdir() if f.is_file() and f.suffix.lower() in EXTENSIONS] + files.sort() + + moved = 0 + skipped = 0 + no_date = 0 + + for filepath in files: + date_str = get_exif_date(filepath) + if not date_str: + skipped += 1 + no_date += 1 + print(f' [—] {filepath.name} — нет EXIF даты, оставлен на месте') + continue + + dt = parse_date(date_str) + if not dt: + skipped += 1 + print(f' [?] {filepath.name} — не удалось распарсить дату "{date_str}"') + continue + + dest_dir = folder / str(dt.year) / f'{dt.month:02d}' / f'{dt.day:02d}' + dest_dir.mkdir(parents=True, exist_ok=True) + + dest_path = dest_dir / filepath.name + + # Если файл с таким именем уже есть — добавляем суффикс + if dest_path.exists(): + stem = filepath.stem + suffix = filepath.suffix + counter = 1 + while True: + dest_path = dest_dir / f'{stem}_{counter}{suffix}' + if not dest_path.exists(): + break + counter += 1 + + shutil.move(str(filepath), str(dest_path)) + moved += 1 + print(f' [✓] {filepath.name} → {dt.year}/{dt.month:02d}/{dt.day:02d}/') + + print() + print(f'Готово! Перемещено: {moved}, пропущено: {skipped} (без даты: {no_date})') + + +def main(): + folder = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd() + print(f'Raskidator-3000 — сортировка фотографий') + print(f'Папка: {folder.resolve()}') + print() + organize(folder) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c5eff41 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Pillow>=10.0.0 +piexif>=1.1.3 +pillow-heif>=0.16.0