Huge refactoring of media and change get_metadata function creation

This commit is contained in:
Cédric Leporcq 2021-08-13 19:09:52 +02:00
parent a2cc3a6f0c
commit 1936231ea2
5 changed files with 263 additions and 515 deletions

View File

@ -176,7 +176,7 @@ class ExifTool:
return self._exiftoolproc.process return self._exiftoolproc.process
def setvalue(self, tag, value): def setvalue(self, tag, value):
"""Set tag to value(s); if value is None, will delete tag """Set tag to value(s); if value is None, tag will not be set
Args: Args:
tag: str; name of tag to set tag: str; name of tag to set
@ -191,7 +191,7 @@ class ExifTool:
""" """
if value is None: if value is None:
value = "" return False
command = [f"-{tag}={value}"] command = [f"-{tag}={value}"]
if self.overwrite and not self._context_mgr: if self.overwrite and not self._context_mgr:
command.append("-overwrite_original") command.append("-overwrite_original")

View File

@ -238,12 +238,12 @@ class FileSystem(object):
'title'): 'title'):
if metadata[item]: if metadata[item]:
part = metadata[item] part = metadata[item]
elif item in ('original_name'): elif item == 'original_name':
# First we check if we have metadata['original_name']. # First we check if we have metadata['original_name'].
# We have to do this for backwards compatibility because # We have to do this for backwards compatibility because
# we original did not store this back into EXIF. # we original did not store this back into EXIF.
if metadata[item]: if metadata[item]:
part = os.path.splitext(metadata['original_name'])[0] part = metadata['original_name']
elif item in 'custom': elif item in 'custom':
# Fallback string # Fallback string
part = mask[1:-1] part = mask[1:-1]

View File

