gh-58 Fix all pep8 issues reported in non-test files
This commit is contained in:
parent
3501f2c954
commit
60c4acc1b3
|
@ -1,3 +1,3 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
nosetests -w elodie/tests
|
nosetests -w elodie/tests && pep8 elodie --exclude=tests
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)))
|
||||||
|
|
|
@ -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,6 +117,7 @@ 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
|
||||||
|
@ -103,7 +127,7 @@ def place_name(lat, lon):
|
||||||
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
|
||||||
|
|
|
@ -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,7 +92,7 @@ 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):
|
||||||
|
@ -96,10 +100,13 @@ class Db(object):
|
||||||
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))
|
||||||
|
@ -107,8 +114,7 @@ class Db(object):
|
||||||
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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -20,9 +20,7 @@ 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')
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue