Add list data only flag

With the -r flag data will only be read from the XMP files and printed
out and no other action (LR lookup, Maps lookup) is performed
This commit is contained in:
2018-03-05 15:52:07 +09:00
parent a4bd574e48
commit 41bae7eb5d
2 changed files with 213 additions and 116 deletions

View File

@@ -26,7 +26,7 @@ reverse_geolocate.py [-h] -x
[-l LIGHTROOM FOLDER] [-s] [-l LIGHTROOM FOLDER] [-s]
[-f <overwrite, location, city, state, country, countrycode>] [-f <overwrite, location, city, state, country, countrycode>]
[-g GOOGLE API KEY] [-o] [-e EMIL ADDRESS] [-w] [-g GOOGLE API KEY] [-o] [-e EMIL ADDRESS] [-w]
[-n] [-v] [--debug] [--test] [-r] [-n] [-v] [--debug] [--test]
### Arguments ### Arguments
@@ -40,6 +40,7 @@ Argument | Argument Value | Description
-o, --openstreetmap | | Use OpenStreetMap instead of the default google maps -o, --openstreetmap | | Use OpenStreetMap instead of the default google maps
-e, --email | email address | For OpenStreetMap with a large number of access -e, --email | email address | For OpenStreetMap with a large number of access
-g, --google | Google Maps API Key | If available, to avoid the access limitations to the reverse location lookup -g, --google | Google Maps API Key | If available, to avoid the access limitations to the reverse location lookup
-r, --read-only | | Read only data from the XMP files and print them out. No LR DB connection is done or any map lookups
-w, --write-settings | | Write the Google API key or the OpenStreetMap email into the settings file -w, --write-settings | | Write the Google API key or the OpenStreetMap email into the settings file
-v, --verbose | | More verbose output. Currently not used -v, --verbose | | More verbose output. Currently not used
--debug | | Full detailed debug output. Will print out alot of data --debug | | Full detailed debug output. Will print out alot of data

View File