@ -12,7 +12,7 @@ import logging
# load modules # load modules
from dateutil.parser import parse from dateutil.parser import parse
import re import re
from dozo.exiftool import ExifToolCaching from dozo.exiftool import ExifTool, ExifToolCaching
class Media(): class Media():
@ -34,184 +34,213 @@ class Media():
extensions = PHOTO + AUDIO + VIDEO extensions = PHOTO + AUDIO + VIDEO
def __init__(self, sources=None, ignore_tags=set(), logger=logging.getLogger()): def __init__(self, sources=None, ignore_tags=set(), logger=logging.getLogger()):
self.source = sources self.source = sources
self.reset_cache() self.ignore_tags = ignore_tags
self.date_original = [ self.tags_keys = self.get_tags()
self.exif_metadata = None
self.metadata = None
self.logger = logger
def get_tags(self):
tags_keys = {}
tags_keys['date_original'] = [
'EXIF:DateTimeOriginal', 'EXIF:DateTimeOriginal',
'H264:DateTimeOriginal', 'H264:DateTimeOriginal',
'QuickTime:ContentCreateDate' 'QuickTime:ContentCreateDate'
] ]
self.date_created = [ tags_keys['date_created'] = [
'EXIF:CreateDate', 'EXIF:CreateDate',
'QuickTime:CreationDate', 'QuickTime:CreationDate',
'QuickTime:CreateDate', 'QuickTime:CreateDate',
'QuickTime:CreationDate-und-US', 'QuickTime:CreationDate-und-US',
'QuickTime:MediaCreateDate' 'QuickTime:MediaCreateDate'
] ]
self.date_modified = ['File:FileModifyDate', 'QuickTime:ModifyDate'] tags_keys['date_modified'] = [
self.camera_make_keys = ['EXIF:Make', 'QuickTime:Make'] 'File:FileModifyDate',
self.camera_model_keys = ['EXIF:Model', 'QuickTime:Model'] 'QuickTime:ModifyDate'
self.album_keys = ['XMP-xmpDM:Album', 'XMP:Album'] ]
self.title_keys = ['XMP:Title', 'XMP:DisplayName'] tags_keys['camera_make'] = ['EXIF:Make', 'QuickTime:Make']
self.latitude_keys = [ tags_keys['camera_model'] = ['EXIF:Model', 'QuickTime:Model']
tags_keys['album'] = ['XMP-xmpDM:Album', 'XMP:Album']
tags_keys['title'] = ['XMP:Title', 'XMP:DisplayName']
tags_keys['latitude'] = [
'EXIF:GPSLatitude', 'EXIF:GPSLatitude',
'XMP:GPSLatitude', 'XMP:GPSLatitude',
# 'QuickTime:GPSLatitude', # 'QuickTime:GPSLatitude',
'Composite:GPSLatitude' 'Composite:GPSLatitude'
] ]
self.longitude_keys = [ tags_keys['longitude'] = [
'EXIF:GPSLongitude', 'EXIF:GPSLongitude',
'XMP:GPSLongitude', 'XMP:GPSLongitude',
# 'QuickTime:GPSLongitude', # 'QuickTime:GPSLongitude',
'Composite:GPSLongitude' 'Composite:GPSLongitude'
] ]
self.latitude_ref_key = 'EXIF:GPSLatitudeRef' tags_keys['latitude_ref'] = ['EXIF:GPSLatitudeRef']
self.longitude_ref_key = 'EXIF:GPSLongitudeRef' tags_keys['longitude_ref'] = ['EXIF:GPSLongitudeRef']
self.original_name_key = 'XMP:OriginalFileName' tags_keys['original_name'] = ['XMP:OriginalFileName']
self.set_gps_ref = True
self.metadata = None
self.exif_metadata = None
self.ignore_tags = ignore_tags
self.logger = logger
# Remove ignored tag from list
for tag_regex in self.ignore_tags:
ignored_tags = set()
for key, tags in tags_keys.items():
for n, tag in enumerate(tags):
if re.match(tag_regex, tag):
del(tags_keys[key][n])
def format_metadata(self, **kwargs): return tags_keys
"""Method to consistently return a populated metadata dictionary.
:returns: dict
"""
def get_file_path(self):
"""Get the full path to the video.
:returns: string
"""
return self.source
def get_extension(self):
"""Get the file extension as a lowercased string.
:returns: string or None for a non-video
"""
if(not self.is_valid()):
return None
source = self.source
return os.path.splitext(source)[1][1:]
def get_metadata(self, update_cache=False, album_from_folder=False):
"""Get a dictionary of metadata for any file.
All keys will be present and have a value of None if not obtained.
:returns: dict or None for non-text files
"""
if(not self.is_valid()):
return None
if(isinstance(self.metadata, dict) and update_cache is False):
return self.metadata
source = self.source
folder = os.path.basename(os.path.dirname(source))
album = self.get_album()
if album_from_folder and (album is None or album == ''):
album = folder
self.metadata = {
'date_original': self.get_date_attribute(self.date_original),
'date_created': self.get_date_attribute(self.date_created),
'date_modified': self.get_date_attribute(self.date_modified),
'camera_make': self.get_camera_make(),
'camera_model': self.get_camera_model(),
'latitude': self.get_coordinate('latitude'),
'longitude': self.get_coordinate('longitude'),
'album': album,
'title': self.get_title(),
'mime_type': self.get_mimetype(),
'original_name': self.get_original_name(),
'base_name': os.path.basename(os.path.splitext(source)[0]),
'ext': self.get_extension(),
'directory_path': os.path.dirname(source)
}
return self.metadata
def _del_ignored_tags(self, exif_metadata):
for tag_regex in self.ignore_tags:
ignored_tags = set()
for tag in exif_metadata:
if re.search(tag_regex, tag) is not None:
ignored_tags.add(tag)
for ignored_tag in ignored_tags:
del exif_metadata[ignored_tag]
def get_mimetype(self): def get_mimetype(self):
"""Get the mimetype of the file. """Get the mimetype of the file.
:returns: str or None for unsupported files. :returns: str or None
""" """
if(not self.is_valid()): mimetype = mimetypes.guess_type(self.source)
return None
source = self.source
mimetype = mimetypes.guess_type(source)
if(mimetype is None): if(mimetype is None):
return None return None
return mimetype[0] return mimetype[0]
def _get_key_values(self, key):
"""Get the first value of a tag set
def is_valid(self): :returns: str or None if no exif tag
# Disable extension check """
if self.exif_metadata is None:
return None
for tag in self.tags_keys[key]:
if tag in self.exif_metadata:
yield self.exif_metadata[tag]
def get_value(self, tag):
"""Get given value from EXIF.
:returns: str or None
"""
exiftool_attributes = self.get_exiftool_attributes()
if exiftool_attributes is None:
return None
if(tag not in exiftool_attributes):
return None
return exiftool_attributes[tag]
def get_date_format(self, value):
"""Formate date attribute.
:returns: datetime object or None
"""
# We need to parse a string to datetime format.
# EXIF DateTimeOriginal and EXIF DateTime are both stored
# in %Y:%m:%d %H:%M:%S format
if value is None:
return None
try:
# correct nasty formated date
regex = re.compile(r'(\d{4}):(\d{2}):(\d{2})')
if(re.match(regex , value) is not None): # noqa
value = re.sub(regex , r'\g<1>-\g<2>-\g<3>', value)
return parse(value)
except BaseException or dateutil.parser._parser.ParserError as e:
self.logger.error(e)
return None
def get_coordinates(self, key, value):
"""Get latitude or longitude value
:param str key: Type of coordinate to get. Either "latitude" or
"longitude".
:returns: float or None
"""
if value is None:
return None
if isinstance(value, str) and len(value) == 0:
# If exiftool GPS output is empty, the data returned will be a str
# with 0 length.
# https://github.com/jmathai/elodie/issues/354
return None
# Cast coordinate to a float due to a bug in exiftool's
# -json output format.
# https://github.com/jmathai/elodie/issues/171
# http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,7952.0.html # noqa
this_coordinate = float(value)
direction_multiplier = 1.0
# when self.set_gps_ref != True
if key == 'latitude':
if 'EXIF:GPSLatitudeRef' in self.exif_metadata:
if self.exif_metadata['EXIF:GPSLatitudeRef'] == 'S':
direction_multiplier = -1.0
elif key == 'longitude':
if 'EXIF:GPSLongitudeRef' in self.exif_metadata:
if self.exif_metadata['EXIF:GPSLongitudeRef'] == 'W':
direction_multiplier = -1.0
return this_coordinate * direction_multiplier
return None
def get_metadata(self):
"""Get a dictionary of metadata from exif.
All keys will be present and have a value of None if not obtained.
:returns: dict
"""
# Get metadata from exiftool.
self.exif_metadata = ExifToolCaching(self.source, logger=self.logger).asdict()
# TODO to be removed
self.metadata = {}
# Retrieve selected metadata to dict
if not self.exif_metadata:
return self.metadata
for key in self.tags_keys:
formated_data = None
for value in self._get_key_values(key):
if 'date' in key:
formated_data = self.get_date_format(value)
elif key in ('latitude', 'longitude'):
formated_data = self.get_coordinates(key, value)
else:
if value is not None and value != '':
formated_data = value
else:
formated_data = None
if formated_data:
# Use this data and break
break
self.metadata[key] = formated_data
self.metadata['base_name'] = os.path.basename(os.path.splitext(self.source)[0])
self.metadata['ext'] = os.path.splitext(self.source)[1][1:]
self.metadata['directory_path'] = os.path.dirname(self.source)
return self.metadata
def has_exif_data(self):
"""Check if file has metadata, date original"""
if not self.metadata:
return False
if 'date_original' in self.metadata:
if self.metadata['date_original'] != None:
return True return True
def set_album_from_folder(self, path):
"""Set the album attribute based on the leaf folder name
:returns: bool
"""
metadata = self.get_metadata()
# If this file has an album already set we do not overwrite EXIF
if(not isinstance(metadata, dict) or metadata['album'] is not None):
return False return False
folder = os.path.basename(metadata['directory_path'])
# If folder is empty we skip
if(len(folder) == 0):
return False
status = self.set_album(folder, path)
if status == False:
return False
return True
def set_metadata_basename(self, new_basename):
"""Update the basename attribute in the metadata dict for this instance.
This is used for when we update the EXIF title of a media file. Since
that determines the name of a file if we update the title of a file
more than once it appends to the file name.
i.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
:param str new_basename: New basename of file (with the old title
removed).
"""
self.get_metadata()
self.metadata['base_name'] = new_basename
def set_metadata(self, **kwargs):
"""Method to manually update attributes in metadata.
:params dict kwargs: Named parameters to update.
"""
metadata = self.get_metadata()
for key in kwargs:
if(key in metadata):
self.metadata[key] = kwargs[key]
@classmethod @classmethod
def get_class_by_file(cls, _file, classes, ignore_tags=set(), logger=logging.getLogger()): def get_class_by_file(cls, _file, classes, ignore_tags=set(), logger=logging.getLogger()):
"""Static method to get a media object by file. """Static method to get a media object by file.
@ -233,246 +262,7 @@ class Media():
else: else:
return Media(_file, ignore_tags=ignore_tags, logger=logger) return Media(_file, ignore_tags=ignore_tags, logger=logger)
def set_date_taken(self, date_key, time):
@classmethod
def get_valid_extensions(cls):
"""Static method to access static extensions variable.
:returns: tuple(str)
"""
return cls.extensions
def get_album(self):
"""Get album from EXIF
:returns: None or string
"""
if(not self.is_valid()):
return None
exiftool_attributes = self.get_exiftool_attributes()
if exiftool_attributes is None:
return None
for album_key in self.album_keys:
if album_key in exiftool_attributes:
return exiftool_attributes[album_key]
return None
def get_coordinate(self, type='latitude'):
"""Get latitude or longitude of media from EXIF
:param str type: Type of coordinate to get. Either "latitude" or
"longitude".
:returns: float or None if not present in EXIF or a non-photo file
"""
exif = self.get_exiftool_attributes()
if not exif:
return None
# The lat/lon _keys array has an order of precedence.
# The first key is writable and we will give the writable
# key precence when reading.
direction_multiplier = 1.0
for key in self.latitude_keys + self.longitude_keys:
if key not in exif:
continue
if isinstance(exif[key], six.string_types) and len(exif[key]) == 0:
# If exiftool GPS output is empty, the data returned will be a str
# with 0 length.
# https://github.com/jmathai/elodie/issues/354
continue
# Cast coordinate to a float due to a bug in exiftool's
# -json output format.
# https://github.com/jmathai/elodie/issues/171
# http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,7952.0.html # noqa
this_coordinate = float(exif[key])
# TODO: verify that we need to check ref key
# when self.set_gps_ref != True
if type == 'latitude' and key in self.latitude_keys:
if self.latitude_ref_key in exif and \
exif[self.latitude_ref_key] == 'S':
direction_multiplier = -1.0
return this_coordinate * direction_multiplier
elif type == 'longitude' and key in self.longitude_keys:
if self.longitude_ref_key in exif and \
exif[self.longitude_ref_key] == 'W':
direction_multiplier = -1.0
return this_coordinate * direction_multiplier
return None
def get_exiftool_attributes(self):
"""Get attributes for the media object from exiftool.
:returns: dict, or False if exiftool was not available.
"""
source = self.source
#Cache exif metadata results and use if already exists for media
if(self.exif_metadata is None):
self.exif_metadata = ExifToolCaching(source, logger=self.logger).asdict()
for tag_regex in self.ignore_tags:
ignored_tags = set()
for tag in self.exif_metadata:
if re.search(tag_regex, tag) is not None:
ignored_tags.add(tag)
for ignored_tag in ignored_tags:
del self.exif_metadata[ignored_tag]
if not self.exif_metadata:
return False
return self.exif_metadata
def get_date_attribute(self, tag):
"""Get a date attribute.
:returns: time object or None
"""
exif = self.get_exiftool_attributes()
if not exif:
return None
# We need to parse a string from EXIF into a timestamp.
# EXIF DateTimeOriginal and EXIF DateTime are both stored
# in %Y:%m:%d %H:%M:%S format
# we split on a space and then r':|-' -> convert to int -> .timetuple()
# the conversion in the local timezone
# EXIF DateTime is already stored as a timestamp
# Sourced from https://github.com/photo/frontend/blob/master/src/libraries/models/Photo.php#L500 # noqa
for key in tag:
try:
if(key in exif):
# correct nasty formated date
regex = re.compile(r'(\d{4}):(\d{2}):(\d{2})')
if(re.match(regex , exif[key]) is not None): # noqa
exif[key] = re.sub(regex , r'\g<1>-\g<2>-\g<3>', exif[key])
return parse(exif[key])
# if(re.match('\d{4}(-|:)\d{2}(-|:)\d{2}', exif[key]) is not None): # noqa
# dt, tm = exif[key].split(' ')
# dt_list = compile(r'-|:').split(dt)
# dt_list = dt_list + compile(r'-|:').split(tm)
# dt_list = map(int, dt_list)
# return datetime(*dt_list)
except BaseException or dateutil.parser._parser.ParserError as e:
self.logger.error(e)
return None
return None
def get_camera_make(self):
"""Get the camera make stored in EXIF.
:returns: str
"""
if(not self.is_valid()):
return None
exiftool_attributes = self.get_exiftool_attributes()
if exiftool_attributes is None:
return None
for camera_make_key in self.camera_make_keys:
if camera_make_key in exiftool_attributes:
return exiftool_attributes[camera_make_key]
return None
def get_camera_model(self):
"""Get the camera make stored in EXIF.
:returns: str
"""
if(not self.is_valid()):
return None
exiftool_attributes = self.get_exiftool_attributes()
if exiftool_attributes is None:
return None
for camera_model_key in self.camera_model_keys:
if camera_model_key in exiftool_attributes:
return exiftool_attributes[camera_model_key]
return None
def get_original_name(self):
"""Get the original name stored in EXIF.
:returns: str
"""
if(not self.is_valid()):
return None
exiftool_attributes = self.get_exiftool_attributes()
if exiftool_attributes is None:
return None
if(self.original_name_key not in exiftool_attributes):
return None
return exiftool_attributes[self.original_name_key]
def get_title(self):
"""Get the title for a photo of video
:returns: str or None if no title is set or not a valid media type
"""
if(not self.is_valid()):
return None
exiftool_attributes = self.get_exiftool_attributes()
if exiftool_attributes is None:
return None
for title_key in self.title_keys:
if title_key in exiftool_attributes:
return exiftool_attributes[title_key]
return None
def reset_cache(self):
"""Resets any internal cache
"""
self.exiftool_attributes = None
self.exif_metadata = None
def set_album(self, name, path):
"""Set album EXIF tag if not already set.
:returns: True, False, None
"""
if self.get_album() is not None:
return None
tags = {}
for key in self.album_keys:
tags[key] = name
status = self.__set_tags(tags, path)
self.reset_cache()
return status
def set_date_original(self, time, path):
"""Set the date/time a photo was taken. """Set the date/time a photo was taken.
:param datetime time: datetime object of when the photo was taken :param datetime time: datetime object of when the photo was taken
@ -481,110 +271,47 @@ class Media():
if(time is None): if(time is None):
return False return False
tags = {}
formatted_time = time.strftime('%Y:%m:%d %H:%M:%S') formatted_time = time.strftime('%Y:%m:%d %H:%M:%S')
for key in self.date_original: status = self.set_value('date_original', formatted_time)
tags[key] = formatted_time
status = self.__set_tags(tags, path)
if status == False: if status == False:
# exif attribute date_original d'ont exist # exif attribute date_original d'ont exist
for key in self.date_created: status = self.set_value('date_created', formatted_time)
tags[key] = formatted_time
status = self.__set_tags(tags, path)
self.reset_cache()
return status
def set_location(self, latitude, longitude, path):
if(not self.is_valid()):
return None
# The lat/lon _keys array has an order of precedence.
# The first key is writable and we will give the writable
# key precence when reading.
# TODO check
# tags = {
# self.latitude_keys[0]: latitude,
# self.longitude_keys[0]: longitude,
# }
tags = {}
for key in self.latitude_keys:
tags[key] = latitude
for key in self.longitude_keys:
tags[key] = longitude
# If self.set_gps_ref == True then it means we are writing an EXIF
# GPS tag which requires us to set the reference key.
# That's because the lat/lon are absolute values.
# TODO set_gps_ref = False for Video ?
if self.set_gps_ref:
if latitude < 0:
tags[self.latitude_ref_key] = 'S'
if longitude < 0:
tags[self.longitude_ref_key] = 'W'
status = self.__set_tags(tags, path)
self.reset_cache()
return status return status
def set_coordinates(self, latitude, longitude):
status = []
if self.metadata['latitude_ref']:
latitude = abs(latitude)
if latitude > 0:
status.append(self.set_value('latitude_ref', 'N'))
else:
status.append(self.set_value('latitude_ref', 'S'))
def set_original_name(self, path, name=None): status.append(self.set_value('latitude', latitude))
"""Sets the original name EXIF tag if not already set.
:returns: True, False, None if self.metadata['longitude_ref']:
""" longitude = abs(longitude)
# If EXIF original name tag is set then we return. if longitude > 0:
if self.get_original_name() is not None: status.append(self.set_value('latitude_ref', 'E'))
return None else:
status.append(self.set_value('longitude_ref', 'W'))
if name == None: status.append(self.set_value('longitude', longitude))
name = os.path.basename(self.source)
tags = {self.original_name_key: name} if all(status):
status = self.__set_tags(tags, path) return True
self.reset_cache() else:
return False
return status def set_album_from_folder(self, path):
"""Set the album attribute based on the leaf folder name
def set_title(self, title, path):
"""Set title for a photo.
:param str title: Title of the photo.
:returns: bool :returns: bool
""" """
if(not self.is_valid()): folder = os.path.basename(os.path.dirname(self.source))
return None
if(title is None): return set_value(self, 'album', folder)
return None
tags = {}
for key in self.title_keys:
tags[key] = title
status = self.__set_tags(tags, path)
self.reset_cache()
return status
def __set_tags(self, tags, path):
if(not self.is_valid()):
return None
status = ''
for tag, value in tags.items():
status = ExifToolCaching(path, self.logger).setvalue(tag, value)
if status.decode().find('unchanged') != -1 or status == '':
return False
if status.decode().find('error') != -1:
return False
return True
def get_all_subclasses(cls=None): def get_all_subclasses(cls=None):

