Added FileSystem.process_file and adjust.py

This commit is contained in:
Jaisen Mathai 2015-10-13 20:26:55 -07:00
parent 35de02ba6f
commit ad62b0d61d
10 changed files with 371 additions and 42 deletions

View File

@ -6,3 +6,15 @@ pip install requests
brew install exiftool
Need config.ini for reverse lookup
Need pyexiv2. Here's how to install on OS X using homebrew...on your own to use other tools.
Sourced from http://stackoverflow.com/a/18817419/1318758
May need to run these.
brew rm $(brew deps pyexiv2)
brew rm pyexiv2
Definietly need to run these
brew install boost --build-from-source
brew install pyexiv2

106
adjust.py Executable file
View File

@ -0,0 +1,106 @@
#!/usr/bin/env python
import os
import pyexiv2
import re
import shutil
import sys
import time
from datetime import datetime
from elodie import arguments
from elodie import geolocation
from elodie.media.photo import Media
from elodie.media.photo import Photo
from elodie.media.video import Video
from elodie.filesystem import FileSystem
from elodie.localstorage import Db
def parse_arguments(args):
config = {
'time': None,
'location': None,
'process': 'yes'
}
config.update(args)
return config
def main(config, args):
for arg in args:
if(arg[:2] == '--'):
continue
elif(not os.path.exists(arg)):
print 'Could not find %s' % arg
continue
file_path = arg
destination = os.path.dirname(os.path.dirname(os.path.dirname(file_path)))
_class = None
extension = os.path.splitext(file_path)[1][1:].lower()
if(extension in Photo.get_valid_extensions()):
_class = Photo
elif(extension in Video.get_valid_extensions()):
_class = Video
if(_class is None):
continue
write = False
exif_metadata = pyexiv2.ImageMetadata(file_path)
exif_metadata.read()
if(config['location'] is not None):
location_coords = geolocation.coordinates_by_name(config['location'])
if(location_coords is not None and 'latitude' in location_coords and 'longitude' in location_coords):
print 'Queueing location to exif ...',
exif_metadata['Exif.GPSInfo.GPSLatitude'] = geolocation.decimal_to_dms(location_coords['latitude'])
exif_metadata['Exif.GPSInfo.GPSLatitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLatitudeRef', 'N' if location_coords['latitude'] >= 0 else 'S')
exif_metadata['Exif.GPSInfo.GPSLongitude'] = geolocation.decimal_to_dms(location_coords['longitude'])
exif_metadata['Exif.GPSInfo.GPSLongitudeRef'] = pyexiv2.ExifTag('Exif.GPSInfo.GPSLongitudeRef', 'E' if location_coords['longitude'] >= 0 else 'W')
write = True
print 'OK'
if(config['time'] is not None):
time_string = config['time']
print '%r' % time_string
time_format = '%Y-%m-%d %H:%M:%S'
if(re.match('^\d{4}-\d{2}-\d{2}$', time_string)):
time_string = '%s 00:00:00' % time_string
if(re.match('^\d{4}-\d{2}-\d{2}$', time_string) is None and re.match('^\d{4}-\d{2}-\d{2} \d{2}:\d{2}\d{2}$', time_string)):
print 'Invalid time format. Use YYYY-mm-dd hh:ii:ss or YYYY-mm-dd'
sys.exit(1)
if(time_format is not None):
print 'Queueing time to exif ...',
exif_metadata['Exif.Photo.DateTimeOriginal'].value = datetime.strptime(time_string, time_format)
exif_metadata['Exif.Image.DateTime'].value = datetime.strptime(time_string, time_format)
print '%r' % datetime.strptime(time_string, time_format)
write = True
print 'OK'
if(write == True):
exif_metadata.write()
exif_metadata = pyexiv2.ImageMetadata(file_path)
exif_metadata.read()
media = _class(file_path)
dest_path = filesystem.process_file(file_path, destination, media, move=True, allowDuplicate=True)
print '%s ...' % dest_path,
print 'OK'
# If the folder we moved the file out of or its parent are empty we delete it.
filesystem.delete_directory_if_empty(os.path.dirname(file_path))
filesystem.delete_directory_if_empty(os.path.dirname(os.path.dirname(file_path)))
db = Db()
filesystem = FileSystem()
args = arguments.parse(sys.argv[1:], None, ['time=','location=','process='], './adjust.py --time=<string time> --location=<string location> --process=no file1 file2...fileN')
config = parse_arguments(args)
if __name__ == '__main__':
main(config, sys.argv)
sys.exit(0)

View File

