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
|
|
|
|
by all the media types, but isn't itself used to represent anything. Its
|
|
|
|
sub-classes (:class:`~elodie.media.audio.Audio`,
|
|
|
|
:class:`~elodie.media.photo.Photo`, and :class:`~elodie.media.video.Video`)
|
|
|
|
are used to represent the actual files.
|
|
|
|
|
|
|
|
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
2015-10-07 08:47:51 +02:00
|
|
|
"""
|
2016-03-12 20:09:28 +01:00
|
|
|
from __future__ import print_function
|
2015-10-07 08:47:51 +02:00
|
|
|
|
2017-01-22 09:19:44 +01:00
|
|
|
import os
|
2020-01-18 18:03:40 +01:00
|
|
|
import six
|
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
|
2016-04-07 10:08:33 +02:00
|
|
|
from elodie.media.base import Base
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2016-04-07 10:08:33 +02:00
|
|
|
class Media(Base):
|
2015-10-20 10:17:09 +02:00
|
|
|
|
2016-01-08 23:49:06 +01:00
|
|
|
"""The base class for all media objects.
|
|
|
|
|
|
|
|
: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
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
def __init__(self, source=None):
|
2016-04-07 10:08:33 +02:00
|
|
|
super(Media, self).__init__(source)
|
2021-04-17 05:08:58 +02:00
|
|
|
self.date_original = ['EXIF:DateTimeOriginal']
|
|
|
|
self.date_created = ['EXIF:CreateDate']
|
|
|
|
self.date_modified = ['File:FileModifyDate']
|
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']
|
2016-06-21 20:19:40 +02:00
|
|
|
self.title_key = 'XMP:Title'
|
|
|
|
self.latitude_keys = ['EXIF:GPSLatitude']
|
|
|
|
self.longitude_keys = ['EXIF:GPSLongitude']
|
|
|
|
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
|
2020-01-14 08:04:33 +01:00
|
|
|
self.exif_metadata = None
|
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
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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]
|
|
|
|
|
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
|
|
|
|
|
|
|
|
if(self.title_key not in exiftool_attributes):
|
2015-10-28 08:19:21 +01:00
|
|
|
return None
|
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
return exiftool_attributes[self.title_key]
|
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
|
2016-04-07 10:08:33 +02:00
|
|
|
super(Media, self).reset_cache()
|
2016-03-13 21:37:06 +01:00
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
def set_album(self, album):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Set album for a photo
|
|
|
|
|
|
|
|
:param str name: Name of album
|
|
|
|
:returns: bool
|
|
|
|
"""
|
2016-06-21 20:19:40 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2016-06-24 06:34:51 +02:00
|
|
|
tags = {self.album_keys[0]: album}
|
2016-06-21 20:19:40 +02:00
|
|
|
status = self.__set_tags(tags)
|
|
|
|
self.reset_cache()
|
|
|
|
|
|
|
|
return status
|
|
|
|
|
2021-04-17 05:08:58 +02:00
|
|
|
def set_date_original(self, time):
|
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
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
status = self.__set_tags(tags)
|
|
|
|
self.reset_cache()
|
|
|
|
return status
|
2015-12-03 08:47:54 +01:00
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
def set_location(self, latitude, longitude):
|
|
|
|
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.
|
|
|
|
tags = {
|
|
|
|
self.latitude_keys[0]: latitude,
|
|
|
|
self.longitude_keys[0]: 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.
|
|
|
|
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)
|
|
|
|
self.reset_cache()
|
|
|
|
|
|
|
|
return status
|
|
|
|
|
2017-01-23 08:18:28 +01:00
|
|
|
def set_original_name(self, 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(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
# If EXIF original name tag is set then we return.
|
|
|
|
if self.get_original_name() is not None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
source = self.source
|
2017-01-23 08:18:28 +01:00
|
|
|
|
|
|
|
if not name:
|
|
|
|
name = os.path.basename(source)
|
|
|
|
|
2017-01-22 09:19:44 +01:00
|
|
|
tags = {self.original_name_key: name}
|
|
|
|
status = self.__set_tags(tags)
|
|
|
|
self.reset_cache()
|
|
|
|
return status
|
|
|
|
|
2016-06-21 20:19:40 +02:00
|
|
|
def set_title(self, title):
|
|
|
|
"""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
|
|
|
|
|
|
|
|
tags = {self.title_key: title}
|
|
|
|
status = self.__set_tags(tags)
|
2016-03-13 21:37:06 +01:00
|
|
|
self.reset_cache()
|
2016-06-21 20:19:40 +02:00
|
|
|
|
|
|
|
return status
|
|
|
|
|
|
|
|
def __set_tags(self, tags):
|
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
source = self.source
|
|
|
|
|
|
|
|
status = ''
|
2020-01-14 08:04:33 +01:00
|
|
|
status = ExifTool().set_tags(tags,source)
|
2016-06-21 20:19:40 +02:00
|
|
|
|
|
|
|
return status != ''
|