gh-58 Fix all pep8 issues reported in non-test files

This commit is contained in:
Jaisen Mathai 2016-01-01 23:23:06 -08:00
parent 3501f2c954
commit 60c4acc1b3
9 changed files with 362 additions and 183 deletions

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
nosetests -w elodie/tests nosetests -w elodie/tests && pep8 elodie --exclude=tests

View File

@ -1,8 +1,10 @@
""" """
""" """
import sys, getopt import getopt
import sys
from re import sub from re import sub
def parse(argv, options, long_options, usage): def parse(argv, options, long_options, usage):
try: try:
opts, args = getopt.getopt(argv, options, long_options) opts, args = getopt.getopt(argv, options, long_options)

View File

@ -1,6 +1,6 @@
""" """
Author: Jaisen Mathai <jaisen@jmathai.com> Author: Jaisen Mathai <jaisen@jmathai.com>
Video package that handles all video operations General file system methods
""" """
import os import os
import re import re
@ -11,14 +11,13 @@ from elodie import geolocation
from elodie import constants from elodie import constants
from elodie.localstorage import Db from elodie.localstorage import Db
"""
General file system methods
"""
class FileSystem: class FileSystem:
""" """
Create a directory if it does not already exist.. Create a directory if it does not already exist..
@param, directory_name, string, A fully qualified path of the directory to create. @param, directory_name, string, A fully qualified path of the
directory to create.
""" """
def create_directory(self, directory_path): def create_directory(self, directory_path):
try: try:
@ -35,10 +34,11 @@ class FileSystem:
""" """
Delete a directory only if it's empty. Delete a directory only if it's empty.
Instead of checking first using `len([name for name in os.listdir(directory_path)]) == 0` Instead of checking first using `len([name for name in
we catch the OSError exception. os.listdir(directory_path)]) == 0` we catch the OSError exception.
@param, directory_name, string, A fully qualified path of the directory to delete. @param, directory_name, string, A fully qualified path of the directory
to delete.
""" """
def delete_directory_if_empty(self, directory_path): def delete_directory_if_empty(self, directory_path):
try: try:
@ -60,7 +60,10 @@ class FileSystem:
for dirname, dirnames, filenames in os.walk(path): for dirname, dirnames, filenames in os.walk(path):
# print path to all filenames. # print path to all filenames.
for filename in filenames: for filename in filenames:
if(extensions == None or filename.lower().endswith(extensions)): if(
extensions is None or
filename.lower().endswith(extensions)
):
files.append('%s/%s' % (dirname, filename)) files.append('%s/%s' % (dirname, filename))
return files return files
@ -75,7 +78,8 @@ class FileSystem:
""" """
Generate file name for a photo or video using its metadata. Generate file name for a photo or video using its metadata.
We use an ISO8601-like format for the file name prefix. We use an ISO8601-like format for the file name prefix.
Instead of colons as the separator for hours, minutes and seconds we use a hyphen. Instead of colons as the separator for hours, minutes and seconds we use a
hyphen.
https://en.wikipedia.org/wiki/ISO_8601#General_principles https://en.wikipedia.org/wiki/ISO_8601#General_principles
@param, media, Photo|Video, A Photo or Video instance @param, media, Photo|Video, A Photo or Video instance
@ -86,21 +90,39 @@ class FileSystem:
return None return None
metadata = media.get_metadata() metadata = media.get_metadata()
if(metadata == None): if(metadata is None):
return None return None
# If the file has EXIF title we use that in the file name (i.e. my-favorite-photo-img_1234.jpg) # If the file has EXIF title we use that in the file name
# (i.e. my-favorite-photo-img_1234.jpg)
# We want to remove the date prefix we add to the name. # We want to remove the date prefix we add to the name.
# This helps when re-running the program on file which were already processed. # This helps when re-running the program on file which were already
base_name = re.sub('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-', '', metadata['base_name']) # processed.
base_name = re.sub(
'^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-',
'',
metadata['base_name']
)
if(len(base_name) == 0): if(len(base_name) == 0):
base_name = metadata['base_name'] base_name = metadata['base_name']
if('title' in metadata and metadata['title'] is not None and len(metadata['title']) > 0):
if(
'title' in metadata and
metadata['title'] is not None and
len(metadata['title']) > 0
):
title_sanitized = re.sub('\W+', '-', metadata['title'].strip()) title_sanitized = re.sub('\W+', '-', metadata['title'].strip())
base_name = base_name.replace('-%s' % title_sanitized, '') base_name = base_name.replace('-%s' % title_sanitized, '')
base_name = '%s-%s' % (base_name, title_sanitized) base_name = '%s-%s' % (base_name, title_sanitized)
file_name = '%s-%s.%s' % (time.strftime('%Y-%m-%d_%H-%M-%S', metadata['date_taken']), base_name, metadata['extension']) file_name = '%s-%s.%s' % (
time.strftime(
'%Y-%m-%d_%H-%M-%S',
metadata['date_taken']
),
base_name,
metadata['extension']
)
return file_name.lower() return file_name.lower()
""" """
@ -125,8 +147,14 @@ class FileSystem:
if(metadata['album'] is not None): if(metadata['album'] is not None):
path.append(metadata['album']) path.append(metadata['album'])
elif(metadata['latitude'] is not None and metadata['longitude'] is not None): elif(
place_name = geolocation.place_name(metadata['latitude'], metadata['longitude']) metadata['latitude'] is not None and
metadata['longitude'] is not None
):
place_name = geolocation.place_name(
metadata['latitude'],
metadata['longitude']
)
if(place_name is not None): if(place_name is not None):
path.append(place_name) path.append(place_name)
@ -134,7 +162,7 @@ class FileSystem:
if(len(path) < 2): if(len(path) < 2):
path.append('Unknown Location') path.append('Unknown Location')
#return '/'.join(path[::-1]) # return '/'.join(path[::-1])
return '/'.join(path) return '/'.join(path)
def process_file(self, _file, destination, media, **kwargs): def process_file(self, _file, destination, media, **kwargs):
@ -156,20 +184,24 @@ class FileSystem:
db = Db() db = Db()
checksum = db.checksum(_file) checksum = db.checksum(_file)
if(checksum == None): if(checksum is None):
if(constants.debug == True): if(constants.debug is True):
print 'Could not get checksum for %s. Skipping...' % _file print 'Could not get checksum for %s. Skipping...' % _file
return return
# If duplicates are not allowed and this hash exists in the db then we return # If duplicates are not allowed and this hash exists in the db then we
if(allowDuplicate == False and db.check_hash(checksum) == True): # return
if(constants.debug == True): if(allowDuplicate is False and db.check_hash(checksum) is True):
print '%s already exists at %s. Skipping...' % (_file, db.get_hash(checksum)) if(constants.debug is True):
print '%s already exists at %s. Skipping...' % (
_file,
db.get_hash(checksum)
)
return return
self.create_directory(dest_directory) self.create_directory(dest_directory)
if(move == True): if(move is True):
stat = os.stat(_file) stat = os.stat(_file)
shutil.move(_file, dest_path) shutil.move(_file, dest_path)
os.utime(dest_path, (stat.st_atime, stat.st_mtime)) os.utime(dest_path, (stat.st_atime, stat.st_mtime))
@ -191,22 +223,34 @@ class FileSystem:
video_file_path = video.get_file_path() video_file_path = video.get_file_path()
# Initialize date taken to what's returned from the metadata function. # Initialize date taken to what's returned from the metadata function.
# If the folder and file name follow a time format of YYYY-MM/DD-IMG_0001.JPG then we override the date_taken # If the folder and file name follow a time format of
# YYYY-MM/DD-IMG_0001.JPG then we override the date_taken
(year, month, day) = [None] * 3 (year, month, day) = [None] * 3
directory = os.path.dirname(video_file_path) directory = os.path.dirname(video_file_path)
# If the directory matches we get back a match with groups() = (year, month) # If the directory matches we get back a match with
# groups() = (year, month)
year_month_match = re.search('(\d{4})-(\d{2})', directory) year_month_match = re.search('(\d{4})-(\d{2})', directory)
if(year_month_match is not None): if(year_month_match is not None):
(year, month) = year_month_match.groups() (year, month) = year_month_match.groups()
day_match = re.search('^(\d{2})', os.path.basename(video.get_file_path())) day_match = re.search(
'^(\d{2})',
os.path.basename(video.get_file_path())
)
if(day_match is not None): if(day_match is not None):
day = day_match.group(1) day = day_match.group(1)
# check if the file system path indicated a date and if so we override the metadata value # check if the file system path indicated a date and if so we
# override the metadata value
if(year is not None and month is not None): if(year is not None and month is not None):
if(day is not None): if(day is not None):
date_taken = time.strptime('{}-{}-{}'.format(year, month, day), '%Y-%m-%d') date_taken = time.strptime(
'{}-{}-{}'.format(year, month, day),
'%Y-%m-%d'
)
else: else:
date_taken = time.strptime('{}-{}'.format(year, month), '%Y-%m') date_taken = time.strptime(
'{}-{}'.format(year, month),
'%Y-%m'
)
os.utime(video_file_path, (time.time(), time.mktime(date_taken))) os.utime(video_file_path, (time.time(), time.mktime(date_taken)))