@ -4,21 +4,18 @@ import sys, getopt
from re import sub
def parse(argv, options, long_options, usage):
def help():
print 'Usage: %s' % usage
try:
opts, args = getopt.getopt(argv, options, long_options)
except getopt.GetoptError:
print 'Unknown arguments'
help()
print usage
sys.exit(2)
return_arguments = {}
for opt, arg in opts:
if opt == '-h':
help()
print usage
sys.exit()
else:
return_arguments[sub('^-+', '', opt)] = arg
return return_arguments

View File

@ -4,9 +4,11 @@ Video package that handles all video operations
"""
import os
import re
import shutil
import time
from elodie import geolocation
from elodie.localstorage import Db
"""
General file system methods
@ -21,6 +23,18 @@ class FileSystem:
if not os.path.exists(directory_path):
os.makedirs(directory_path)
"""
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.
@param, directory_name, string, A fully qualified path of the directory to delete.
"""
def delete_directory_if_empty(self, directory_path):
try:
os.rmdir(directory_path)
except OSError:
pass
"""
Recursively get all files which match a path and extension.
@ -99,6 +113,46 @@ class FileSystem:
return '/'.join(path)
def process_file(self, _file, destination, media, **kwargs):
move = False
if('move' in kwargs):
move = kwargs['move']
allowDuplicate = False
if('allowDuplicate' in kwargs):
allowDuplicate = kwargs['allowDuplicate']
metadata = media.get_metadata()
directory_name = self.get_folder_path(date=metadata['date_taken'], latitude=metadata['latitude'], longitude=metadata['longitude'])
dest_directory = '%s/%s' % (destination, directory_name)
file_name = self.get_file_name(media)
dest_path = '%s/%s' % (dest_directory, file_name)
db = Db()
checksum = db.checksum(_file)
if(checksum == None):
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):
print '%s already exists at %s. Skipping...' % (_file, db.get_hash(checksum))
return
self.create_directory(dest_directory)
if(move == True):
shutil.move(_file, dest_path)
else:
shutil.copy2(_file, dest_path)
db.add_hash(checksum, dest_path)
db.update_hash_db()
return dest_path
"""
Set the modification time on the file based on the file path.
Noop if the path doesn't match the format YYYY-MM/DD-IMG_0001.JPG.

View File

@ -1,12 +1,77 @@
from os import path
from ConfigParser import ConfigParser
import fractions
import pyexiv2
import math
import requests
import sys
import urllib
def reverse_lookup(lat, lon):
if(lat is None or lon is None):
return None
class Fraction(fractions.Fraction):
"""Only create Fractions from floats.
>>> Fraction(0.3)
Fraction(3, 10)
>>> Fraction(1.1)
Fraction(11, 10)
"""
def __new__(cls, value, ignore=None):
"""Should be compatible with Python 2.6, though untested."""
return fractions.Fraction.from_float(value).limit_denominator(99999)
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):
# 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.
# 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'):
use_location = location['latLng']
break
return {
'latitude': use_location['lat'],
'longitude': use_location['lng']
}
return None
def decimal_to_dms(decimal):
"""Convert decimal degrees into degrees, minutes, seconds.
>>> decimal_to_dms(50.445891)
[Fraction(50, 1), Fraction(26, 1), Fraction(113019, 2500)]
>>> decimal_to_dms(-125.976893)
[Fraction(125, 1), Fraction(58, 1), Fraction(92037, 2500)]
"""
remainder, degrees = math.modf(abs(decimal))
remainder, minutes = math.modf(remainder * 60)
# @TODO figure out a better and more proper way to do seconds
return (pyexiv2.Rational(degrees, 1), pyexiv2.Rational(minutes, 1), pyexiv2.Rational(int(remainder*1000000000), 1000000000))
def dms_to_decimal(degrees, minutes, seconds, sign=' '):
"""Convert degrees, minutes, seconds into decimal degrees.
>>> dms_to_decimal(10, 10, 10)
10.169444444444444
>>> dms_to_decimal(8, 9, 10, 'S')
-8.152777777777779
"""
return (-1 if sign[0] in 'SWsw' else 1) * (
float(degrees) +
float(minutes) / 60 +
float(seconds) / 3600
)
def get_key():
config_file = '%s/config.ini' % path.dirname(path.dirname(path.abspath(__file__)))
if not path.exists(config_file):
return None
@ -16,19 +81,7 @@ def reverse_lookup(lat, lon):
if('MapQuest' not in config.sections()):
return None
key = config.get('MapQuest', 'key')
try:
r = requests.get('https://open.mapquestapi.com/nominatim/v1/reverse.php?key=%s&lat=%s&lon=%s&format=json' % (key, lat, lon))
return r.json()
except requests.exceptions.RequestException as e:
print e
return None
except ValueError as e:
print r.text
print e
return None
return config.get('MapQuest', 'key')
def place_name(lat, lon):
geolocation_info = reverse_lookup(lat, lon)
@ -42,3 +95,41 @@ def place_name(lat, lon):
elif('country' in address):
return address['country']
return None
def reverse_lookup(lat, lon):
if(lat is None or lon is None):
return None
key = get_key()
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))
return r.json()
except requests.exceptions.RequestException as e:
print e
return None
except ValueError as e:
print r.text
print e
return None
def lookup(name):
if(name is None or len(name) == 0):
return None
key = get_key()
try:
params = {'format': 'json', 'key': key, 'location': name}
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))
return r.json()
except requests.exceptions.RequestException as e:
print e
return None
except ValueError as e:
print r.text
print e
return None

