Files
Raskidator-3000/organize.py
T

154 lines
5.1 KiB
Python

#!/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()