|
|
|
@@ -22,7 +22,7 @@ import re
|
|
|
|
|
# Note XMPFiles does not work with sidecar files, need to read via XMPMeta
|
|
|
|
|
from libxmp import XMPMeta, consts
|
|
|
|
|
from shutil import copyfile, get_terminal_size
|
|
|
|
|
from math import ceil
|
|
|
|
|
from math import ceil, radians, sin, cos, atan2, sqrt
|
|
|
|
|
|
|
|
|
|
##############################################################
|
|
|
|
|
# FUNCTIONS
|
|
|
|
@@ -70,6 +70,20 @@ class readable_dir(argparse.Action):
|
|
|
|
|
raise argparse.ArgumentTypeError("readable_dir:{0} is not a readable dir".format(prospective_dir))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# check distance values are valid
|
|
|
|
|
class distance_values(argparse.Action):
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
|
m = re.match(r'^(\d+)\s?(m|km)$', values)
|
|
|
|
|
if m:
|
|
|
|
|
# convert to int in meters
|
|
|
|
|
values = int(m.group(1))
|
|
|
|
|
if m.group(2) == 'km':
|
|
|
|
|
values *= 1000
|
|
|
|
|
setattr(namespace, self.dest, values)
|
|
|
|
|
else:
|
|
|
|
|
raise argparse.ArgumentTypeError("distance_values:{0} is not a valid argument".format(values))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# MAIN FUNCTIONS
|
|
|
|
|
|
|
|
|
|
# METHOD: reverseGeolocate
|
|
|
|
@@ -114,7 +128,7 @@ def reverseGeolocateInit(longitude, latitude):
|
|
|
|
|
'error_message': ''
|
|
|
|
|
}
|
|
|
|
|
# error if long/lat is not valid
|
|
|
|
|
latlong_re = re.compile('^\d+\.\d+$')
|
|
|
|
|
latlong_re = re.compile(r'^\d+\.\d+$')
|
|
|
|
|
if not latlong_re.match(str(longitude)) or not latlong_re.match(str(latitude)):
|
|
|
|
|
geolocation['status'] = 'ERROR'
|
|
|
|
|
geolocation['error_message'] = 'Latitude {} or Longitude {} are not valid'.format(latitude, longitude)
|
|
|
|
@@ -187,7 +201,7 @@ def reverseGeolocateOpenStreetMap(longitude, latitude):
|
|
|
|
|
# dict with location, city, state, country, country code
|
|
|
|
|
# if not fillable, entry is empty
|
|
|
|
|
# SAMPLE: http://maps.googleapis.com/maps/api/geocode/json?latlng=<latitude>,<longitude>&language=<lang>&sensor=false&key=<api key>
|
|
|
|
|
def reverseGeolocateGoogle(longitude, latitude):
|
|
|
|
|
def reverseGeolocateGoogle(longitude, latitude): # noqa: C901
|
|
|
|
|
# init
|
|
|
|
|
geolocation = reverseGeolocateInit(longitude, latitude)
|
|
|
|
|
temp_geolocation = geolocation.copy()
|
|
|
|
@@ -224,7 +238,7 @@ def reverseGeolocateGoogle(longitude, latitude):
|
|
|
|
|
'CountryCode': ['country'],
|
|
|
|
|
'Country': ['country'],
|
|
|
|
|
'State': ['administrative_area_level_1', 'administrative_area_level_2'],
|
|
|
|
|
'City': ['locality'],
|
|
|
|
|
'City': ['locality', 'administrative_area_level_3'],
|
|
|
|
|
'Location': ['sublocality_level_1', 'sublocality_level_2', 'route'],
|
|
|
|
|
}
|
|
|
|
|
# print("Error: {}".format(response.json()['status']))
|
|
|
|
@@ -275,7 +289,6 @@ def reverseGeolocateGoogle(longitude, latitude):
|
|
|
|
|
geolocation['error_message'] = response.json()['error_message']
|
|
|
|
|
geolocation['status'] = response.json()['status']
|
|
|
|
|
print("Error in request: {} {}".format(geolocation['status'], geolocation['error_message']))
|
|
|
|
|
|
|
|
|
|
# return
|
|
|
|
|
return geolocation
|
|
|
|
|
|
|
|
|
@@ -315,7 +328,7 @@ def convertLongToDMS(lat_long):
|
|
|
|
|
# number used in google/lr internal
|
|
|
|
|
def longLatReg(longitude, latitude):
|
|
|
|
|
# regex
|
|
|
|
|
latlong_re = re.compile('^(\d+),(\d+\.\d+)([NESW]{1})$')
|
|
|
|
|
latlong_re = re.compile(r'^(\d+),(\d+\.\d+)([NESW]{1})$')
|
|
|
|
|
# dict for loop
|
|
|
|
|
lat_long = {
|
|
|
|
|
'longitude': longitude,
|
|
|
|
@@ -343,6 +356,27 @@ def convertDMStoLong(lat_long):
|
|
|
|
|
return longLatReg(lat_long, '0,0.0N')['longitude']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# METHOD: getDistance
|
|
|
|
|
# PARAMS: from long/lat, to long_lat
|
|
|
|
|
# RETURN: distance in meters
|
|
|
|
|
# DESC : calculates the difference between two coordinates
|
|
|
|
|
def getDistance(from_longitude, from_latitude, to_longitude, to_latitude):
|
|
|
|
|
# earth radius in meters
|
|
|
|
|
earth_radius = 6378137.0
|
|
|
|
|
# convert all from radians with pre convert DMS to long and to float
|
|
|
|
|
from_longitude = radians(float(convertDMStoLong(from_longitude)))
|
|
|
|
|
from_latitude = radians(float(convertDMStoLat(from_latitude)))
|
|
|
|
|
to_longitude = radians(float(convertDMStoLong(to_longitude)))
|
|
|
|
|
to_latitude = radians(float(convertDMStoLat(to_latitude)))
|
|
|
|
|
# distance from - to
|
|
|
|
|
distance_longitude = from_longitude - to_longitude
|
|
|
|
|
distance_latitude = from_latitude - to_latitude
|
|
|
|
|
# main distance calculation
|
|
|
|
|
distance = sin(distance_latitude / 2)**2 + cos(from_latitude) * cos(to_latitude) * sin(distance_longitude / 2)**2
|
|
|
|
|
distance = 2 * atan2(sqrt(distance), sqrt(1 - distance))
|
|
|
|
|
return earth_radius * distance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# METHOD: checkOverwrite
|
|
|
|
|
# PARAMS: data: value field, key: XMP key, field_controls: array from args
|
|
|
|
|
# RETURN: true/false
|
|
|
|
@@ -486,7 +520,7 @@ def formatLen(string, length):
|
|
|
|
|
# RETURN: number found in the BK string or 0 for none
|
|
|
|
|
# DESC : gets the BK number for sorting in the file list
|
|
|
|
|
def fileSortNumber(file):
|
|
|
|
|
m = re.match('.*\.BK\.(\d+)\.xmp$', file)
|
|
|
|
|
m = re.match(r'.*\.BK\.(\d+)\.xmp$', file)
|
|
|
|
|
return int(m.group(1)) if m is not None else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -610,125 +644,178 @@ parser = argparse.ArgumentParser(
|
|
|
|
|
|
|
|
|
|
# xmp folder (or folders), or file (or files)
|
|
|
|
|
# note that the target directory or file needs to be writeable
|
|
|
|
|
parser.add_argument('-i', '--include-source',
|
|
|
|
|
required=True,
|
|
|
|
|
nargs='*',
|
|
|
|
|
action=writable_dir_folder,
|
|
|
|
|
dest='xmp_sources',
|
|
|
|
|
metavar='XMP SOURCE FOLDER',
|
|
|
|
|
help='The source folder or folders with the XMP files that need reverse geo encoding to be set. Single XMP files can be given here'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-i',
|
|
|
|
|
'--include-source',
|
|
|
|
|
required=True,
|
|
|
|
|
nargs='*',
|
|
|
|
|
action=writable_dir_folder,
|
|
|
|
|
dest='xmp_sources',
|
|
|
|
|
metavar='XMP SOURCE FOLDER',
|
|
|
|
|
help='The source folder or folders with the XMP files that need reverse geo encoding to be set. Single XMP files can be given here'
|
|
|
|
|
)
|
|
|
|
|
# exclude folders
|
|
|
|
|
parser.add_argument('-x', '--exclude-source',
|
|
|
|
|
nargs='*',
|
|
|
|
|
action=writable_dir_folder,
|
|
|
|
|
dest='exclude_sources',
|
|
|
|
|
metavar='EXCLUDE XMP SOURCE FOLDER',
|
|
|
|
|
help='Folders and files that will be excluded.'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-x',
|
|
|
|
|
'--exclude-source',
|
|
|
|
|
nargs='*',
|
|
|
|
|
action=writable_dir_folder,
|
|
|
|
|
dest='exclude_sources',
|
|
|
|
|
metavar='EXCLUDE XMP SOURCE FOLDER',
|
|
|
|
|
help='Folders and files that will be excluded.'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# LR database (base folder)
|
|
|
|
|
# get .lrcat file in this folder
|
|
|
|
|
parser.add_argument('-l', '--lightroom',
|
|
|
|
|
# required=True,
|
|
|
|
|
action=readable_dir,
|
|
|
|
|
dest='lightroom_folder',
|
|
|
|
|
metavar='LIGHTROOM FOLDER',
|
|
|
|
|
help='Lightroom catalogue base folder'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-l',
|
|
|
|
|
'--lightroom',
|
|
|
|
|
# required=True,
|
|
|
|
|
action=readable_dir,
|
|
|
|
|
dest='lightroom_folder',
|
|
|
|
|
metavar='LIGHTROOM FOLDER',
|
|
|
|
|
help='Lightroom catalogue base folder'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# strict LR check with base path next to the file base name
|
|
|
|
|
parser.add_argument('-s', '--strict',
|
|
|
|
|
dest='lightroom_strict',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Do strict check for Lightroom files including Path in query'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-s',
|
|
|
|
|
'--strict',
|
|
|
|
|
dest='lightroom_strict',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Do strict check for Lightroom files including Path in query'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# set behaviour override
|
|
|
|
|
# FLAG: default: only set not filled
|
|
|
|
|
# other: overwrite all or overwrite if one is missing, overwrite specifc field (as defined below)
|
|
|
|
|
# fields: Location, City, State, Country, CountryCode
|
|
|
|
|
parser.add_argument('-f', '--field',
|
|
|
|
|
action='append',
|
|
|
|
|
type=str.lower, # make it lowercase for check
|
|
|
|
|
choices=['overwrite', 'location', 'city', 'state', 'country', 'countrycode'],
|
|
|
|
|
dest='field_controls',
|
|
|
|
|
metavar='<overwrite, location, city, state, country, countrycode>',
|
|
|
|
|
help='On default only set fields that are not set yet. Options are: '\
|
|
|
|
|
'Overwrite (write all new), Location, City, State, Country, CountryCode. '\
|
|
|
|
|
'Multiple can be given for combination overwrite certain fields only or set only certain fields. '\
|
|
|
|
|
'If with overwrite the field will be overwritten if already set, else it will be always skipped.'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-f',
|
|
|
|
|
'--field',
|
|
|
|
|
action='append',
|
|
|
|
|
type=str.lower, # make it lowercase for check
|
|
|
|
|
choices=['overwrite', 'location', 'city', 'state', 'country', 'countrycode'],
|
|
|
|
|
dest='field_controls',
|
|
|
|
|
metavar='<overwrite, location, city, state, country, countrycode>',
|
|
|
|
|
help='On default only set fields that are not set yet. Options are: '\
|
|
|
|
|
'Overwrite (write all new), Location, City, State, Country, CountryCode. '\
|
|
|
|
|
'Multiple can be given for combination overwrite certain fields only or set only certain fields. '\
|
|
|
|
|
'If with overwrite the field will be overwritten if already set, else it will be always skipped.'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-d',
|
|
|
|
|
'--fuzzy-cache',
|
|
|
|
|
type=str.lower,
|
|
|
|
|
action=distance_values,
|
|
|
|
|
nargs='?',
|
|
|
|
|
const='10m', # default is 10m
|
|
|
|
|
dest='fuzzy_distance',
|
|
|
|
|
metavar='FUZZY DISTANCE',
|
|
|
|
|
help='Allow fuzzy distance cache lookup. Optional distance can be given, '\
|
|
|
|
|
'if not set default of 10m is used. '\
|
|
|
|
|
'Allowed argument is in the format of 12m or 12km'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Google Maps API key to overcome restrictions
|
|
|
|
|
parser.add_argument('-g', '--google',
|
|
|
|
|
dest='google_api_key',
|
|
|
|
|
metavar='GOOGLE API KEY',
|
|
|
|
|
help='Set a Google API Maps key to overcome the default lookup limitations'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-g',
|
|
|
|
|
'--google',
|
|
|
|
|
dest='google_api_key',
|
|
|
|
|
metavar='GOOGLE API KEY',
|
|
|
|
|
help='Set a Google API Maps key to overcome the default lookup limitations'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# use open street maps
|
|
|
|
|
parser.add_argument('-o', '--openstreetmap',
|
|
|
|
|
dest='use_openstreetmap',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Use openstreetmap instead of Google'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-o',
|
|
|
|
|
'--openstreetmap',
|
|
|
|
|
dest='use_openstreetmap',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Use openstreetmap instead of Google'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# email of open street maps requests
|
|
|
|
|
parser.add_argument('-e', '--email',
|
|
|
|
|
dest='email',
|
|
|
|
|
metavar='EMIL ADDRESS',
|
|
|
|
|
help='An email address for OpenStreetMap'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-e',
|
|
|
|
|
'--email',
|
|
|
|
|
dest='email',
|
|
|
|
|
metavar='EMIL ADDRESS',
|
|
|
|
|
help='An email address for OpenStreetMap'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# write api/email settings to config file
|
|
|
|
|
parser.add_argument('-w', '--write-settings',
|
|
|
|
|
dest='config_write',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Write Google API or OpenStreetMap email to config file'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-w',
|
|
|
|
|
'--write-settings',
|
|
|
|
|
dest='config_write',
|
|
|
|
|
action='store_true',
|
|
|
|
|
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'
|
|
|
|
|
)
|
|
|
|
|
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'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# only list unset ones
|
|
|
|
|
parser.add_argument('-u', '--unset-only',
|
|
|
|
|
dest='unset_only',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Only list unset XMP files'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-u',
|
|
|
|
|
'--unset-only',
|
|
|
|
|
dest='unset_only',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Only list unset XMP files'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# only list unset GPS codes
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-p',
|
|
|
|
|
'--unset-gps-only',
|
|
|
|
|
dest='unset_gps_only',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Only list unset XMP files for GPS fields'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# don't try to do auto adjust in list view
|
|
|
|
|
parser.add_argument('-a', '--no-autoadjust',
|
|
|
|
|
dest='no_autoadjust',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Don\'t try to auto adjust columns'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-a',
|
|
|
|
|
'--no-autoadjust',
|
|
|
|
|
dest='no_autoadjust',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Don\'t try to auto adjust columns'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# compact view, compresses columns down to a minimum
|
|
|
|
|
parser.add_argument('-c', '--compact',
|
|
|
|
|
dest='compact_view',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Very compact list view'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-c',
|
|
|
|
|
'--compact',
|
|
|
|
|
dest='compact_view',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Very compact list view'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Do not create backup files
|
|
|
|
|
parser.add_argument('-n', '--nobackup',
|
|
|
|
|
dest='no_xmp_backup',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Do not create a backup from the XMP file'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-n',
|
|
|
|
|
'--nobackup',
|
|
|
|
|
dest='no_xmp_backup',
|
|
|
|
|
action='store_true',
|
|
|
|
|
help='Do not create a backup from the XMP file'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# verbose args for more detailed output
|
|
|
|
|
parser.add_argument('-v', '--verbose',
|
|
|
|
|
action='count',
|
|
|
|
|
dest='verbose',
|
|
|
|
|
help='Set verbose output level'
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
'-v',
|
|
|
|
|
'--verbose',
|
|
|
|
|
action='count',
|
|
|
|
|
dest='verbose',
|
|
|
|
|
help='Set verbose output level'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# debug flag
|
|
|
|
|
parser.add_argument('--debug', action='store_true', dest='debug', help='Set detailed debug output')
|
|
|
|
@@ -753,11 +840,12 @@ if not args.unset_only:
|
|
|
|
|
args.unset_only = 0
|
|
|
|
|
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### ARGUMENT VARS: I: {incl}, X: {excl}, L: {lr}, F: {fc}, M: {osm}, G: {gp}, E: {em}, R: {read}, U: {us}, A: {adj}, C: {cmp}, N: {nbk}, W: {wrc}, V: {v}, D: {d}, T: {t}".format(
|
|
|
|
|
print("### ARGUMENT VARS: I: {incl}, X: {excl}, L: {lr}, F: {fc}, D: {fdist}, M: {osm}, G: {gp}, E: {em}, R: {read}, U: {us}, A: {adj}, C: {cmp}, N: {nbk}, W: {wrc}, V: {v}, D: {d}, T: {t}".format(
|
|
|
|
|
incl=args.xmp_sources,
|
|
|
|
|
excl=args.exclude_sources,
|
|
|
|
|
lr=args.lightroom_folder,
|
|
|
|
|
fc=args.field_controls,
|
|
|
|
|
fdist=args.fuzzy_distance,
|
|
|
|
|
osm=args.use_openstreetmap,
|
|
|
|
|
gp=args.google_api_key,
|
|
|
|
|
em=args.email,
|
|
|
|
@@ -790,7 +878,7 @@ if args.email and not args.use_openstreetmap:
|
|
|
|
|
error = True
|
|
|
|
|
# if email and not basic valid email (@ .)
|
|
|
|
|
if args.email:
|
|
|
|
|
if not re.match('^.+@.+\.[A-Za-z]{1,}$', args.email):
|
|
|
|
|
if not re.match(r'^.+@.+\.[A-Za-z]{1,}$', args.email):
|
|
|
|
|
print("Not a valid email for OpenStreetMap: {}".format(args.email))
|
|
|
|
|
error = True
|
|
|
|
|
# on error exit here
|
|
|
|
@@ -887,6 +975,7 @@ count = {
|
|
|
|
|
'read': 0,
|
|
|
|
|
'map': 0,
|
|
|
|
|
'cache': 0,
|
|
|
|
|
'fuzzy_cache': 0,
|
|
|
|
|
'lightroom': 0,
|
|
|
|
|
'changed': 0,
|
|
|
|
|
'failed': 0,
|
|
|
|
@@ -932,6 +1021,8 @@ if args.lightroom_folder:
|
|
|
|
|
cur = lrdb.cursor()
|
|
|
|
|
# flag that we have Lightroom DB
|
|
|
|
|
use_lightroom = True
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### USE Lightroom {}".format(use_lightroom))
|
|
|
|
|
|
|
|
|
|
# on error exit here
|
|
|
|
|
if error:
|
|
|
|
@@ -955,8 +1046,8 @@ for xmp_file_source in args.xmp_sources:
|
|
|
|
|
# 2) file is not in exclude list
|
|
|
|
|
# 3) full folder is not in exclude list
|
|
|
|
|
if file.endswith(".xmp") and ".BK." not in file \
|
|
|
|
|
and "{}/{}".format(root, file) not in args.exclude_sources \
|
|
|
|
|
and root.rstrip('/') not in [x.rstrip('/') for x in args.exclude_sources]:
|
|
|
|
|
and "{}/{}".format(root, file) not in args.exclude_sources \
|
|
|
|
|
and root.rstrip('/') not in [x.rstrip('/') for x in args.exclude_sources]:
|
|
|
|
|
if "{}/{}".format(root, file) not in work_files:
|
|
|
|
|
work_files.append("{}/{}".format(root, file))
|
|
|
|
|
count['all'] += 1
|
|
|
|
@@ -1040,7 +1131,7 @@ if args.read_only:
|
|
|
|
|
|
|
|
|
|
# ### MAIN WORK LOOP
|
|
|
|
|
# now we just loop through each file and work on them
|
|
|
|
|
for xmp_file in work_files:
|
|
|
|
|
for xmp_file in work_files: # noqa: C901
|
|
|
|
|
if not args.read_only:
|
|
|
|
|
print("---> {}: ".format(xmp_file), end='')
|
|
|
|
|
|
|
|
|
@@ -1054,12 +1145,16 @@ for xmp_file in work_files:
|
|
|
|
|
# read fields from the XMP file and store in hash
|
|
|
|
|
xmp.parse_from_str(strbuffer)
|
|
|
|
|
for xmp_field in xmp_fields:
|
|
|
|
|
data_set[xmp_field] = xmp.get_property(xmp_fields[xmp_field], xmp_field)
|
|
|
|
|
# need to check if propert exist or it will the exempi routine will fail
|
|
|
|
|
if xmp.does_property_exist(xmp_fields[xmp_field], xmp_field):
|
|
|
|
|
data_set[xmp_field] = xmp.get_property(xmp_fields[xmp_field], xmp_field)
|
|
|
|
|
else:
|
|
|
|
|
data_set[xmp_field] = ''
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### => XMP: {}:{} => {}".format(xmp_fields[xmp_field], xmp_field, data_set[xmp_field]))
|
|
|
|
|
if args.read_only:
|
|
|
|
|
# view only if list all or if data is unset
|
|
|
|
|
if not args.unset_only or (args.unset_only and '' in data_set.values()):
|
|
|
|
|
if (not args.unset_only and not args.unset_gps_only) or (args.unset_only and '' in data_set.values()) or (args.unset_gps_only and (not data_set['GPSLatitude'] or not data_set['GPSLongitude'])):
|
|
|
|
|
# for read only we print out the data formatted
|
|
|
|
|
# headline check, do we need to print that
|
|
|
|
|
count['read'] = printHeader(header_line.format(page_no=page_no, page_all=page_all), count['read'], header_repeat)
|
|
|
|
@@ -1140,23 +1235,59 @@ for xmp_file in work_files:
|
|
|
|
|
# run this through the overwrite checker to get unset if we have a forced overwrite
|
|
|
|
|
has_unset = False
|
|
|
|
|
failed = False
|
|
|
|
|
from_cache = 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'])
|
|
|
|
|
cache_key = '{}#{}'.format(data_set['GPSLongitude'], data_set['GPSLatitude'])
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### *** CACHE: {}: {}".format(cache_key, 'NO' if cache_key not in data_cache else 'YES'))
|
|
|
|
|
# main chache check = identical
|
|
|
|
|
# second cache level check is on distance:
|
|
|
|
|
# default distance is 10m, can be set via flag
|
|
|
|
|
# check distance to previous cache entries (reverse newest to oldest) and match before we do google lookup
|
|
|
|
|
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
|
|
|
|
|
has_fuzzy_cache = False
|
|
|
|
|
if args.fuzzy_distance:
|
|
|
|
|
shortest_distance = args.fuzzy_distance
|
|
|
|
|
best_match_latlong = ''
|
|
|
|
|
# check if we have fuzzy distance, if no valid found do maps lookup
|
|
|
|
|
for _cache_key in data_cache:
|
|
|
|
|
# split up cache key so we can use in the distance calc method
|
|
|
|
|
to_lat_long = _cache_key.split('#')
|
|
|
|
|
# get the distance based on current set + cached set
|
|
|
|
|
# print("Lookup f-long {} f-lat {} t-long {} t-lat {}".format(data_set['GPSLongitude'], data_set['GPSLatitude'], to_lat_long[0], to_lat_long[1]))
|
|
|
|
|
distance = getDistance(from_longitude=data_set['GPSLongitude'], from_latitude=data_set['GPSLatitude'], to_longitude=to_lat_long[0], to_latitude=to_lat_long[1])
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### **= FUZZY CACHE: => distance: {} (m), shortest: {}".format(distance, shortest_distance))
|
|
|
|
|
if distance <= shortest_distance:
|
|
|
|
|
# set new distance and keep current best matching location
|
|
|
|
|
shortest_distance = distance
|
|
|
|
|
best_match_latlong = _cache_key
|
|
|
|
|
has_fuzzy_cache = True
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### ***= FUZZY CACHE: YES => Best match: {}".format(best_match_latlong))
|
|
|
|
|
if not has_fuzzy_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
|
|
|
|
|
from_cache = False
|
|
|
|
|
else:
|
|
|
|
|
maps_location = data_cache[best_match_latlong]
|
|
|
|
|
# cache this one, because the next one will match this one too
|
|
|
|
|
# we don't need to loop search again for the same fuzzy location
|
|
|
|
|
data_cache[cache_key] = maps_location
|
|
|
|
|
count['cache'] += 1
|
|
|
|
|
count['fuzzy_cache'] += 1
|
|
|
|
|
from_cache = True
|
|
|
|
|
else:
|
|
|
|
|
# load location from cache
|
|
|
|
|
maps_location = data_cache[cache_key]
|
|
|
|
|
count['cache'] += 1
|
|
|
|
|
from_cache = True
|
|
|
|
|
# overwrite sets (note options check here)
|
|
|
|
|
if args.debug:
|
|
|
|
|
print("### Map Location ({}): {}".format(map_type, maps_location))
|
|
|
|
@@ -1199,8 +1330,11 @@ for xmp_file in work_files:
|
|
|
|
|
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]")
|
|
|
|
|
print("[TEST] Would write {} {}".format(data_set, xmp_file), end='')
|
|
|
|
|
if from_cache:
|
|
|
|
|
print("[UPDATED FROM CACHE]")
|
|
|
|
|
else:
|
|
|
|
|
print("[UPDATED]")
|
|
|
|
|
count['changed'] += 1
|
|
|
|
|
elif failed:
|
|
|
|
|
print("[FAILED]")
|
|
|
|
@@ -1216,23 +1350,24 @@ if use_lightroom:
|
|
|
|
|
lrdb.close()
|
|
|
|
|
|
|
|
|
|
# end stats only if we write
|
|
|
|
|
print("{}".format('=' * 39))
|
|
|
|
|
print("XMP Files found : {:9,}".format(count['all']))
|
|
|
|
|
print("{}".format('=' * 40))
|
|
|
|
|
print("XMP Files found : {:9,}".format(count['all']))
|
|
|
|
|
if args.read_only:
|
|
|
|
|
print("XMP Files listed : {:9,}".format(count['listed']))
|
|
|
|
|
print("XMP Files listed : {:9,}".format(count['listed']))
|
|
|
|
|
if not args.read_only:
|
|
|
|
|
print("Updated : {:9,}".format(count['changed']))
|
|
|
|
|
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']))
|
|
|
|
|
print("Updated : {:9,}".format(count['changed']))
|
|
|
|
|
print("Skipped : {:9,}".format(count['skipped']))
|
|
|
|
|
print("New GeoLocation from Map : {:9,}".format(count['map']))
|
|
|
|
|
print("GeoLocation from Cache : {:9,}".format(count['cache']))
|
|
|
|
|
print("GeoLocation from Fuzzy Cache : {:9,}".format(count['fuzzy_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']))
|
|
|
|
|
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("{}".format('-' * 40))
|
|
|
|
|
print("Files that failed to update:")
|
|
|
|
|
print("{}".format(', '.join(failed_files)))
|
|
|
|
|
|
|
|
|
|