View File

@ -0,0 +1,3 @@
from media import Media
from photo import Photo
from video import Video

View File

@ -66,3 +66,13 @@ class Media(object):
return None
return mimetype[0]
def get_class_by_file(Media, _file):
extension = os.path.splitext(_file)[1][1:].lower()
if(extension in Photo.get_valid_extensions()):
return Photo
elif(extension in Video.get_valid_extensions()):
return Video
else:
return None

View File

@ -161,5 +161,5 @@ class Photo(Media):
@returns, tuple
"""
@classmethod
def get_valid_extensions(Video):
return Video.__valid_extensions
def get_valid_extensions(Photo):
return Photo.__valid_extensions

View File

@ -10,8 +10,31 @@ from elodie.media.video import Video
from elodie.filesystem import FileSystem
from elodie.localstorage import Db
db = Db()
filesystem = FileSystem()
def help():
return """
usage: ./import.py --type=photo --source=/path/to/photos --destination=/path/to/destination
--type Valid values are 'photo' or 'video'. Only files of *type* are imported.
--file Full path to a photo or video to be imported. The --type argument should match the file type of the file.
@TODO: Automatically determine *type* from *file*
--source Full path to a directory which will be recursively crawled for files of *type*.
--destination Full path to a directory where organized photos will be placed.
"""
def parse_arguments(args):
config = {
'type': 'photo',
'file': None,
'source': None,
'destination': None
}
if('destination' not in args):
help()
sys.exit(2)
config.update(args)
return config
def process_file(_file, destination, media):
checksum = db.checksum(_file)
@ -35,25 +58,20 @@ def process_file(_file, destination, media):
filesystem.create_directory(dest_directory)
print '%s -> %s' % (_file, dest_path)
shutil.copy2(_file, dest_path)
#shutil.move(_file, dest_path)
#shutil.copy2(_file, dest_path)
shutil.move(_file, dest_path)
db.add_hash(checksum, dest_path)
def main(argv):
args = arguments.parse(argv, None, ['file=','type=','source=','destination='], './import.py --type=<photo or video> --source=<source directory> -destination=<destination directory>')
if('destination' not in args):
print 'No destination passed in'
sys.exit(2)
destination = args['destination']
if('type' in args and args['type'] == 'photo'):
destination = config['destination']
if(config['type'] == 'photo'):
media_type = Photo
else:
media_type = Video
if('source' in args):
source = args['source']
if(config['source'] is not None):
source = config['source']
write_counter = 0
for current_file in filesystem.get_all_files(source, media_type.get_valid_extensions()):
@ -71,15 +89,21 @@ def main(argv):
# If there's anything we haven't written to the hash database then write it now
if(write_counter % 10 != 10):
db.update_hash_db()
elif('file' in args):
media = media_type(args['file'])
elif(config['file'] is not None):
media = media_type(config['file'])
if(media_type.__name__ == 'Video'):
filesystem.set_date_from_path_video(media)
process_file(args['file'], destination, media)
process_file(config['file'], destination, media)
db.update_hash_db()
else:
help()
db = Db()
filesystem = FileSystem()
args = arguments.parse(sys.argv[1:], None, ['file=','type=','source=','destination='], help())
config = parse_arguments(args)
if __name__ == '__main__':
main(sys.argv[1:])
main(config)
sys.exit(0)

32
tests/scripts/datetime.py Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python
import os
import shutil
import sys
from elodie import arguments
from elodie.media.photo import Photo
from elodie.media.video import Video
def main(argv):
args = arguments.parse(argv, None, ['file=','type='], './import.py --type=<photo or video> --file=<path to file>')
if('file' not in args):
print 'No file specified'
sys.exit(1)
if('type' in args and args['type'] == 'photo'):
media_type = Photo
else:
media_type = Video
media = media_type(args['file'])
metadata = media.get_metadata()
output = {'date_taken': metadata['date_taken']}
print '%r' % output
if __name__ == '__main__':
main(sys.argv[1:])
sys.exit(0)