diff --git a/README.md b/README.md index 1c78df4..cb03e3f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ -# photo-dater +# PhotoOrganizer_FileName -Date photos based on name of file \ No newline at end of file +Organizes photos based on file name. +- File name must be of format yyyyMMdd_xx.jpg + - Where xx is an index +- Sets exif data +- Puts into a pre specified directory structure + diff --git a/pytest/example_pics/u_001.jpg b/pytest/example_pics/u_001.jpg new file mode 100755 index 0000000..09c16c2 Binary files /dev/null and b/pytest/example_pics/u_001.jpg differ diff --git a/pytest/example_pics/u_002.jpg b/pytest/example_pics/u_002.jpg new file mode 100755 index 0000000..a268687 Binary files /dev/null and b/pytest/example_pics/u_002.jpg differ diff --git a/pytest/fixtures.py b/pytest/fixtures.py new file mode 100644 index 0000000..19304b9 --- /dev/null +++ b/pytest/fixtures.py @@ -0,0 +1,43 @@ + +import os +import pytest + +from helpers import * + +log_file_name = 'example.log' + +@pytest.fixture(autouse=True) +def cleanup_logs(): + if os.path.exists(log_file_name): + os.remove(log_file_name) + yield + if os.path.exists(log_file_name): + os.remove(log_file_name) + +@pytest.fixture +def tmp_dir(): + tmp_dir_name = gen_rand_name(12) + tmp_dir_path = os.path.join("/tmp/photosorter/", tmp_dir_name) + os.makedirs(tmp_dir_path, exist_ok=True) + return tmp_dir_path + +@pytest.fixture +def log_file(): + return log_file_name + +@pytest.fixture +def in_out_dirs(tmp_dir): + in_dir_name = gen_rand_name(12) + out_dir_name = gen_rand_name(12) + + while out_dir_name == in_dir_name: + out_dir_name = gen_rand_name(12) + + in_dir_path = os.path.join(tmp_dir, in_dir_name) + out_dir_path = os.path.join(tmp_dir, out_dir_name) + + os.mkdir(in_dir_path) + os.mkdir(out_dir_path) + + return (in_dir_path, out_dir_path) + diff --git a/pytest/helpers.py b/pytest/helpers.py new file mode 100644 index 0000000..9042963 --- /dev/null +++ b/pytest/helpers.py @@ -0,0 +1,25 @@ +import random +import string +import subprocess + +def run_successfull_cmd(command): + command = command.split() + output = subprocess.run(command, capture_output=True, encoding='UTF-8') + assert output.returncode == 0 + return output.stdout + +def gen_rand_name(max_name_len): + name_len = random.randint(3, max_name_len) + return ''.join(random.choice(string.ascii_letters) for x in range(name_len)) + +def check_log_string(log_file, search_str): + lines = '' + with open(log_file, 'r') as my_log: + lines = my_log.readlines() + for line in lines: + if search_str in line: + return True + + print(lines) + return False + diff --git a/pytest/test_bad_files.py b/pytest/test_bad_files.py new file mode 100644 index 0000000..8cdc910 --- /dev/null +++ b/pytest/test_bad_files.py @@ -0,0 +1,68 @@ + +import os +import pytest +import random +import shutil + +from pathlib import Path + +from helpers import * +from fixtures import * + +cmd = 'python3 updatescannedpics.py' + +def test_bad_filename(in_out_dirs, log_file): + in_dir, out_dir = in_out_dirs + + file_name = gen_rand_name(12) + ".jpg" + Path(os.path.join(in_dir, file_name)).touch() + + command = f'{cmd} -i {in_dir} -o {out_dir}' + output = run_successfull_cmd(command) + + expected = f"File name not compatable: {file_name}" + assert check_log_string(log_file, expected) + +def test_bad_jpg_format(in_out_dirs, log_file): + in_dir, out_dir = in_out_dirs + + year = random.randint(1900, 2000) + month = random.randint(1, 12) + day = random.randint(1, 28) + index = random.randint(0, 99) + + file_name = f"{year}{month:02d}{day:02d}_{index:02d}.jpg" + Path(os.path.join(in_dir, file_name)).touch() + + command = f'{cmd} -i {in_dir} -o {out_dir}' + output = run_successfull_cmd(command) + + expected = f"Bad file format for {file_name}" + assert check_log_string(log_file, expected) + + +def test_bad_jpg_year(in_out_dirs, log_file): + in_dir, out_dir = in_out_dirs + + year_low = random.randint(1000, 1799) + year_high = random.randint(2010, 2100) + month = random.randint(1, 12) + day = random.randint(1, 28) + index = random.randint(0, 99) + + file_name_high = f"{year_high}{month:02d}{day:02d}_{index:02d}.jpg" + file_name_low = f"{year_low}{month:02d}{day:02d}_{index:02d}.jpg" + + shutil.copyfile("pytest/example_pics/u_001.jpg", f"{in_dir}/{file_name_high}") + shutil.copyfile("pytest/example_pics/u_002.jpg", f"{in_dir}/{file_name_low}") + + command = f'{cmd} -m -i {in_dir} -o {out_dir}' + output = run_successfull_cmd(command) + + expected = [ + f"Year too great: {year_high}", + f"Year too early: {year_low}" + ] + + for exp in expected: + assert check_log_string(log_file, exp) diff --git a/pytest/test_cmd_arguments.py b/pytest/test_cmd_arguments.py new file mode 100644 index 0000000..be80db8 --- /dev/null +++ b/pytest/test_cmd_arguments.py @@ -0,0 +1,43 @@ + +import pytest +import random +import string +import subprocess + +from helpers import * +from fixtures import * + +cmd = 'python3 updatescannedpics.py' + +@pytest.mark.parametrize("cmd_line", ["-h", "--help"]) +def test_help(cmd_line): + command = f'{cmd} {cmd_line}'.split() + output = subprocess.run(command, capture_output=True, encoding='UTF-8') + + assert output.returncode == 1 + assert output.stdout == "-i Input Directory\n-o Output Directory\n-m My Pre-Configured Directory Layout\nDefaults directory to PWD\n" + +def test_bad_param(): + bad_options = [char for char in string.ascii_letters] + bad_options.remove('h') + bad_options.remove('i') + bad_options.remove('o') + bad_options.remove('m') + + rand_bad_opt = random.choice(bad_options) + command = f'{cmd} -{rand_bad_opt}'.split() + output = subprocess.run(command, capture_output=True, encoding='UTF-8') + + assert output.returncode == 2 + assert output.stdout == f"option -{rand_bad_opt} not recognized\n-i Input Directory\n-o Output Directory\n-m My Pre-Configured Directory Layout\nDefaults directory to PWD\n" + +@pytest.mark.parametrize("cmd_line, expected", [("", "Not Using"), ("-m", "Using")]) +def test_my_format(cmd_line, expected, log_file): + command = f'{cmd} {cmd_line}' + run_successfull_cmd(command) + + with open(log_file, 'r') as my_log: + lines = my_log.readlines() + assert f"{expected} pre formated directories" in lines[2] + + diff --git a/pytest/test_file_paths.py b/pytest/test_file_paths.py new file mode 100644 index 0000000..d34acf5 --- /dev/null +++ b/pytest/test_file_paths.py @@ -0,0 +1,36 @@ + +import os +import pytest +import random + +from helpers import * +from fixtures import * + +cmd = 'python3 updatescannedpics.py' + +def test_file_path(log_file): + command = f'{cmd}' + run_successfull_cmd(command) + + with open(log_file, 'r') as my_log: + lines = my_log.readlines() + assert "Input Dir = ." in lines[0] + assert "Output Dir = ." in lines[1] + assert "Not Using pre formated directories" in lines[2] + +@pytest.mark.parametrize("option, log_start", [("-i", "Input Dir = "), ("--indir", "Input Dir = "), ("-o", "Output Dir = "), ("--outdir", "Output Dir = ")]) +@pytest.mark.parametrize("in_dir_start", ["", "./", "/", "../"]) +def test_set_indir(option, log_start, in_dir_start, log_file): + indir_name = gen_rand_name(12) + + command = f'{cmd} {option} {in_dir_start}{indir_name}' + run_successfull_cmd(command) + + expected_start = in_dir_start + if len(in_dir_start) == 0: + expected_start = './' + + expected_file_path = f"{log_start}{expected_start}{indir_name}" + + assert check_log_string(log_file, expected_file_path) + diff --git a/pytest/test_proper_copy.py b/pytest/test_proper_copy.py new file mode 100644 index 0000000..8aad683 --- /dev/null +++ b/pytest/test_proper_copy.py @@ -0,0 +1,69 @@ +from datetime import datetime + +import os +import time +import piexif +import pytest +import random +import shutil + +from helpers import * +from fixtures import * + +cmd = 'python3 updatescannedpics.py' + +def test_diff_in_out(in_out_dirs): + in_dir, out_dir = in_out_dirs + + year = random.randint(1900, 2000) + month = random.randint(1, 12) + day = random.randint(1, 28) + index = random.randint(0, 99) + + file_name = f"{year}{month:02d}{day:02d}_{index:02d}.jpg" + in_file = os.path.join(in_dir, file_name) + out_file = os.path.join(out_dir, file_name) + + shutil.copyfile("pytest/example_pics/u_001.jpg", in_file) + + command = f"{cmd} -i {in_dir} -o {out_dir}" + print(command) + output = run_successfull_cmd(command) + + exif_dict = piexif.load(out_file) + + test_date = datetime(year, month, day, 0, 0, 0) + test_date_str = test_date.strftime("%Y:%m:%d %H:%M:%S") + + assert test_date_str == exif_dict["0th"][piexif.ImageIFD.DateTime].decode('utf-8') + assert test_date_str == exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode('utf-8') + assert test_date_str == exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode('utf-8') + +def test_same_in_out(in_out_dirs): + in_dir, out_dir = in_out_dirs + out_dir = in_dir + + year = random.randint(1900, 2000) + month = random.randint(1, 12) + day = random.randint(1, 28) + index = random.randint(0, 99) + + file_name = f"{year}{month:02d}{day:02d}_{index:02d}.jpg" + in_file = out_file = os.path.join(in_dir, file_name) + + shutil.copyfile("pytest/example_pics/u_001.jpg", in_file) + + command = f"{cmd} -i {in_dir} -o {out_dir}" + print(command) + output = run_successfull_cmd(command) + + exif_dict = piexif.load(out_file) + + test_date = datetime(year, month, day, 0, 0, 0) + test_date_str = test_date.strftime("%Y:%m:%d %H:%M:%S") + + assert test_date_str == exif_dict["0th"][piexif.ImageIFD.DateTime].decode('utf-8') + assert test_date_str == exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode('utf-8') + assert test_date_str == exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal].decode('utf-8') + + diff --git a/updatescannedpics.py b/updatescannedpics.py new file mode 100644 index 0000000..462608d --- /dev/null +++ b/updatescannedpics.py @@ -0,0 +1,136 @@ +# Adding comment + +from datetime import datetime + +import os +import re +import sys +import glob +import time +import getopt +import piexif +import shutil +import logging + +help_str = "-i Input Directory\n-o Output Directory\n-m My Pre-Configured Directory Layout\nDefaults directory to PWD" + +logging.basicConfig(filename='example.log', level=logging.INFO) + +try: + opts, args = getopt.getopt(sys.argv[1:], "hi:o:m", ["help", "indir=", "outdir="]) +except getopt.GetoptError as err: + print(str(err) + f"\n{help_str}") + sys.exit(2) + +indir = "." +outdir = "." +myformat = False +for o, a in opts: + if o in ("-h", "--help"): + print(help_str) + exit(1) + elif o in ("-i", "--indir"): + if a.find('/') == 0 or a.find("./") == 0 or a.find("../") == 0: + indir = a + else: + indir = "./" + a + elif o in("-o", "--outdir"): + if a.find('/') == 0 or a.find("./") == 0 or a.find("../") == 0: + outdir = a + else: + outdir = "./" + a + elif o == "-m": + myformat = True + +logging.info("Input Dir = " + indir) +logging.info("Output Dir = " + outdir) +logging.info(("Using" if myformat else "Not Using") + " pre formated directories") + +folderDate0 = datetime(1800, 1, 1, 0, 0, 0) +folderDate1 = datetime(1900, 1, 1, 0, 0, 0) +folderDate2 = datetime(1950, 1, 1, 0, 0, 0) +folderDate3 = datetime(1980, 1, 1, 0, 0, 0) +folderDate4 = datetime(1990, 1, 1, 0, 0, 0) +folderDate5 = datetime(1995, 1, 1, 0, 0, 0) +folderDate6 = datetime(2000, 1, 1, 0, 0, 0) +folderDate7 = datetime(2010, 1, 1, 0, 0, 0) + +for fullpath in glob.glob(indir + "/*.jpg"): + logging.info("==========") + logging.info("fullpath: " + fullpath) + + filename = os.path.basename(fullpath) + + try: + namedate = filename.split("_")[0] + date = datetime.strptime(namedate, "%Y%m%d") + strdate = date.strftime("%Y:%m:%d %H:%M:%S") + except Exception as err: + logging.error(f"File name not compatable: {filename} :: {err}") + continue + + try: + exif_dict = piexif.load(fullpath) + logging.debug(exif_dict) + + exif_dict["0th"][piexif.ImageIFD.DateTime]=strdate + exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal]=strdate + exif_dict["Exif"][piexif.ExifIFD.DateTimeDigitized]=strdate + + exif_bytes=piexif.dump(exif_dict) + + piexif.insert(exif_bytes, fullpath) + + modTime = time.mktime(date.timetuple()) + os.utime(fullpath, (modTime, modTime)) + + exif_dict = piexif.load(fullpath) + logging.debug(exif_dict) + + copy = False + endyear = 2006 + foldername = outdir + if myformat: + if date > folderDate7: + logging.error("Year too great: " + str(date.year)) + continue + elif date < folderDate0: + logging.error("Year too early: " + str(date.year)) + continue + + if date < folderDate1: + foldername = "1800-1899" + elif date < folderDate2: + foldername = "1900-1949" + elif date < folderDate3: + foldername = "1950-1979" + elif date < folderDate4: + foldername = "1980-1990" + elif date < folderDate5: + foldername = "1990-1994" + elif date < folderDate6: + foldername = "1995-1999" + else: + foldername = date.year + + foldername = foldername + str(foldername) + + logging.info(f"Outfolder: {foldername}") + + if not os.path.isdir(foldername): + os.mkdir(foldername) + + dest = foldername + "/" + filename + + while os.path.exists(dest): + parts = re.split("[_\.]", filename) + parts[1] = int(parts[1]) + 1 + filename = "%s_%02d.%s" % (parts[0], parts[1], parts[2]) + dest = foldername + "/" + filename + + shutil.copyfile(fullpath, dest) + os.utime(dest, (modTime, modTime)) + except Exception as err: + logging.error(f"Bad file format for {filename} :: {err}") + continue +