Add support for reading/writing titles for photos and videos
This commit is contained in:
parent
c3b233fff2
commit
62f6e56bdb
|
@ -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
|
||||
|
|
|
@ -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__)))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
23
update.py
23
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):
|
||||
|
@ -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)
|
||||
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)))
|
||||
|
|
Loading…
Reference in New Issue