Huge refactoring of media and change get_metadata function creation
This commit is contained in:
parent
a2cc3a6f0c
commit
1936231ea2
|
@ -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")
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,183 +34,212 @@ 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
|
|
||||||
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 self.exif_metadata is None:
|
||||||
|
return None
|
||||||
|
|
||||||
# If this file has an album already set we do not overwrite EXIF
|
for tag in self.tags_keys[key]:
|
||||||
if(not isinstance(metadata, dict) or metadata['album'] is not None):
|
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
|
return False
|
||||||
|
|
||||||
folder = os.path.basename(metadata['directory_path'])
|
if 'date_original' in self.metadata:
|
||||||
# If folder is empty we skip
|
if self.metadata['date_original'] != None:
|
||||||
if(len(folder) == 0):
|
return True
|
||||||
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]
|
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@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()):
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue