@@ -7,6 +7,8 @@ def main():
77 from datetime import datetime as _datetime
88
99 import piexif as _piexif
10+ from fractions import Fraction # piexif requires some values to be stored as rationals
11+ import math
1012
1113 parser = _argparse .ArgumentParser (
1214 prog = 'Photos takeout helper' ,
@@ -85,12 +87,11 @@ def main():
8587
8688 _os .makedirs (FIXED_DIR , exist_ok = True )
8789
88-
8990 def for_all_files_recursive (
90- dir ,
91- file_function = lambda fo , fi : True ,
92- folder_function = lambda fo : True ,
93- filter_fun = lambda file : True
91+ dir ,
92+ file_function = lambda fo , fi : True ,
93+ folder_function = lambda fo : True ,
94+ filter_fun = lambda file : True
9495 ):
9596 for file in _os .listdir (dir ):
9697 file = dir + '/' + file
@@ -104,21 +105,18 @@ def for_all_files_recursive(
104105 print ('Found something weird...' )
105106 print (file )
106107
107-
108108 def is_photo (file ):
109109 what = _os .path .splitext (file .lower ())[1 ]
110110 if what not in photo_formats :
111111 return False
112112 return True
113113
114-
115114 def is_video (file ):
116115 what = _os .path .splitext (file .lower ())[1 ]
117116 if what not in video_formats :
118117 return False
119118 return True
120119
121-
122120 # PART 1: removing duplicates
123121
124122 # THIS IS PARTLY COPIED FROM STACKOVERFLOW
@@ -165,15 +163,13 @@ def find_duplicates(path, filter_fun=lambda file: True):
165163
166164 return duplicates
167165
168-
169166 # Removes all duplicates in folder
170167 def remove_duplicates (dir ):
171168 duplicates = find_duplicates (dir , lambda f : (is_photo (f ) or is_video (f )))
172169 for file in duplicates :
173170 _os .remove (file )
174171 return True
175172
176-
177173 # PART 2: Fixing metadata and date-related stuff
178174
179175 # Returns json dict
@@ -189,7 +185,6 @@ def find_json_for_file(dir, file):
189185 else :
190186 raise FileNotFoundError ('Couldnt find json for file: ' + file )
191187
192-
193188 # Returns date in 2019:01:01 23:59:59 format
194189 def get_date_from_folder_name (dir ):
195190 dir = _os .path .basename (_os .path .normpath (dir ))
@@ -225,7 +220,6 @@ def set_creation_date_from_str(file, str_datetime):
225220 print (e )
226221 _os .utime (file , (timestamp , timestamp ))
227222
228-
229223 def set_creation_date_from_exif (file ):
230224 exif_dict = _piexif .load (file )
231225 tags = [['0th' , TAG_DATE_TIME ], ['Exif' , TAG_DATE_TIME_ORIGINAL ], ['Exif' , TAG_DATE_TIME_DIGITIZED ]]
@@ -240,7 +234,6 @@ def set_creation_date_from_exif(file):
240234 raise IOError ('No DateTime in given exif' )
241235 set_creation_date_from_str (file , datetime_str )
242236
243-
244237 def set_file_exif_date (file , creation_date ):
245238 try :
246239 exif_dict = _piexif .load (file )
@@ -255,15 +248,111 @@ def set_file_exif_date(file, creation_date):
255248 try :
256249 _piexif .insert (_piexif .dump (exif_dict ), file )
257250 except Exception as e :
258- print ('Couldnt insert exif!' )
251+ print ("Couldn't insert exif!" )
259252 print (e )
260253
261-
262254 def get_date_str_from_json (json ):
263255 return _datetime .fromtimestamp (
264256 int (json ['photoTakenTime' ]['timestamp' ])
265257 ).strftime ('%Y:%m:%d %H:%M:%S' )
266258
259+ def change_to_rational (number ):
260+ """convert a number to rantional
261+ Keyword arguments: number
262+ return: tuple like (1, 2), (numerator, denominator)
263+ """
264+ f = Fraction (str (number ))
265+ return f .numerator , f .denominator
266+
267+ # got this here https://github.com/hMatoba/piexifjs/issues/1#issuecomment-260176317
268+ def degToDmsRational (degFloat ):
269+ min_float = degFloat % 1 * 60
270+ sec_float = min_float % 1 * 60
271+ deg = math .floor (degFloat )
272+ deg_min = math .floor (min_float )
273+ sec = round (sec_float * 100 )
274+
275+ return [(deg , 1 ), (deg_min , 1 ), (sec , 100 )]
276+
277+ def set_file_geo_data (file , json ):
278+ """
279+ Reads the geoData from google and saves it to the EXIF. This works assuming that the geodata looks like -100.12093, 50.213143. Something like that.
280+
281+ Written by DalenW.
282+ :param file:
283+ :param json:
284+ :return:
285+ """
286+
287+ # prevents crashes
288+ try :
289+ exif_dict = _piexif .load (file )
290+ except (_piexif .InvalidImageDataError , ValueError ):
291+ exif_dict = {'0th' : {}, 'Exif' : {}}
292+
293+ # fetches geo data from the photos editor first.
294+ longitude = float (json ['geoData' ]['longitude' ])
295+ latitude = float (json ['geoData' ]['latitude' ])
296+ altitude = float (json ['geoData' ]['altitude' ])
297+
298+ # fallbacks to GeoData Exif if it wasn't set in the photos editor.
299+ # https://github.com/TheLastGimbus/GooglePhotosTakeoutHelper/pull/5#discussion_r531792314
300+ longitude = float (json ['geoData' ]['longitude' ])
301+ latitude = float (json ['geoData' ]['latitude' ])
302+ altitude = json ['geoData' ]['altitude' ]
303+ # Prioritise geoData set from GPhotos editor
304+ if longitude == 0 and latitude == 0 :
305+ longitude = float (json ['geoDataExif' ]['longitude' ])
306+ latitude = float (json ['geoDataExif' ]['latitude' ])
307+ altitude = json ['geoDataExif' ]['altitude' ]
308+
309+ # latitude >= 0: North latitude -> "N"
310+ # latitude < 0: South latitude -> "S"
311+ # longitude >= 0: East longitude -> "E"
312+ # longitude < 0: West longitude -> "W"
313+
314+ if longitude >= 0 :
315+ longitude_ref = 'E'
316+ else :
317+ longitude_ref = 'W'
318+ longitude = longitude * - 1
319+
320+ if latitude >= 0 :
321+ latitude_ref = 'N'
322+ else :
323+ latitude_ref = 'S'
324+ latitude = latitude * - 1
325+
326+ # referenced from https://gist.github.com/c060604/8a51f8999be12fc2be498e9ca56adc72
327+ gps_ifd = {
328+ _piexif .GPSIFD .GPSVersionID : (2 , 0 , 0 , 0 )
329+ }
330+
331+ # skips it if it's empty
332+ if latitude != 0 or longitude != 0 :
333+ gps_ifd .update ({
334+ _piexif .GPSIFD .GPSLatitudeRef : latitude_ref ,
335+ _piexif .GPSIFD .GPSLatitude : degToDmsRational (latitude ),
336+
337+ _piexif .GPSIFD .GPSLongitudeRef : longitude_ref ,
338+ _piexif .GPSIFD .GPSLongitude : degToDmsRational (longitude )
339+ })
340+
341+ if altitude != 0 :
342+ gps_ifd .update ({
343+ _piexif .GPSIFD .GPSAltitudeRef : 1 ,
344+ _piexif .GPSIFD .GPSAltitude : change_to_rational (round (altitude ))
345+ })
346+
347+ gps_exif = {"GPS" : gps_ifd }
348+ exif_dict .update (gps_exif )
349+
350+ try :
351+ _piexif .insert (_piexif .dump (exif_dict ), file )
352+ except Exception as e :
353+ print ("Couldn't insert geo exif!" )
354+ # local variable 'new_value' referenced before assignment means that one of the GPS values is incorrect
355+ print (e )
267356
268357 # Fixes ALL metadata, takes just file and dir and figures it out
269358 def fix_metadata (dir , file ):
@@ -282,12 +371,13 @@ def fix_metadata(dir, file):
282371 try :
283372 google_json = find_json_for_file (dir , file )
284373 date = get_date_str_from_json (google_json )
374+ set_file_geo_data (file , google_json )
285375 set_file_exif_date (file , date )
286376 set_creation_date_from_str (file , date )
287377 has_nice_date = True
288378 return
289379 except FileNotFoundError :
290- print ('Couldnt find json for file :/' )
380+ print ("Couldn't find json for file :/" )
291381
292382 if has_nice_date :
293383 return
@@ -298,7 +388,6 @@ def fix_metadata(dir, file):
298388 set_creation_date_from_str (file , date )
299389 return True
300390
301-
302391 # PART 3: Copy all photos and videos to target folder
303392
304393 # Makes a new name like 'photo(1).jpg'
@@ -316,15 +405,13 @@ def new_name_if_exists(file_name, watch_for_duplicates=True):
316405 new_name = split [0 ] + '(' + str (i ) + ')' + split [1 ]
317406 i += 1
318407
319-
320408 def copy_to_target (dir , file ):
321409 if is_photo (file ) or is_video (file ):
322410 new_file = new_name_if_exists (FIXED_DIR + '/' + _os .path .basename (file ),
323411 watch_for_duplicates = not args .keep_duplicates )
324412 _shutil .copy2 (file , new_file )
325413 return True
326414
327-
328415 def copy_to_target_and_divide (dir , file ):
329416 creation_date = _os .path .getmtime (file )
330417 date = _datetime .fromtimestamp (creation_date )
@@ -337,7 +424,6 @@ def copy_to_target_and_divide(dir, file):
337424 _shutil .copy2 (file , new_file )
338425 return True
339426
340-
341427 if not args .keep_duplicates :
342428 print ('=====================' )
343429 print ('Removing duplicates...' )
@@ -383,5 +469,6 @@ def copy_to_target_and_divide(dir, file):
383469 print ('Sooo... what now? You can see README.md for what nice G Photos alternatives I found and recommend' )
384470 print ('Have a nice day!' )
385471
472+
386473if __name__ == '__main__' :
387474 main ()
0 commit comments