Source

scripts / sort_photos.py

Full commit
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Sort photos
-----------

:copyright:  (c) Andrey Mikhaylenko, 2012

"""

import os
import datetime
import filecmp

from argh import command, dispatch_command
import pyexiv2


MOVED = 1
SKIPPED = 2
REMOVED = 3
ERROR = 4

EXTENSIONS = '.jpg, .jpeg, .JPG, .JPEG'


def get_image_datetime(path):
    metadata = pyexiv2.ImageMetadata(path)
    metadata.read()

    raw_dt = None
    possible_exif_keys = 'Exif.Photo.DateTimeOriginal', 'Exif.Image.DateTime'
    for key in possible_exif_keys:
        try:
            raw_dt = metadata[key].value  # not always converted to datetime :(
        except KeyError:
            continue
        else:
            break

    if not raw_dt:
        raise KeyError(u'{0} has no date/time EXIF data. '
                       u'Tags: {1}'.format(path, sorted(metadata.exif_keys)))

    if isinstance(raw_dt, datetime.datetime):
        dt = raw_dt
    else:
        dt = datetime.datetime.strptime(raw_dt[:-2], '%Y:%m:%d %H:%M:%S')
    assert 2000 < dt.year, 'a reasonable date would be after year ~2000'
    return dt


def move_image(name, src_dir, dest_dir, dry_run=False, verbose=False,
               delete_dupes=False):
    """ Перемещает указанный файл в каталог вида: год/месяц/день.
    Дата извлекается из EXIF.

    Примерный шаблон работы::

        $src_dir/$name → $dest_dir/$year/$month/$day/$name

    TODO:

    * извлекать дату из EXIF
      * корректно обрабатывать ситуацию с отсутствием EXIF или битой датой
    * создавать каталоги (год, месяц, день), если не существуют
    * проверять, нет ли там уже файла с этим именем
      * если есть, проверять, идентично ли содержание файлов
        * если идентично, затирать исходный
        * если различается, ничего не трогать и орать о конфликте имен
      * если нет, перемещать файл туда

    """
    if verbose:
        print
        print name
    assert '/' not in name, 'filename must not contain path information'
    src = os.path.join(src_dir, name)

    # извлекаем дату
    try:
        dt = get_image_datetime(src)
    except KeyError as e:
        print '  ERROR:', e.message
        return ERROR

    # make year/month/day/ directory
    date_dirs = u'{0.year}/{0.month:02d}/{0.day:02d}'.format(dt)
    inner_dest_dir = os.path.join(dest_dir, date_dirs)
    if not dry_run:
        if not os.path.exists(inner_dest_dir):
            if verbose:
                print '  mkdir -p', inner_dest_dir
            os.makedirs(inner_dest_dir)

    dest = os.path.join(inner_dest_dir, name)
    if verbose:
        print '  trying', src, '->', dest

    # make sure the file does not exist there
    if os.path.exists(dest):
        if verbose:
            print '  the file is already there'
        if filecmp.cmp(src, dest):
            if verbose:
                print '  files are identical'
            if delete_dupes:
                if not dry_run:
                    os.remove(src)
                return REMOVED
            else:
                return SKIPPED
        else:
            print '  ERROR: files differ, name conflict:'
            print '   ', src
            print '   ', dest
            return ERROR
    else:
        if verbose:
            print '  moving'#, src, dest
        if not dry_run:
            os.rename(src, dest)
        return MOVED


@command
def move_images(src_dir, dest_dir, dry_run=False, verbose=False,
               extensions=EXTENSIONS, delete_dupes=False):
    """ Moves images from src_dir/foo.jpg to dest_dir/year/month/day/foo.jpg.
    The dates are extracted from EXIF.

    :param delete_dupes:
        Removes previously copied files instead of skipping them.

    """
    allowed_extensions = [x.strip() for x in EXTENSIONS.split(',')]
    names = sorted(os.listdir(src_dir))
    statuses = {}
    for name in names:
        _, ext = os.path.splitext(name)
        if not ext in allowed_extensions:
            continue

        if not os.path.isfile(os.path.join(src_dir, name)):
            continue

        status = move_image(name, src_dir, dest_dir,
                            dry_run=dry_run, verbose=verbose,
                            delete_dupes=delete_dupes)
        statuses.setdefault(status, 0)
        statuses[status] += 1

    print 'MOVED:', statuses.get(MOVED, 0)
    print 'ERROR:', statuses.get(ERROR, 0)
    print 'SKIPPED:', statuses.get(SKIPPED, 0)
    print 'REMOVED:', statuses.get(REMOVED, 0)


if __name__ == '__main__':
    dispatch_command(move_images)