#!/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_file_created_date(filepath: Path) -> datetime | None: """Возвращает дату создания файла из файловой системы (Windows: st_ctime).""" try: ts = filepath.stat().st_ctime return datetime.fromtimestamp(ts) except Exception: return None 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: dt = None date_source = None # 1. EXIF DateTimeOriginal date_str = get_exif_date(filepath) if date_str: dt = parse_date(date_str) # 2. Fallback: дата создания файла (Windows: st_ctime) if dt is None: dt = get_file_created_date(filepath) if dt: date_source = 'дата создания файла' if dt is None: skipped += 1 no_date += 1 print(f' [—] {filepath.name} — нет даты (EXIF + CreatedDate), оставлен на месте') 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 src = f' ({date_source})' if date_source else '' print(f' [✓] {filepath.name} → {dt.year}/{dt.month:02d}/{dt.day:02d}/{src}') 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()