Add support for reading/writing titles for photos and videos

This commit is contained in:
Jaisen Mathai 2015-10-28 00:19:21 -07:00
parent c3b233fff2
commit 62f6e56bdb
8 changed files with 138 additions and 57 deletions

View File

@ -8,9 +8,9 @@ You don't love me yet but you will.
I only do 3 things.
* Firstly I organize your existing collection of photos.
* Second I help make it easy for all the photos you haven't taken yet to flow into the exact location they belong.
* Third but not least I promise to do all this without a yucky propietary database that some colleagues of mine use.
* Third but not least I promise to do all this without a yucky propietary database that some friends of mine use.
*NOTE: make sure you've installed me and my friends before running the commands below. [Instructions](#install-everything-you-need) at the bottom of this page.*
*NOTE: make sure you've installed everything I need before running the commands below. [Instructions](#install-everything-you-need) at the bottom of this page.*
## See me in action
@ -53,7 +53,7 @@ You'll notice that your photos are now organized by date and location. Some phot
Don't fret if your photos don't have much EXIF information. I'll show you how you can fix them up later on but let's walk before we run.
Back to your photos. When I'm done you should see something like this. Notice that I've renamed your files by adding the date and time they were taken. This helps keep them in chronological order when using most viewing applications. You'll can thank me later.
Back to your photos. When I'm done you should see something like this. Notice that I've renamed your files by adding the date and time they were taken. This helps keep them in chronological order when using most viewing applications. You'll thank me later.
```
├── 2015-06-Jun

View File

@ -1,5 +1,6 @@
from os import path
debug = False
application_directory = '{}/.elodie'.format(path.expanduser('~'))
hash_db = '{}/hash.json'.format(application_directory)
script_directory = path.dirname(path.dirname(path.abspath(__file__)))

View File

@ -8,6 +8,7 @@ import shutil
import time
from elodie import geolocation
from elodie import constants
from elodie.localstorage import Db
"""
@ -59,27 +60,31 @@ class FileSystem:
return os.getcwd()
"""
Generate file name for a 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.
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, video, Video, A Video instance
@returns, string or None for non-videos
@param, media, Photo|Video, A Photo or Video instance
@returns, string or None for non-photo or non-videos
"""
def get_file_name(self, video):
if(not video.is_valid()):
def get_file_name(self, media):
if(not media.is_valid()):
return None
metadata = video.get_metadata()
metadata = media.get_metadata()
if(metadata == None):
return None
# 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'])
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):
title_sanitized = re.sub('\W+', '-', metadata['title'].strip())
base_name = '%s-%s' % (title_sanitized , base_name)
file_name = '%s-%s.%s' % (time.strftime('%Y-%m-%d_%H-%M-%S', metadata['date_taken']), base_name, metadata['extension'])
return file_name.lower()
@ -137,11 +142,13 @@ class FileSystem:
db = Db()
checksum = db.checksum(_file)
if(checksum == None):
if(constants.debug == 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))
return

View File

@ -8,6 +8,8 @@ import requests
import sys
import urllib
from elodie import constants
class Fraction(fractions.Fraction):
"""Only create Fractions from floats.
@ -108,9 +110,11 @@ def reverse_lookup(lat, 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:
if(constants.debug == True):
print e
return None
except ValueError as e:
if(constants.debug == True):
print r.text
print e
return None
@ -123,13 +127,16 @@ 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))
return r.json()
except requests.exceptions.RequestException as e:
if(constants.debug == True):
print e
return None
except ValueError as e:
if(constants.debug == True):
print r.text
print e
return None

View File

@ -125,6 +125,7 @@ class Media(object):
seconds_since_epoch = time.mktime(exif[key].value.timetuple())
break;
except BaseException as e:
if(constants.debug == True):
print e
pass
@ -164,13 +165,26 @@ class Media(object):
process_output = subprocess.Popen(['%s "%s"' % (exiftool, source)], stdout=subprocess.PIPE, shell=True)
output = process_output.stdout.read()
# Get album from exiftool output
album = None
album_regex = re.search('Album +: +(.+)', output)
if(album_regex is not None):
album = album_regex.group(1)
# Get title from exiftool output
title = None
for key in ['Displayname', 'Headline', 'Title', 'ImageDescription']:
title_regex = re.search('%s +: +(.+)' % key, output)
if(title_regex is not None):
title_return = title_regex.group(1).strip()
if(len(title_return) > 0):
title = title_return
break;
self.exiftool_attributes = {
'album': album
'album': album,
'title': title
}
return self.exiftool_attributes
@ -205,6 +219,7 @@ class Media(object):
'latitude': self.get_coordinate('latitude'),
'longitude': self.get_coordinate('longitude'),
'album': self.get_album(),
'title': self.get_title(),
'mime_type': self.get_mimetype(),
'base_name': os.path.splitext(os.path.basename(source))[0],
'extension': self.get_extension()
@ -228,6 +243,22 @@ class Media(object):
return mimetype[0]
"""
Get the title for a photo of video
@returns, string or None if no title is set or not a valid media type
"""
def get_title(self):
if(not self.is_valid()):
return None
exiftool_attributes = self.get_exiftool_attributes()
if('title' not in exiftool_attributes):
return None
return exiftool_attributes['title']
"""
Set album for a photo
@ -245,6 +276,8 @@ class Media(object):
source = self.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)
streamdata = process_output.communicate()[0]
if(process_output.returncode != 0):

View File

@ -95,10 +95,29 @@ class Photo(Media):
exif_metadata.write()
return True
"""
Set lat/lon for a photo
@param, latitude, float, Latitude of the file
@param, longitude, float, Longitude of the file
@returns, boolean
"""
def set_title(self, title):
if(title is None):
return False
source = self.source
exif_metadata = pyexiv2.ImageMetadata(source)
exif_metadata.read()
exif_metadata['Xmp.dc.title'] = title
exif_metadata.write()
return True
"""
Static method to access static __valid_extensions variable.
def set_location(self, latitude, longitude):
return None
@returns, tuple
"""

View File

@ -16,6 +16,7 @@ import shutil
import subprocess
import time
from elodie import constants
from elodie import plist_parser
from media import Media
@ -123,30 +124,6 @@ class Video(Media):
process_output = subprocess.Popen(['%s "%s"' % (exiftool, source)], stdout=subprocess.PIPE, shell=True)
return process_output.stdout.read()
"""
Get a dictionary of metadata for a video.
All keys will be present and have a value of None if not obtained.
@returns, dictionary or None for non-video files
"""
def get_metadata(self):
if(not self.is_valid()):
return None
source = self.source
metadata = {
"date_taken": self.get_date_taken(),
"latitude": self.get_coordinate('latitude'),
"longitude": self.get_coordinate('longitude'),
"album": self.get_album(),
#"length": self.get_duration(),
"mime_type": self.get_mimetype(),
"base_name": os.path.splitext(os.path.basename(source))[0],
"extension": self.get_extension()
}
return metadata
"""
Set the date/time a photo was taken
@ -176,6 +153,20 @@ class Video(Media):
result = self.__update_using_plist(latitude=latitude, longitude=longitude)
return result
"""
Set title for a video
@param, title, string, Title for the file
@returns, boolean
"""
def set_title(self, title):
if(title is None):
return False
result = self.__update_using_plist(title=title)
return result
"""
Updates video metadata using avmetareadwrite.
@ -196,11 +187,13 @@ class Video(Media):
"""
def __update_using_plist(self, **kwargs):
if('latitude' not in kwargs and 'longitude' not in kwargs and 'time' not in kwargs):
if(constants.debug == True):
print 'No lat/lon passed into __create_plist'
return False
avmetareadwrite = find_executable('avmetareadwrite')
if(avmetareadwrite is None):
if(constants.debug == True):
print 'Could not find avmetareadwrite'
return False
@ -214,6 +207,7 @@ class Video(Media):
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):
print 'Failed to generate plist file'
return False
@ -258,11 +252,16 @@ class Video(Media):
plist.update_key('common/creationDate', time_string)
plist_should_be_written = True
if('title' in kwargs):
if(len(kwargs['title']) > 0):
plist.update_key('common/title', kwargs['title'])
plist_should_be_written = True
if(plist_should_be_written is True):
plist_final = plist_temp.name
plist.write_file(plist_final)
else:
if(constants.debug == True):
print 'Nothing to update, plist unchanged'
return False
@ -279,6 +278,7 @@ class Video(Media):
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
return False
@ -286,6 +286,7 @@ class Video(Media):
check_media = Video(temp_movie)
check_metadata = check_media.get_metadata()
if(check_metadata['latitude'] is None or check_metadata['longitude'] is None or check_metadata['date_taken'] is None):
if(constants.debug == True):
print 'Something went wrong updating video metadata'
return False

View File

@ -10,6 +10,7 @@ import time
from datetime import datetime
from elodie import arguments
from elodie import constants
from elodie import geolocation
from elodie.media.photo import Media
from elodie.media.photo import Photo
@ -31,13 +32,15 @@ def parse_arguments(args):
def main(config, args):
location_coords = None
for arg in args:
file_path = arg
if(arg[:2] == '--'):
continue
elif(not os.path.exists(arg)):
if(constants.debug == True):
print 'Could not find %s' % arg
print '{"source":"%s", "error_msg":"Could not find %s"}' % (file_path, arg)
continue
file_path = arg
destination = os.path.dirname(os.path.dirname(os.path.dirname(file_path)))
_class = None
@ -60,7 +63,9 @@ def main(config, args):
if(location_coords is not None and 'latitude' in location_coords and 'longitude' in location_coords):
location_status = media.set_location(location_coords['latitude'], location_coords['longitude'])
if(location_status != True):
if(constants.debug == True):
print 'Failed to update location'
print '{"source":"%s", "error_msg":"Failed to update location"}' % file_path
sys.exit(1)
updated = True
@ -72,7 +77,9 @@ def main(config, args):
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)):
if(constants.debug == True):
print 'Invalid time format. Use YYYY-mm-dd hh:ii:ss or YYYY-mm-dd'
print '{"source":"%s", "error_msg":"Invalid time format. Use YYYY-mm-dd hh:ii:ss or YYYY-mm-dd"}' % file_path
sys.exit(1)
if(time_format is not None):
@ -84,10 +91,16 @@ def main(config, args):
media.set_album(config['album'])
updated = True
if(config['title'] is not None):
media.set_title(config['title'])
updated = True
if(updated == True):
dest_path = filesystem.process_file(file_path, destination, media, move=True, allowDuplicate=True)
if(constants.debug == True):
print '%s -> %s' % (file_path, dest_path)
print '{"source":"%s", "destination":"%s"}' % (file_path, dest_path)
# 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)))