View File

@ -56,24 +56,15 @@ class TestFilesystem:
'{%Y-%m-%b}' '{%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') subdirs = Path('a', 'b', 'c', 'd')
for file_path in self.file_paths: for file_path in self.file_paths:
media = Media(str(file_path)) media = Media(str(file_path))
exif_tags = {}
for key in ('album', 'camera_make', 'camera_model', 'latitude',
'longitude', 'original_name', 'title'):
exif_tags[key] = media.tags_keys[key]
exif_data = ExifToolCaching(str(file_path)).asdict() exif_data = ExifToolCaching(str(file_path)).asdict()
metadata = media.get_metadata() metadata = media.get_metadata()
for item, regex in items.items(): for item, regex in items.items():
@ -127,25 +118,22 @@ class TestFilesystem:
metadata = media.get_metadata() metadata = media.get_metadata()
date_taken = filesystem.get_date_taken(metadata) date_taken = filesystem.get_date_taken(metadata)
dates = {} date_filename = None
for key, date in ('original', media.date_original), ('created', for tag in media.tags_keys['original_name']:
media.date_created), ('modified', media.date_modified): if tag in exif_data:
dates[key] = media.get_date_attribute(date) date_filename = filesystem.get_date_from_string(exif_data[tag])
break
if media.original_name_key in exif_data: if not date_filename:
date_filename = filesystem.get_date_from_string(
exif_data[media.original_name_key])
else:
date_filename = filesystem.get_date_from_string(file_path.name) date_filename = filesystem.get_date_from_string(file_path.name)
if dates['original']: if media.metadata['date_original']:
assert date_taken == dates['original'] assert date_taken == media.metadata['date_original']
elif date_filename: elif date_filename:
assert date_taken == date_filename assert date_taken == date_filename
elif dates['created']: elif media.metadata['date_created']:
assert date_taken == dates['created'] assert date_taken == media.metadata['date_created']
elif dates['modified']: elif media.metadata['date_modified']:
assert date_taken == dates['modified'] assert date_taken == media.metadata['date_modified']
def test_sort_files(self, tmp_path): def test_sort_files(self, tmp_path):
db = Db(tmp_path) db = Db(tmp_path)

