diff --git a/dozo/exiftool.py b/dozo/exiftool.py index 67b3a17..1a314b4 100644 --- a/dozo/exiftool.py +++ b/dozo/exiftool.py @@ -20,6 +20,12 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF) EXIFTOOL_PROCESSES = [] +def exiftool_is_running(): + ps = subprocess.run(["ps"], capture_output=True) + stdout = ps.stdout.decode("utf-8") + return "exiftool" in stdout + + @atexit.register def terminate_exiftool(): """Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool """ diff --git a/samples/images/IMG_0476_2.CR2 b/samples/images/IMG_0476_2.CR2 new file mode 100644 index 0000000..0d6c08a Binary files /dev/null and b/samples/images/IMG_0476_2.CR2 differ diff --git a/samples/images/IMG_0670B_NOGPS.MOV b/samples/images/IMG_0670B_NOGPS.MOV new file mode 100644 index 0000000..65ba0db Binary files /dev/null and b/samples/images/IMG_0670B_NOGPS.MOV differ diff --git a/samples/images/IMG_1064.jpeg b/samples/images/IMG_1064.jpeg new file mode 100644 index 0000000..829a234 Binary files /dev/null and b/samples/images/IMG_1064.jpeg differ diff --git a/samples/images/IMG_1693.tif b/samples/images/IMG_1693.tif new file mode 100644 index 0000000..6965441 Binary files /dev/null and b/samples/images/IMG_1693.tif differ diff --git a/samples/images/IMG_1994.JPG b/samples/images/IMG_1994.JPG new file mode 100755 index 0000000..c187281 Binary files /dev/null and b/samples/images/IMG_1994.JPG differ diff --git a/samples/images/IMG_1994.cr2 b/samples/images/IMG_1994.cr2 new file mode 100755 index 0000000..6cd12fb Binary files /dev/null and b/samples/images/IMG_1994.cr2 differ diff --git a/samples/images/IMG_1997.JPG b/samples/images/IMG_1997.JPG new file mode 100755 index 0000000..143e953 Binary files /dev/null and b/samples/images/IMG_1997.JPG differ diff --git a/samples/images/IMG_1997.cr2 b/samples/images/IMG_1997.cr2 new file mode 100755 index 0000000..f2adc62 Binary files /dev/null and b/samples/images/IMG_1997.cr2 differ diff --git a/samples/images/IMG_3092.heic b/samples/images/IMG_3092.heic new file mode 100644 index 0000000..8f10381 Binary files /dev/null and b/samples/images/IMG_3092.heic differ diff --git a/samples/images/IMG_3984.jpeg b/samples/images/IMG_3984.jpeg new file mode 100644 index 0000000..ee8fcaf Binary files /dev/null and b/samples/images/IMG_3984.jpeg differ diff --git a/samples/images/IMG_9975.jpeg b/samples/images/IMG_9975.jpeg new file mode 100644 index 0000000..9e7d855 Binary files /dev/null and b/samples/images/IMG_9975.jpeg differ diff --git a/samples/images/Pumkins1.jpg b/samples/images/Pumkins1.jpg new file mode 100644 index 0000000..3748a6b Binary files /dev/null and b/samples/images/Pumkins1.jpg differ diff --git a/samples/images/Pumkins2.jpg b/samples/images/Pumkins2.jpg new file mode 100644 index 0000000..4d88368 Binary files /dev/null and b/samples/images/Pumkins2.jpg differ diff --git a/samples/images/Pumpkins3.jpg b/samples/images/Pumpkins3.jpg new file mode 100644 index 0000000..775207c Binary files /dev/null and b/samples/images/Pumpkins3.jpg differ diff --git a/samples/images/Pumpkins4.jpg b/samples/images/Pumpkins4.jpg new file mode 100644 index 0000000..b8e8f8e Binary files /dev/null and b/samples/images/Pumpkins4.jpg differ diff --git a/samples/images/St James Park.jpg b/samples/images/St James Park.jpg new file mode 100644 index 0000000..494d60d Binary files /dev/null and b/samples/images/St James Park.jpg differ diff --git a/samples/images/St James Park_edited.jpg b/samples/images/St James Park_edited.jpg new file mode 100644 index 0000000..088893a Binary files /dev/null and b/samples/images/St James Park_edited.jpg differ diff --git a/samples/images/Tulips.jpg b/samples/images/Tulips.jpg new file mode 100644 index 0000000..3f09e25 Binary files /dev/null and b/samples/images/Tulips.jpg differ diff --git a/samples/images/badimage.jpeg b/samples/images/badimage.jpeg new file mode 100644 index 0000000..62899a8 Binary files /dev/null and b/samples/images/badimage.jpeg differ diff --git a/samples/images/exiftool_warning.heic b/samples/images/exiftool_warning.heic new file mode 100644 index 0000000..8f10381 Binary files /dev/null and b/samples/images/exiftool_warning.heic differ diff --git a/samples/images/faceinfo/141137669_1c98c16119_b.jpg b/samples/images/faceinfo/141137669_1c98c16119_b.jpg new file mode 100644 index 0000000..a523898 Binary files /dev/null and b/samples/images/faceinfo/141137669_1c98c16119_b.jpg differ diff --git a/samples/images/faceinfo/15895161088_a12290f781_k.jpg b/samples/images/faceinfo/15895161088_a12290f781_k.jpg new file mode 100644 index 0000000..225cacb Binary files /dev/null and b/samples/images/faceinfo/15895161088_a12290f781_k.jpg differ diff --git a/samples/images/faceinfo/23309711462_a0a93a94fc_o.jpg b/samples/images/faceinfo/23309711462_a0a93a94fc_o.jpg new file mode 100644 index 0000000..6511655 Binary files /dev/null and b/samples/images/faceinfo/23309711462_a0a93a94fc_o.jpg differ diff --git a/samples/images/faceinfo/2403994289_04f3ed0ec3_k.jpg b/samples/images/faceinfo/2403994289_04f3ed0ec3_k.jpg new file mode 100644 index 0000000..81019a2 Binary files /dev/null and b/samples/images/faceinfo/2403994289_04f3ed0ec3_k.jpg differ diff --git a/samples/images/faceinfo/363449752_2bced341c5_h.jpg b/samples/images/faceinfo/363449752_2bced341c5_h.jpg new file mode 100644 index 0000000..e48e781 Binary files /dev/null and b/samples/images/faceinfo/363449752_2bced341c5_h.jpg differ diff --git a/samples/images/faceinfo/3809603052_5c7b07c2a9_k.jpg b/samples/images/faceinfo/3809603052_5c7b07c2a9_k.jpg new file mode 100644 index 0000000..4f5c41b Binary files /dev/null and b/samples/images/faceinfo/3809603052_5c7b07c2a9_k.jpg differ diff --git a/samples/images/faceinfo/399012465_0b39739ebe_h.jpg b/samples/images/faceinfo/399012465_0b39739ebe_h.jpg new file mode 100644 index 0000000..f0903d5 Binary files /dev/null and b/samples/images/faceinfo/399012465_0b39739ebe_h.jpg differ diff --git a/samples/images/faceinfo/45207044465_7a4bdde206_k.jpg b/samples/images/faceinfo/45207044465_7a4bdde206_k.jpg new file mode 100644 index 0000000..004dfd2 Binary files /dev/null and b/samples/images/faceinfo/45207044465_7a4bdde206_k.jpg differ diff --git a/samples/images/faceinfo/5117042252_97f6f6092f_k.jpg b/samples/images/faceinfo/5117042252_97f6f6092f_k.jpg new file mode 100644 index 0000000..3f895eb Binary files /dev/null and b/samples/images/faceinfo/5117042252_97f6f6092f_k.jpg differ diff --git a/samples/images/faceinfo/5388153693_eb7efc7641_k.jpg b/samples/images/faceinfo/5388153693_eb7efc7641_k.jpg new file mode 100644 index 0000000..a82aaf9 Binary files /dev/null and b/samples/images/faceinfo/5388153693_eb7efc7641_k.jpg differ diff --git a/samples/images/faceinfo/6422472677_515e308d9e_o.jpg b/samples/images/faceinfo/6422472677_515e308d9e_o.jpg new file mode 100644 index 0000000..64ebdf2 Binary files /dev/null and b/samples/images/faceinfo/6422472677_515e308d9e_o.jpg differ diff --git a/samples/images/faceinfo/6969547134_c49b0ae563_k.jpg b/samples/images/faceinfo/6969547134_c49b0ae563_k.jpg new file mode 100644 index 0000000..9af1857 Binary files /dev/null and b/samples/images/faceinfo/6969547134_c49b0ae563_k.jpg differ diff --git a/samples/images/faceinfo/8703591799_f95674e2a9_k.jpg b/samples/images/faceinfo/8703591799_f95674e2a9_k.jpg new file mode 100644 index 0000000..9e018f1 Binary files /dev/null and b/samples/images/faceinfo/8703591799_f95674e2a9_k.jpg differ diff --git a/samples/images/faceinfo/exif1.jpg b/samples/images/faceinfo/exif1.jpg new file mode 100644 index 0000000..e52a32b Binary files /dev/null and b/samples/images/faceinfo/exif1.jpg differ diff --git a/samples/images/faceinfo/exif3.jpg b/samples/images/faceinfo/exif3.jpg new file mode 100644 index 0000000..77c6667 Binary files /dev/null and b/samples/images/faceinfo/exif3.jpg differ diff --git a/samples/images/faceinfo/exif6.jpg b/samples/images/faceinfo/exif6.jpg new file mode 100644 index 0000000..7bd1f38 Binary files /dev/null and b/samples/images/faceinfo/exif6.jpg differ diff --git a/samples/images/faceinfo/exif8.jpg b/samples/images/faceinfo/exif8.jpg new file mode 100644 index 0000000..736063d Binary files /dev/null and b/samples/images/faceinfo/exif8.jpg differ diff --git a/samples/images/placeinfo/IMG_1523.jpg b/samples/images/placeinfo/IMG_1523.jpg new file mode 100644 index 0000000..0be47a2 Binary files /dev/null and b/samples/images/placeinfo/IMG_1523.jpg differ diff --git a/samples/images/placeinfo/IMG_1523_no_location.jpg b/samples/images/placeinfo/IMG_1523_no_location.jpg new file mode 100644 index 0000000..b023590 Binary files /dev/null and b/samples/images/placeinfo/IMG_1523_no_location.jpg differ diff --git a/samples/images/placeinfo/IMG_1760.jpg b/samples/images/placeinfo/IMG_1760.jpg new file mode 100644 index 0000000..5a8b20a Binary files /dev/null and b/samples/images/placeinfo/IMG_1760.jpg differ diff --git a/samples/images/placeinfo/IMG_1760_Seattle.jpg b/samples/images/placeinfo/IMG_1760_Seattle.jpg new file mode 100644 index 0000000..d246cd5 Binary files /dev/null and b/samples/images/placeinfo/IMG_1760_Seattle.jpg differ diff --git a/samples/images/placeinfo/IMG_3439.jpg b/samples/images/placeinfo/IMG_3439.jpg new file mode 100644 index 0000000..6b504d2 Binary files /dev/null and b/samples/images/placeinfo/IMG_3439.jpg differ diff --git a/samples/images/placeinfo/IMG_4547.jpg b/samples/images/placeinfo/IMG_4547.jpg new file mode 100644 index 0000000..ca03f66 Binary files /dev/null and b/samples/images/placeinfo/IMG_4547.jpg differ diff --git a/samples/images/screenshot-really-a-png.jpeg b/samples/images/screenshot-really-a-png.jpeg new file mode 100644 index 0000000..5846efe Binary files /dev/null and b/samples/images/screenshot-really-a-png.jpeg differ diff --git a/samples/images/wedding.jpg b/samples/images/wedding.jpg new file mode 100644 index 0000000..5f29bd3 Binary files /dev/null and b/samples/images/wedding.jpg differ diff --git a/samples/images/wedding_edited.jpg b/samples/images/wedding_edited.jpg new file mode 100644 index 0000000..88caa53 Binary files /dev/null and b/samples/images/wedding_edited.jpg differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..53025e0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +""" pytest test configuration """ + +import pytest + +from dozo.exiftool import _ExifToolProc + + +@pytest.fixture(autouse=True) +def reset_singletons(): + """ Need to clean up any ExifTool singletons between tests """ + _ExifToolProc.instance = None + + diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..60dedf2 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,75 @@ +from configparser import RawConfigParser +from pathlib import Path +import pytest +import shutil +import tempfile +from unittest import mock + +from dozo import config + +# Helpers +import random +import string + +def random_char(y): + return ''.join(random.choice(string.printable) for x in range(y)) + +def write_random_file(file_path): + with open(file_path, 'w') as conf_file: + conf_file.write(random_char(20)) + +class TestConfig: + + @pytest.fixture(scope="module") + def conf_path(self): + tmp_path = tempfile.mkdtemp(prefix='dozo-') + yield Path(tmp_path, "dozo.conf") + shutil.rmtree(tmp_path) + + @pytest.fixture(scope="module") + def conf(self, conf_path): + return config.load_config(conf_path) + + def test_write(self, conf_path): + conf = RawConfigParser() + conf['Path'] = { + 'day_begins': '4', + 'dirs_path':'%u{%Y-%m}/{city}|{city}-{%Y}/{folders[:1]}/{folder}', + 'name':'{%Y-%m-%b-%H-%M-%S}-{basename}.%l{ext}' + } + conf['Geolocation'] = { + 'geocoder': 'Nominatium' + } + + config.write(conf_path, conf) + assert conf_path.is_file() + + def test_load_config(self, conf): + """ + Read files from config and return variables + """ + # test valid config file + assert conf['Path']['dirs_path'] == '%u{%Y-%m}/{city}|{city}-{%Y}/{folders[:1]}/{folder}' + assert conf['Path']['name'] == '{%Y-%m-%b-%H-%M-%S}-{basename}.%l{ext}' + assert conf['Path']['day_begins'] == '4' + assert conf['Geolocation']['geocoder'] == 'Nominatium' + + def test_load_config_no_exist(self): + # test file not exist + conf = config.load_config('filename') + assert conf == {} + + def test_load_config_invalid(self, conf_path): + # test invalid config + write_random_file(conf_path) + with pytest.raises(Exception) as e: + config.load_config(conf_path) + assert e.typename == 'MissingSectionHeaderError' + + def test_get_path_definition(self, conf): + """ + Get path definition from config + """ + path = config.get_path_definition(conf) + assert path == '%u{%Y-%m}/{city}|{city}-{%Y}/{folders[:1]}/{folder}/{%Y-%m-%b-%H-%M-%S}-{basename}.%l{ext}' + diff --git a/tests/test_dozo.py b/tests/test_dozo.py new file mode 100644 index 0000000..b2765c5 --- /dev/null +++ b/tests/test_dozo.py @@ -0,0 +1,19 @@ +import pytest + +CONTENT = "content" + +class TestDozo: + @pytest.mark.skip() + def test__sort(self): + assert 0 + +def test_needsfiles(tmpdir): + assert tmpdir + +def test_create_file(tmp_path): + d = tmp_path / "sub" + d.mkdir() + p = d / "hello.txt" + p.write_text(CONTENT) + assert p.read_text() == CONTENT + assert len(list(tmp_path.iterdir())) == 1 diff --git a/tests/test_exiftool.py b/tests/test_exiftool.py index cdfe261..c2e8285 100644 --- a/tests/test_exiftool.py +++ b/tests/test_exiftool.py @@ -1,6 +1,5 @@ import json import pytest -import subprocess import dozo.exiftool from dozo.exiftool import get_exiftool_path @@ -178,15 +177,11 @@ def test_exiftool_terminate(): """ Test that exiftool process is terminated when exiftool.terminate() is called """ exif1 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD) - ps = subprocess.run(["ps"], capture_output=True) - stdout = ps.stdout.decode("utf-8") - assert "exiftool" in stdout + assert dozo.exiftool.exiftool_is_running() dozo.exiftool.terminate_exiftool() - ps = subprocess.run(["ps"], capture_output=True) - stdout = ps.stdout.decode("utf-8") - assert "exiftool" not in stdout + assert not dozo.exiftool.exiftool_is_running() # verify we can create a new instance after termination exif2 = dozo.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD) diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 0000000..6e4afbf --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,205 @@ +# TODO to be removed later +from datetime import datetime +import os +import pytest +from pathlib import Path +import re +import shutil +from sys import platform +import tempfile +from time import sleep + +from dozo import constants +from dozo.database import Db +from dozo.filesystem import FileSystem +from dozo.media.media import Media +from dozo.exiftool import ExifToolCaching, exiftool_is_running, terminate_exiftool + + +DOZO_PATH = Path(__file__).parent.parent + + +@pytest.mark.skip() +class TestDb: + pass + +class TestFilesystem: + def setup_class(cls): + cls.SRCPATH = tempfile.mkdtemp(prefix='dozo-src') + filenames = ['photo.png', 'plain.jpg', 'text.txt', 'withoutextension', + 'no-exif.jpg'] + cls.FILE_PATHS = set() + for filename in filenames: + source_path = Path(cls.SRCPATH, filename) + file_path = Path(DOZO_PATH, 'samples', filename) + shutil.copyfile(file_path, source_path) + cls.FILE_PATHS.add(source_path) + cls.path_format = constants.default_path + '/' + constants.default_name + + def teardown_class(self): + terminate_exiftool() + assert not exiftool_is_running() + + def test_get_part(self): + """ + Test all parts + """ + # Item to search for: + filesystem = FileSystem() + + items = filesystem.get_items() + masks = [ + '{album}', + '{basename}', + '{camera_make}', + '{camera_model}', + '{city}', + '{"custom"}', + '{country}', + '{ext}', + '{folder}', + '{folders[1:3]}', + '{location}', + '{name}', + '{original_name}', + '{state}', + '{title}', + '{%Y-%m-%d}', + '{%Y-%m-%d_%H-%M-%S}', + '{%Y-%m-%b}' + ] + + media = Media() + exif_tags = { + 'album': media.album_keys, + 'camera_make': media.camera_make_keys, + 'camera_model': media.camera_model_keys, + # 'date_original': media.date_original, + # 'date_created': media.date_created, + # 'date_modified': media.date_modified, + 'latitude': media.latitude_keys, + 'longitude': media.longitude_keys, + 'original_name': [media.original_name_key], + 'title': media.title_keys + } + + subdirs = Path('a', 'b', 'c', 'd') + + for file_path in self.FILE_PATHS: + media = Media(str(file_path)) + exif_data = ExifToolCaching(str(file_path)).asdict() + metadata = media.get_metadata() + + for item, regex in items.items(): + for mask in masks: + matched = re.search(regex, mask) + if matched: + part = filesystem.get_part(item, mask[1:-1], metadata, + {}, subdirs) + # check if part is correct + assert isinstance(part, str) + expected_part = '' + if item == 'basename': + expected_part = file_path.stem + elif item == 'date': + assert datetime.strptime(part, mask[1:-1]) + expected_part = part + elif item == 'folder': + expected_part = subdirs.name + elif item == 'folders': + if platform == "win32": + assert '\\' in part + else: + assert '/' in part + expected_part = part + elif item == 'ext': + expected_part = file_path.suffix[1:] + if item == 'name': + expected_part = file_path.stem + for i, rx in filesystem.match_date_from_string(expected_part): + expected_part = re.sub(rx, '', expected_part) + elif item == 'custom': + expected_part = mask[2:-2] + elif item in ('city', 'country', 'location', 'state'): + expected_part = part + elif item in exif_tags.keys(): + f = False + for key in exif_tags[item]: + if key in exif_data: + f = True + expected_part = exif_data[key] + break + if f == False: + expected_part = '' + + assert part == expected_part + + + def test_get_date_taken(self): + filesystem = FileSystem() + for file_path in self.FILE_PATHS: + exif_data = ExifToolCaching(str(file_path)).asdict() + media = Media(str(file_path)) + metadata = media.get_metadata() + date_taken = filesystem.get_date_taken(metadata) + + dates = {} + for key, date in ('original', media.date_original), ('created', + media.date_created), ('modified', media.date_modified): + dates[key] = media.get_date_attribute(date) + + if media.original_name_key in exif_data: + date_filename = filesystem.get_date_from_string( + exif_data[media.original_name_key]) + else: + date_filename = filesystem.get_date_from_string(file_path.name) + + if dates['original']: + assert date_taken == dates['original'] + elif date_filename: + assert date_taken == date_filename + elif dates['created']: + assert date_taken == dates['created'] + elif dates['modified']: + assert date_taken == dates['modified'] + + def test_sort_files(self, tmp_path): + db = Db(tmp_path) + filesystem = FileSystem(path_format=self.path_format) + + summary, has_errors = filesystem.sort_files([self.SRCPATH], tmp_path, db) + + # Summary is created and there is no errors + assert summary, summary + assert not has_errors, has_errors + + # TODO check if path follow path_format + + # TODO make another class? + def test_sort_file(self, tmp_path): + + for mode in 'copy', 'move': + filesystem = FileSystem(path_format=self.path_format, mode=mode) + # copy mode + src_path = Path(self.SRCPATH, 'photo.png') + dest_path = Path(tmp_path,'photo_copy.png') + src_checksum = filesystem.checksum(src_path) + result_copy = filesystem.sort_file(src_path, dest_path) + assert result_copy + # Ensure files remain the same + assert filesystem.checkcomp(dest_path, src_checksum) + + if mode == 'copy': + assert src_path.exists() + else: + assert not src_path.exists() + + # TODO check for conflicts + + + # TODO check date + +# filesystem.sort_files +#- Sort similar images into a directory +# filesystem.sort_similar + diff --git a/tests/test_media.py b/tests/test_media.py new file mode 100644 index 0000000..886e36f --- /dev/null +++ b/tests/test_media.py @@ -0,0 +1,44 @@ + +import pytest +from pathlib import Path +import shutil +import tempfile + +from dozo import constants +from dozo.media.media import Media +from dozo.media.audio import Audio +from dozo.media.photo import Photo +from dozo.media.video import Video +from dozo.exiftool import ExifToolCaching + +DOZO_PATH = Path(__file__).parent.parent + +class TestMetadata: + + def setup_class(cls): + cls.SRCPATH = tempfile.mkdtemp(prefix='dozo-src') + filenames = ['invalid.jpg', 'photo.png', 'plain.jpg', 'text.txt', 'withoutextension'] + cls.file_paths = set() + for filename in filenames: + source_path = Path(cls.SRCPATH, filename) + file_path = Path(DOZO_PATH, 'samples', filename) + shutil.copyfile(file_path, source_path) + cls.file_paths.add(source_path) + cls.path_format = constants.default_path + '/' + constants.default_name + + def test_get_exiftool_attribute(self, tmp_path): + for file_path in self.file_paths: + exif_data = ExifToolCaching(str(file_path)).asdict() + ignore_tags = ('File:FileModifyDate', 'File:FileAccessDate') + exif_data_filtered = {} + for key in exif_data: + if key not in ignore_tags: + exif_data_filtered[key] = exif_data[key] + media = Media(str(file_path), ignore_tags) + exif = media.get_exiftool_attributes() + # Ensure returned value is a dictionary + assert isinstance(exif, dict) + for tag in ignore_tags: + assert tag not in exif + assert exif == exif_data_filtered +