View File

@ -11,6 +11,7 @@ import urllib
from elodie import constants from elodie import constants
from elodie.localstorage import Db from elodie.localstorage import Db
class Fraction(fractions.Fraction): class Fraction(fractions.Fraction):
"""Only create Fractions from floats. """Only create Fractions from floats.
>>> Fraction(0.3) >>> Fraction(0.3)
@ -22,6 +23,7 @@ class Fraction(fractions.Fraction):
"""Should be compatible with Python 2.6, though untested.""" """Should be compatible with Python 2.6, though untested."""
return fractions.Fraction.from_float(value).limit_denominator(99999) return fractions.Fraction.from_float(value).limit_denominator(99999)
def coordinates_by_name(name): def coordinates_by_name(name):
# Try to get cached location first # Try to get cached location first
db = Db() db = Db()
@ -36,15 +38,27 @@ def coordinates_by_name(name):
geolocation_info = lookup(name) geolocation_info = lookup(name)
if(geolocation_info is not None): if(geolocation_info is not None):
if('results' in geolocation_info and len(geolocation_info['results']) != 0 and if(
'locations' in geolocation_info['results'][0] and len(geolocation_info['results'][0]['locations']) != 0): 'results' in geolocation_info and
len(geolocation_info['results']) != 0 and
'locations' in geolocation_info['results'][0] and
len(geolocation_info['results'][0]['locations']) != 0
):
# By default we use the first entry unless we find one with geocodeQuality=city. # By default we use the first entry unless we find one with
use_location = geolocation_info['results'][0]['locations'][0]['latLng'] # geocodeQuality=city.
# Loop over the locations to see if we come accross a geocodeQuality=city. geolocation_result = geolocation_info['results'][0]
use_location = geolocation_result['locations'][0]['latLng']
# Loop over the locations to see if we come accross a
# geocodeQuality=city.
# If we find a city we set that to the use_location and break # If we find a city we set that to the use_location and break
for location in geolocation_info['results'][0]['locations']: for location in geolocation_result['locations']:
if('latLng' in location and 'lat' in location['latLng'] and 'lng' in location['latLng'] and location['geocodeQuality'].lower() == 'city'): if(
'latLng' in location and
'lat' in location['latLng'] and
'lng' in location['latLng'] and
location['geocodeQuality'].lower() == 'city'
):
use_location = location['latLng'] use_location = location['latLng']
break break
@ -55,33 +69,42 @@ def coordinates_by_name(name):
return None return None
def decimal_to_dms(decimal, signed=True): def decimal_to_dms(decimal, signed=True):
# if decimal is negative we need to make the degrees and minutes negative also # if decimal is negative we need to make the degrees and minutes
# negative also
sign = 1 sign = 1
if(decimal < 0): if(decimal < 0):
sign = -1 sign = -1
# http://anothergisblog.blogspot.com/2011/11/convert-decimal-degree-to-degrees.html # http://anothergisblog.blogspot.com/2011/11/convert-decimal-degree-to-degrees.html # noqa
degrees = int(decimal) degrees = int(decimal)
subminutes = abs((decimal - int(decimal)) * 60) subminutes = abs((decimal - int(decimal)) * 60)
minutes = int(subminutes) * sign minutes = int(subminutes) * sign
subseconds = abs((subminutes - int(subminutes)) * 60) * sign subseconds = abs((subminutes - int(subminutes)) * 60) * sign
subseconds_fraction = Fraction(subseconds) subseconds_fraction = Fraction(subseconds)
if(signed == False): if(signed is False):
degrees = abs(degrees) degrees = abs(degrees)
minutes = abs(minutes) minutes = abs(minutes)
subseconds_fraction = Fraction(abs(subseconds)) subseconds_fraction = Fraction(abs(subseconds))
return (pyexiv2.Rational(degrees, 1), pyexiv2.Rational(minutes, 1), pyexiv2.Rational(subseconds_fraction.numerator, subseconds_fraction.denominator)) return (
pyexiv2.Rational(degrees, 1),
def dms_to_decimal(degrees, minutes, seconds, sign=' '): pyexiv2.Rational(minutes, 1),
return (-1 if sign[0] in 'SWsw' else 1) * ( pyexiv2.Rational(subseconds_fraction.numerator, subseconds_fraction.denominator) # noqa
float(degrees) +
float(minutes) / 60 +
float(seconds) / 3600
) )
def dms_to_decimal(degrees, minutes, seconds, direction=' '):
sign = 1
if(direction[0] in 'NEne'):
sign = -1
return (
float(degrees) + float(minutes) / 60 + float(seconds) / 3600
) * sign
def get_key(): def get_key():
config_file = '%s/config.ini' % constants.application_directory config_file = '%s/config.ini' % constants.application_directory
if not path.exists(config_file): if not path.exists(config_file):
@ -94,16 +117,17 @@ def get_key():
return config.get('MapQuest', 'key') return config.get('MapQuest', 'key')
def place_name(lat, lon): def place_name(lat, lon):
# Try to get cached location first # Try to get cached location first
db = Db() db = Db()
# 3km distace radious for a match # 3km distace radious for a match
cached_place_name = db.get_location_name(lat, lon,3000) cached_place_name = db.get_location_name(lat, lon, 3000)
if(cached_place_name is not None): if(cached_place_name is not None):
return cached_place_name return cached_place_name
lookup_place_name = None; lookup_place_name = None
geolocation_info = reverse_lookup(lat, lon) geolocation_info = reverse_lookup(lat, lon)
if(geolocation_info is not None): if(geolocation_info is not None):
if('address' in geolocation_info): if('address' in geolocation_info):
@ -130,18 +154,22 @@ def reverse_lookup(lat, lon):
try: try:
params = {'format': 'json', 'key': key, 'lat': lat, 'lon': lon} params = {'format': 'json', 'key': key, 'lat': lat, 'lon': lon}
r = requests.get('http://open.mapquestapi.com/nominatim/v1/reverse.php?%s' % urllib.urlencode(params)) r = requests.get(
'http://open.mapquestapi.com/nominatim/v1/reverse.php?%s' %
urllib.urlencode(params)
)
return r.json() return r.json()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
if(constants.debug == True): if(constants.debug is True):
print e print e
return None return None
except ValueError as e: except ValueError as e:
if(constants.debug == True): if(constants.debug is True):
print r.text print r.text
print e print e
return None return None
def lookup(name): def lookup(name):
if(name is None or len(name) == 0): if(name is None or len(name) == 0):
return None return None
@ -150,16 +178,19 @@ def lookup(name):
try: try:
params = {'format': 'json', 'key': key, 'location': name} params = {'format': 'json', 'key': key, 'location': name}
if(constants.debug == True): if(constants.debug is True):
print 'http://open.mapquestapi.com/geocoding/v1/address?%s' % urllib.urlencode(params) print 'http://open.mapquestapi.com/geocoding/v1/address?%s' % urllib.urlencode(params) # noqa
r = requests.get('http://open.mapquestapi.com/geocoding/v1/address?%s' % urllib.urlencode(params)) r = requests.get(
'http://open.mapquestapi.com/geocoding/v1/address?%s' %
urllib.urlencode(params)
)
return r.json() return r.json()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
if(constants.debug == True): if(constants.debug is True):
print e print e
return None return None
except ValueError as e: except ValueError as e:
if(constants.debug == True): if(constants.debug is True):
print r.text print r.text
print e print e
return None return None

