2015-10-07 08:47:51 +02:00
|
|
|
"""
|
2016-01-08 23:49:06 +01:00
|
|
|
The media module provides a base :class:`Media` class for all objects that
|
|
|
|
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
|
|
|
"""
|
|
|
|
|
|
|
|
# load modules
|
2015-10-21 08:51:14 +02:00
|
|
|
from elodie import constants
|
2016-01-08 01:45:55 +01:00
|
|
|
from elodie.dependencies import get_exiftool
|
2015-10-21 08:51:14 +02:00
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
import mimetypes
|
|
|
|
import os
|
2015-10-14 09:39:30 +02:00
|
|
|
import pyexiv2
|
2015-10-07 08:47:51 +02:00
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
class Media(object):
|
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 = {
|
|
|
|
'latitude' : 'latitude_ref',
|
|
|
|
'longitude': 'longitude_ref'
|
|
|
|
}
|
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
def __init__(self, source=None):
|
|
|
|
self.source = source
|
2015-10-14 09:39:30 +02:00
|
|
|
self.exif_map = {
|
2016-01-02 08:23:06 +01:00
|
|
|
'date_taken': ['Exif.Photo.DateTimeOriginal', 'Exif.Image.DateTime'], # , 'EXIF FileDateTime'], # noqa
|
2015-10-14 09:39:30 +02:00
|
|
|
'latitude': 'Exif.GPSInfo.GPSLatitude',
|
|
|
|
'latitude_ref': 'Exif.GPSInfo.GPSLatitudeRef',
|
|
|
|
'longitude': 'Exif.GPSInfo.GPSLongitude',
|
|
|
|
'longitude_ref': 'Exif.GPSInfo.GPSLongitudeRef',
|
|
|
|
}
|
2015-10-21 08:51:14 +02:00
|
|
|
self.exiftool_attributes = None
|
2015-10-29 09:12:52 +01:00
|
|
|
self.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()
|
2015-11-02 11:11:53 +01:00
|
|
|
if(exiftool_attributes is None or 'album' not in exiftool_attributes):
|
2015-10-14 09:39:30 +02:00
|
|
|
return None
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2015-10-21 08:51:14 +02:00
|
|
|
return exiftool_attributes['album']
|
2015-10-07 08:47:51 +02:00
|
|
|
|
|
|
|
def get_file_path(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get the full path to the video.
|
|
|
|
|
|
|
|
:returns: string
|
|
|
|
"""
|
2015-10-07 08:47:51 +02:00
|
|
|
return self.source
|
|
|
|
|
|
|
|
def is_valid(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""The default is_valid() always returns false.
|
2015-12-04 09:54:21 +01:00
|
|
|
|
2016-01-08 23:49:06 +01:00
|
|
|
This should be overridden in a child class to return true if the
|
|
|
|
source is valid, and false otherwise.
|
|
|
|
|
|
|
|
:returns: bool
|
|
|
|
"""
|
|
|
|
return False
|
2015-10-14 09:39:30 +02:00
|
|
|
|
|
|
|
def get_exif(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Read EXIF from a photo file.
|
|
|
|
|
|
|
|
We store the result in a member variable so we can call get_exif()
|
|
|
|
often without performance degredation.
|
|
|
|
|
|
|
|
:returns: list or none for a non-photo file
|
|
|
|
"""
|
2015-10-14 09:39:30 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2015-10-14 09:39:30 +02:00
|
|
|
if(self.exif is not None):
|
|
|
|
return self.exif
|
|
|
|
|
|
|
|
source = self.source
|
|
|
|
self.exif = pyexiv2.ImageMetadata(source)
|
|
|
|
self.exif.read()
|
|
|
|
|
|
|
|
return self.exif
|
|
|
|
|
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.
|
|
|
|
"""
|
2015-10-21 08:51:14 +02:00
|
|
|
if(self.exiftool_attributes is not None):
|
|
|
|
return self.exiftool_attributes
|
|
|
|
|
2016-01-08 01:45:55 +01:00
|
|
|
exiftool = get_exiftool()
|
2015-10-21 08:51:14 +02:00
|
|
|
if(exiftool is None):
|
|
|
|
return False
|
|
|
|
|
|
|
|
source = self.source
|
2016-01-02 08:23:06 +01:00
|
|
|
process_output = subprocess.Popen(
|
2016-01-27 13:54:56 +01:00
|
|
|
'%s "%s"' % (exiftool, source),
|
2016-01-02 08:23:06 +01:00
|
|
|
stdout=subprocess.PIPE,
|
2016-01-26 20:01:05 +01:00
|
|
|
shell=True,
|
|
|
|
universal_newlines=True
|
2016-01-02 08:23:06 +01:00
|
|
|
)
|
2015-10-21 08:51:14 +02:00
|
|
|
output = process_output.stdout.read()
|
|
|
|
|
2015-10-28 08:19:21 +01:00
|
|
|
# Get album from exiftool output
|
2015-10-21 08:51:14 +02:00
|
|
|
album = None
|
|
|
|
album_regex = re.search('Album +: +(.+)', output)
|
|
|
|
if(album_regex is not None):
|
|
|
|
album = album_regex.group(1)
|
|
|
|
|
2015-10-28 08:19:21 +01:00
|
|
|
# Get title from exiftool output
|
|
|
|
title = None
|
|
|
|
for key in ['Displayname', 'Headline', 'Title', 'ImageDescription']:
|
|
|
|
title_regex = re.search('%s +: +(.+)' % key, output)
|
|
|
|
if(title_regex is not None):
|
|
|
|
title_return = title_regex.group(1).strip()
|
|
|
|
if(len(title_return) > 0):
|
|
|
|
title = title_return
|
2016-01-02 08:23:06 +01:00
|
|
|
break
|
2015-10-28 08:19:21 +01:00
|
|
|
|
2015-10-21 08:51:14 +02:00
|
|
|
self.exiftool_attributes = {
|
2015-10-28 08:19:21 +01:00
|
|
|
'album': album,
|
|
|
|
'title': title
|
2015-10-21 08:51:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return self.exiftool_attributes
|
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
def get_extension(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get the file extension as a lowercased string.
|
|
|
|
|
|
|
|
:returns: string or None for a non-video
|
|
|
|
"""
|
2015-10-07 08:47:51 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
source = self.source
|
|
|
|
return os.path.splitext(source)[1][1:].lower()
|
2015-10-14 09:39:30 +02:00
|
|
|
|
2015-12-03 08:47:54 +01:00
|
|
|
def get_metadata(self, update_cache=False):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get a dictionary of metadata for a photo.
|
|
|
|
|
|
|
|
All keys will be present and have a value of None if not obtained.
|
|
|
|
|
|
|
|
:returns: dict or None for non-photo files
|
|
|
|
"""
|
2015-10-14 09:39:30 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
2016-01-02 08:23:06 +01:00
|
|
|
if(self.metadata is not None and update_cache is False):
|
2015-10-29 09:12:52 +01:00
|
|
|
return self.metadata
|
|
|
|
|
2015-10-14 09:39:30 +02:00
|
|
|
source = self.source
|
|
|
|
|
2015-10-29 09:12:52 +01:00
|
|
|
self.metadata = {
|
2015-10-14 09:39:30 +02:00
|
|
|
'date_taken': self.get_date_taken(),
|
|
|
|
'latitude': self.get_coordinate('latitude'),
|
|
|
|
'longitude': self.get_coordinate('longitude'),
|
|
|
|
'album': self.get_album(),
|
2015-10-28 08:19:21 +01:00
|
|
|
'title': self.get_title(),
|
2015-10-14 09:39:30 +02:00
|
|
|
'mime_type': self.get_mimetype(),
|
|
|
|
'base_name': os.path.splitext(os.path.basename(source))[0],
|
2015-12-03 08:47:54 +01:00
|
|
|
'extension': self.get_extension(),
|
|
|
|
'directory_path': os.path.dirname(source)
|
2015-10-14 09:39:30 +02:00
|
|
|
}
|
|
|
|
|
2015-10-29 09:12:52 +01:00
|
|
|
return self.metadata
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2015-10-07 08:47:51 +02:00
|
|
|
def get_mimetype(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get the mimetype of the file.
|
|
|
|
|
|
|
|
:returns: str or None for a non-video
|
|
|
|
"""
|
2015-10-07 08:47:51 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
source = self.source
|
|
|
|
mimetype = mimetypes.guess_type(source)
|
2016-01-02 08:23:06 +01:00
|
|
|
if(mimetype is None):
|
2015-10-07 08:47:51 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
return mimetype[0]
|
2016-01-02 08:23:06 +01: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()
|
|
|
|
|
2015-11-02 11:11:53 +01:00
|
|
|
if(exiftool_attributes is None or 'title' not in exiftool_attributes):
|
2015-10-28 08:19:21 +01:00
|
|
|
return None
|
|
|
|
|
|
|
|
return exiftool_attributes['title']
|
2015-10-14 05:26:55 +02:00
|
|
|
|
2015-10-21 08:51:14 +02:00
|
|
|
def set_album(self, name):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Set album for a photo
|
|
|
|
|
|
|
|
:param str name: Name of album
|
|
|
|
:returns: bool
|
|
|
|
"""
|
2015-10-21 08:51:14 +02:00
|
|
|
if(name is None):
|
|
|
|
return False
|
|
|
|
|
2016-01-08 01:45:55 +01:00
|
|
|
exiftool = get_exiftool()
|
2015-10-21 08:51:14 +02:00
|
|
|
if(exiftool is None):
|
|
|
|
return False
|
|
|
|
|
|
|
|
source = self.source
|
2015-12-01 09:39:05 +01:00
|
|
|
stat = os.stat(source)
|
2015-10-21 08:51:14 +02:00
|
|
|
exiftool_config = constants.exiftool_config
|
2016-01-02 08:23:06 +01:00
|
|
|
if(constants.debug is True):
|
|
|
|
print '%s -config "%s" -xmp-elodie:Album="%s" "%s"' % (exiftool, exiftool_config, name, source) # noqa
|
|
|
|
process_output = subprocess.Popen(
|
2016-01-27 15:28:18 +01:00
|
|
|
'%s -config "%s" -xmp-elodie:Album="%s" "%s"' %
|
2016-02-12 00:24:42 +01:00
|
|
|
(exiftool, exiftool_config, name, source),
|
2016-01-02 08:23:06 +01:00
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
shell=True
|
|
|
|
)
|
2016-01-02 18:34:43 +01:00
|
|
|
process_output.communicate()
|
2015-12-01 09:39:05 +01:00
|
|
|
|
2015-10-21 08:51:14 +02:00
|
|
|
if(process_output.returncode != 0):
|
|
|
|
return False
|
|
|
|
|
2015-12-01 09:39:05 +01:00
|
|
|
os.utime(source, (stat.st_atime, stat.st_mtime))
|
|
|
|
|
2015-10-26 10:06:48 +01:00
|
|
|
exiftool_backup_file = '%s%s' % (source, '_original')
|
|
|
|
if(os.path.isfile(exiftool_backup_file) is True):
|
|
|
|
os.remove(exiftool_backup_file)
|
2015-12-03 08:47:54 +01:00
|
|
|
|
|
|
|
self.set_metadata(album=name)
|
|
|
|
return True
|
|
|
|
|
|
|
|
def set_album_from_folder(self):
|
|
|
|
metadata = self.get_metadata()
|
|
|
|
|
|
|
|
print 'huh/'
|
|
|
|
|
|
|
|
# If this file has an album already set we do not overwrite EXIF
|
|
|
|
if(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
|
|
|
|
|
|
|
|
self.set_album(folder)
|
2015-10-21 08:51:14 +02:00
|
|
|
return True
|
|
|
|
|
2015-10-29 09:12:52 +01:00
|
|
|
def set_metadata_basename(self, new_basename):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""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).
|
|
|
|
"""
|
2015-10-29 09:12:52 +01:00
|
|
|
self.get_metadata()
|
|
|
|
self.metadata['base_name'] = new_basename
|
|
|
|
|
2015-12-03 08:47:54 +01:00
|
|
|
def set_metadata(self, **kwargs):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Method to manually update attributes in metadata.
|
|
|
|
|
|
|
|
:params dict kwargs: Named parameters to update.
|
|
|
|
"""
|
2015-12-03 08:47:54 +01:00
|
|
|
metadata = self.get_metadata()
|
|
|
|
for key in kwargs:
|
|
|
|
if(key in metadata):
|
|
|
|
self.metadata[key] = kwargs[key]
|
|
|
|
|
2015-10-20 10:17:09 +02:00
|
|
|
@classmethod
|
2016-01-02 18:34:43 +01:00
|
|
|
def get_class_by_file(cls, _file, classes):
|
2015-10-14 05:26:55 +02:00
|
|
|
extension = os.path.splitext(_file)[1][1:].lower()
|
2015-10-20 10:17:09 +02:00
|
|
|
|
|
|
|
for i in classes:
|
2015-12-19 05:08:12 +01:00
|
|
|
if(extension in i.extensions):
|
2015-10-20 10:17:09 +02:00
|
|
|
return i(_file)
|
2015-10-14 05:26:55 +02:00
|
|
|
|
2015-10-20 10:17:09 +02:00
|
|
|
return None
|
2016-01-08 23:49:06 +01:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_valid_extensions(cls):
|
|
|
|
"""Static method to access static extensions variable.
|
|
|
|
|
|
|
|
:returns: tuple(str)
|
|
|
|
"""
|
|
|
|
return cls.extensions
|