2015-10-07 08:47:51 +02:00
|
|
|
"""
|
2016-04-07 10:08:33 +02:00
|
|
|
The media module provides a base :class:`Media` class for media objects that
|
2016-01-08 23:49:06 +01:00
|
|
|
are tracked by Elodie. The Media class provides some base functionality used
|
2021-06-20 19:51:21 +02:00
|
|
|
by all the media types. Its sub-classes (:class:`~elodie.media.Audio`,
|
|
|
|
:class:`~elodie.media.Photo`, and :class:`~elodie.media.Video`)
|
2016-01-08 23:49:06 +01:00
|
|
|
are used to represent the actual files.
|
|
|
|
|
|
|
|
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
2015-10-07 08:47:51 +02:00
|
|
|
"""
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
import mimetypes
|
2017-01-22 09:19:44 +01:00
|
|
|
import os
|
2020-01-18 18:03:40 +01:00
|
|
|
import six
|
2021-07-16 21:26:42 +02:00
|
|
|
import logging
|
2017-01-22 09:19:44 +01:00
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
# load modules
|
2021-04-17 05:08:58 +02:00
|
|
|
from elodie import log
|
|
|
|
from dateutil.parser import parse
|
|
|
|
import re
|
2016-06-21 20:19:40 +02:00
|
|
|
from elodie.external.pyexiftool import ExifTool
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
# TODO remove
|
|
|
|
# try: # Py3k compatibility
|
|
|
|
# basestring
|
|
|
|
# except NameError:
|
|
|
|
# basestring = (bytes, str)
|
2015-10-20 10:17:09 +02:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
class Media():
|
|
|
|
|
|
|
|
"""The media class for all media objects.
|
2016-01-08 23:49:06 +01:00
|
|
|
|
|
|
|
:param str source: The fully qualified path to the video file.
|
2015-10-07 08:47:51 +02:00
|
|
|
"""
|
2016-01-08 23:49:06 +01:00
|
|
|
|
|
|
|
__name__ = 'Media'
|
|
|
|
|
2016-02-12 20:22:26 +01:00
|
|
|
d_coordinates = {
|
2016-02-14 09:55:39 +01:00
|
|
|
'latitude': 'latitude_ref',
|
|
|
|
'longitude': 'longitude_ref'
|
|
|
|
}
|
2016-02-12 20:22:26 +01:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
PHOTO = ('arw', 'cr2', 'dng', 'gif', 'heic', 'jpeg', 'jpg', 'nef', 'png', 'rw2')
|
|
|
|
AUDIO = ('m4a',)
|
|
|
|
VIDEO = ('avi', 'm4v', 'mov', 'mp4', 'mpg', 'mpeg', '3gp', 'mts')
|
|
|
|
|
|
|
|
extensions = PHOTO + AUDIO + VIDEO
|
|
|
|
|
|
|
|
|
2021-07-17 16:47:31 +02:00
|
|
|
def __init__(self, sources=None, ignore_tags=set()):
|
2021-06-20 19:51:21 +02:00
|
|
|
self.source = sources
|
|
|
|
self.reset_cache()
|
|
|
|
self.date_original = [
|
|
|
|
'EXIF:DateTimeOriginal',
|
|
|
|
'H264:DateTimeOriginal',
|
|
|
|
'QuickTime:ContentCreateDate'
|
|
|
|
]
|
|
|
|
self.date_created = [
|
|
|
|
'EXIF:CreateDate',
|
|
|
|
'QuickTime:CreationDate',
|
|
|
|
'QuickTime:CreateDate',
|
|
|
|
'QuickTime:CreationDate-und-US',
|
|
|
|
'QuickTime:MediaCreateDate'
|
|
|
|
]
|
2021-06-20 08:35:28 +02:00
|
|
|
self.date_modified = ['File:FileModifyDate', 'QuickTime:ModifyDate']
|
2017-11-15 08:14:26 +01:00
|
|
|
self.camera_make_keys = ['EXIF:Make', 'QuickTime:Make']
|
|
|
|
self.camera_model_keys = ['EXIF:Model', 'QuickTime:Model']
|
2016-06-24 06:34:51 +02:00
|
|
|
self.album_keys = ['XMP-xmpDM:Album', 'XMP:Album']
|
2021-06-20 19:51:21 +02:00
|
|
|
self.title_keys = ['XMP:Title', 'XMP:DisplayName']
|
|
|
|
self.latitude_keys = [
|
|
|
|
'EXIF:GPSLatitude',
|
|
|
|
'XMP:GPSLatitude',
|
|
|
|
# 'QuickTime:GPSLatitude',
|
|
|
|
'Composite:GPSLatitude'
|
|
|
|
]
|
|
|
|
self.longitude_keys = [
|
|
|
|
'EXIF:GPSLongitude',
|
|
|
|
'XMP:GPSLongitude',
|
|
|
|
# 'QuickTime:GPSLongitude',
|
|
|
|
'Composite:GPSLongitude'
|
|
|
|
]
|
2016-06-21 20:19:40 +02:00
|
|
|
self.latitude_ref_key = 'EXIF:GPSLatitudeRef'
|
|
|
|
self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
|
2017-01-22 09:19:44 +01:00
|
|
|
self.original_name_key = 'XMP:OriginalFileName'
|
2016-06-21 20:19:40 +02:00
|
|
|
self.set_gps_ref = True
|
2021-06-20 19:51:21 +02:00
|
|
|
self.metadata = None
|
2020-01-14 08:04:33 +01:00
|
|
|
self.exif_metadata = None
|
2021-07-17 16:47:31 +02:00
|
|
|
self.ignore_tags = ignore_tags
|
2015-10-14 09:39:30 +02:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
def format_metadata(self, **kwargs):
|
|
|
|
"""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
|
2021-07-29 18:42:31 +02:00
|
|
|
return os.path.splitext(source)[1][1:]
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
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]),
|
2021-07-29 18:42:31 +02:00
|
|
|
'ext': self.get_extension(),
|
2021-06-20 19:51:21 +02:00
|
|
|
'directory_path': os.path.dirname(source)
|
|
|
|
}
|
|
|
|
|
|
|
|
return self.metadata
|
|
|
|
|
|
|
|
|
|
|
|
def get_mimetype(self):
|
|
|
|
"""Get the mimetype of the file.
|
|
|
|
|
|
|
|
:returns: str or None for unsupported files.
|
|
|
|
"""
|
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
source = self.source
|
|
|
|
mimetype = mimetypes.guess_type(source)
|
|
|
|
if(mimetype is None):
|
|
|
|
return None
|
|
|
|
|
|
|
|
return mimetype[0]
|
|
|
|
|
|
|
|
|
|
|
|
def is_valid(self):
|
2021-06-27 07:18:35 +02:00
|
|
|
# Disable extension check
|
|
|
|
return True
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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
|
2021-07-17 16:47:31 +02:00
|
|
|
def get_class_by_file(cls, _file, classes, ignore_tags=set()):
|
2021-06-20 19:51:21 +02:00
|
|
|
"""Static method to get a media object by file.
|
|
|
|
"""
|
|
|
|
basestring = (bytes, str)
|
|
|
|
if not isinstance(_file, basestring) or not os.path.isfile(_file):
|
|
|
|
return None
|
|
|
|
|
|
|
|
extension = os.path.splitext(_file)[1][1:].lower()
|
|
|
|
|
|
|
|
if len(extension) > 0:
|
|
|
|
for i in classes:
|
|
|
|
if(extension in i.extensions):
|
2021-07-17 16:47:31 +02:00
|
|
|
return i(_file, ignore_tags=ignore_tags)
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2021-07-16 21:26:42 +02:00
|
|
|
exclude_list = ['.DS_Store', '.directory']
|
|
|
|
if os.path.basename(_file) == '.DS_Store':
|
|
|
|
return None
|
|
|
|
else:
|
2021-07-17 16:47:31 +02:00
|
|
|
return Media(_file, ignore_tags=ignore_tags)
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_valid_extensions(cls):
|
|
|
|
"""Static method to access static extensions variable.
|
|
|
|
|
|
|
|
:returns: tuple(str)
|
|
|
|
"""
|
|
|
|
return cls.extensions
|
|
|
|
|
|
|
|
|
2015-10-14 09:39:30 +02:00
|
|
|
def get_album(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get album from EXIF
|
|
|
|
|
|
|
|
:returns: None or string
|
|
|
|
"""
|
2015-10-14 09:39:30 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
2015-10-21 08:51:14 +02:00
|
|
|
exiftool_attributes = self.get_exiftool_attributes()
|
2016-06-21 20:19:40 +02:00
|
|
|
if exiftool_attributes is None:
|
2015-10-14 09:39:30 +02:00
|
|
|
return None
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2016-06-24 06:34:51 +02:00
|
|
|
for album_key in self.album_keys:
|
|
|
|
if album_key in exiftool_attributes:
|
|
|
|
return exiftool_attributes[album_key]
|
2015-10-07 08:47:51 +02:00
|
|
|
|
2016-06-24 06:34:51 +02:00
|
|
|
return None
|
2016-01-08 23:49:06 +01:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
def get_coordinate(self, type='latitude'):
|
|
|
|
"""Get latitude or longitude of media from EXIF
|
2016-01-08 23:49:06 +01:00
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
: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
|
2016-01-08 23:49:06 +01:00
|
|
|
"""
|
2015-10-14 09:39:30 +02:00
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
exif = self.get_exiftool_attributes()
|
|
|
|
if not exif:
|
|
|
|
return None
|
2015-10-14 09:39:30 +02:00
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
# 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.
|
2017-01-03 09:49:56 +01:00
|
|
|
direction_multiplier = 1.0
|
2016-06-21 20:19:40 +02:00
|
|
|
for key in self.latitude_keys + self.longitude_keys:
|
2017-01-03 09:49:56 +01:00
|
|
|
if key not in exif:
|
|
|
|
continue
|
2020-01-18 18:03:40 +01:00
|
|
|
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
|
2017-01-03 09:49:56 +01:00
|
|
|
|
2017-01-22 09:19:44 +01:00
|
|
|
# Cast coordinate to a float due to a bug in exiftool's
|
2017-01-03 09:49:56 +01:00
|
|
|
# -json output format.
|
|
|
|
# https://github.com/jmathai/elodie/issues/171
|
2017-01-22 09:19:44 +01:00
|
|
|
# http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,7952.0.html # noqa
|
2017-01-03 09:49:56 +01:00
|
|
|
this_coordinate = float(exif[key])
|
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
# TODO: verify that we need to check ref key
|
|
|
|
# when self.set_gps_ref != True
|
2017-01-03 09:49:56 +01:00
|
|
|
if type == 'latitude' and key in self.latitude_keys:
|
2016-06-24 06:31:58 +02:00
|
|
|
if self.latitude_ref_key in exif and \
|
|
|
|
exif[self.latitude_ref_key] == 'S':
|
2017-01-03 09:49:56 +01:00
|
|
|
direction_multiplier = -1.0
|
|
|
|
return this_coordinate * direction_multiplier
|
|
|
|
elif type == 'longitude' and key in self.longitude_keys:
|
2016-06-24 06:31:58 +02:00
|
|
|
if self.longitude_ref_key in exif and \
|
|
|
|
exif[self.longitude_ref_key] == 'W':
|
2017-01-03 09:49:56 +01:00
|
|
|
direction_multiplier = -1.0
|
|
|
|
return this_coordinate * direction_multiplier
|
2016-06-21 20:19:40 +02:00
|
|
|
|
|
|
|
return None
|
2015-10-14 09:39:30 +02:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2015-10-21 08:51:14 +02:00
|
|
|
def get_exiftool_attributes(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get attributes for the media object from exiftool.
|
|
|
|
|
|
|
|
:returns: dict, or False if exiftool was not available.
|
|
|
|
"""
|
2016-06-21 20:19:40 +02:00
|
|
|
source = self.source
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2020-01-14 08:04:33 +01:00
|
|
|
#Cache exif metadata results and use if already exists for media
|
|
|
|
if(self.exif_metadata is None):
|
|
|
|
self.exif_metadata = ExifTool().get_metadata(source)
|
2021-07-17 16:47:31 +02:00
|
|
|
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]
|
|
|
|
|
2020-01-14 08:04:33 +01:00
|
|
|
|
|
|
|
if not self.exif_metadata:
|
|
|
|
return False
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2020-01-14 08:04:33 +01:00
|
|
|
return self.exif_metadata
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2021-04-17 05:08:58 +02:00
|
|
|
|
|
|
|
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('(\d{4}):(\d{2}):(\d{2})')
|
|
|
|
if(re.match(regex , exif[key]) is not None): # noqa
|
|
|
|
exif[key] = re.sub(regex ,'\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:
|
|
|
|
log.error(e)
|
|
|
|
return None
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2017-11-15 08:14:26 +01:00
|
|
|
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
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2017-11-15 08:14:26 +01:00
|
|
|
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
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2017-01-22 09:19:44 +01:00
|
|
|
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]
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2015-10-28 08:19:21 +01:00
|
|
|
def get_title(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get the title for a photo of video
|
|
|
|
|
|
|
|
:returns: str or None if no title is set or not a valid media type
|
|
|
|
"""
|
2015-10-28 08:19:21 +01:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
exiftool_attributes = self.get_exiftool_attributes()
|
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
if exiftool_attributes is None:
|
|
|
|
return None
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
for title_key in self.title_keys:
|
|
|
|
if title_key in exiftool_attributes:
|
|
|
|
return exiftool_attributes[title_key]
|
|
|
|
|
|
|
|
return None
|
2015-10-28 08:19:21 +01:00
|
|
|
|
2015-10-14 05:26:55 +02:00
|
|
|
|
2016-03-13 21:37:06 +01:00
|
|
|
def reset_cache(self):
|
2016-04-07 10:08:33 +02:00
|
|
|
"""Resets any internal cache
|
|
|
|
"""
|
2016-03-13 21:37:06 +01:00
|
|
|
self.exiftool_attributes = None
|
2020-01-14 08:04:33 +01:00
|
|
|
self.exif_metadata = None
|
2021-06-20 19:51:21 +02:00
|
|
|
|
2016-03-13 21:37:06 +01:00
|
|
|
|
2021-04-17 15:37:37 +02:00
|
|
|
def set_album(self, name, path):
|
|
|
|
"""Set album EXIF tag if not already set.
|
2016-01-08 23:49:06 +01:00
|
|
|
|
2021-04-17 15:37:37 +02:00
|
|
|
:returns: True, False, None
|
2016-01-08 23:49:06 +01:00
|
|
|
"""
|
2021-04-17 15:37:37 +02:00
|
|
|
if self.get_album() is not None:
|
2016-06-21 20:19:40 +02:00
|
|
|
return None
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
tags = {}
|
|
|
|
for key in self.album_keys:
|
|
|
|
tags[key] = name
|
|
|
|
status = self.__set_tags(tags, path)
|
2016-06-21 20:19:40 +02:00
|
|
|
self.reset_cache()
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
return status
|
|
|
|
|
|
|
|
|
|
|
|
def set_date_original(self, time, path):
|
2016-06-21 20:19:40 +02:00
|
|
|
"""Set the date/time a photo was taken.
|
|
|
|
|
|
|
|
:param datetime time: datetime object of when the photo was taken
|
|
|
|
:returns: bool
|
|
|
|
"""
|
|
|
|
if(time is None):
|
2015-10-21 08:51:14 +02:00
|
|
|
return False
|
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
tags = {}
|
|
|
|
formatted_time = time.strftime('%Y:%m:%d %H:%M:%S')
|
2021-04-17 05:08:58 +02:00
|
|
|
for key in self.date_original:
|
2016-06-21 20:19:40 +02:00
|
|
|
tags[key] = formatted_time
|
2015-12-01 09:39:05 +01:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
status = self.__set_tags(tags, path)
|
2021-06-20 08:35:28 +02:00
|
|
|
if status == False:
|
|
|
|
# exif attribute date_original d'ont exist
|
|
|
|
for key in self.date_created:
|
|
|
|
tags[key] = formatted_time
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
status = self.__set_tags(tags, path)
|
2016-06-21 20:19:40 +02:00
|
|
|
self.reset_cache()
|
|
|
|
return status
|
2015-12-03 08:47:54 +01:00
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
def set_location(self, latitude, longitude, path):
|
2016-06-21 20:19:40 +02:00
|
|
|
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.
|
2021-06-20 19:51:21 +02:00
|
|
|
# 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
|
2016-06-21 20:19:40 +02:00
|
|
|
|
|
|
|
# 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.
|
2021-06-20 19:51:21 +02:00
|
|
|
# TODO set_gps_ref = False for Video ?
|
2016-06-21 20:19:40 +02:00
|
|
|
if self.set_gps_ref:
|
|
|
|
if latitude < 0:
|
|
|
|
tags[self.latitude_ref_key] = 'S'
|
|
|
|
|
|
|
|
if longitude < 0:
|
|
|
|
tags[self.longitude_ref_key] = 'W'
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
status = self.__set_tags(tags, path)
|
2016-06-21 20:19:40 +02:00
|
|
|
self.reset_cache()
|
|
|
|
|
|
|
|
return status
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
def set_original_name(self, path, name=None):
|
2017-01-22 09:19:44 +01:00
|
|
|
"""Sets the original name EXIF tag if not already set.
|
|
|
|
|
|
|
|
:returns: True, False, None
|
|
|
|
"""
|
|
|
|
# If EXIF original name tag is set then we return.
|
|
|
|
if self.get_original_name() is not None:
|
|
|
|
return None
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
if name == None:
|
|
|
|
name = os.path.basename(self.source)
|
2017-01-23 08:18:28 +01:00
|
|
|
|
2017-01-22 09:19:44 +01:00
|
|
|
tags = {self.original_name_key: name}
|
2021-06-20 19:51:21 +02:00
|
|
|
status = self.__set_tags(tags, path)
|
2017-01-22 09:19:44 +01:00
|
|
|
self.reset_cache()
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
return status
|
|
|
|
|
|
|
|
|
|
|
|
def set_title(self, title, path):
|
2016-06-21 20:19:40 +02:00
|
|
|
"""Set title for a photo.
|
|
|
|
|
|
|
|
:param str title: Title of the photo.
|
|
|
|
:returns: bool
|
|
|
|
"""
|
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
if(title is None):
|
|
|
|
return None
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
tags = {}
|
|
|
|
for key in self.title_keys:
|
|
|
|
tags[key] = title
|
|
|
|
status = self.__set_tags(tags, path)
|
2016-03-13 21:37:06 +01:00
|
|
|
self.reset_cache()
|
2016-06-21 20:19:40 +02:00
|
|
|
|
|
|
|
return status
|
|
|
|
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
def __set_tags(self, tags, path):
|
2016-06-21 20:19:40 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
status = ''
|
2021-06-20 19:51:21 +02:00
|
|
|
status = ExifTool().set_tags(tags, path)
|
2021-06-20 08:35:28 +02:00
|
|
|
if status.decode().find('unchanged') != -1 or status == '':
|
|
|
|
return False
|
|
|
|
if status.decode().find('error') != -1:
|
|
|
|
return False
|
2016-06-21 20:19:40 +02:00
|
|
|
|
2021-06-20 08:35:28 +02:00
|
|
|
return True
|
2021-06-20 19:51:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_all_subclasses(cls=None):
|
|
|
|
"""Module method to get all subclasses of Media.
|
|
|
|
"""
|
|
|
|
subclasses = set()
|
|
|
|
|
|
|
|
this_class = Media
|
|
|
|
if cls is not None:
|
|
|
|
this_class = cls
|
|
|
|
|
|
|
|
subclasses.add(this_class)
|
|
|
|
|
|
|
|
this_class_subclasses = this_class.__subclasses__()
|
|
|
|
for child_class in this_class_subclasses:
|
|
|
|
subclasses.update(get_all_subclasses(child_class))
|
|
|
|
|
|
|
|
return subclasses
|
|
|
|
|
2021-07-16 21:26:42 +02:00
|
|
|
|
2021-07-17 16:47:31 +02:00
|
|
|
def get_media_class(_file, ignore_tags=set()):
|
2021-07-16 21:26:42 +02:00
|
|
|
if not os.path.exists(_file):
|
|
|
|
logging.warning(f'Could not find {_file}')
|
|
|
|
logging.error(f'Could not find {_file}')
|
|
|
|
return False
|
|
|
|
|
2021-07-17 16:47:31 +02:00
|
|
|
media = Media.get_class_by_file(_file, get_all_subclasses(), ignore_tags=set())
|
2021-07-16 21:26:42 +02:00
|
|
|
if not media:
|
|
|
|
logging.warning(f'File{_file} is not supported')
|
|
|
|
logging.error(f'File {_file} can\'t be imported')
|
|
|
|
return False
|
|
|
|
|
|
|
|
return media
|
|
|
|
|