View File

@ -6,9 +6,11 @@ import sys
from elodie import constants from elodie import constants
class Db(object): class Db(object):
def __init__(self): def __init__(self):
# verify that the application directory (~/.elodie) exists, else create it # verify that the application directory (~/.elodie) exists,
# else create it
if not os.path.exists(constants.application_directory): if not os.path.exists(constants.application_directory):
os.makedirs(constants.application_directory) os.makedirs(constants.application_directory)
@ -20,7 +22,8 @@ class Db(object):
self.hash_db = {} self.hash_db = {}
# We know from above that this file exists so we open it for reading only. # We know from above that this file exists so we open it
# for reading only.
with open(constants.hash_db, 'r') as f: with open(constants.hash_db, 'r') as f:
try: try:
self.hash_db = json.load(f) self.hash_db = json.load(f)
@ -35,7 +38,8 @@ class Db(object):
self.location_db = [] self.location_db = []
# We know from above that this file exists so we open it for reading only. # We know from above that this file exists so we open it
# for reading only.
with open(constants.location_db, 'r') as f: with open(constants.location_db, 'r') as f:
try: try:
self.location_db = json.load(f) self.location_db = json.load(f)
@ -44,14 +48,14 @@ class Db(object):
def add_hash(self, key, value, write=False): def add_hash(self, key, value, write=False):
self.hash_db[key] = value self.hash_db[key] = value
if(write == True): if(write is True):
self.update_hash_db() self.update_hash_db()
def check_hash(self, key): def check_hash(self, key):
return key in self.hash_db return key in self.hash_db
def get_hash(self, key): def get_hash(self, key):
if(self.check_hash(key) == True): if(self.check_hash(key) is True):
return self.hash_db[key] return self.hash_db[key]
return None return None
@ -88,27 +92,29 @@ class Db(object):
data['long'] = longitude data['long'] = longitude
data['name'] = place data['name'] = place
self.location_db.append(data) self.location_db.append(data)
if(write == True): if(write is True):
self.update_location_db() self.update_location_db()
def get_location_name(self, latitude, longitude,threshold_m): def get_location_name(self, latitude, longitude, threshold_m):
last_d = sys.maxint last_d = sys.maxint
name = None name = None
for data in self.location_db: for data in self.location_db:
# As threshold is quite smal use simple math # As threshold is quite smal use simple math
# From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points # From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points # noqa
# convert decimal degrees to radians # convert decimal degrees to radians
lon1, lat1, lon2, lat2 = map(radians, [longitude, latitude, data['long'], data['lat']]) lon1, lat1, lon2, lat2 = map(
radians,
[longitude, latitude, data['long'], data['lat']]
)
R = 6371000 # radius of the earth in m R = 6371000 # radius of the earth in m
x = (lon2 - lon1) * cos( 0.5*(lat2+lat1) ) x = (lon2 - lon1) * cos(0.5*(lat2+lat1))
y = lat2 - lat1 y = lat2 - lat1
d = R * sqrt( x*x + y*y ) d = R * sqrt(x*x + y*y)
# Use if closer then threshold_km reuse lookup # Use if closer then threshold_km reuse lookup
if(d <= threshold_m and d < last_d): if(d <= threshold_m and d < last_d):
#print "Found in cached location dist: %d m" % d name = data['name']
name = data['name'];
last_d = d last_d = d
return name return name

