2015-10-07 10:48:01 +02:00
|
|
|
"""
|
2016-01-08 23:49:06 +01:00
|
|
|
The photo module contains the :class:`Photo` class, which is used to track
|
|
|
|
image objects (JPG, DNG, etc.).
|
|
|
|
|
|
|
|
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
|
2015-10-07 10:48:01 +02:00
|
|
|
"""
|
|
|
|
|
2015-12-11 08:07:01 +01:00
|
|
|
import imghdr
|
2015-10-07 10:48:01 +02:00
|
|
|
import os
|
|
|
|
import re
|
2015-10-21 08:51:14 +02:00
|
|
|
import subprocess
|
2015-10-07 10:48:01 +02:00
|
|
|
import time
|
2016-06-21 20:19:40 +02:00
|
|
|
from datetime import datetime
|
|
|
|
from re import compile
|
|
|
|
|
2015-10-07 10:48:01 +02:00
|
|
|
|
2015-12-20 09:30:30 +01:00
|
|
|
from elodie import constants
|
2015-10-20 10:17:09 +02:00
|
|
|
from elodie import geolocation
|
2016-06-21 20:19:40 +02:00
|
|
|
from elodie.external.pyexiftool import ExifTool
|
|
|
|
from media import Media
|
2015-10-07 10:48:01 +02:00
|
|
|
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2015-10-07 10:48:01 +02:00
|
|
|
class Photo(Media):
|
2016-01-08 23:49:06 +01:00
|
|
|
|
|
|
|
"""A photo object.
|
|
|
|
|
|
|
|
:param str source: The fully qualified path to the photo file
|
|
|
|
"""
|
|
|
|
|
2015-11-19 11:31:32 +01:00
|
|
|
__name__ = 'Photo'
|
2016-01-08 23:49:06 +01:00
|
|
|
|
|
|
|
#: Valid extensions for photo files.
|
2016-04-13 08:47:41 +02:00
|
|
|
extensions = ('arw', 'dng', 'gif', 'jpeg', 'jpg', 'nef', 'rw2')
|
2015-10-07 10:48:01 +02:00
|
|
|
|
|
|
|
def __init__(self, source=None):
|
|
|
|
super(Photo, self).__init__(source)
|
|
|
|
|
|
|
|
# We only want to parse EXIF once so we store it here
|
|
|
|
self.exif = None
|
2016-01-02 08:23:06 +01:00
|
|
|
|
2015-10-07 10:48:01 +02:00
|
|
|
def get_duration(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get the duration of a photo in seconds. Uses ffmpeg/ffprobe.
|
|
|
|
|
|
|
|
:returns: str or None for a non-photo file
|
|
|
|
"""
|
2015-10-07 10:48:01 +02:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
|
|
|
|
|
|
|
source = self.source
|
2016-01-02 08:23:06 +01:00
|
|
|
result = subprocess.Popen(
|
|
|
|
['ffprobe', source],
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.STDOUT
|
|
|
|
)
|
2015-10-07 10:48:01 +02:00
|
|
|
for key in result.stdout.readlines():
|
|
|
|
if 'Duration' in key:
|
2016-01-02 08:23:06 +01:00
|
|
|
return re.search(
|
|
|
|
'(\d{2}:\d{2}.\d{2})',
|
|
|
|
key
|
|
|
|
).group(1).replace('.', ':')
|
2015-10-07 10:48:01 +02:00
|
|
|
return None
|
|
|
|
|
2015-12-12 09:43:49 +01:00
|
|
|
def get_date_taken(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Get the date which the photo was taken.
|
|
|
|
|
|
|
|
The date value returned is defined by the min() of mtime and ctime.
|
|
|
|
|
|
|
|
:returns: time object or None for non-photo files or 0 timestamp
|
|
|
|
"""
|
2015-12-12 09:43:49 +01:00
|
|
|
if(not self.is_valid()):
|
|
|
|
return None
|
2016-06-21 20:19:40 +02:00
|
|
|
|
2015-12-12 09:43:49 +01:00
|
|
|
source = self.source
|
2016-01-02 08:23:06 +01:00
|
|
|
seconds_since_epoch = min(os.path.getmtime(source), os.path.getctime(source)) # noqa
|
2016-06-21 20:19:40 +02:00
|
|
|
|
|
|
|
exif = self.get_exiftool_attributes()
|
|
|
|
if not exif:
|
|
|
|
return seconds_since_epoch
|
|
|
|
|
2015-12-12 09:43:49 +01:00
|
|
|
# We need to parse a string from EXIF into a timestamp.
|
2016-01-02 08:23:06 +01:00
|
|
|
# EXIF DateTimeOriginal and EXIF DateTime are both stored
|
|
|
|
# in %Y:%m:%d %H:%M:%S format
|
2016-06-21 20:19:40 +02:00
|
|
|
# we use split on a space and then r':|-' -> convert to int -> .timetuple()
|
2016-01-02 08:23:06 +01:00
|
|
|
# the conversion in the local timezone
|
2015-12-12 09:43:49 +01:00
|
|
|
# EXIF DateTime is already stored as a timestamp
|
2016-01-02 08:23:06 +01:00
|
|
|
# Sourced from https://github.com/photo/frontend/blob/master/src/libraries/models/Photo.php#L500 # noqa
|
2015-12-12 09:43:49 +01:00
|
|
|
for key in self.exif_map['date_taken']:
|
|
|
|
try:
|
|
|
|
if(key in exif):
|
2016-06-21 20:19:40 +02:00
|
|
|
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)
|
|
|
|
time_tuple = datetime(*dt_list).timetuple()
|
|
|
|
seconds_since_epoch = time.mktime(time_tuple)
|
2016-01-02 08:23:06 +01:00
|
|
|
break
|
2015-12-12 09:43:49 +01:00
|
|
|
except BaseException as e:
|
2016-01-02 08:23:06 +01:00
|
|
|
if(constants.debug is True):
|
2015-12-12 09:43:49 +01:00
|
|
|
print e
|
|
|
|
pass
|
|
|
|
|
|
|
|
if(seconds_since_epoch == 0):
|
|
|
|
return None
|
|
|
|
|
|
|
|
return time.gmtime(seconds_since_epoch)
|
|
|
|
|
2015-12-11 08:07:01 +01:00
|
|
|
def is_valid(self):
|
2016-01-08 23:49:06 +01:00
|
|
|
"""Check the file extension against valid file extensions.
|
|
|
|
|
|
|
|
The list of valid file extensions come from self.extensions. This
|
|
|
|
also checks whether the file is an image.
|
|
|
|
|
|
|
|
:returns: bool
|
|
|
|
"""
|
2015-12-11 08:07:01 +01:00
|
|
|
source = self.source
|
|
|
|
|
|
|
|
# gh-4 This checks if the source file is an image.
|
|
|
|
# It doesn't validate against the list of supported types.
|
|
|
|
if(imghdr.what(source) is None):
|
2016-01-02 08:23:06 +01:00
|
|
|
return False
|
2015-12-11 08:07:01 +01:00
|
|
|
|
|
|
|
return os.path.splitext(source)[1][1:].lower() in self.extensions
|