@@ -9,14 +9,11 @@
# * all data is translated into English with long vowl system (aka ou or oo is ō) # * all data is translated into English with long vowl system (aka ou or oo is ō)
# MUST HAVE: Python XMP Toolkit (http://python-xmp-toolkit.readthedocs.io/) # MUST HAVE: Python XMP Toolkit (http://python-xmp-toolkit.readthedocs.io/)
import argparse import argparse, sqlite3, requests, configparser, textwrap
import os, sys, re import os, sys, re
# Note XMPFiles does not work with sidecar files, need to read via XMPMeta # Note XMPFiles does not work with sidecar files, need to read via XMPMeta
from libxmp import XMPMeta, XMPError, consts from libxmp import XMPMeta, XMPError, consts
import sqlite3
import requests
from shutil import copyfile from shutil import copyfile
import configparser
############################################################## ##############################################################
### FUNCTIONS ### FUNCTIONS
@@ -293,6 +290,12 @@ def longLatReg(longitude, latitude):
lat_long[element] *= -1 lat_long[element] *= -1
return lat_long return lat_long
# wrapper calls for DMS to Lat/Long
def convertDMStoLat(lat_long):
return longLatReg('0,0.0N', lat_long)['latitude']
def convertDMStoLong(lat_long):
return longLatReg(lat_long, '0,0.0N')['longitude']
# METHOD: checkOverwrite # METHOD: checkOverwrite
# PARAMS: data: value field, key: XMP key, field_controls: array from args # PARAMS: data: value field, key: XMP key, field_controls: array from args
# RETURN: true/false # RETURN: true/false
@@ -325,6 +328,32 @@ def checkOverwrite(data, key, field_controls):
)) ))
return status return status
# METHOD: shortenPath
# PARAMS: path = string, length = int
# RETURN: shortend path with ... in front
# DESC : shortes a path from the left so it fits into lenght
def shortenPath(path, length = 30):
length = length - 3;
if len(path) > length:
path = "{} {}".format("..", path[len(path) - length:])
return path;
# METHOD: printHeader
# PARAMS: header string, header seperator, line counter, print header counter trigger
# RETURN: line counter +1
# DESC : prints header line and header seperator line
def printHeader(header, header_seperator, lines = 0, header_line = 0):
if lines == header_line:
# blank line, might be used for page numbers, or other info
print("{}".format(''))
# print header
print("{}".format(header))
# print line with length of header string
# print("{}".format('-' * len(header)))
print("{}".format(header_seperator))
lines += 1
return lines
############################################################## ##############################################################
### ARGUMENT PARSNING ### ARGUMENT PARSNING
############################################################## ##############################################################
@@ -398,12 +427,19 @@ parser.add_argument('-e', '--email',
) )
# write api/email settings to config file # write api/email settings to config file
parser.add_argument('-w', '--write-seettings', parser.add_argument('-w', '--write-settings',
dest = 'config_write', dest = 'config_write',
action = 'store_true', action = 'store_true',
help = 'Write Google API or OpenStreetMap email to config file' help = 'Write Google API or OpenStreetMap email to config file'
) )
# only read data and print on screen, do not write anything
parser.add_argument('-r', '--read-only',
dest = 'read_only',
action = 'store_true',
help = 'Read current values from the XMP file only, do not read from LR or lookup any data and write back'
)
# Do not create backup files # Do not create backup files
parser.add_argument('-n', '--nobackup', parser.add_argument('-n', '--nobackup',
dest = 'no_xmp_backup', dest = 'no_xmp_backup',
@@ -559,6 +595,7 @@ cur = ''
# count variables # count variables
count = { count = {
'all': 0, 'all': 0,
'read': 0,
'map': 0, 'map': 0,
'cache': 0, 'cache': 0,
'lightroom': 0, 'lightroom': 0,
@@ -632,15 +669,57 @@ for xmp_file_source in args.xmp_sources:
if args.debug: if args.debug:
print("### Work Files {}".format(work_files)) print("### Work Files {}".format(work_files))
# if we have read only we print list format style
if args.read_only:
# various string lengths
format_length ={
'filename': 40,
'latitude': 18,
'longitude': 18,
'code': 4,
'country': 20,
'state': 20,
'city': 25,
'location': 30
}
# after how many lines do we reprint the header
header_repeat = 40;
# the formatted line for the output
format_line = " {filename:<" + str(format_length['filename']) + "} | {latitude:>" + str(format_length['latitude']) + "} | {longitude:>" + str(format_length['longitude']) + "} | {code:<" + str(format_length['code']) + "} | {country:<" + str(format_length['country']) + "} | {state:<" + str(format_length['state']) + "} | {city:<" + str(format_length['city']) + "} | {location:<" + str(format_length['location']) + "} "
# header line
header = format_line.format(
filename = "File",
latitude = "Latitude",
longitude = "Longitude",
code = 'Code',
country = 'Country',
state = 'State',
city = 'City',
location = 'Location'
)
# header seperator line
header_seperator = "{}+{}+{}+{}+{}+{}+{}+{}".format(
'-' * (format_length['filename'] + 2),
'-' * (format_length['latitude'] + 2),
'-' * (format_length['longitude'] + 2),
'-' * (format_length['code'] + 2),
'-' * (format_length['country'] + 2),
'-' * (format_length['state'] + 2),
'-' * (format_length['city'] + 2),
'-' * (format_length['location'] + 2)
)
# print header
printHeader(header, header_seperator)
# now we just loop through each file and work on them # now we just loop through each file and work on them
for xmp_file in work_files: for xmp_file in work_files:
print("---> {}: ".format(xmp_file), end = '') if not args.read_only:
print("---> {}: ".format(xmp_file), end = '')
#### ACTION FLAGs #### ACTION FLAGs
write_file = False write_file = False
lightroom_data_ok = True lightroom_data_ok = True
#### LIGHTROOM DB READING #### LIGHTROOM DB READING
# read in data from DB if we uave lightroom folder # read in data from DB if we uave lightroom folder
if use_lightroom: if use_lightroom and not args.read_only:
# get the base file name, we need this for lightroom # get the base file name, we need this for lightroom
xmp_file_basename = os.path.splitext(os.path.split(xmp_file)[1])[0] xmp_file_basename = os.path.splitext(os.path.split(xmp_file)[1])[0]
# for strict check we need to get the full path, and add / as the LR stores the last folder with / # for strict check we need to get the full path, and add / as the LR stores the last folder with /
@@ -676,119 +755,136 @@ for xmp_file in work_files:
data_set[xmp_field] = xmp.get_property(xmp_fields[xmp_field], xmp_field) data_set[xmp_field] = xmp.get_property(xmp_fields[xmp_field], xmp_field)
if args.debug: if args.debug:
print("### => XMP: {}:{} => {}".format(xmp_fields[xmp_field], xmp_field, data_set[xmp_field])) print("### => XMP: {}:{} => {}".format(xmp_fields[xmp_field], xmp_field, data_set[xmp_field]))
# create a duplicate copy for later checking if something changed if args.read_only:
data_set_original = data_set.copy() # for read only we print out the data formatted
# headline check, do we need to print that
# check if LR exists and use this to compare to XMP data count['read'] = printHeader(header, header_seperator, count['read'], header_repeat)
# is LR GPS and no XMP GPS => use LR and set XMP # the data content
# same for location names print(format_line.format(
# if missing in XMP but in LR -> set in XMP filename = shortenPath(xmp_file, format_length['filename']), # shorten from the left
# if missing in both do lookup in Maps latitude = str(convertDMStoLat(data_set['GPSLatitude']))[:format_length['latitude']], # cut off from the right
if use_lightroom and lightroom_data_ok: longitude = str(convertDMStoLong(data_set['GPSLongitude']))[:format_length['longitude']],
# check lat/long separate code = data_set['CountryCode'][:2].center(4), # is only 2 chars
if lrdb_row['gpsLatitude'] and not data_set['GPSLatitude']: country = textwrap.shorten(data_set['Country'], width = format_length['country'], placeholder = '..'), # shorten from the right
# we need to convert to the Degree,Min.sec[NSEW] format state = textwrap.shorten(data_set['State'], width = format_length['state'], placeholder = '..'),
data_set['GPSLatitude'] = convertLatToDMS(lrdb_row['gpsLatitude']) city = textwrap.shorten(data_set['City'], width = format_length['city'], placeholder = '..'),
if lrdb_row['gpsLongitude'] and not data_set['GPSLongitude']: location = textwrap.shorten(data_set['Location'], width = format_length['location'], placeholder = '..')
data_set['GPSLongitude'] = convertLongToDMS(lrdb_row['gpsLongitude']) ))
# now check Location, City, etc
for loc in data_set_loc:
# overwrite original set (read from XMP) with LR data if original data is missing
if lrdb_row[loc] and not data_set[loc]:
data_set[loc] = lrdb_row[loc]
if args.debug:
print("### -> LR: {} => {}".format(loc, lrdb_row[loc]))
# base set done, now check if there is anything unset in the data_set, if yes do a lookup in maps
# run this through the overwrite checker to get unset if we have a forced overwrite
has_unset = False
failed = False
for loc in data_set_loc:
if checkOverwrite(data_set[loc], loc, args.field_controls):
has_unset = True
if has_unset:
# check if lat/long is in cache
cache_key = '{}.#.{}'.format(data_set['GPSLatitude'], data_set['GPSLongitude'])
if args.debug:
print("### *** CACHE: {}: {}".format(cache_key, 'NO' if cache_key not in data_cache else 'YES'))
if cache_key not in data_cache:
# get location from maps (google or openstreetmap)
maps_location = reverseGeolocate(latitude = data_set['GPSLatitude'], longitude = data_set['GPSLongitude'], map_type = map_type)
# cache data with Lat/Long
data_cache[cache_key] = maps_location
else:
# load location from cache
maps_location = data_cache[cache_key]
count['cache'] += 1
# overwrite sets (note options check here)
if args.debug:
print("### Map Location ({}): {}".format(map_type, maps_location))
# must have at least the country set to write anything back
if maps_location['Country']:
for loc in data_set_loc:
# only write to XMP if overwrite check passes
if checkOverwrite(data_set[loc], loc, args.field_controls):
data_set[loc] = maps_location[loc]
xmp.set_property(xmp_fields[loc], loc, maps_location[loc])
write_file = True
if write_file:
count['map'] += 1
else:
print("(!) Could not geo loaction data ", end = '')
failed = True
else: else:
if args.debug: # create a duplicate copy for later checking if something changed
print("Lightroom data use: {}, Lightroom data ok: {}".format(use_lightroom, lightroom_data_ok)) data_set_original = data_set.copy()
# check if the data_set differs from the original (LR db load) # check if LR exists and use this to compare to XMP data
# if yes write, else skip # is LR GPS and no XMP GPS => use LR and set XMP
# same for location names
# if missing in XMP but in LR -> set in XMP
# if missing in both do lookup in Maps
if use_lightroom and lightroom_data_ok: if use_lightroom and lightroom_data_ok:
for key in data_set: # check lat/long separate
# if not the same (to original data) and passes overwrite check if lrdb_row['gpsLatitude'] and not data_set['GPSLatitude']:
if data_set[key] != data_set_original[key] and checkOverwrite(data_set_original[key], key, args.field_controls): # we need to convert to the Degree,Min.sec[NSEW] format
xmp.set_property(xmp_fields[key], key, data_set[key]) data_set['GPSLatitude'] = convertLatToDMS(lrdb_row['gpsLatitude'])
write_file = True; if lrdb_row['gpsLongitude'] and not data_set['GPSLongitude']:
if write_file: data_set['GPSLongitude'] = convertLongToDMS(lrdb_row['gpsLongitude'])
count['lightroom'] += 1 # now check Location, City, etc
# if we have the write flag set, write data for loc in data_set_loc:
if write_file: # overwrite original set (read from XMP) with LR data if original data is missing
if not args.test: if lrdb_row[loc] and not data_set[loc]:
# use copyfile to create a backup copy data_set[loc] = lrdb_row[loc]
if not args.no_xmp_backup: if args.debug:
copyfile(xmp_file, "{}.BK{}".format(os.path.splitext(xmp_file)[0], os.path.splitext(xmp_file)[1])) print("### -> LR: {} => {}".format(loc, lrdb_row[loc]))
# write back to riginal file # base set done, now check if there is anything unset in the data_set, if yes do a lookup in maps
with open(xmp_file, 'w') as fptr: # run this through the overwrite checker to get unset if we have a forced overwrite
fptr.write(xmp.serialize_to_str(omit_packet_wrapper=True)) has_unset = False
failed = False
for loc in data_set_loc:
if checkOverwrite(data_set[loc], loc, args.field_controls):
has_unset = True
if has_unset:
# check if lat/long is in cache
cache_key = '{}.#.{}'.format(data_set['GPSLatitude'], data_set['GPSLongitude'])
if args.debug:
print("### *** CACHE: {}: {}".format(cache_key, 'NO' if cache_key not in data_cache else 'YES'))
if cache_key not in data_cache:
# get location from maps (google or openstreetmap)
maps_location = reverseGeolocate(latitude = data_set['GPSLatitude'], longitude = data_set['GPSLongitude'], map_type = map_type)
# cache data with Lat/Long
data_cache[cache_key] = maps_location
else:
# load location from cache
maps_location = data_cache[cache_key]
count['cache'] += 1
# overwrite sets (note options check here)
if args.debug:
print("### Map Location ({}): {}".format(map_type, maps_location))
# must have at least the country set to write anything back
if maps_location['Country']:
for loc in data_set_loc:
# only write to XMP if overwrite check passes
if checkOverwrite(data_set[loc], loc, args.field_controls):
data_set[loc] = maps_location[loc]
xmp.set_property(xmp_fields[loc], loc, maps_location[loc])
write_file = True
if write_file:
count['map'] += 1
else:
print("(!) Could not geo loaction data ", end = '')
failed = True
else: else:
print("[TEST] Would write {} ".format(data_set, xmp_file), end = '') if args.debug:
print("[UPDATED]") print("Lightroom data use: {}, Lightroom data ok: {}".format(use_lightroom, lightroom_data_ok))
count['changed'] += 1 # check if the data_set differs from the original (LR db load)
elif failed: # if yes write, else skip
print("[FAILED]") if use_lightroom and lightroom_data_ok:
count['failed'] += 1 for key in data_set:
# log data to array for post print # if not the same (to original data) and passes overwrite check
failed_files.append(xmp_file) if data_set[key] != data_set_original[key] and checkOverwrite(data_set_original[key], key, args.field_controls):
else: xmp.set_property(xmp_fields[key], key, data_set[key])
print("[SKIP]") write_file = True;
count['skipped'] += 1 if write_file:
count['lightroom'] += 1
# if we have the write flag set, write data
if write_file:
if not args.test:
# use copyfile to create a backup copy
if not args.no_xmp_backup:
copyfile(xmp_file, "{}.BK{}".format(os.path.splitext(xmp_file)[0], os.path.splitext(xmp_file)[1]))
# write back to riginal file
with open(xmp_file, 'w') as fptr:
fptr.write(xmp.serialize_to_str(omit_packet_wrapper=True))
else:
print("[TEST] Would write {} ".format(data_set, xmp_file), end = '')
print("[UPDATED]")
count['changed'] += 1
elif failed:
print("[FAILED]")
count['failed'] += 1
# log data to array for post print
failed_files.append(xmp_file)
else:
print("[SKIP]")
count['skipped'] += 1
# close DB connection # close DB connection
lrdb.close()
# end stats
print("{}".format('=' * 37))
print("XMP Files found : {:7,}".format(count['all']))
print("Updated : {:7,}".format(count['changed']))
print("Skipped : {:7,}".format(count['skipped']))
print("New GeoLocation from Map : {:7,}".format(count['map']))
print("GeoLocation from Cache : {:7,}".format(count['cache']))
print("Failed reverse GeoLocate : {:7,}".format(count['failed']))
if use_lightroom: if use_lightroom:
print("GeoLocaction from Lightroom : {:7,}".format(count['lightroom'])) lrdb.close()
print("No Lightroom data found : {:7,}".format(count['not_found']))
print("More than one found in LR : {:7,}".format(count['many_found'])) # end stats only if we write
# if we have failed data print("{}".format('=' * 39))
if len(failed_files) > 0: print("XMP Files found : {:9,}".format(count['all']))
print("{}".format('-' * 37)) if not args.read_only:
print("Files that failed to update:") print("Updated : {:9,}".format(count['changed']))
print("{}".format(', '.join(failed_files))) print("Skipped : {:9,}".format(count['skipped']))
print("New GeoLocation from Map : {:9,}".format(count['map']))
print("GeoLocation from Cache : {:9,}".format(count['cache']))
print("Failed reverse GeoLocate : {:9,}".format(count['failed']))
if use_lightroom:
print("GeoLocaction from Lightroom : {:9,}".format(count['lightroom']))
print("No Lightroom data found : {:9,}".format(count['not_found']))
print("More than one found in LR : {:9,}".format(count['many_found']))
# if we have failed data
if len(failed_files) > 0:
print("{}".format('-' * 39))
print("Files that failed to update:")
print("{}".format(', '.join(failed_files)))
# __END__ # __END__