Reverse Geolocate Clean up run

This commit is contained in:
2022-11-11 23:08:41 +09:00
parent 65e352e8a4
commit b7de6d8119
4 changed files with 785 additions and 710 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic"
}

View File

@@ -4,16 +4,28 @@ Reverse GeoLocate from XMP sidecar files with optional LightRoom DB read
This script will update any of the Country Code, Country, State, City and Location data that is missing in sidecard files. If a Lightroom DB is set, it will read any data from the database and fill in the fields before it tries to get the location name from google with the Latitude and Longitude found in either the XMP sidecar file or the LR database.
#### Installing and setting up
## Development Setup
```sh
python3 -m venv .venv
source .venv/bin/activate
.venv/bin/python -m pip install -U pip setuptools wheel
```
Then install the requests and python-xmp-toolkit modules from below
## Installing and setting up
The script uses the following external non defauly python libraries
* xmp toolkit
* requests
install both with the pip3 command
```
pip3 install requests
pip3 install python-xmp-toolkit
```sh
pip install requests
pip install python-xmp-toolkit
```
XMP Toolkit also needs the [Exempi Library](http://libopenraw.freedesktop.org/wiki/Exempi). This one can be install via brew or macports directly.

View File

@@ -3,15 +3,14 @@
# AUTHOR : Clemens Schwaighofer
# DATE : 2018/2/20
# LICENSE: GPLv3
# DESC : Set the reverse Geo location (name) from Lat/Long data in XMP files in a lightroom catalogue
# DESC :
# Set the reverse Geo location (name) from Lat/Long data in XMP files
# in a lightroom catalogue
# * tries to get pre-set geo location from LR catalog
# * if not found tries to get data from Google
# * 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/)
import argparse
import sqlite3
import requests
import configparser
import unicodedata
# import textwrap
@@ -19,10 +18,13 @@ import glob
import os
import sys
import re
# Note XMPFiles does not work with sidecar files, need to read via XMPMeta
from libxmp import XMPMeta, consts
import argparse
import sqlite3
from shutil import copyfile, get_terminal_size
from math import ceil, radians, sin, cos, atan2, sqrt
import requests
# Note XMPFiles does not work with sidecar files, need to read via XMPMeta
from libxmp import XMPMeta, consts
##############################################################
# FUNCTIONS
@@ -32,13 +34,16 @@ from math import ceil, radians, sin, cos, atan2, sqrt
# this is used by isLatin and onlyLatinChars
cache_latin_letters = {}
# ARGPARSE HELPERS
# call: writable_dir_folder
# checks if this is a writeable folder OR file
# AND it works on nargs *
class writable_dir_folder(argparse.Action):
class WritableDirFolder(argparse.Action):
"""
checks if this is a writeable folder OR file
AND it works on nargs *
Args:
argparse (_type_): _description_
"""
def __call__(self, parser, namespace, values, option_string=None):
# we loop through list (this is because of nargs *)
for prospective_dir in values:
@@ -632,42 +637,49 @@ def getBackupFileCounter(xmp_file):
return bk_file_counter
##############################################################
# ARGUMENT PARSNING
# ARGUMENT PARSING
##############################################################
def argument_parser():
"""
Parses the command line arguments
parser = argparse.ArgumentParser(
Returns:
Namespace: parsed arguments
"""
parser = argparse.ArgumentParser(
description='Reverse Geoencoding based on set Latitude/Longitude data in XMP files',
# formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='Sample: (todo)'
)
)
# xmp folder (or folders), or file (or files)
# note that the target directory or file needs to be writeable
parser.add_argument(
# 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,
action=WritableDirFolder,
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(
)
# exclude folders
parser.add_argument(
'-x',
'--exclude-source',
nargs='*',
action=writable_dir_folder,
action=WritableDirFolder,
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(
# LR database (base folder)
# get .lrcat file in this folder
parser.add_argument(
'-l',
'--lightroom',
# required=True,
@@ -675,22 +687,23 @@ parser.add_argument(
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(
# 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'
)
)
# 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(
# 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',
@@ -698,13 +711,17 @@ parser.add_argument(
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.'
)
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(
parser.add_argument(
'-d',
'--fuzzy-cache',
type=str.lower,
@@ -713,186 +730,190 @@ parser.add_argument(
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. '\
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(
# 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'
)
)
# use open street maps
parser.add_argument(
# use open street maps
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(
# email of open street maps requests
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(
# 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'
)
)
# only read data and print on screen, do not write anything
parser.add_argument(
# 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'
)
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(
# only list unset ones
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(
# 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(
# 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'
)
)
# compact view, compresses columns down to a minimum
parser.add_argument(
# compact view, compresses columns down to a minimum
parser.add_argument(
'-c',
'--compact',
dest='compact_view',
action='store_true',
help='Very compact list view'
)
)
# Do not create backup files
parser.add_argument(
# 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'
)
)
# verbose args for more detailed output
parser.add_argument(
# verbose args for more detailed output
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')
# test flag
parser.add_argument('--test', action='store_true', dest='test', help='Do not write data back to file')
# debug flag
parser.add_argument(
'--debug', action='store_true', dest='debug', help='Set detailed debug output'
)
# test flag
parser.add_argument(
'--test', action='store_true', dest='test', help='Do not write data back to file'
)
# read in the argumens
args = parser.parse_args()
# read in the argumens
return parser.parse_args()
##############################################################
# MAIN CODE
##############################################################
# init verbose to 0 if not set
if not args.verbose:
def main():
"""
Main code run
"""
args = argument_parser()
# init verbose to 0 if not set
if not args.verbose:
args.verbose = 0
# init exclude source to list if not set
if not args.exclude_sources:
# init exclude source to list if not set
if not args.exclude_sources:
args.exclude_sources = []
# init args unset (for list view) with 0 if unset
if not args.unset_only:
# init args unset (for list view) with 0 if unset
if not args.unset_only:
args.unset_only = 0
if args.debug:
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,
read=args.read_only,
us=args.unset_only,
adj=args.no_autoadjust,
cmp=args.compact_view,
nbk=args.no_xmp_backup,
wrc=args.config_write,
v=args.verbose,
d=args.debug,
t=args.test
))
if args.debug:
print(
"### ARGUMENT VARS: "
f"I: {args.xmp_sources}, X: {args.exclude_sources}, L: {args.lightroom_folder}, "
f"F: {args.field_controls}, D: {args.fuzzy_distance}, M: {args.use_openstreetmap}, "
f"G: {args.google_api_key}, E: {args.email}, R: {args.read_only}, U: {args.unset_only}, "
f"A: {args.no_autoadjust}, C: {args.compact_view}, N: {args.no_xmp_backup}, "
f"W: {args.config_write}, V: {args.verbose}, D: {args.debug}, T: {args.test}"
)
# error flag
error = False
# set search map type
map_type = 'google' if not args.use_openstreetmap else 'openstreetmap'
# if -g and -o, error
if args.google_api_key and args.use_openstreetmap:
# error flag
error = False
# set search map type
map_type = 'google' if not args.use_openstreetmap else 'openstreetmap'
# if -g and -o, error
if args.google_api_key and args.use_openstreetmap:
print("You cannot set a Google API key and use OpenStreetMap at the same time")
error = True
# or if -g and -e
if args.google_api_key and args.email:
# or if -g and -e
if args.google_api_key and args.email:
print("You cannot set a Google API key and OpenStreetMap email at the same time")
error = True
# or -e and no -o
if args.email and not args.use_openstreetmap:
# or -e and no -o
if args.email and not args.use_openstreetmap:
print("You cannot set an OpenStreetMap email and not use OpenStreetMap")
error = True
# if email and not basic valid email (@ .)
if args.email:
# if email and not basic valid email (@ .)
if 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
if error:
# on error exit here
if error:
sys.exit(1)
config = configparser.ConfigParser()
# try to find config file in following order
# $HOME/.config/
config_file = 'reverse_geolocate.cfg'
config_folder = os.path.expanduser('~/.config/reverseGeolocate/')
config_data = '{}{}'.format(config_folder, config_file)
# if file exists read, if not skip unless we have write flag and google api or openstreetmaps email
if os.path.isfile(config_data):
config = configparser.ConfigParser()
# try to find config file in following order
# $HOME/.config/
config_file = 'reverse_geolocate.cfg'
config_folder = os.path.expanduser('~/.config/reverseGeolocate/')
config_data = '{}{}'.format(config_folder, config_file)
# if file exists read, if not skip unless we have write flag and google api or openstreetmaps email
if os.path.isfile(config_data):
config.read(config_data)
# check if api group & setting is there. also never overwrite argument given data
if 'API' in config:
@@ -902,8 +923,8 @@ if os.path.isfile(config_data):
if 'openstreetmapemail' in config['API']:
if not args.email:
args.email = config['API']['openstreetmapemail']
# write data if exists and changed
if args.config_write and (args.google_api_key or args.email):
# write data if exists and changed
if args.config_write and (args.google_api_key or args.email):
config_change = False
# check if new value differs, if yes, change and write
if 'API' not in config:
@@ -920,21 +941,21 @@ if args.config_write and (args.google_api_key or args.email):
os.makedirs(config_folder)
with open(config_data, 'w') as fptr:
config.write(fptr)
if args.debug:
if args.debug:
print("### OVERRIDE API: G: {}, O: {}".format(args.google_api_key, args.email))
# The XMP fields const lookup values
# XML/XMP
# READ:
# exif:GPSLatitude
# exif:GPSLongitude
# READ for if filled
# Iptc4xmpCore:Location
# photoshop:City
# photoshop:State
# photoshop:Country
# Iptc4xmpCore:CountryCode
xmp_fields = {
# The XMP fields const lookup values
# XML/XMP
# READ:
# exif:GPSLatitude
# exif:GPSLongitude
# READ for if filled
# Iptc4xmpCore:Location
# photoshop:City
# photoshop:State
# photoshop:Country
# Iptc4xmpCore:CountryCode
xmp_fields = {
'GPSLatitude': consts.XMP_NS_EXIF, # EXIF GPSLat/Long are stored in Degree,Min.Sec[NESW] format
'GPSLongitude': consts.XMP_NS_EXIF,
'Location': consts.XMP_NS_IPTCCore,
@@ -942,11 +963,11 @@ xmp_fields = {
'State': consts.XMP_NS_Photoshop,
'Country': consts.XMP_NS_Photoshop,
'CountryCode': consts.XMP_NS_IPTCCore
}
# non lat/long fields (for loc loops)
data_set_loc = ('Location', 'City', 'State', 'Country', 'CountryCode')
# one xmp data set
data_set = {
}
# non lat/long fields (for loc loops)
data_set_loc = ('Location', 'City', 'State', 'Country', 'CountryCode')
# one xmp data set
data_set = {
'GPSLatitude': '',
'GPSLongitude': '',
'Location': '',
@@ -954,22 +975,22 @@ data_set = {
'State': '',
'Country': '',
'CountryCode': ''
}
# original set for compare (is constant unchanged)
data_set_original = {}
# cache set to avoid double lookups for identical Lat/Ling
data_cache = {}
# work files, all files + folders we need to work on
work_files = []
# all failed files
failed_files = []
# use lightroom
use_lightroom = False
# cursors & query
query = ''
cur = ''
# count variables
count = {
}
# original set for compare (is constant unchanged)
data_set_original = {}
# cache set to avoid double lookups for identical Lat/Ling
data_cache = {}
# work files, all files + folders we need to work on
work_files = []
# all failed files
failed_files = []
# use lightroom
use_lightroom = False
# cursors & query
query = ''
cur = ''
# count variables
count = {
'all': 0,
'listed': 0,
'read': 0,
@@ -982,10 +1003,10 @@ count = {
'skipped': 0,
'not_found': 0,
'many_found': 0,
}
}
# do lightroom stuff only if we have the lightroom folder
if args.lightroom_folder:
# do lightroom stuff only if we have the lightroom folder
if args.lightroom_folder:
# query string for lightroom DB check
query = 'SELECT Adobe_images.id_local, AgLibraryFile.baseName, AgLibraryRootFolder.absolutePath, AgLibraryRootFolder.name as realtivePath, AgLibraryFolder.pathFromRoot, AgLibraryFile.originalFilename, '
query += 'AgHarvestedExifMetadata.gpsLatitude, AgHarvestedExifMetadata.gpsLongitude, '
@@ -1024,15 +1045,15 @@ if args.lightroom_folder:
if args.debug:
print("### USE Lightroom {}".format(use_lightroom))
# on error exit here
if error:
# on error exit here
if error:
sys.exit(1)
# init the XML meta for handling
xmp = XMPMeta()
# init the XML meta for handling
xmp = XMPMeta()
# loop through the xmp_sources (folder or files) and read in the XMP data for LAT/LONG, other data
for xmp_file_source in args.xmp_sources:
# loop through the xmp_sources (folder or files) and read in the XMP data for LAT/LONG, other data
for xmp_file_source in args.xmp_sources:
# if folder, open and loop
# NOTE: we do check for folders in there, if there are we recourse traverse them
# also check that folder is not in exclude list
@@ -1056,11 +1077,11 @@ for xmp_file_source in args.xmp_sources:
if xmp_file_source not in work_files and xmp_file_source not in args.exclude_sources:
work_files.append(xmp_file_source)
count['all'] += 1
if args.debug:
if args.debug:
print("### Work Files {}".format(work_files))
# if we have read only we print list format style
if args.read_only:
# if we have read only we print list format style
if args.read_only:
# adjust the output width for the list view
format_length = outputListWidthAdjust()
@@ -1089,8 +1110,8 @@ if args.read_only:
# header title
# seperator line
header_line = '''{}
{}
{}'''.format(
{}
{}'''.format(
'> Page {page_no:,}/{page_all:,}', # can later be set to something else, eg page numbers
# pre replace path length before we add the header titles
format_line.format(
@@ -1129,9 +1150,9 @@ if args.read_only:
if not work_files:
print("{:<60}".format('[!!!] No files found'))
# ### MAIN WORK LOOP
# now we just loop through each file and work on them
for xmp_file in work_files: # noqa: C901
# ### MAIN WORK LOOP
# now we just loop through each file and work on them
for xmp_file in work_files: # noqa: C901
if not args.read_only:
print("---> {}: ".format(xmp_file), end='')
@@ -1230,8 +1251,9 @@ for xmp_file in work_files: # noqa: C901
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
print(f"### -> LR: {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
@@ -1241,13 +1263,17 @@ for xmp_file in work_files: # noqa: C901
has_unset = True
if has_unset:
# check if lat/long is in cache
cache_key = '{}#{}'.format(data_set['GPSLongitude'], data_set['GPSLatitude'])
cache_key = f"{data_set['GPSLongitude']}#{data_set['GPSLatitude']}"
if args.debug:
print("### *** CACHE: {}: {}".format(cache_key, 'NO' if cache_key not in data_cache else 'YES'))
print(
f"### *** CACHE: {cache_key}: "
f"{'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
# check distance to previous cache entries (reverse newest to oldest)
# and match before we do google lookup
if cache_key not in data_cache:
has_fuzzy_cache = False
if args.fuzzy_distance:
@@ -1258,20 +1284,39 @@ for xmp_file in work_files: # noqa: C901
# 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])
# print(
# f"Lookup f-long {data_set['GPSLongitude']} "
# f"f-lat {data_set['GPSLatitude']} "
# f"t-long {to_lat_long[0]} t-lat {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))
print(
f"### **= FUZZY CACHE: => distance: {distance} (m), "
f"shortest: {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))
print(
"### ***= FUZZY CACHE: YES => "
f"Best match: {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)
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
@@ -1290,7 +1335,7 @@ for xmp_file in work_files: # noqa: C901
from_cache = True
# overwrite sets (note options check here)
if args.debug:
print("### Map Location ({}): {}".format(map_type, maps_location))
print(f"### Map Location ({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:
@@ -1306,7 +1351,7 @@ for xmp_file in work_files: # noqa: C901
failed = True
else:
if args.debug:
print("Lightroom data use: {}, Lightroom data ok: {}".format(use_lightroom, lightroom_data_ok))
print(f"Lightroom data use: {use_lightroom}, Lightroom data ok: {lightroom_data_ok}")
# check if the data_set differs from the original (LR db load)
# if yes write, else skip
if use_lightroom and lightroom_data_ok:
@@ -1345,30 +1390,37 @@ for xmp_file in work_files: # noqa: C901
print("[SKIP]")
count['skipped'] += 1
# close DB connection
if use_lightroom:
# close DB connection
if use_lightroom:
lrdb.close()
# end stats only if we write
print("{}".format('=' * 40))
print("XMP Files found : {:9,}".format(count['all']))
if args.read_only:
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("GeoLocation from Fuzzy Cache : {:9,}".format(count['fuzzy_cache']))
print("Failed reverse GeoLocate : {:9,}".format(count['failed']))
# end stats only if we write
print(f"{'=' * 40}")
print(f"XMP Files found : {count['all']:9,}")
if args.read_only:
print(f"XMP Files listed : {count['listed']:9,}")
if not args.read_only:
print(f"Updated : {count['changed']:9,}")
print(f"Skipped : {count['skipped']:9,}")
print(f"New GeoLocation from Map : {count['map']:9,}")
print(f"GeoLocation from Cache : {count['cache']:9,}")
print(f"GeoLocation from Fuzzy Cache : {count['fuzzy_cache']:9,}")
print(f"Failed reverse GeoLocate : {count['failed']:9,}")
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(f"GeoLocaction from Lightroom : {count['lightroom']:9,}")
print(f"No Lightroom data found : {count['not_found']:9,}")
print(f"More than one found in LR : {count['many_found']:9,}")
# if we have failed data
if len(failed_files) > 0:
print("{}".format('-' * 40))
print(f"{'-' * 40}")
print("Files that failed to update:")
print("{}".format(', '.join(failed_files)))
print(f"{', '.join(failed_files)}")
##############################################################
# MAIN RUN
##############################################################
main()
# __END__

8
requirement.txt Normal file
View File

@@ -0,0 +1,8 @@
certifi==2022.9.24
charset-normalizer==2.1.1
idna==3.4
install==1.3.5
python-xmp-toolkit==2.0.1
pytz==2022.6
requests==2.28.1
urllib3==1.26.12