diff --git a/.pre-commit b/.pre-commit index 20c3816..1a375d3 100755 --- a/.pre-commit +++ b/.pre-commit @@ -1,3 +1,3 @@ #!/usr/bin/env bash -nosetests -w elodie/tests +nosetests -w elodie/tests && pep8 elodie --exclude=tests diff --git a/elodie/arguments.py b/elodie/arguments.py index d3b11dd..23afc73 100644 --- a/elodie/arguments.py +++ b/elodie/arguments.py @@ -1,15 +1,17 @@ """ """ -import sys, getopt +import getopt +import sys from re import sub + def parse(argv, options, long_options, usage): try: opts, args = getopt.getopt(argv, options, long_options) except getopt.GetoptError: print usage sys.exit(2) - + return_arguments = {} for opt, arg in opts: if opt == '-h': diff --git a/elodie/filesystem.py b/elodie/filesystem.py index a200a30..9d56fa8 100644 --- a/elodie/filesystem.py +++ b/elodie/filesystem.py @@ -1,6 +1,6 @@ """ Author: Jaisen Mathai -Video package that handles all video operations +General file system methods """ import os import re @@ -11,14 +11,13 @@ from elodie import geolocation from elodie import constants from elodie.localstorage import Db -""" -General file system methods -""" + class FileSystem: """ 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): try: @@ -35,10 +34,11 @@ class FileSystem: """ Delete a directory only if it's empty. - Instead of checking first using `len([name for name in os.listdir(directory_path)]) == 0` - we catch the OSError exception. + Instead of checking first using `len([name for name in + 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): try: @@ -60,7 +60,10 @@ class FileSystem: for dirname, dirnames, filenames in os.walk(path): # print path to all 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)) return files @@ -75,7 +78,8 @@ class FileSystem: """ Generate file name for a photo or video using its metadata. 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 @param, media, Photo|Video, A Photo or Video instance @@ -86,21 +90,39 @@ class FileSystem: return None metadata = media.get_metadata() - if(metadata == None): + if(metadata is 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. - # This helps when re-running the program on file which were already processed. - base_name = re.sub('^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-', '', metadata['base_name']) + # This helps when re-running the program on file which were already + # 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): 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()) base_name = base_name.replace('-%s' % 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() """ @@ -125,8 +147,14 @@ class FileSystem: if(metadata['album'] is not None): path.append(metadata['album']) - elif(metadata['latitude'] is not None and metadata['longitude'] is not None): - place_name = geolocation.place_name(metadata['latitude'], metadata['longitude']) + elif( + 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): path.append(place_name) @@ -134,7 +162,7 @@ class FileSystem: if(len(path) < 2): path.append('Unknown Location') - #return '/'.join(path[::-1]) + # return '/'.join(path[::-1]) return '/'.join(path) def process_file(self, _file, destination, media, **kwargs): @@ -156,20 +184,24 @@ class FileSystem: db = Db() checksum = db.checksum(_file) - if(checksum == None): - if(constants.debug == True): + if(checksum is None): + if(constants.debug is True): print 'Could not get checksum for %s. Skipping...' % _file return - # If duplicates are not allowed and this hash exists in the db then we return - if(allowDuplicate == False and db.check_hash(checksum) == True): - if(constants.debug == True): - print '%s already exists at %s. Skipping...' % (_file, db.get_hash(checksum)) + # If duplicates are not allowed and this hash exists in the db then we + # return + if(allowDuplicate is False and db.check_hash(checksum) is True): + if(constants.debug is True): + print '%s already exists at %s. Skipping...' % ( + _file, + db.get_hash(checksum) + ) return self.create_directory(dest_directory) - if(move == True): + if(move is True): stat = os.stat(_file) shutil.move(_file, dest_path) os.utime(dest_path, (stat.st_atime, stat.st_mtime)) @@ -191,22 +223,34 @@ class FileSystem: video_file_path = video.get_file_path() # 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 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) if(year_month_match is not None): (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): 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(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: - 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))) diff --git a/elodie/geolocation.py b/elodie/geolocation.py index 41ec7e2..458b398 100644 --- a/elodie/geolocation.py +++ b/elodie/geolocation.py @@ -11,6 +11,7 @@ import urllib from elodie import constants from elodie.localstorage import Db + class Fraction(fractions.Fraction): """Only create Fractions from floats. >>> Fraction(0.3) @@ -22,6 +23,7 @@ class Fraction(fractions.Fraction): """Should be compatible with Python 2.6, though untested.""" return fractions.Fraction.from_float(value).limit_denominator(99999) + def coordinates_by_name(name): # Try to get cached location first db = Db() @@ -36,57 +38,78 @@ def coordinates_by_name(name): geolocation_info = lookup(name) if(geolocation_info is not None): - if('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): + if( + '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. - use_location = geolocation_info['results'][0]['locations'][0]['latLng'] - # Loop over the locations to see if we come accross a geocodeQuality=city. + # By default we use the first entry unless we find one with + # 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 - for location in geolocation_info['results'][0]['locations']: - if('latLng' in location and 'lat' in location['latLng'] and 'lng' in location['latLng'] and location['geocodeQuality'].lower() == 'city'): + 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' + ): use_location = location['latLng'] break - + return { 'latitude': use_location['lat'], 'longitude': use_location['lng'] } - + return None + 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 if(decimal < 0): 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) subminutes = abs((decimal - int(decimal)) * 60) minutes = int(subminutes) * sign subseconds = abs((subminutes - int(subminutes)) * 60) * sign subseconds_fraction = Fraction(subseconds) - if(signed == False): + if(signed is False): degrees = abs(degrees) minutes = abs(minutes) subseconds_fraction = Fraction(abs(subseconds)) - return (pyexiv2.Rational(degrees, 1), pyexiv2.Rational(minutes, 1), pyexiv2.Rational(subseconds_fraction.numerator, subseconds_fraction.denominator)) - -def dms_to_decimal(degrees, minutes, seconds, sign=' '): - return (-1 if sign[0] in 'SWsw' else 1) * ( - float(degrees) + - float(minutes) / 60 + - float(seconds) / 3600 + return ( + pyexiv2.Rational(degrees, 1), + pyexiv2.Rational(minutes, 1), + pyexiv2.Rational(subseconds_fraction.numerator, subseconds_fraction.denominator) # noqa ) + +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(): config_file = '%s/config.ini' % constants.application_directory if not path.exists(config_file): return None - + config = ConfigParser() config.read(config_file) if('MapQuest' not in config.sections()): @@ -94,16 +117,17 @@ def get_key(): return config.get('MapQuest', 'key') + def place_name(lat, lon): # Try to get cached location first db = Db() # 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): return cached_place_name - lookup_place_name = None; + lookup_place_name = None geolocation_info = reverse_lookup(lat, lon) if(geolocation_info is not None): if('address' in geolocation_info): @@ -130,18 +154,22 @@ def reverse_lookup(lat, lon): try: 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() except requests.exceptions.RequestException as e: - if(constants.debug == True): + if(constants.debug is True): print e return None except ValueError as e: - if(constants.debug == True): + if(constants.debug is True): print r.text print e return None + def lookup(name): if(name is None or len(name) == 0): return None @@ -150,16 +178,19 @@ def lookup(name): try: params = {'format': 'json', 'key': key, 'location': name} - if(constants.debug == True): - print '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)) + if(constants.debug is True): + 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) + ) return r.json() except requests.exceptions.RequestException as e: - if(constants.debug == True): + if(constants.debug is True): print e return None except ValueError as e: - if(constants.debug == True): + if(constants.debug is True): print r.text print e return None diff --git a/elodie/localstorage.py b/elodie/localstorage.py index 9e9af3a..93080fd 100644 --- a/elodie/localstorage.py +++ b/elodie/localstorage.py @@ -6,9 +6,11 @@ import sys from elodie import constants + class Db(object): 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): os.makedirs(constants.application_directory) @@ -20,7 +22,8 @@ class Db(object): 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: try: self.hash_db = json.load(f) @@ -35,7 +38,8 @@ class Db(object): 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: try: self.location_db = json.load(f) @@ -44,14 +48,14 @@ class Db(object): def add_hash(self, key, value, write=False): self.hash_db[key] = value - if(write == True): + if(write is True): self.update_hash_db() def check_hash(self, key): return key in self.hash_db def get_hash(self, key): - if(self.check_hash(key) == True): + if(self.check_hash(key) is True): return self.hash_db[key] return None @@ -88,27 +92,29 @@ class Db(object): data['long'] = longitude data['name'] = place self.location_db.append(data) - if(write == True): + if(write is True): 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 name = None for data in self.location_db: # 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 - 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 - x = (lon2 - lon1) * cos( 0.5*(lat2+lat1) ) + x = (lon2 - lon1) * cos(0.5*(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 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 return name diff --git a/elodie/media/media.py b/elodie/media/media.py index 3dcc76b..2cb18d0 100644 --- a/elodie/media/media.py +++ b/elodie/media/media.py @@ -1,6 +1,6 @@ """ Author: Jaisen Mathai -Media package that handles all video operations +Media package that's a parent class for media objects """ # load modules @@ -17,9 +17,7 @@ import re import subprocess import time -""" -Media class for general video operations -""" + class Media(object): # class / static variable accessible through get_valid_extensions() __name__ = 'Media' @@ -30,7 +28,7 @@ class Media(object): def __init__(self, source=None): self.source = source 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_ref': 'Exif.GPSInfo.GPSLatitudeRef', 'longitude': 'Exif.GPSInfo.GPSLongitude', @@ -51,7 +49,7 @@ class Media(object): exiftool_attributes = self.get_exiftool_attributes() if(exiftool_attributes is None or 'album' not in exiftool_attributes): return None - + return exiftool_attributes['album'] """ @@ -65,12 +63,11 @@ class Media(object): # If exiftool wasn't found we try to brute force the homebrew location if(exiftool is None): 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 exiftool - """ Get the full path to the video. @@ -88,14 +85,15 @@ class Media(object): """ 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 """ def get_exif(self): if(not self.is_valid()): return None - + if(self.exif is not None): return self.exif @@ -114,7 +112,11 @@ class Media(object): return False 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() # Get album from exiftool output @@ -131,7 +133,7 @@ class Media(object): title_return = title_regex.group(1).strip() if(len(title_return) > 0): title = title_return - break; + break self.exiftool_attributes = { 'album': album, @@ -140,7 +142,6 @@ class Media(object): return self.exiftool_attributes - """ Get the file extension as a lowercased string. @@ -163,7 +164,7 @@ class Media(object): if(not self.is_valid()): 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 source = self.source @@ -181,7 +182,7 @@ class Media(object): } return self.metadata - + """ Get the mimetype of the file. @@ -193,11 +194,11 @@ class Media(object): source = self.source mimetype = mimetypes.guess_type(source) - if(mimetype == None): + if(mimetype is None): return None return mimetype[0] - + """ Get the title for a photo of video @@ -232,9 +233,14 @@ class Media(object): source = self.source stat = os.stat(source) exiftool_config = constants.exiftool_config - if(constants.debug == True): - print '%s -config "%s" -xmp-elodie:Album="%s" "%s"' % (exiftool, exiftool_config, name, source) - process_output = subprocess.Popen(['%s -config "%s" -xmp-elodie:Album="%s" "%s"' % (exiftool, exiftool_config, name, source)], stdout=subprocess.PIPE, shell=True) + if(constants.debug is True): + 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 + ) streamdata = process_output.communicate()[0] if(process_output.returncode != 0): @@ -266,14 +272,16 @@ class Media(object): self.set_album(folder) 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. - 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 - @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): self.get_metadata() diff --git a/elodie/media/photo.py b/elodie/media/photo.py index d9e3c5c..3adcaf0 100644 --- a/elodie/media/photo.py +++ b/elodie/media/photo.py @@ -20,9 +20,7 @@ from elodie import constants from media import Media from elodie import geolocation -""" -Photo class for general photo operations -""" + class Photo(Media): __name__ = 'Photo' extensions = ('jpg', 'jpeg', 'nef', 'dng', 'gif') @@ -35,7 +33,7 @@ class Photo(Media): # We only want to parse EXIF once so we store it here self.exif = None - + """ Get the duration of a photo in seconds. Uses ffmpeg/ffprobe @@ -47,11 +45,17 @@ class Photo(Media): return None source = self.source - result = subprocess.Popen(['ffprobe', source], - stdout = subprocess.PIPE, stderr = subprocess.STDOUT) + result = subprocess.Popen( + ['ffprobe', source], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) for key in result.stdout.readlines(): 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 """ @@ -63,26 +67,40 @@ class Photo(Media): if(not self.is_valid()): 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() if(key not in exif): return None 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 - 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 + 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 coords = exif[key].value 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: - 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: return None @@ -97,21 +115,23 @@ class Photo(Media): return None 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. - # EXIF DateTimeOriginal and EXIF DateTime are both stored in %Y:%m:%d %H:%M:%S format - # we use date.strptime -> .timetuple -> time.mktime to do the conversion in the local timezone + # EXIF DateTimeOriginal and EXIF DateTime are both stored + # 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 - # 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() for key in self.exif_map['date_taken']: try: if(key in exif): - if(re.match('\d{4}(-|:)\d{2}(-|:)\d{2}', str(exif[key].value)) is not None): - seconds_since_epoch = time.mktime(exif[key].value.timetuple()) - break; + 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()) # noqa + break except BaseException as e: - if(constants.debug == True): + if(constants.debug is True): print e pass @@ -121,8 +141,9 @@ class Photo(Media): 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 """ def is_valid(self): @@ -131,7 +152,7 @@ class Photo(Media): # gh-4 This checks if the source file is an image. # It doesn't validate against the list of supported types. if(imghdr.what(source) is None): - return False; + return False 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.read() - exif_metadata['Exif.GPSInfo.GPSLatitude'] = geolocation.decimal_to_dms(latitude, False) - exif_metadata['Exif.GPSInfo.GPSLatitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLatitudeRef', 'N' if latitude >= 0 else 'S') - exif_metadata['Exif.GPSInfo.GPSLongitude'] = geolocation.decimal_to_dms(longitude, False) - exif_metadata['Exif.GPSInfo.GPSLongitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLongitudeRef', 'E' if longitude >= 0 else 'W') + 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') # noqa + 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') # noqa exif_metadata.write() return True diff --git a/elodie/media/video.py b/elodie/media/video.py index 6b9fac0..ea2d65e 100644 --- a/elodie/media/video.py +++ b/elodie/media/video.py @@ -20,12 +20,10 @@ from elodie import constants from elodie import plist_parser from media import Media -""" -Video class for general video operations -""" + class Video(Media): __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 @@ -43,12 +41,11 @@ class Video(Media): avmetareadwrite = find_executable('avmetareadwrite') if(avmetareadwrite is None): 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 avmetareadwrite - """ Get latitude or longitude of photo from EXIF @@ -71,7 +68,7 @@ class Video(Media): 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'): decimal_degrees = decimal_degrees * -1 @@ -89,9 +86,10 @@ class Video(Media): source = self.source # 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 - 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 exif_data = self.get_exif() for key in ['Creation Date', 'Media Create Date']: @@ -99,7 +97,12 @@ class Video(Media): if(date is not None): date_string = date.group(1) 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): seconds_since_epoch = exif_seconds_since_epoch time_found_in_exif = True @@ -111,7 +114,7 @@ class Video(Media): return None return time.gmtime(seconds_since_epoch) - + """ Get the duration of a video in seconds. Uses ffmpeg/ffprobe @@ -123,16 +126,23 @@ class Video(Media): return None source = self.source - result = subprocess.Popen(['ffprobe', source], - stdout = subprocess.PIPE, stderr = subprocess.STDOUT) + result = subprocess.Popen( + ['ffprobe', source], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) for key in result.stdout.readlines(): 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 """ 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 """ @@ -142,12 +152,17 @@ class Video(Media): return None 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() """ - 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 """ def is_valid(self): @@ -168,8 +183,14 @@ class Video(Media): source = self.source result = self.__update_using_plist(time=date_taken_as_datetime) - if(result == True): - os.utime(source, (int(time.time()), time.mktime(date_taken_as_datetime.timetuple()))) + if(result is True): + os.utime( + source, + ( + int(time.time()), + time.mktime(date_taken_as_datetime.timetuple()) + ) + ) return result @@ -185,7 +206,7 @@ class Video(Media): if(latitude is None or longitude is None): return False - result = self.__update_using_plist(latitude=latitude, longitude=longitude) + result = self.__update_using_plist(latitude=latitude, longitude=longitude) # noqa return result """ @@ -210,9 +231,11 @@ class Video(Media): 1) Check if avmetareadwrite is installed 2) Export a plist file to a temporary location from the source 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 - 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 @param, latitude, float, Latitude of the file @@ -221,33 +244,49 @@ class Video(Media): @returns, boolean """ 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(constants.debug == True): + if( + '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' return False avmetareadwrite = self.get_avmetareadwrite() if(avmetareadwrite is None): - if(constants.debug == True): + if(constants.debug is True): print 'Could not find avmetareadwrite' return False 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: - # 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 - avmetareadwrite_generate_plist_command = '%s -p "%s" "%s"' % (avmetareadwrite, plist_temp.name, source) - write_process = subprocess.Popen([avmetareadwrite_generate_plist_command], stdout=subprocess.PIPE, shell=True) + avmetareadwrite_generate_plist_command = '%s -p "%s" "%s"' % ( + avmetareadwrite, + plist_temp.name, + source + ) + write_process = subprocess.Popen( + [avmetareadwrite_generate_plist_command], + stdout=subprocess.PIPE, + shell=True + ) streamdata = write_process.communicate()[0] if(write_process.returncode != 0): - if(constants.debug == True): + if(constants.debug is True): print 'Failed to generate plist file' return False 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 if('latitude' in kwargs and 'longitude' in kwargs): latitude = str(abs(kwargs['latitude'])).lstrip('0') @@ -258,10 +297,16 @@ class Video(Media): lat_sign = '+' if latitude > 0 else '-' # We need to zeropad the longitude. # 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 '-' - longitude_str = '{:9.5f}'.format(abs(longitude)).replace(' ', '0') - lat_lon_str = '%s%s%s%s' % (lat_sign, latitude, lon_sign, longitude_str) + longitude_str = '{:9.5f}'.format(abs(longitude)).replace(' ', '0') # noqa + lat_lon_str = '%s%s%s%s' % ( + lat_sign, + latitude, + lon_sign, + longitude_str + ) plist.update_key('common/location', lat_lon_str) plist_should_be_written = True @@ -277,13 +322,12 @@ class Video(Media): hms = [int(x) for x in time_parts[1].split(':')] 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: d = datetime(ymd[0], ymd[1], ymd[2], 12, 00, 00) offset = time.strftime("%z", time.gmtime(time.time())) - time_string = d.strftime('%Y-%m-%dT%H:%M:%S{}'.format(offset)) - #2015-10-09T17:11:30-0700 + time_string = d.strftime('%Y-%m-%dT%H:%M:%S{}'.format(offset)) # noqa plist.update_key('common/creationDate', time_string) plist_should_be_written = True @@ -296,13 +340,15 @@ class Video(Media): plist_final = plist_temp.name plist.write_file(plist_final) else: - if(constants.debug == True): + if(constants.debug is True): print 'Nothing to update, plist unchanged' return False # We create a temporary file to save the modified file to. - # If the modification is successful we will update the existing file. - # We can't call self.get_metadata else we will run into infinite loops + # If the modification is successful we will update the + # existing file. + # We can't call self.get_metadata else we will run into + # infinite loops # metadata = self.get_metadata() temp_movie = None with tempfile.NamedTemporaryFile() as temp_file: @@ -310,23 +356,44 @@ class Video(Media): # We need to block until the child process completes. # http://stackoverflow.com/a/5631819/1318758 - avmetareadwrite_command = '%s -a %s "%s" "%s"' % (avmetareadwrite, plist_final, source, temp_movie) - update_process = subprocess.Popen([avmetareadwrite_command], stdout=subprocess.PIPE, shell=True) + avmetareadwrite_command = '%s -a %s "%s" "%s"' % ( + avmetareadwrite, + plist_final, + source, + temp_movie + ) + update_process = subprocess.Popen( + [avmetareadwrite_command], + stdout=subprocess.PIPE, + shell=True + ) streamdata = update_process.communicate()[0] if(update_process.returncode != 0): - if(constants.debug == True): - print '%s did not complete successfully' % avmetareadwrite_command + if(constants.debug is True): + print '%s did not complete successfully' % avmetareadwrite_command # noqa 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_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(constants.debug == True): + 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(constants.debug is True): print 'Something went wrong updating video metadata' 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) stat = os.stat(source) shutil.move(temp_movie, source) @@ -343,6 +410,7 @@ class Video(Media): def get_valid_extensions(Video): return Video.extensions + class Transcode(object): # Constructor takes a video object as it's parameter def __init__(self, video=None): diff --git a/elodie/plist_parser.py b/elodie/plist_parser.py index 43307ad..e20d813 100644 --- a/elodie/plist_parser.py +++ b/elodie/plist_parser.py @@ -2,6 +2,7 @@ Author: Jaisen Mathai Parse OS X plists. Wraps standard lib plistlib (https://docs.python.org/3/library/plistlib.html) +Plist class to parse and interact with a plist file. """ # load modules @@ -9,9 +10,7 @@ from os import path import plistlib -""" -Plist class to parse and interact with a plist file. -""" + class Plist(object): def __init__(self, source): if(path.isfile(source) == False):