View File

@ -1,6 +1,7 @@
from datetime import datetime
import pytest import pytest
from pathlib import Path from pathlib import Path
import re
import shutil import shutil
import tempfile import tempfile
@ -10,28 +11,60 @@ from dozo.media.media import Media
from dozo.media.audio import Audio from dozo.media.audio import Audio
from dozo.media.photo import Photo from dozo.media.photo import Photo
from dozo.media.video import Video from dozo.media.video import Video
from dozo.exiftool import ExifToolCaching from dozo.exiftool import ExifTool, ExifToolCaching
DOZO_PATH = Path(__file__).parent.parent DOZO_PATH = Path(__file__).parent.parent
CACHING = True
class TestMetadata: class TestMetadata:
def setup_class(cls): def setup_class(cls):
cls.src_paths, cls.file_paths = copy_sample_files() cls.src_paths, cls.file_paths = copy_sample_files()
cls.ignore_tags = ('EXIF:CreateDate', 'File:FileModifyDate',
'File:FileAccessDate', 'EXIF:Make', 'Composite:LightValue')
def test_get_exiftool_attribute(self, tmp_path): def get_media(self):
for file_path in self.file_paths: for file_path in self.file_paths:
exif_data = ExifToolCaching(str(file_path)).asdict() self.exif_data = ExifTool(str(file_path)).asdict()
ignore_tags = ('File:FileModifyDate', 'File:FileAccessDate') yield Media(str(file_path), self.ignore_tags)
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
def test_get_metadata(self):
for media in self.get_media():
result = media.get_metadata()
assert result
assert isinstance(media.metadata, dict), media.metadata
#check if all tags key are present
for tags_key, tags in media.tags_keys.items():
assert tags_key in media.metadata
for tag in tags:
for tag_regex in self.ignore_tags:
assert not re.match(tag_regex, tag)
# Check for valid type
for key, value in media.metadata.items():
if value or value == '':
if 'date' in key:
assert isinstance(value, datetime)
elif key in ('latitude', 'longitude'):
assert isinstance(value, float)
else:
assert isinstance(value, str)
else:
assert value is None
# Check if has_exif_data() is True if 'date_original' key is
# present, else check if it's false
has_exif_data = False
for tag in media.tags_keys['date_original']:
if tag in media.exif_metadata:
if media.get_date_format(media.exif_metadata[tag]):
has_exif_data = True
assert media.has_exif_data()
break
if has_exif_data == False:
assert not media.has_exif_data()
# Will be changed to get_metadata
# check if metatadata type are correct
# if(isinstance(self.metadata, dict) and update_cache is False):
# return self.metadata
# Album for folder implemented other place