From 62f6e56bdbb64c748fd1fc6345ea3d3d8accbec7 Mon Sep 17 00:00:00 2001 From: Jaisen Mathai Date: Wed, 28 Oct 2015 00:19:21 -0700 Subject: [PATCH] Add support for reading/writing titles for photos and videos --- README.md | 6 ++--- elodie/constants.py | 1 + elodie/filesystem.py | 23 ++++++++++------ elodie/geolocation.py | 21 ++++++++++----- elodie/media/media.py | 37 ++++++++++++++++++++++++-- elodie/media/photo.py | 23 ++++++++++++++-- elodie/media/video.py | 61 ++++++++++++++++++++++--------------------- update.py | 23 ++++++++++++---- 8 files changed, 138 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index f03724e..21bd45b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/elodie/constants.py b/elodie/constants.py index 4afa0b0..c324876 100644 --- a/elodie/constants.py +++ b/elodie/constants.py @@ -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__))) diff --git a/elodie/filesystem.py b/elodie/filesystem.py index 9bb4161..51072be 100644 --- a/elodie/filesystem.py +++ b/elodie/filesystem.py @@ -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,12 +142,14 @@ class FileSystem: db = Db() checksum = db.checksum(_file) if(checksum == None): - print 'Could not get checksum for %s. Skipping...' % _file + 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): - print '%s already exists at %s. Skipping...' % (_file, db.get_hash(checksum)) + if(constants.debug == True): + print '%s already exists at %s. Skipping...' % (_file, db.get_hash(checksum)) return self.create_directory(dest_directory) diff --git a/elodie/geolocation.py b/elodie/geolocation.py index ca88cd3..3f787de 100644 --- a/elodie/geolocation.py +++ b/elodie/geolocation.py @@ -8,6 +8,8 @@ import requests import sys import urllib +from elodie import constants + class Fraction(fractions.Fraction): """Only create Fractions from floats. @@ -108,11 +110,13 @@ 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: - print e + if(constants.debug == True): + print e return None except ValueError as e: - print r.text - print e + if(constants.debug == True): + print r.text + print e return None def lookup(name): @@ -123,13 +127,16 @@ def lookup(name): try: params = {'format': 'json', 'key': key, 'location': name} - print 'http://open.mapquestapi.com/geocoding/v1/address?%s' % urllib.urlencode(params) + 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: - print e + if(constants.debug == True): + print e return None except ValueError as e: - print r.text - print e + if(constants.debug == True): + print r.text + print e return None diff --git a/elodie/media/media.py b/elodie/media/media.py index 043a542..f5d10c4 100644 --- a/elodie/media/media.py +++ b/elodie/media/media.py @@ -125,7 +125,8 @@ class Media(object): seconds_since_epoch = time.mktime(exif[key].value.timetuple()) break; except BaseException as e: - print e + if(constants.debug == True): + print e pass if(seconds_since_epoch == 0): @@ -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() @@ -227,6 +242,22 @@ class Media(object): return None 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): diff --git a/elodie/media/photo.py b/elodie/media/photo.py index f986d9a..c7cc5c1 100644 --- a/elodie/media/photo.py +++ b/elodie/media/photo.py @@ -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 """ diff --git a/elodie/media/video.py b/elodie/media/video.py index 7507a9d..3a1a0d4 100644 --- a/elodie/media/video.py +++ b/elodie/media/video.py @@ -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,12 +187,14 @@ class Video(Media): """ def __update_using_plist(self, **kwargs): if('latitude' not in kwargs and 'longitude' not in kwargs and 'time' not in kwargs): - print 'No lat/lon passed into __create_plist' + if(constants.debug == True): + print 'No lat/lon passed into __create_plist' return False avmetareadwrite = find_executable('avmetareadwrite') if(avmetareadwrite is None): - print 'Could not find avmetareadwrite' + if(constants.debug == True): + print 'Could not find avmetareadwrite' return False source = self.source @@ -214,7 +207,8 @@ 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): - print 'Failed to generate plist file' + if(constants.debug == True): + print 'Failed to generate plist file' return False plist = plist_parser.Plist(plist_temp.name) @@ -258,12 +252,17 @@ 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: - print 'Nothing to update, plist unchanged' + if(constants.debug == True): + print 'Nothing to update, plist unchanged' return False # We create a temporary file to save the modified file to. @@ -279,14 +278,16 @@ class Video(Media): update_process = subprocess.Popen([avmetareadwrite_command], stdout=subprocess.PIPE, shell=True) streamdata = update_process.communicate()[0] if(update_process.returncode != 0): - print '%s did not complete successfully' % avmetareadwrite_command + if(constants.debug == True): + print '%s did not complete successfully' % avmetareadwrite_command return False # 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(check_metadata['latitude'] is None or check_metadata['longitude'] is None or check_metadata['date_taken'] is None): - print 'Something went wrong updating video metadata' + if(constants.debug == True): + print 'Something went wrong updating video metadata' return False # Copy file information from original source to temporary file before copying back over diff --git a/update.py b/update.py index 1360dca..850e688 100755 --- a/update.py +++ b/update.py @@ -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)): - print 'Could not find %s' % 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): - print 'Failed to update location' + 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)): - print 'Invalid time format. Use YYYY-mm-dd hh:ii:ss or YYYY-mm-dd' + 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): @@ -83,11 +90,17 @@ def main(config, args): if(config['album'] is not None): 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) - print '%s -> %s' % (file_path, dest_path) + 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)))