View File

@ -1,6 +1,6 @@
""" """
Author: Jaisen Mathai <jaisen@jmathai.com> Author: Jaisen Mathai <jaisen@jmathai.com>
Media package that handles all video operations Media package that's a parent class for media objects
""" """
# load modules # load modules
@ -17,9 +17,7 @@ import re
import subprocess import subprocess
import time import time
"""
Media class for general video operations
"""
class Media(object): class Media(object):
# class / static variable accessible through get_valid_extensions() # class / static variable accessible through get_valid_extensions()
__name__ = 'Media' __name__ = 'Media'
@ -30,7 +28,7 @@ class Media(object):
def __init__(self, source=None): def __init__(self, source=None):
self.source = source self.source = source
self.exif_map = { self.exif_map = {
'date_taken': ['Exif.Photo.DateTimeOriginal', 'Exif.Image.DateTime'], #, 'EXIF FileDateTime'], 'date_taken': ['Exif.Photo.DateTimeOriginal', 'Exif.Image.DateTime'], # , 'EXIF FileDateTime'], # noqa
'latitude': 'Exif.GPSInfo.GPSLatitude', 'latitude': 'Exif.GPSInfo.GPSLatitude',
'latitude_ref': 'Exif.GPSInfo.GPSLatitudeRef', 'latitude_ref': 'Exif.GPSInfo.GPSLatitudeRef',
'longitude': 'Exif.GPSInfo.GPSLongitude', 'longitude': 'Exif.GPSInfo.GPSLongitude',
@ -65,12 +63,11 @@ class Media(object):
# If exiftool wasn't found we try to brute force the homebrew location # If exiftool wasn't found we try to brute force the homebrew location
if(exiftool is None): if(exiftool is None):
exiftool = '/usr/local/bin/exiftool' exiftool = '/usr/local/bin/exiftool'
if(not os.path.isfile(exiftool) or not os.access(exiftool, os.X_OK)): if(not os.path.isfile(exiftool) or not os.access(exiftool, os.X_OK)): # noqa
return None return None
return exiftool return exiftool
""" """
Get the full path to the video. Get the full path to the video.
@ -88,7 +85,8 @@ class Media(object):
""" """
Read EXIF from a photo file. Read EXIF from a photo file.
We store the result in a member variable so we can call get_exif() often without performance degredation 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 @returns, list or none for a non-photo file
""" """
@ -114,7 +112,11 @@ class Media(object):
return False return False
source = self.source source = self.source
process_output = subprocess.Popen(['%s "%s"' % (exiftool, source)], stdout=subprocess.PIPE, shell=True) process_output = subprocess.Popen(
['%s "%s"' % (exiftool, source)],
stdout=subprocess.PIPE,
shell=True
)
output = process_output.stdout.read() output = process_output.stdout.read()
# Get album from exiftool output # Get album from exiftool output
@ -131,7 +133,7 @@ class Media(object):
title_return = title_regex.group(1).strip() title_return = title_regex.group(1).strip()
if(len(title_return) > 0): if(len(title_return) > 0):
title = title_return title = title_return
break; break
self.exiftool_attributes = { self.exiftool_attributes = {
'album': album, 'album': album,
@ -140,7 +142,6 @@ class Media(object):
return self.exiftool_attributes return self.exiftool_attributes
""" """
Get the file extension as a lowercased string. Get the file extension as a lowercased string.
@ -163,7 +164,7 @@ class Media(object):
if(not self.is_valid()): if(not self.is_valid()):
return None return None
if(self.metadata is not None and update_cache == False): if(self.metadata is not None and update_cache is False):
return self.metadata return self.metadata
source = self.source source = self.source
@ -193,7 +194,7 @@ class Media(object):
source = self.source source = self.source
mimetype = mimetypes.guess_type(source) mimetype = mimetypes.guess_type(source)
if(mimetype == None): if(mimetype is None):
return None return None
return mimetype[0] return mimetype[0]
@ -232,9 +233,14 @@ class Media(object):
source = self.source source = self.source
stat = os.stat(source) stat = os.stat(source)
exiftool_config = constants.exiftool_config exiftool_config = constants.exiftool_config
if(constants.debug == True): if(constants.debug is True):
print '%s -config "%s" -xmp-elodie:Album="%s" "%s"' % (exiftool, exiftool_config, name, source) print '%s -config "%s" -xmp-elodie:Album="%s" "%s"' % (exiftool, exiftool_config, name, source) # noqa
process_output = subprocess.Popen(['%s -config "%s" -xmp-elodie:Album="%s" "%s"' % (exiftool, exiftool_config, name, source)], stdout=subprocess.PIPE, shell=True) process_output = subprocess.Popen(
['%s -config "%s" -xmp-elodie:Album="%s" "%s"' %
(exiftool, exiftool_config, name, source)],
stdout=subprocess.PIPE,
shell=True
)
streamdata = process_output.communicate()[0] streamdata = process_output.communicate()[0]
if(process_output.returncode != 0): if(process_output.returncode != 0):
@ -266,14 +272,16 @@ class Media(object):
self.set_album(folder) self.set_album(folder)
return True return True
""" """
Specifically update the basename attribute in the metadata dictionary for this instance. Specifically update the basename attribute in the metadata
dictionary for this instance.
This is used for when we update the EXIF title of a media file. 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. 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 I.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
@param, string, new_basename, New basename of file (with the old title removed @param, string, new_basename, New basename of file
(with the old title removed)
""" """
def set_metadata_basename(self, new_basename): def set_metadata_basename(self, new_basename):
self.get_metadata() self.get_metadata()

View File

@ -20,9 +20,7 @@ from elodie import constants
from media import Media from media import Media
from elodie import geolocation from elodie import geolocation
"""
Photo class for general photo operations
"""
class Photo(Media): class Photo(Media):
__name__ = 'Photo' __name__ = 'Photo'
extensions = ('jpg', 'jpeg', 'nef', 'dng', 'gif') extensions = ('jpg', 'jpeg', 'nef', 'dng', 'gif')
@ -47,11 +45,17 @@ class Photo(Media):
return None return None
source = self.source source = self.source
result = subprocess.Popen(['ffprobe', source], result = subprocess.Popen(
stdout = subprocess.PIPE, stderr = subprocess.STDOUT) ['ffprobe', source],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
for key in result.stdout.readlines(): for key in result.stdout.readlines():
if 'Duration' in key: if 'Duration' in key:
return re.search('(\d{2}:\d{2}.\d{2})', key).group(1).replace('.', ':') return re.search(
'(\d{2}:\d{2}.\d{2})',
key
).group(1).replace('.', ':')
return None return None
""" """
@ -63,26 +67,40 @@ class Photo(Media):
if(not self.is_valid()): if(not self.is_valid()):
return None return None
key = self.exif_map['longitude'] if type == 'longitude' else self.exif_map['latitude'] key = self.exif_map['latitude']
if(type == 'longitude'):
key = self.exif_map['longitude']
exif = self.get_exif() exif = self.get_exif()
if(key not in exif): if(key not in exif):
return None return None
try: try:
# this is a hack to get the proper direction by negating the values for S and W # this is a hack to get the proper direction by negating the
# values for S and W
latdir = 1 latdir = 1
if(type == 'latitude' and str(exif[self.exif_map['latitude_ref']].value) == 'S'): if(type == 'latitude' and str(exif[self.exif_map['latitude_ref']].value) == 'S'): # noqa
latdir = -1 latdir = -1
londir = 1 londir = 1
if(type =='longitude' and str(exif[self.exif_map['longitude_ref']].value) == 'W'): if(type == 'longitude' and str(exif[self.exif_map['longitude_ref']].value) == 'W'): # noqa
londir = -1 londir = -1
coords = exif[key].value coords = exif[key].value
if(type == 'latitude'): if(type == 'latitude'):
return float(str(LatLon.Latitude(degree=coords[0], minute=coords[1], second=coords[2]))) * latdir lat_val = LatLon.Latitude(
degree=coords[0],
minute=coords[1],
second=coords[2]
)
return float(str(lat_val)) * latdir
else: else:
return float(str(LatLon.Longitude(degree=coords[0], minute=coords[1], second=coords[2]))) * londir lon_val = LatLon.Longitude(
degree=coords[0],
minute=coords[1],
second=coords[2]
)
return float(str(lon_val)) * londir
except KeyError: except KeyError:
return None return None
@ -97,21 +115,23 @@ class Photo(Media):
return None return None
source = self.source source = self.source
seconds_since_epoch = min(os.path.getmtime(source), os.path.getctime(source)) seconds_since_epoch = min(os.path.getmtime(source), os.path.getctime(source)) # noqa
# We need to parse a string from EXIF into a timestamp. # 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 # EXIF DateTimeOriginal and EXIF DateTime are both stored
# we use date.strptime -> .timetuple -> time.mktime to do the conversion in the local timezone # in %Y:%m:%d %H:%M:%S format
# we use date.strptime -> .timetuple -> time.mktime to do
# the conversion in the local timezone
# EXIF DateTime is already stored as a timestamp # EXIF DateTime is already stored as a timestamp
# Sourced from https://github.com/photo/frontend/blob/master/src/libraries/models/Photo.php#L500 # Sourced from https://github.com/photo/frontend/blob/master/src/libraries/models/Photo.php#L500 # noqa
exif = self.get_exif() exif = self.get_exif()
for key in self.exif_map['date_taken']: for key in self.exif_map['date_taken']:
try: try:
if(key in exif): if(key in exif):
if(re.match('\d{4}(-|:)\d{2}(-|:)\d{2}', str(exif[key].value)) is not None): if(re.match('\d{4}(-|:)\d{2}(-|:)\d{2}', str(exif[key].value)) is not None): # noqa
seconds_since_epoch = time.mktime(exif[key].value.timetuple()) seconds_since_epoch = time.mktime(exif[key].value.timetuple()) # noqa
break; break
except BaseException as e: except BaseException as e:
if(constants.debug == True): if(constants.debug is True):
print e print e
pass pass
@ -121,7 +141,8 @@ class Photo(Media):
return time.gmtime(seconds_since_epoch) return time.gmtime(seconds_since_epoch)
""" """
Check the file extension against valid file extensions as returned by self.extensions Check the file extension against valid file extensions as returned
by self.extensions
@returns, boolean @returns, boolean
""" """
@ -131,7 +152,7 @@ class Photo(Media):
# gh-4 This checks if the source file is an image. # gh-4 This checks if the source file is an image.
# It doesn't validate against the list of supported types. # It doesn't validate against the list of supported types.
if(imghdr.what(source) is None): if(imghdr.what(source) is None):
return False; return False
return os.path.splitext(source)[1][1:].lower() in self.extensions return os.path.splitext(source)[1][1:].lower() in self.extensions
@ -172,10 +193,10 @@ class Photo(Media):
exif_metadata = pyexiv2.ImageMetadata(source) exif_metadata = pyexiv2.ImageMetadata(source)
exif_metadata.read() exif_metadata.read()
exif_metadata['Exif.GPSInfo.GPSLatitude'] = geolocation.decimal_to_dms(latitude, False) exif_metadata['Exif.GPSInfo.GPSLatitude'] = geolocation.decimal_to_dms(latitude, False) # noqa
exif_metadata['Exif.GPSInfo.GPSLatitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLatitudeRef', 'N' if latitude >= 0 else 'S') exif_metadata['Exif.GPSInfo.GPSLatitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLatitudeRef', 'N' if latitude >= 0 else 'S') # noqa
exif_metadata['Exif.GPSInfo.GPSLongitude'] = geolocation.decimal_to_dms(longitude, False) exif_metadata['Exif.GPSInfo.GPSLongitude'] = geolocation.decimal_to_dms(longitude, False) # noqa
exif_metadata['Exif.GPSInfo.GPSLongitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLongitudeRef', 'E' if longitude >= 0 else 'W') exif_metadata['Exif.GPSInfo.GPSLongitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLongitudeRef', 'E' if longitude >= 0 else 'W') # noqa
exif_metadata.write() exif_metadata.write()
return True return True

View File

@ -20,12 +20,10 @@ from elodie import constants
from elodie import plist_parser from elodie import plist_parser
from media import Media from media import Media
"""
Video class for general video operations
"""
class Video(Media): class Video(Media):
__name__ = 'Video' __name__ = 'Video'
extensions = ('avi','m4v','mov','mp4','3gp') extensions = ('avi', 'm4v', 'mov', 'mp4', '3gp')
""" """
@param, source, string, The fully qualified path to the video file @param, source, string, The fully qualified path to the video file
@ -43,12 +41,11 @@ class Video(Media):
avmetareadwrite = find_executable('avmetareadwrite') avmetareadwrite = find_executable('avmetareadwrite')
if(avmetareadwrite is None): if(avmetareadwrite is None):
avmetareadwrite = '/usr/bin/avmetareadwrite' avmetareadwrite = '/usr/bin/avmetareadwrite'
if(not os.path.isfile(avmetareadwrite) or not os.access(avmetareadwrite, os.X_OK)): if(not os.path.isfile(avmetareadwrite) or not os.access(avmetareadwrite, os.X_OK)): # noqa
return None return None
return avmetareadwrite return avmetareadwrite
""" """
Get latitude or longitude of photo from EXIF Get latitude or longitude of photo from EXIF
@ -71,7 +68,7 @@ class Video(Media):
direction = direction.group(0) direction = direction.group(0)
decimal_degrees = float(coordinate[0]) + float(coordinate[1])/60 + float(coordinate[2])/3600 decimal_degrees = float(coordinate[0]) + float(coordinate[1])/60 + float(coordinate[2])/3600 # noqa
if(direction == 'S' or direction == 'W'): if(direction == 'S' or direction == 'W'):
decimal_degrees = decimal_degrees * -1 decimal_degrees = decimal_degrees * -1
@ -89,9 +86,10 @@ class Video(Media):
source = self.source source = self.source
# We need to parse a string from EXIF into a timestamp. # We need to parse a string from EXIF into a timestamp.
# We use date.strptime -> .timetuple -> time.mktime to do the conversion in the local timezone # We use date.strptime -> .timetuple -> time.mktime to do the
# conversion in the local timezone
# If the time is not found in EXIF we update EXIF # If the time is not found in EXIF we update EXIF
seconds_since_epoch = min(os.path.getmtime(source), os.path.getctime(source)) seconds_since_epoch = min(os.path.getmtime(source), os.path.getctime(source)) # noqa
time_found_in_exif = False time_found_in_exif = False
exif_data = self.get_exif() exif_data = self.get_exif()
for key in ['Creation Date', 'Media Create Date']: for key in ['Creation Date', 'Media Create Date']:
@ -99,7 +97,12 @@ class Video(Media):
if(date is not None): if(date is not None):
date_string = date.group(1) date_string = date.group(1)
try: try:
exif_seconds_since_epoch = time.mktime(datetime.strptime(date_string, '%Y:%m:%d %H:%M:%S').timetuple()) exif_seconds_since_epoch = time.mktime(
datetime.strptime(
date_string,
'%Y:%m:%d %H:%M:%S'
).timetuple()
)
if(exif_seconds_since_epoch < seconds_since_epoch): if(exif_seconds_since_epoch < seconds_since_epoch):
seconds_since_epoch = exif_seconds_since_epoch seconds_since_epoch = exif_seconds_since_epoch
time_found_in_exif = True time_found_in_exif = True
@ -123,16 +126,23 @@ class Video(Media):
return None return None
source = self.source source = self.source
result = subprocess.Popen(['ffprobe', source], result = subprocess.Popen(
stdout = subprocess.PIPE, stderr = subprocess.STDOUT) ['ffprobe', source],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
for key in result.stdout.readlines(): for key in result.stdout.readlines():
if 'Duration' in key: if 'Duration' in key:
return re.search('(\d{2}:\d{2}.\d{2})', key).group(1).replace('.', ':') return re.search(
'(\d{2}:\d{2}.\d{2})',
key
).group(1).replace('.', ':')
return None return None
""" """
Get exif data from video file. Get exif data from video file.
Not all video files have exif and this currently relies on the CLI exiftool program Not all video files have exif and this currently relies on
the CLI exiftool program
@returns, string or None if exiftool is not found @returns, string or None if exiftool is not found
""" """
@ -142,11 +152,16 @@ class Video(Media):
return None return None
source = self.source source = self.source
process_output = subprocess.Popen(['%s "%s"' % (exiftool, source)], stdout=subprocess.PIPE, shell=True) process_output = subprocess.Popen(
['%s "%s"' % (exiftool, source)],
stdout=subprocess.PIPE,
shell=True
)
return process_output.stdout.read() return process_output.stdout.read()
""" """
Check the file extension against valid file extensions as returned by self.extensions Check the file extension against valid file extensions as
returned by self.extensions
@returns, boolean @returns, boolean
""" """
@ -168,8 +183,14 @@ class Video(Media):
source = self.source source = self.source
result = self.__update_using_plist(time=date_taken_as_datetime) result = self.__update_using_plist(time=date_taken_as_datetime)
if(result == True): if(result is True):
os.utime(source, (int(time.time()), time.mktime(date_taken_as_datetime.timetuple()))) os.utime(
source,
(
int(time.time()),
time.mktime(date_taken_as_datetime.timetuple())
)
)
return result return result
@ -185,7 +206,7 @@ class Video(Media):
if(latitude is None or longitude is None): if(latitude is None or longitude is None):
return False return False
result = self.__update_using_plist(latitude=latitude, longitude=longitude) result = self.__update_using_plist(latitude=latitude, longitude=longitude) # noqa
return result return result
""" """
@ -210,9 +231,11 @@ class Video(Media):
1) Check if avmetareadwrite is installed 1) Check if avmetareadwrite is installed
2) Export a plist file to a temporary location from the source file 2) Export a plist file to a temporary location from the source file
3) Regex replace values in the plist file 3) Regex replace values in the plist file
4) Update the source file using the updated plist and save it to a temporary location 4) Update the source file using the updated plist and save it to a
temporary location
5) Validate that the metadata in the updated temorary movie is valid 5) Validate that the metadata in the updated temorary movie is valid
6) Copystat permission and time bits from the source file to the temporary movie 6) Copystat permission and time bits from the source file to the
temporary movie
7) Move the temporary file to overwrite the source file 7) Move the temporary file to overwrite the source file
@param, latitude, float, Latitude of the file @param, latitude, float, Latitude of the file
@ -221,33 +244,49 @@ class Video(Media):
@returns, boolean @returns, boolean
""" """
def __update_using_plist(self, **kwargs): def __update_using_plist(self, **kwargs):
if('latitude' not in kwargs and 'longitude' not in kwargs and 'time' not in kwargs and 'title' not in kwargs): if(
if(constants.debug == True): 'latitude' not in kwargs and
'longitude' not in kwargs and
'time' not in kwargs and
'title' not in kwargs
):
if(constants.debug is True):
print 'No lat/lon passed into __create_plist' print 'No lat/lon passed into __create_plist'
return False return False
avmetareadwrite = self.get_avmetareadwrite() avmetareadwrite = self.get_avmetareadwrite()
if(avmetareadwrite is None): if(avmetareadwrite is None):
if(constants.debug == True): if(constants.debug is True):
print 'Could not find avmetareadwrite' print 'Could not find avmetareadwrite'
return False return False
source = self.source source = self.source
# First we need to write the plist for an existing file to a temporary location # First we need to write the plist for an existing file
# to a temporary location
with tempfile.NamedTemporaryFile() as plist_temp: with tempfile.NamedTemporaryFile() as plist_temp:
# We need to write the plist file in a child process but also block for it to be complete. # We need to write the plist file in a child process
# but also block for it to be complete.
# http://stackoverflow.com/a/5631819/1318758 # http://stackoverflow.com/a/5631819/1318758
avmetareadwrite_generate_plist_command = '%s -p "%s" "%s"' % (avmetareadwrite, plist_temp.name, source) avmetareadwrite_generate_plist_command = '%s -p "%s" "%s"' % (
write_process = subprocess.Popen([avmetareadwrite_generate_plist_command], stdout=subprocess.PIPE, shell=True) avmetareadwrite,
plist_temp.name,
source
)
write_process = subprocess.Popen(
[avmetareadwrite_generate_plist_command],
stdout=subprocess.PIPE,
shell=True
)
streamdata = write_process.communicate()[0] streamdata = write_process.communicate()[0]
if(write_process.returncode != 0): if(write_process.returncode != 0):
if(constants.debug == True): if(constants.debug is True):
print 'Failed to generate plist file' print 'Failed to generate plist file'
return False return False
plist = plist_parser.Plist(plist_temp.name) plist = plist_parser.Plist(plist_temp.name)
# Depending on the kwargs that were passed in we regex the plist_text before we write it back. # Depending on the kwargs that were passed in we regex
# the plist_text before we write it back.
plist_should_be_written = False plist_should_be_written = False
if('latitude' in kwargs and 'longitude' in kwargs): if('latitude' in kwargs and 'longitude' in kwargs):
latitude = str(abs(kwargs['latitude'])).lstrip('0') latitude = str(abs(kwargs['latitude'])).lstrip('0')
@ -258,10 +297,16 @@ class Video(Media):
lat_sign = '+' if latitude > 0 else '-' lat_sign = '+' if latitude > 0 else '-'
# We need to zeropad the longitude. # We need to zeropad the longitude.
# No clue why - ask Apple. # No clue why - ask Apple.
# We set the sign to + or - and then we take the absolute value and fill it. # We set the sign to + or - and then we take the absolute value
# and fill it.
lon_sign = '+' if longitude > 0 else '-' lon_sign = '+' if longitude > 0 else '-'
longitude_str = '{:9.5f}'.format(abs(longitude)).replace(' ', '0') longitude_str = '{:9.5f}'.format(abs(longitude)).replace(' ', '0') # noqa
lat_lon_str = '%s%s%s%s' % (lat_sign, latitude, lon_sign, longitude_str) lat_lon_str = '%s%s%s%s' % (
lat_sign,
latitude,
lon_sign,
longitude_str
)
plist.update_key('common/location', lat_lon_str) plist.update_key('common/location', lat_lon_str)
plist_should_be_written = True plist_should_be_written = True
@ -277,13 +322,12 @@ class Video(Media):
hms = [int(x) for x in time_parts[1].split(':')] hms = [int(x) for x in time_parts[1].split(':')]
if(hms is not None): if(hms is not None):
d = datetime(ymd[0], ymd[1], ymd[2], hms[0], hms[1], hms[2]) d = datetime(ymd[0], ymd[1], ymd[2], hms[0], hms[1], hms[2]) # noqa
else: else:
d = datetime(ymd[0], ymd[1], ymd[2], 12, 00, 00) d = datetime(ymd[0], ymd[1], ymd[2], 12, 00, 00)
offset = time.strftime("%z", time.gmtime(time.time())) offset = time.strftime("%z", time.gmtime(time.time()))
time_string = d.strftime('%Y-%m-%dT%H:%M:%S{}'.format(offset)) time_string = d.strftime('%Y-%m-%dT%H:%M:%S{}'.format(offset)) # noqa
#2015-10-09T17:11:30-0700
plist.update_key('common/creationDate', time_string) plist.update_key('common/creationDate', time_string)
plist_should_be_written = True plist_should_be_written = True
@ -296,13 +340,15 @@ class Video(Media):
plist_final = plist_temp.name plist_final = plist_temp.name
plist.write_file(plist_final) plist.write_file(plist_final)
else: else:
if(constants.debug == True): if(constants.debug is True):
print 'Nothing to update, plist unchanged' print 'Nothing to update, plist unchanged'
return False return False
# We create a temporary file to save the modified file to. # We create a temporary file to save the modified file to.
# If the modification is successful we will update the existing file. # If the modification is successful we will update the
# We can't call self.get_metadata else we will run into infinite loops # existing file.
# We can't call self.get_metadata else we will run into
# infinite loops
# metadata = self.get_metadata() # metadata = self.get_metadata()
temp_movie = None temp_movie = None
with tempfile.NamedTemporaryFile() as temp_file: with tempfile.NamedTemporaryFile() as temp_file:
@ -310,23 +356,44 @@ class Video(Media):
# We need to block until the child process completes. # We need to block until the child process completes.
# http://stackoverflow.com/a/5631819/1318758 # http://stackoverflow.com/a/5631819/1318758
avmetareadwrite_command = '%s -a %s "%s" "%s"' % (avmetareadwrite, plist_final, source, temp_movie) avmetareadwrite_command = '%s -a %s "%s" "%s"' % (
update_process = subprocess.Popen([avmetareadwrite_command], stdout=subprocess.PIPE, shell=True) avmetareadwrite,
plist_final,
source,
temp_movie
)
update_process = subprocess.Popen(
[avmetareadwrite_command],
stdout=subprocess.PIPE,
shell=True
)
streamdata = update_process.communicate()[0] streamdata = update_process.communicate()[0]
if(update_process.returncode != 0): if(update_process.returncode != 0):
if(constants.debug == True): if(constants.debug is True):
print '%s did not complete successfully' % avmetareadwrite_command print '%s did not complete successfully' % avmetareadwrite_command # noqa
return False return False
# Before we do anything destructive we confirm that the file is in tact. # Before we do anything destructive we confirm that the
# file is in tact.
check_media = Video(temp_movie) check_media = Video(temp_movie)
check_metadata = check_media.get_metadata() check_metadata = check_media.get_metadata()
if(('latitude' in kwargs and 'longitude' in kwargs and check_metadata['latitude'] is None and check_metadata['longitude'] is None) or ('time' in kwargs and check_metadata['date_taken'] is None)): if(
if(constants.debug == True): (
'latitude' in kwargs and
'longitude' in kwargs and
check_metadata['latitude'] is None and
check_metadata['longitude'] is None
) or (
'time' in kwargs and
check_metadata['date_taken'] is None
)
):
if(constants.debug is True):
print 'Something went wrong updating video metadata' print 'Something went wrong updating video metadata'
return False return False
# Copy file information from original source to temporary file before copying back over # Copy file information from original source to temporary file
# before copying back over
shutil.copystat(source, temp_movie) shutil.copystat(source, temp_movie)
stat = os.stat(source) stat = os.stat(source)
shutil.move(temp_movie, source) shutil.move(temp_movie, source)
@ -343,6 +410,7 @@ class Video(Media):
def get_valid_extensions(Video): def get_valid_extensions(Video):
return Video.extensions return Video.extensions
class Transcode(object): class Transcode(object):
# Constructor takes a video object as it's parameter # Constructor takes a video object as it's parameter
def __init__(self, video=None): def __init__(self, video=None):

View File

@ -2,6 +2,7 @@
Author: Jaisen Mathai <jaisen@jmathai.com> Author: Jaisen Mathai <jaisen@jmathai.com>
Parse OS X plists. Parse OS X plists.
Wraps standard lib plistlib (https://docs.python.org/3/library/plistlib.html) Wraps standard lib plistlib (https://docs.python.org/3/library/plistlib.html)
Plist class to parse and interact with a plist file.
""" """
# load modules # load modules
@ -9,9 +10,7 @@ from os import path
import plistlib import plistlib
"""
Plist class to parse and interact with a plist file.
"""
class Plist(object): class Plist(object):
def __init__(self, source): def __init__(self, source):
if(path.isfile(source) == False): if(path.isfile(source) == False):