Skip to content

Instantly share code, notes, and snippets.

@pkkid
Created April 3, 2024 23:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pkkid/488517c04bcb5fe28fff7d1905173fdc to your computer and use it in GitHub Desktop.
Save pkkid/488517c04bcb5fe28fff7d1905173fdc to your computer and use it in GitHub Desktop.
Sort Photos by Month
#!/usr/bin/python3
"""
Sort Photos by Month.
install python3-pil
"""
import logging, datetime
import argparse, os, sys
import hashlib
from PIL import Image
log = logging.getLogger()
logformat = logging.Formatter('%(asctime)s %(module)12s:%(lineno)-4s %(levelname)-9s %(message)s')
streamhandler = logging.StreamHandler(sys.stdout)
streamhandler.setFormatter(logformat)
log.addHandler(streamhandler)
log.setLevel(logging.INFO)
UNKNOWN = 'Unknown'
DESTDIR = '/media/Synology/Photos/ByMonth'
PHOTOS = ['.jpg','.png']
VIDEOS = ['.mov','.mp4']
class PhotoAlreadySorted(Exception):
pass
class SortByMonth:
def __init__(self, opts):
self.opts = opts
self.cwd = os.getcwd()
def sort_photos(self, dir=None):
dir = dir or self.cwd
# make sure were not in the destination directory
if dir.startswith(DESTDIR):
return None
# iterate all the files in this directory
for filename in sorted(os.listdir(dir)):
filepath = os.path.join(dir, filename)
name, ext = os.path.splitext(os.path.basename(filepath.lower()))
if ext in PHOTOS + VIDEOS:
self.sort_photo(filepath, ext)
if self.opts.recurse and os.path.isdir(filepath):
self.sort_photos(filepath)
if not os.listdir(dir):
log.info('Removing empty directory: %s' % dir)
os.rmdir(dir)
def sort_photo(self, filepath, ext):
try:
photohash = self.get_filehash(filepath)
photodate = self.get_photodate(filepath, ext)
newfilepath = self.get_newfilepath(filepath, photohash, photodate, ext)
self.move_photo(filepath, newfilepath)
except PhotoAlreadySorted:
log.warning('Deleting photo already sorted: %s (%s)' % (filepath, photohash))
os.unlink(filepath)
def get_photodate(self, filepath, ext):
try:
if ext in VIDEOS:
return datetime.datetime.fromtimestamp(int(os.stat(filepath).st_mtime))
exif = Image.open(filepath)._getexif()
if 36867 in exif:
return datetime.datetime.strptime(exif[36867], '%Y:%m:%d %H:%M:%S')
except Exception:
pass
return UNKNOWN
def get_filehash(self, filepath):
return hashlib.md5(open(filepath, 'rb').read()).hexdigest()
def get_newfilepath(self, filepath, photohash, photodate, ext):
count = 0
# Different algorythm if we dont know the date
if photodate == UNKNOWN:
filename = os.path.splitext(os.path.basename(filepath.lower()))[0]
newfilepath = '%s/Unknown/%s%s' % (DESTDIR, filename, ext)
while os.path.exists(newfilepath):
if self.get_filehash(newfilepath) == photohash:
raise PhotoAlreadySorted()
count += 1
newfilepath = '%s/Unknown/%s-%s%s' % (DESTDIR, filename, count, ext)
return newfilepath
# Rename to YYYY-MM/YYYY-MM-DD-HHMMSS.jpg
monthstr = photodate.strftime('%Y-%m')
monthstr = 'Videos/%s' % monthstr if ext in VIDEOS else monthstr
datetimestr = photodate.strftime('%Y-%m-%d-%H%M%S')
newfilepath = '%s/%s/%s%s' % (DESTDIR, monthstr, datetimestr, ext)
while os.path.exists(newfilepath):
if self.get_filehash(newfilepath) == photohash:
raise PhotoAlreadySorted()
count += 1
newfilepath = '%s/%s/%s-%s%s' % (DESTDIR, monthstr, datetimestr, count, ext)
return newfilepath
def move_photo(self, filepath, newfilepath):
log.info('Moving %s -> %s' % (filepath, newfilepath))
os.makedirs(os.path.dirname(newfilepath), exist_ok=True)
os.rename(filepath, newfilepath)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='sort photos by month')
parser.add_argument('-r', '--recurse', default=False, action='store_true', help='recurse directories.')
opts = parser.parse_args()
SortByMonth(opts).sort_photos()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment