photein 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3490aa4f980df5aed87bc73a127ab9908229edb18e5369377e75c459704f91f5
4
- data.tar.gz: 4fac58b8aaaf96eda55b07496e7b0fc99bb347863ffe3d50ad75429f7f9940f8
3
+ metadata.gz: 362d9c0535398b17f736549119b3079e52fdc7c22451014d8039b1ff7172914f
4
+ data.tar.gz: f2a87140f5c536f5bc36663bf7d40c42314199ec3a3247d88a2b619507a360c6
5
5
  SHA512:
6
- metadata.gz: 8a414155ee73eac20adf275cb57d593f433531b7e7c70390b5cfa887e16b1e2247f39aea0e31cecc1e2e31b8cd0e093e640c3cf3558ced57a3573893540a8e71
7
- data.tar.gz: c8187a8cfa3f54bf5e2e1e1c9ea8ba4ccd02242fee55b220f8528380c77f8f1c7769bac0e497bf129bbd5a895640124360f8b74ec06d7423236ff052dd2d067c
6
+ metadata.gz: 93814ee95d41d740892978276ed15863daa8832a4c6d4dcc62cbda22d114781e910d74fdbacbb508a78dce9cbbd1e14c5f9c131215b680c3c11865863dc11d18
7
+ data.tar.gz: 31384d711c87386c22b52439985ce4aec5d4d413ab8271e184f0b32cd4dad1f2628d0d88bf74f27e0a248beb304b45d0b33f20961e33b72e343cbe8affdcc7e7
data/README.md CHANGED
@@ -41,7 +41,39 @@ based on metadata timestamps, filename timestamps, or file creation times.
41
41
  > where and how your photos should be stored on disk
42
42
  > (looking at you, Apple Photos 👀), Photein is not for you.
43
43
 
44
- It can also optimize photos and videos for reduced file size.
44
+ It can also:
45
+
46
+ * optimize photos and videos for reduced file size
47
+ * shift all timestamps by a given interval
48
+ (for when you forget to update the clock on your camera
49
+ for, _e.g.,_ daylight savings or traveling across time zones)
50
+ * backfill GPS metadata / `OffsetTime*` tags based on a given IANA time zone
51
+
52
+ > 🤔 **Why would you want that?**
53
+ >
54
+ > One unfortunate quirk of this problem space is that
55
+ > the EXIF standard does not cover video file formats,
56
+ > meaning photos and videos do not have the same set of metadata fields.
57
+ > Worse yet, the field we care about most (timestamps) is inconsistently defined,
58
+ > with **photo timestamps recorded in local time and video timestamps in UTC**.
59
+ >
60
+ > (I haven’t been able to find the text of these specs—supposedly,
61
+ > they are expensive—but it’s been [stated on a few occasions
62
+ > by a pretty authoritative member of the Exiftool forum](https://exiftool.org/forum/index.php?msg=51915).
63
+ > [That same user has noted that](https://exiftool.org/forum/index.php?msg=59329)
64
+ > “there is no standard for embedding EXIF data in a video file.
65
+ > Not that that has stopped a lot of camera makers from forcing it into the file.”)
66
+ >
67
+ > Photein attempts to set _all_ filenames in local time,
68
+ > applying an offset to videos based on the time zone inferred
69
+ > from their GPS location tags—which is only possible if they are present.
70
+ > Other photo utilities (like [immich](https://immich.app)) follow the same strategy,
71
+ > meaning that setting GPS tags can improve filename consistency elsewhere, too.
72
+ >
73
+ > (What about `OffsetTime*`, then? Those tags are superfluous for our needs
74
+ > since they can only be set on photos, which are already in local time—but
75
+ > implementation was straightforward, and they serve the parallel purpose
76
+ > of backfilling missing time zone info, so I figured what the heck. ¯\\\_(ツ)\_/¯)
45
77
 
46
78
  What _doesn’t_ it do?
47
79
  ---------------------
@@ -0,0 +1,599 @@
1
+ {
2
+ "Africa/Abidjan": [5.3252258,-4.019603],
3
+ "Africa/Accra": [5.559284600000001,-0.1974306],
4
+ "Africa/Addis_Ababa": [9.019193599999999,38.7524635],
5
+ "Africa/Algiers": [36.753768,3.0587561],
6
+ "Africa/Asmara": [15.3317304,38.9300394],
7
+ "Africa/Asmera": [15.3317304,38.9300394],
8
+ "Africa/Bamako": [12.6391699,-8.0025475],
9
+ "Africa/Bangui": [4.362156,18.5827765],
10
+ "Africa/Banjul": [13.454375,-16.5753186],
11
+ "Africa/Bissau": [11.8632196,-15.5843227],
12
+ "Africa/Blantyre": [-15.7666707,35.0167866],
13
+ "Africa/Brazzaville": [-4.2744405,15.2812803],
14
+ "Africa/Bujumbura": [-3.361378,29.3598782],
15
+ "Africa/Cairo": [30.0444196,31.2357116],
16
+ "Africa/Casablanca": [33.5731104,-7.589843399999999],
17
+ "Africa/Ceuta": [35.8893874,-5.3213455],
18
+ "Africa/Conakry": [9.5090945,-13.7119312],
19
+ "Africa/Dakar": [14.716677,-17.4676861],
20
+ "Africa/Dar_es_Salaam": [-6.8158925,39.2819353],
21
+ "Africa/Djibouti": [11.825138,42.590275],
22
+ "Africa/Douala": [4.0510564,9.767868699999999],
23
+ "Africa/El_Aaiun": [27.1500384,-13.1990758],
24
+ "Africa/Freetown": [8.487080299999999,-13.2354918],
25
+ "Africa/Gaborone": [-24.6580003,25.9077037],
26
+ "Africa/Harare": [-17.8262928,31.0503723],
27
+ "Africa/Johannesburg": [-26.205647,28.0337185],
28
+ "Africa/Juba": [4.853856599999999,31.58252539999999],
29
+ "Africa/Kampala": [0.3151692,32.5816313],
30
+ "Africa/Khartoum": [15.597431,32.535587],
31
+ "Africa/Kigali": [-1.9440727,30.0618851],
32
+ "Africa/Kinshasa": [-4.3032527,15.310528],
33
+ "Africa/Lagos": [6.5243793,3.3792057],
34
+ "Africa/Libreville": [0.4077972,9.440283299999999],
35
+ "Africa/Lome": [6.129557699999999,1.2196502],
36
+ "Africa/Luanda": [-8.8146556,13.2301756],
37
+ "Africa/Lubumbashi": [-11.6876026,27.5026174],
38
+ "Africa/Lusaka": [-15.4154677,28.2773267],
39
+ "Africa/Malabo": [3.7549606,8.7821344],
40
+ "Africa/Maputo": [-25.969248,32.5731746],
41
+ "Africa/Maseru": [-29.3150767,27.4869229],
42
+ "Africa/Mbabane": [-26.3263561,31.1441558],
43
+ "Africa/Mogadishu": [2.037148,45.3379067],
44
+ "Africa/Monrovia": [6.3156068,-10.8073698],
45
+ "Africa/Nairobi": [-1.2920659,36.8219462],
46
+ "Africa/Ndjamena": [12.1191352,15.0502875],
47
+ "Africa/Niamey": [13.5115963,2.1253854],
48
+ "Africa/Nouakchott": [18.0735299,-15.9582372],
49
+ "Africa/Ouagadougou": [12.3714277,-1.5196603],
50
+ "Africa/Porto-Novo": [6.478623799999999,2.6203212],
51
+ "Africa/Sao_Tome": [0.18636,6.613080999999999],
52
+ "Africa/Timbuktu": [16.7665887,-3.0025615],
53
+ "Africa/Tripoli": [32.8877109,13.187186],
54
+ "Africa/Tunis": [36.7994568,10.1831972],
55
+ "Africa/Windhoek": [-22.5649344,17.0842147],
56
+ "America/Adak": [51.8667982,-176.6385354],
57
+ "America/Anchorage": [61.2175758,-149.8996785],
58
+ "America/Anguilla": [18.220554,-63.06861499999999],
59
+ "America/Antigua": [17.060816,-61.796428],
60
+ "America/Araguaina": [-7.191563299999999,-48.2094801],
61
+ "America/Argentina/Buenos_Aires": [-34.6036844,-58.3815591],
62
+ "America/Argentina/Catamarca": [-28.469581,-65.7795441],
63
+ "America/Argentina/ComodRivadavia": [-45.7901229,-67.46797939999999],
64
+ "America/Argentina/Cordoba": [-31.42008329999999,-64.1887761],
65
+ "America/Argentina/Jujuy": [-22.6633212,-66.2367172],
66
+ "America/Argentina/La_Rioja": [-29.9001725,-66.99880109999999],
67
+ "America/Argentina/Mendoza": [-32.8894587,-68.84583859999999],
68
+ "America/Argentina/Rio_Gallegos": [-51.6230485,-69.2168291],
69
+ "America/Argentina/Salta": [-25.2529539,-64.7162415],
70
+ "America/Argentina/San_Juan": [-31.5351074,-68.5385941],
71
+ "America/Argentina/San_Luis": [-33.8769018,-66.2367172],
72
+ "America/Argentina/Tucuman": [-26.8082848,-65.2175903],
73
+ "America/Argentina/Ushuaia": [-54.8019121,-68.3029511],
74
+ "America/Aruba": [12.52111,-69.968338],
75
+ "America/Asuncion": [-25.2637399,-57.57592599999999],
76
+ "America/Atikokan": [48.7575098,-91.62182919999999],
77
+ "America/Atka": [52.2099851,-174.2030272],
78
+ "America/Bahia": [-11.4098737,-41.2808577],
79
+ "America/Bahia_Banderas": [20.8074834,-105.2480864],
80
+ "America/Barbados": [13.193887,-59.543198],
81
+ "America/Belem": [-1.4563432,-48.501299],
82
+ "America/Belize": [17.189877,-88.49764999999999],
83
+ "America/Blanc-Sablon": [51.4264451,-57.1313148],
84
+ "America/Boa_Vista": [-2.7670314,-65.2945869],
85
+ "America/Bogota": [4.710988599999999,-74.072092],
86
+ "America/Boise": [43.6150186,-116.2023137],
87
+ "America/Buenos_Aires": [-35.4891431,-62.97563270000001],
88
+ "America/Cambridge_Bay": [69.1168641,-105.0596814],
89
+ "America/Campo_Grande": [-20.4648517,-54.6218477],
90
+ "America/Cancun": [21.161908,-86.85152789999999],
91
+ "America/Caracas": [10.4805937,-66.90360629999999],
92
+ "America/Catamarca": [-28.469581,-65.7795441],
93
+ "America/Cayenne": [4.937977399999999,-52.3354386],
94
+ "America/Cayman": [19.3133,-81.2546],
95
+ "America/Chicago": [41.8781136,-87.6297982],
96
+ "America/Chihuahua": [28.6433753,-106.0587908],
97
+ "America/Ciudad_Juarez": [31.6903638,-106.4245478],
98
+ "America/Coral_Harbour": [64.138834,-83.16989699999999],
99
+ "America/Cordoba": [-31.42008329999999,-64.1887761],
100
+ "America/Costa_Rica": [9.748916999999999,-83.753428],
101
+ "America/Creston": [49.0955401,-116.5135079],
102
+ "America/Cuiaba": [-15.5954635,-56.0925824],
103
+ "America/Curacao": [12.16957,-68.99002],
104
+ "America/Danmarkshavn": [76.7721545,-18.6787352],
105
+ "America/Dawson": [64.0639058,-139.4332999],
106
+ "America/Dawson_Creek": [55.75962740000001,-120.2376623],
107
+ "America/Denver": [39.7392358,-104.990251],
108
+ "America/Detroit": [42.331427,-83.0457538],
109
+ "America/Dominica": [15.414999,-61.37097600000001],
110
+ "America/Edmonton": [53.5460983,-113.4937266],
111
+ "America/Eirunepe": [-6.6623703,-69.8674302],
112
+ "America/El_Salvador": [13.794185,-88.89653],
113
+ "America/Ensenada": [31.8667427,-116.5963713],
114
+ "America/Fort_Nelson": [58.8050174,-122.697236],
115
+ "America/Fort_Wayne": [41.079273,-85.1393513],
116
+ "America/Fortaleza": [-3.7327203,-38.5270134],
117
+ "America/Glace_Bay": [46.1969191,-59.9570044],
118
+ "America/Godthab": [64.17432339999999,-51.7372787],
119
+ "America/Goose_Bay": [53.3086181,-60.37623929999999],
120
+ "America/Grand_Turk": [21.4674584,-71.13891009999999],
121
+ "America/Grenada": [12.1165,-61.67899999999999],
122
+ "America/Guadeloupe": [16.265,-61.55099999999999],
123
+ "America/Guatemala": [15.783471,-90.23075899999999],
124
+ "America/Guayaquil": [-2.1891341,-79.8899031],
125
+ "America/Guyana": [4.860416,-58.93018],
126
+ "America/Halifax": [44.6508608,-63.5923256],
127
+ "America/Havana": [23.133865,-82.3585654],
128
+ "America/Hermosillo": [29.0729673,-110.9559192],
129
+ "America/Indiana/Indianapolis": [39.76909,-86.158018],
130
+ "America/Indiana/Knox": [41.2958751,-86.6250139],
131
+ "America/Indiana/Marengo": [38.36922759999999,-86.34359169999999],
132
+ "America/Indiana/Petersburg": [38.4919935,-87.2786244],
133
+ "America/Indiana/Tell_City": [37.9514447,-86.76776629999999],
134
+ "America/Indiana/Vevay": [38.7478401,-85.0671725],
135
+ "America/Indiana/Vincennes": [38.677269,-87.5286325],
136
+ "America/Indiana/Winamac": [41.0514299,-86.6030648],
137
+ "America/Indianapolis": [39.76909,-86.158018],
138
+ "America/Inuvik": [68.3593674,-133.7294462],
139
+ "America/Iqaluit": [63.74669300000001,-68.5169669],
140
+ "America/Jamaica": [18.109581,-77.29750799999999],
141
+ "America/Jujuy": [-24.1857864,-65.2994767],
142
+ "America/Juneau": [58.30049329999999,-134.4201306],
143
+ "America/Kentucky/Louisville": [38.2468618,-85.76637239999999],
144
+ "America/Kentucky/Monticello": [36.8297937,-84.8491126],
145
+ "America/Knox_IN": [41.2958751,-86.6250139],
146
+ "America/Kralendijk": [12.1498095,-68.27631289999999],
147
+ "America/La_Paz": [-16.489689,-68.11929359999999],
148
+ "America/Lima": [-12.0466888,-77.04308859999999],
149
+ "America/Los_Angeles": [34.0549076,-118.242643],
150
+ "America/Louisville": [38.2468618,-85.76637239999999],
151
+ "America/Lower_Princes": [38.7945952,-106.5348379],
152
+ "America/Maceio": [-9.666041699999999,-35.7352167],
153
+ "America/Managua": [12.1149926,-86.2361744],
154
+ "America/Manaus": [-3.1190275,-60.0217314],
155
+ "America/Marigot": [38.7945952,-106.5348379],
156
+ "America/Martinique": [14.641528,-61.024174],
157
+ "America/Matamoros": [25.8690294,-97.50273759999999],
158
+ "America/Mazatlan": [23.2494148,-106.4111425],
159
+ "America/Mendoza": [-32.8894587,-68.84583859999999],
160
+ "America/Menominee": [45.10776269999999,-87.6142737],
161
+ "America/Merida": [20.9673702,-89.5925857],
162
+ "America/Metlakatla": [55.12878600000001,-131.5744824],
163
+ "America/Mexico_City": [19.4326077,-99.133208],
164
+ "America/Miquelon": [46.8852,-56.3159],
165
+ "America/Moncton": [46.0878165,-64.7782313],
166
+ "America/Monterrey": [25.6866142,-100.3161126],
167
+ "America/Montevideo": [-34.9055016,-56.1851147],
168
+ "America/Montreal": [45.5018869,-73.56739189999999],
169
+ "America/Montserrat": [16.742498,-62.187366],
170
+ "America/Nassau": [25.0443312,-77.3503609],
171
+ "America/New_York": [40.7127753,-74.0059728],
172
+ "America/Nipigon": [49.0125155,-88.26328050000001],
173
+ "America/Nome": [64.50059089999999,-165.4086406],
174
+ "America/Noronha": [-3.844797615405311,-32.42689168991757],
175
+ "America/North_Dakota/Beulah": [47.2633403,-101.7779462],
176
+ "America/North_Dakota/Center": [47.1163849,-101.299594],
177
+ "America/North_Dakota/New_Salem": [46.844999,-101.4112545],
178
+ "America/Nuuk": [64.17432339999999,-51.7372787],
179
+ "America/Ojinaga": [29.5458844,-104.4082908],
180
+ "America/Panama": [8.537981,-80.782127],
181
+ "America/Pangnirtung": [66.1465578,-65.7012182],
182
+ "America/Paramaribo": [5.8520355,-55.2038278],
183
+ "America/Phoenix": [33.4483771,-112.0740373],
184
+ "America/Port-au-Prince": [18.594395,-72.3074326],
185
+ "America/Port_of_Spain": [10.6603435,-61.5086355],
186
+ "America/Porto_Acre": [-9.5936386,-67.5411079],
187
+ "America/Porto_Velho": [-8.7635475,-63.89717229999999],
188
+ "America/Puerto_Rico": [18.220833,-66.590149],
189
+ "America/Punta_Arenas": [-53.1633845,-70.9078263],
190
+ "America/Rainy_River": [48.68071439696585,-94.02755887688653],
191
+ "America/Rankin_Inlet": [62.808375,-92.0852853],
192
+ "America/Recife": [-8.0578381,-34.8828969],
193
+ "America/Regina": [50.4452112,-104.6188944],
194
+ "America/Resolute": [38.7945952,-106.5348379],
195
+ "America/Rio_Branco": [-9.9740249,-67.8098191],
196
+ "America/Rosario": [-32.9587022,-60.69304159999999],
197
+ "America/Santa_Isabel": [38.7945952,-106.5348379],
198
+ "America/Santarem": [39.2366618,-8.686011900000002],
199
+ "America/Santiago": [-33.4488897,-70.66926549999999],
200
+ "America/Santo_Domingo": [18.4626178,-69.93609239999999],
201
+ "America/Sao_Paulo": [-23.5557714,-46.6395571],
202
+ "America/Scoresbysund": [70.5,-25],
203
+ "America/Shiprock": [36.7855543,-108.6870322],
204
+ "America/Sitka": [57.05321929999999,-135.3345513],
205
+ "America/St_Barthelemy": [17.9,-62.833333],
206
+ "America/St_Johns": [47.5556097,-52.7452511],
207
+ "America/St_Kitts": [17.357822,-62.782998],
208
+ "America/St_Lucia": [13.909444,-60.978893],
209
+ "America/St_Thomas": [18.3380965,-64.8940946],
210
+ "America/St_Vincent": [12.984305,-61.287228],
211
+ "America/Swift_Current": [50.285069,-107.7971722],
212
+ "America/Tegucigalpa": [14.065049,-87.1715002],
213
+ "America/Thule": [76.5308268,-68.7071466],
214
+ "America/Thunder_Bay": [48.3808951,-89.24768229999999],
215
+ "America/Tijuana": [32.5149469,-117.0382471],
216
+ "America/Toronto": [43.653226,-79.3831843],
217
+ "America/Tortola": [18.4334704,-64.63327819999999],
218
+ "America/Vancouver": [49.2827291,-123.1207375],
219
+ "America/Virgin": [18.335765,-64.89633499999999],
220
+ "America/Whitehorse": [60.7197137,-135.0522761],
221
+ "America/Winnipeg": [49.8954221,-97.1385145],
222
+ "America/Yakutat": [59.54533929999999,-139.7268441],
223
+ "America/Yellowknife": [62.4539717,-114.3717886],
224
+ "Antarctica/Casey": [-82.86275189999999,135],
225
+ "Antarctica/Davis": [-82.86275189999999,135],
226
+ "Antarctica/DumontDUrville": [-82.86275189999999,135],
227
+ "Antarctica/Macquarie": [-82.86275189999999,135],
228
+ "Antarctica/Mawson": [-82.86275189999999,135],
229
+ "Antarctica/McMurdo": [-77.8455159,166.6697692],
230
+ "Antarctica/Palmer": [-82.86275189999999,135],
231
+ "Antarctica/Rothera": [-67.5682461,-68.1241591],
232
+ "Antarctica/South_Pole": [-90.0000,45.0000],
233
+ "Antarctica/Syowa": [-82.86275189999999,135],
234
+ "Antarctica/Troll": [-72.01138899999999,2.535],
235
+ "Antarctica/Vostok": [-82.86275189999999,135],
236
+ "Arctic/Longyearbyen": [78.2253231,15.6256365],
237
+ "Asia/Aden": [15.552727,48.516388],
238
+ "Asia/Almaty": [43.2379761,76.8828618],
239
+ "Asia/Amman": [31.9543786,35.9105776],
240
+ "Asia/Anadyr": [64.73366129999999,177.4968266],
241
+ "Asia/Aqtau": [43.67186299999999,51.2120512],
242
+ "Asia/Aqtobe": [50.2839339,57.166978],
243
+ "Asia/Ashgabat": [37.9600766,58.32606289999999],
244
+ "Asia/Ashkhabad": [37.9600766,58.32606289999999],
245
+ "Asia/Atyrau": [47.0944959,51.9238373],
246
+ "Asia/Baghdad": [33.315241,44.3660671],
247
+ "Asia/Bahrain": [26.0667,50.5577],
248
+ "Asia/Baku": [40.40926169999999,49.8670924],
249
+ "Asia/Bangkok": [13.7563309,100.5017651],
250
+ "Asia/Barnaul": [53.3497499,83.78357369999999],
251
+ "Asia/Beirut": [33.8937913,35.5017767],
252
+ "Asia/Bishkek": [42.8746212,74.5697617],
253
+ "Asia/Brunei": [4.535277,114.727669],
254
+ "Asia/Calcutta": [22.5743545,88.3628734],
255
+ "Asia/Chita": [34.047863,100.6196553],
256
+ "Asia/Choibalsan": [48.0951271,114.5356247],
257
+ "Asia/Chongqing": [29.5656843,106.5511838],
258
+ "Asia/Chungking": [29.5656843,106.5511838],
259
+ "Asia/Colombo": [6.9270786,79.861243],
260
+ "Asia/Dacca": [23.804093,90.4152376],
261
+ "Asia/Damascus": [33.5132192,36.2768193],
262
+ "Asia/Dhaka": [23.804093,90.4152376],
263
+ "Asia/Dili": [-8.5563336,125.5798233],
264
+ "Asia/Dubai": [25.2048493,55.2707828],
265
+ "Asia/Dushanbe": [38.5597722,68.7870384],
266
+ "Asia/Famagusta": [35.1208296,33.9382218],
267
+ "Asia/Gaza": [31.5016951,34.4668445],
268
+ "Asia/Harbin": [45.7567307,126.6424173],
269
+ "Asia/Hebron": [31.532569,35.09982600000001],
270
+ "Asia/Ho_Chi_Minh": [10.8230989,106.6296638],
271
+ "Asia/Hong_Kong": [22.3193039,114.1693611],
272
+ "Asia/Hovd": [47.9795218,91.634756],
273
+ "Asia/Irkutsk": [52.2854834,104.2890222],
274
+ "Asia/Istanbul": [41.0082376,28.9783589],
275
+ "Asia/Jakarta": [-6.1944491,106.8229198],
276
+ "Asia/Jayapura": [-2.5916025,140.6689995],
277
+ "Asia/Jerusalem": [31.768319,35.21371],
278
+ "Asia/Kabul": [34.5553494,69.207486],
279
+ "Asia/Kamchatka": [56.1327377,159.5314398],
280
+ "Asia/Karachi": [24.8607343,67.00113639999999],
281
+ "Asia/Kashgar": [39.4677199,75.99381],
282
+ "Asia/Kathmandu": [27.7103145,85.32216339999999],
283
+ "Asia/Katmandu": [27.7103145,85.32216339999999],
284
+ "Asia/Khandyga": [62.65640789999999,135.5539801],
285
+ "Asia/Kolkata": [22.5743545,88.3628734],
286
+ "Asia/Krasnoyarsk": [56.01528339999999,92.8932476],
287
+ "Asia/Kuala_Lumpur": [3.1499222,101.6944619],
288
+ "Asia/Kuching": [1.5533785,110.359503],
289
+ "Asia/Kuwait": [29.31166,47.481766],
290
+ "Asia/Macao": [22.198745,113.543873],
291
+ "Asia/Macau": [22.198745,113.543873],
292
+ "Asia/Magadan": [59.55943970000001,150.8127537],
293
+ "Asia/Makassar": [-5.1615828,119.4359281],
294
+ "Asia/Manila": [14.5995124,120.9842195],
295
+ "Asia/Muscat": [23.5880307,58.3828717],
296
+ "Asia/Nicosia": [35.1855659,33.38227639999999],
297
+ "Asia/Novokuznetsk": [53.7595935,87.12157049999999],
298
+ "Asia/Novosibirsk": [54.9832693,82.89638309999999],
299
+ "Asia/Omsk": [54.9913545,73.3645204],
300
+ "Asia/Oral": [51.227821,51.3865431],
301
+ "Asia/Phnom_Penh": [11.5563738,104.9282099],
302
+ "Asia/Pontianak": [-0.0263303,109.3425039],
303
+ "Asia/Pyongyang": [39.0392193,125.7625241],
304
+ "Asia/Qatar": [25.354826,51.183884],
305
+ "Asia/Qostanay": [53.2198089,63.6354232],
306
+ "Asia/Qyzylorda": [44.84883139999999,65.4822686],
307
+ "Asia/Rangoon": [16.840939,96.173526],
308
+ "Asia/Riyadh": [24.7135517,46.6752957],
309
+ "Asia/Saigon": [10.8230989,106.6296638],
310
+ "Asia/Sakhalin": [50.6909848,142.9505689],
311
+ "Asia/Samarkand": [39.6585987,66.9756694],
312
+ "Asia/Seoul": [37.550263,126.9970831],
313
+ "Asia/Shanghai": [31.230416,121.473701],
314
+ "Asia/Singapore": [1.352083,103.819836],
315
+ "Asia/Srednekolymsk": [67.43730699999999,153.728674],
316
+ "Asia/Taipei": [25.0329694,121.5654177],
317
+ "Asia/Tashkent": [41.2994958,69.2400734],
318
+ "Asia/Tbilisi": [41.6938026,44.80151679999999],
319
+ "Asia/Tehran": [35.7218583,51.3346954],
320
+ "Asia/Tel_Aviv": [32.0852999,34.78176759999999],
321
+ "Asia/Thimbu": [27.471586,89.6386108],
322
+ "Asia/Thimphu": [27.471586,89.6386108],
323
+ "Asia/Tokyo": [35.6764225,139.650027],
324
+ "Asia/Tomsk": [56.4884295,84.94804689999999],
325
+ "Asia/Ujung_Pandang": [-5.1615828,119.4359281],
326
+ "Asia/Ulaanbaatar": [47.9220509,106.9155007],
327
+ "Asia/Ulan_Bator": [47.9220509,106.9155007],
328
+ "Asia/Urumqi": [43.8266299,87.61687999999999],
329
+ "Asia/Ust-Nera": [64.5671689,143.2265266],
330
+ "Asia/Vientiane": [17.9757058,102.6331035],
331
+ "Asia/Vladivostok": [43.1332484,131.9112975],
332
+ "Asia/Yakutsk": [62.03969120000001,129.7422193],
333
+ "Asia/Yangon": [16.840939,96.173526],
334
+ "Asia/Yekaterinburg": [56.8430993,60.64540859999999],
335
+ "Asia/Yerevan": [40.1872023,44.515209],
336
+ "Atlantic/Azores": [37.7412488,-25.6755944],
337
+ "Atlantic/Bermuda": [32.3078,-64.7505],
338
+ "Atlantic/Canary": [28.2915637,-16.6291304],
339
+ "Atlantic/Cape_Verde": [16.5388,-23.0418],
340
+ "Atlantic/Faeroe": [61.89263500000001,-6.911805999999999],
341
+ "Atlantic/Faroe": [61.89263500000001,-6.911805999999999],
342
+ "Atlantic/Jan_Mayen": [71.031818,-8.2920347],
343
+ "Atlantic/Madeira": [32.6371816,-16.9289441],
344
+ "Atlantic/Reykjavik": [64.14698679999999,-21.9407552],
345
+ "Atlantic/South_Georgia": [-54.4138333,-36.5827165],
346
+ "Atlantic/St_Helena": [-15.9650104,-5.7089241],
347
+ "Atlantic/Stanley": [-51.6927517,-57.86196],
348
+ "Australia/ACT": [-35.4734679,149.0123679],
349
+ "Australia/Adelaide": [-34.9284989,138.6007456],
350
+ "Australia/Brisbane": [-27.4704528,153.0260341],
351
+ "Australia/Broken_Hill": [-31.9595859,141.4608138],
352
+ "Australia/Canberra": [-35.2801846,149.1310324],
353
+ "Australia/Currie": [-39.9301004,143.8537837],
354
+ "Australia/Darwin": [-12.4637333,130.8444446],
355
+ "Australia/Eucla": [-31.6750185,128.8835431],
356
+ "Australia/Hobart": [-42.8826055,147.3257196],
357
+ "Australia/LHI": [-31.5212683,159.0675466],
358
+ "Australia/Lindeman": [-25.274398,133.775136],
359
+ "Australia/Lord_Howe": [-31.5553263,159.0821211],
360
+ "Australia/Melbourne": [-37.8136276,144.9630576],
361
+ "Australia/NSW": [-31.2532183,146.921099],
362
+ "Australia/North": [-25.274398,133.775136],
363
+ "Australia/Perth": [-31.9513993,115.8616783],
364
+ "Australia/Queensland": [-22.575197,144.0847926],
365
+ "Australia/South": [-30.0002315,136.2091547],
366
+ "Australia/Sydney": [-33.8688197,151.2092955],
367
+ "Australia/Tasmania": [-42.0409059,146.8087322],
368
+ "Australia/Victoria": [-36.9847807,143.3906074],
369
+ "Australia/West": [-27.6728168,121.6283098],
370
+ "Australia/Yancowinna": [-25.274398,133.775136],
371
+ "Brazil/Acre": [-9.0237964,-70.81199529999999],
372
+ "Brazil/DeNoronha": [-3.844797615405311,-32.42689168991757],
373
+ "Brazil/East": [-14.235004,-51.92528],
374
+ "Brazil/West": [-14.235004,-51.92528],
375
+ "CET": [50.84770289999999,4.357200100000001],
376
+ "CST6CDT": [41.8781136,-87.6297982],
377
+ "Canada/Atlantic": [43.37822,-64.9405185],
378
+ "Canada/Central": [56.130366,-106.346771],
379
+ "Canada/Eastern": [56.130366,-106.346771],
380
+ "Canada/Mountain": [56.130366,-106.346771],
381
+ "Canada/Newfoundland": [54.2730918,-61.7387586],
382
+ "Canada/Pacific": [56.130366,-106.346771],
383
+ "Canada/Saskatchewan": [52.9399159,-106.4508639],
384
+ "Canada/Yukon": [64.2823274,-135],
385
+ "Chile/Continental": [-35.675147,-71.542969],
386
+ "Chile/EasterIsland": [-27.112723,-109.3496865],
387
+ "Cuba": [21.521757,-77.781167],
388
+ "EET": [37.9838096,23.7275388],
389
+ "EST": [8.537981,-80.782127],
390
+ "EST5EDT": [40.7127753,-74.0059728],
391
+ "Egypt": [26.820553,30.802498],
392
+ "Eire": [53.41291,-8.24389],
393
+ "Etc/GMT": null,
394
+ "Etc/GMT+0": null,
395
+ "Etc/GMT+1": null,
396
+ "Etc/GMT+10": null,
397
+ "Etc/GMT+11": null,
398
+ "Etc/GMT+12": null,
399
+ "Etc/GMT+2": null,
400
+ "Etc/GMT+3": null,
401
+ "Etc/GMT+4": null,
402
+ "Etc/GMT+5": null,
403
+ "Etc/GMT+6": null,
404
+ "Etc/GMT+7": null,
405
+ "Etc/GMT+8": null,
406
+ "Etc/GMT+9": null,
407
+ "Etc/GMT-0": null,
408
+ "Etc/GMT-1": null,
409
+ "Etc/GMT-10": null,
410
+ "Etc/GMT-11": null,
411
+ "Etc/GMT-12": null,
412
+ "Etc/GMT-13": null,
413
+ "Etc/GMT-14": null,
414
+ "Etc/GMT-2": null,
415
+ "Etc/GMT-3": null,
416
+ "Etc/GMT-4": null,
417
+ "Etc/GMT-5": null,
418
+ "Etc/GMT-6": null,
419
+ "Etc/GMT-7": null,
420
+ "Etc/GMT-8": null,
421
+ "Etc/GMT-9": null,
422
+ "Etc/GMT0": null,
423
+ "Etc/Greenwich": null,
424
+ "Etc/UCT": null,
425
+ "Etc/UTC": null,
426
+ "Etc/Universal": null,
427
+ "Etc/Zulu": null,
428
+ "Europe/Amsterdam": [52.3675734,4.9041389],
429
+ "Europe/Andorra": [42.506285,1.521801],
430
+ "Europe/Astrakhan": [46.3586008,48.0569482],
431
+ "Europe/Athens": [37.9838096,23.7275388],
432
+ "Europe/Belfast": [54.59728500000001,-5.93012],
433
+ "Europe/Belgrade": [44.8125449,20.46123],
434
+ "Europe/Berlin": [52.52000659999999,13.404954],
435
+ "Europe/Bratislava": [48.1485965,17.1077477],
436
+ "Europe/Brussels": [50.84770289999999,4.357200100000001],
437
+ "Europe/Bucharest": [44.4267674,26.1025384],
438
+ "Europe/Budapest": [47.497912,19.040235],
439
+ "Europe/Busingen": [47.6969939,8.6904196],
440
+ "Europe/Chisinau": [47.0104529,28.8638103],
441
+ "Europe/Copenhagen": [55.6760968,12.5683371],
442
+ "Europe/Dublin": [53.3498053,-6.2603097],
443
+ "Europe/Gibraltar": [36.140751,-5.353585],
444
+ "Europe/Guernsey": [49.4481982,-2.58949],
445
+ "Europe/Helsinki": [60.16985569999999,24.938379],
446
+ "Europe/Isle_of_Man": [54.236107,-4.548056],
447
+ "Europe/Istanbul": [41.0082376,28.9783589],
448
+ "Europe/Jersey": [49.2137711,-2.1357662],
449
+ "Europe/Kaliningrad": [54.7104264,20.4522144],
450
+ "Europe/Kiev": [50.4503596,30.5245025],
451
+ "Europe/Kirov": [58.6030012,49.6679437],
452
+ "Europe/Kyiv": [50.4503596,30.5245025],
453
+ "Europe/Lisbon": [38.7222524,-9.1393366],
454
+ "Europe/Ljubljana": [46.0569465,14.5057515],
455
+ "Europe/London": [51.5072178,-0.1275862],
456
+ "Europe/Luxembourg": [49.815273,6.129582999999999],
457
+ "Europe/Madrid": [40.41672790000001,-3.7032905],
458
+ "Europe/Malta": [35.937496,14.375416],
459
+ "Europe/Mariehamn": [60.0970945,19.9348339],
460
+ "Europe/Minsk": [53.9006011,27.558972],
461
+ "Europe/Monaco": [43.73841760000001,7.424615799999999],
462
+ "Europe/Moscow": [55.755826,37.6173],
463
+ "Europe/Nicosia": [35.1855659,33.38227639999999],
464
+ "Europe/Oslo": [59.9138688,10.7522454],
465
+ "Europe/Paris": [48.8766796,2.3189725],
466
+ "Europe/Podgorica": [42.4304196,19.2593642],
467
+ "Europe/Prague": [50.0755381,14.4378005],
468
+ "Europe/Riga": [56.9676941,24.1056221],
469
+ "Europe/Rome": [41.8967068,12.4822025],
470
+ "Europe/Samara": [53.203772,50.1606382],
471
+ "Europe/San_Marino": [43.94236,12.457777],
472
+ "Europe/Sarajevo": [43.8562586,18.4130763],
473
+ "Europe/Saratov": [51.5461754,46.0154123],
474
+ "Europe/Simferopol": [44.952117,34.102417],
475
+ "Europe/Skopje": [41.9981294,21.4254355],
476
+ "Europe/Sofia": [42.6977082,23.3218675],
477
+ "Europe/Stockholm": [59.3327036,18.0656255],
478
+ "Europe/Tallinn": [59.43696079999999,24.7535746],
479
+ "Europe/Tirane": [41.3275459,19.8186982],
480
+ "Europe/Tiraspol": [46.84818500000001,29.596805],
481
+ "Europe/Ulyanovsk": [54.3186575,48.397776],
482
+ "Europe/Uzhgorod": [48.6208,22.287883],
483
+ "Europe/Vaduz": [47.1410843,9.5210732],
484
+ "Europe/Vatican": [41.902916,12.453389],
485
+ "Europe/Vienna": [48.20806959999999,16.3713095],
486
+ "Europe/Vilnius": [54.6871555,25.2796514],
487
+ "Europe/Volgograd": [48.708048,44.5133034],
488
+ "Europe/Warsaw": [52.2296756,21.0122287],
489
+ "Europe/Zagreb": [45.8150108,15.981919],
490
+ "Europe/Zaporozhye": [47.83928419999999,35.13870970000001],
491
+ "Europe/Zurich": [47.3768866,8.541694],
492
+ "Factory": null,
493
+ "GB": [55.378051,-3.435973],
494
+ "GB-Eire": [53.41291,-8.24389],
495
+ "GMT": null,
496
+ "GMT+0": null,
497
+ "GMT-0": null,
498
+ "GMT0": null,
499
+ "Greenwich": [51.4933675,0.009821399999999999],
500
+ "HST": [19.8986819,-155.6658568],
501
+ "Hongkong": [22.3193039,114.1693611],
502
+ "Iceland": [64.96305099999999,-19.020835],
503
+ "Indian/Antananarivo": [-18.9184607,47.5211293],
504
+ "Indian/Chagos": [-6.1217629,72.01135874999999],
505
+ "Indian/Christmas": [20.593684,78.96288],
506
+ "Indian/Cocos": [-12.164165,96.87095599999999],
507
+ "Indian/Comoro": [-11.6455,43.3333],
508
+ "Indian/Kerguelen": [-49.3948275,69.35446259999999],
509
+ "Indian/Mahe": [11.7001957,75.5343014],
510
+ "Indian/Maldives": [3.202778,73.22068],
511
+ "Indian/Mauritius": [-20.348404,57.55215200000001],
512
+ "Indian/Mayotte": [-12.8275,45.166244],
513
+ "Indian/Reunion": [-21.115141,55.536384],
514
+ "Iran": [32.427908,53.688046],
515
+ "Israel": [31.046051,34.851612],
516
+ "Jamaica": [18.109581,-77.29750799999999],
517
+ "Japan": [36.204824,138.252924],
518
+ "Kwajalein": [9.189823499999999,167.4242972],
519
+ "Libya": [26.3351,17.228331],
520
+ "MET": [40.7794366,-73.963244],
521
+ "MST": [33.4483771,-112.0740373],
522
+ "MST7MDT": [39.7392358,-104.990251],
523
+ "Mexico/BajaNorte": [30.8406338,-115.2837585],
524
+ "Mexico/BajaSur": [26.0444446,-111.6660725],
525
+ "Mexico/General": [23.634501,-102.552784],
526
+ "NZ": [-36.85088270000001,174.7644881],
527
+ "NZ-CHAT": [-43.9271098,-176.4592091],
528
+ "Navajo": [36.0672173,-109.1880047],
529
+ "PRC": [31.230416,121.473701],
530
+ "PST8PDT": [34.0549076,-118.242643],
531
+ "Pacific/Apia": [-13.831609,-171.7689086],
532
+ "Pacific/Auckland": [-36.85088270000001,174.7644881],
533
+ "Pacific/Bougainville": [-6.375391899999999,155.3807101],
534
+ "Pacific/Chatham": [-43.9271098,-176.4592091],
535
+ "Pacific/Chuuk": [7.134462467372186,151.5074133217651],
536
+ "Pacific/Easter": [-27.112723,-109.3496865],
537
+ "Pacific/Efate": [-17.6577479,168.4297187],
538
+ "Pacific/Enderbury": [-3.1272412,-171.0837442],
539
+ "Pacific/Fakaofo": [-9.380255499999999,-171.2188355],
540
+ "Pacific/Fiji": [-17.713371,178.065032],
541
+ "Pacific/Funafuti": [-8.5211471,179.1961926],
542
+ "Pacific/Galapagos": [-0.3831059167914707,-90.42333442620028],
543
+ "Pacific/Gambier": [40.3759296,-82.397183],
544
+ "Pacific/Guadalcanal": [-9.577328399999999,160.1455805],
545
+ "Pacific/Guam": [13.3823791,144.6972774],
546
+ "Pacific/Honolulu": [21.3098845,-157.8581401],
547
+ "Pacific/Johnston": [41.6729856,-93.6977354],
548
+ "Pacific/Kanton": [-2.8039843,-171.6432353],
549
+ "Pacific/Kiritimati": [1.8721347,-157.4278119],
550
+ "Pacific/Kosrae": [5.3095618,162.9814877],
551
+ "Pacific/Kwajalein": [9.189823499999999,167.4242972],
552
+ "Pacific/Majuro": [7.066667,171.266667],
553
+ "Pacific/Marquesas": [-9.781216200000001,-139.0817124],
554
+ "Pacific/Midway": [28.2072168,-177.3734926],
555
+ "Pacific/Nauru": [-0.522778,166.931503],
556
+ "Pacific/Niue": [-19.054445,-169.867233],
557
+ "Pacific/Norfolk": [36.8507689,-76.28587259999999],
558
+ "Pacific/Noumea": [-22.2716866,166.4398627],
559
+ "Pacific/Pago_Pago": [-14.2732232,-170.7030309],
560
+ "Pacific/Palau": [7.514979999999999,134.58252],
561
+ "Pacific/Pitcairn": [-24.3767537,-128.3242376],
562
+ "Pacific/Pohnpei": [6.951991599999999,158.2793165],
563
+ "Pacific/Ponape": [6.951991599999999,158.2793165],
564
+ "Pacific/Port_Moresby": [-9.4790043,147.1494164],
565
+ "Pacific/Rarotonga": [-21.2292371,-159.7763491],
566
+ "Pacific/Saipan": [15.1850483,145.7467259],
567
+ "Pacific/Samoa": [-13.759029,-172.104629],
568
+ "Pacific/Tahiti": [-17.6509195,-149.4260421],
569
+ "Pacific/Tarawa": [1.4518171,172.9716617],
570
+ "Pacific/Tongatapu": [-21.1465968,-175.2515482],
571
+ "Pacific/Truk": [7.134462467372186,151.5074133217651],
572
+ "Pacific/Wake": [19.279619,166.6499348],
573
+ "Pacific/Wallis": [-14.2938,-178.1165],
574
+ "Pacific/Yap": [9.5556503,138.1399232],
575
+ "Poland": [51.919438,19.145136],
576
+ "Portugal": [39.39987199999999,-8.224454],
577
+ "ROC": [25.0329694,121.5654177],
578
+ "ROK": [37.550263,126.9970831],
579
+ "Singapore": [1.352083,103.819836],
580
+ "Turkey": [38.963745,35.243322],
581
+ "UCT": [-33.957652,18.4611991],
582
+ "US/Alaska": [63.588753,-154.4930619],
583
+ "US/Aleutian": [54.83193995,-165.89470295],
584
+ "US/Arizona": [34.0489281,-111.0937311],
585
+ "US/Central": [38.7945952,-106.5348379],
586
+ "US/East-Indiana": [40.5512165,-85.60236429999999],
587
+ "US/Eastern": [40.7127753,-74.0059728],
588
+ "US/Hawaii": [19.8986819,-155.6658568],
589
+ "US/Indiana-Starke": [41.2715616,-86.6207943],
590
+ "US/Michigan": [44.3148443,-85.60236429999999],
591
+ "US/Mountain": [38.7945952,-106.5348379],
592
+ "US/Pacific": [38.7945952,-106.5348379],
593
+ "Pacific/Samoa": [-13.759029,-172.104629],
594
+ "UTC": null,
595
+ "Universal": null,
596
+ "W-SU": null,
597
+ "WET": null,
598
+ "Zulu": null
599
+ }
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'singleton'
4
5
  require 'optparse'
5
6
 
7
+ require 'tzinfo'
8
+
6
9
  module Photein
7
10
  class Config
8
11
  include Singleton
@@ -17,7 +20,9 @@ module Photein
17
20
  ['-k', '--keep', 'do not delete source files'],
18
21
  ['-i', '--interactive', 'ask whether to import each file found'],
19
22
  ['-n', '--dry-run', 'perform a "no-op" trial run'],
20
- [ '--safe', 'skip files in use by other processes'],
23
+ [ '--shift-timestamp=N', 'adjust metadata timestamps by N hours'],
24
+ [ '--local-tz=TIMEZONE', "backfill missing GPS* metadata on videos\n (to name files in local time instead of UTC)"],
25
+ [ '--safe', 'skip files in use by other processes']
21
26
  ].freeze
22
27
 
23
28
  OPTION_NAMES = OPTIONS
@@ -26,6 +31,13 @@ module Photein
26
31
  .map { |option| option[/\w[a-z\-]+/] }
27
32
  .map(&:to_sym)
28
33
 
34
+ SECONDS_PER_HOUR = 60 * 60
35
+
36
+ TZ_GEOCOORDS = File.expand_path('../../data/tz_coords.json', File.dirname(__FILE__))
37
+ .then(&File.method(:read))
38
+ .then(&JSON.method(:parse))
39
+ .freeze
40
+
29
41
  @params = {}
30
42
 
31
43
  class << self
@@ -44,6 +56,19 @@ module Photein
44
56
  end.tap { |p| p.parse!(into: @params) }
45
57
 
46
58
  @params[:verbose] ||= @params[:'dry-run']
59
+
60
+ raise "invalid --shift-timestamp option (must be integer)" if @params.key?(:'shift-timestamp') && !@params[:'shift-timestamp'].match?(/^-?\d+$/)
61
+
62
+ if @params.key?(:'local-tz')
63
+ if !TZInfo::Timezone.all_identifiers.include?(@params[:'local-tz'])
64
+ raise 'invalid --local-tz option (must be from IANA tz database)'
65
+ end
66
+
67
+ if tz_coordinates.nil?
68
+ raise 'invalid --local-tz option (must reference a location)'
69
+ end
70
+ end
71
+
47
72
  @params.freeze
48
73
 
49
74
  raise "no source directory given" if !@params.key?(:source)
@@ -79,6 +104,22 @@ module Photein
79
104
  web: @params[:'library-web']
80
105
  }.compact.transform_values(&Pathname.method(:new))
81
106
  end
107
+
108
+ def timestamp_delta
109
+ @timestamp_delta ||= @params[:'shift-timestamp'].to_i * SECONDS_PER_HOUR
110
+ end
111
+
112
+ def local_tz
113
+ return @local_tz if defined? @local_tz
114
+
115
+ @local_tz = @params.key?(:'local-tz') ? TZInfo::Timezone.get(@params[:'local-tz']) : nil
116
+ end
117
+
118
+ def tz_coordinates
119
+ return @tz_coordinates if defined? @tz_coordinates
120
+
121
+ @tz_coordinates = @params.key?(:'local-tz') ? TZ_GEOCOORDS[@params[:'local-tz']] : nil
122
+ end
82
123
  end
83
124
  end
84
125
  end
data/lib/photein/image.rb CHANGED
@@ -63,7 +63,7 @@ module Photein
63
63
  raise
64
64
  end
65
65
 
66
- def metadata_stamp
66
+ def timestamp_from_metadata
67
67
  MiniExiftool.new(path.to_s).date_time_original
68
68
  rescue MiniExiftool::Error => e
69
69
  Photein.logger.error(<<~MSG) if e.message.match?(/exiftool: not found/)
@@ -74,7 +74,7 @@ module Photein
74
74
 
75
75
  # NOTE: This may be largely unnecessary:
76
76
  # metadata timestamps are generally present in all cases except WhatsApp
77
- def filename_stamp
77
+ def timestamp_from_filename
78
78
  path.basename(path.extname).to_s.then do |filename|
79
79
  case filename
80
80
  when /^IMG_\d{8}_\d{6}(_\d{3})?$/ # Android DCIM: datetime + optional counter
@@ -100,5 +100,25 @@ module Photein
100
100
 
101
101
  return false
102
102
  end
103
+
104
+ def update_exif_tags(path)
105
+ return if Photein::Config.timestamp_delta.zero? && Photein::Config.local_tz.nil?
106
+
107
+ file = MiniExiftool.new(path)
108
+ file.all_dates = new_timestamp.strftime('%Y:%m:%d %H:%M:%S') if Photein::Config.timestamp_delta != 0
109
+
110
+ if !Photein::Config.local_tz.nil?
111
+ new_timestamp.to_s # "2020-02-14 22:55:30 -0800"
112
+ .split.tap(&:pop).join(' ').then { |time| time + ' UTC' } # "2020-02-14 22:55:30 UTC"
113
+ .then(&Time.method(:parse)) # 2020-02-14 22:55:30 UTC
114
+ .then(&Photein::Config.local_tz.method(:to_local)) # 2020-02-14 22:55:30 +0800
115
+ .strftime('%z').insert(3, ':') # "+08:00"
116
+ .tap { |offset| file.offset_time = offset }
117
+ .tap { |offset| file.offset_time_digitized = offset }
118
+ .tap { |offset| file.offset_time_original = offset }
119
+ end
120
+
121
+ file.save!
122
+ end
103
123
  end
104
124
  end
@@ -7,7 +7,7 @@ require 'time'
7
7
 
8
8
  module Photein
9
9
  class MediaFile
10
- DATE_FORMAT = '%F_%H%M%S'.freeze
10
+ DATE_FORMAT = '%F_%H%M%S'
11
11
 
12
12
  NORMAL_EXTNAME_MAP = {
13
13
  '.jpeg' => '.jpg'
@@ -28,16 +28,15 @@ module Photein
28
28
  next if non_optimizable_format?(lib_type)
29
29
 
30
30
  Thread.new do
31
- dest_basename = timestamp.strftime(DATE_FORMAT)
32
31
  dest_extname = self.class::OPTIMIZATION_FORMAT_MAP.dig(lib_type, extname) || extname
33
32
  dest_path = lib_path
34
- .join(timestamp.strftime('%Y'))
35
- .join("#{dest_basename}#{dest_extname}")
36
- .then(&method(:resolve_name_collision))
33
+ .join(Time.parse(dest_filename).strftime('%Y'))
34
+ .join("#{dest_filename}#{dest_extname}")
35
+ .then(&method(:resolve_name_collision))
37
36
  tempfile = Pathname(Dir.tmpdir)
38
- .join('photein').join(lib_type.to_s)
39
- .tap(&FileUtils.method(:mkdir_p))
40
- .join(dest_path.basename)
37
+ .join('photein').join(lib_type.to_s)
38
+ .tap(&FileUtils.method(:mkdir_p))
39
+ .join(dest_path.basename)
41
40
 
42
41
  optimize(tempfile: tempfile, lib_type: lib_type)
43
42
 
@@ -53,6 +52,8 @@ module Photein
53
52
  FileUtils.cp(path, dest_path, noop: Photein::Config.dry_run)
54
53
  FileUtils.chmod('-x', dest_path, noop: Photein::Config.dry_run)
55
54
  end
55
+
56
+ update_exif_tags(dest_path.realdirpath.to_s) if !Photein::Config.dry_run
56
57
  end
57
58
  end.compact.map(&:join).then do |threads|
58
59
  # e.g.: with --library-web only, .dngs are skipped, so DON'T DELETE!
@@ -63,7 +64,7 @@ module Photein
63
64
  private
64
65
 
65
66
  def corrupted?(result = false)
66
- return result.tap do |r|
67
+ result.tap do |r|
67
68
  Photein.logger.error("#{path.basename}: cannot import corrupted file") if r
68
69
  end
69
70
  end
@@ -76,31 +77,43 @@ module Photein
76
77
  def in_use?
77
78
  out, _err, status = Open3.capture3("lsof '#{path}'")
78
79
 
79
- if status.success? # Do open files ALWAYS return exit status 0? (I think so.)
80
- cmd, pid = out.lines[1]&.split&.first(2)
81
- Photein.logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
82
- return true
83
- else
84
- return false
85
- end
80
+ return false unless status.success? # Do open files ALWAYS return exit status 0? (I think so.)
81
+
82
+ cmd, pid = out.lines[1]&.split&.first(2)
83
+ Photein.logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
84
+ true
86
85
  end
87
86
 
88
- def non_optimizable_format?(lib_type = :master) # may be overridden by subclasses
89
- return false
87
+ def non_optimizable_format?(_lib_type = :master) # may be overridden by subclasses
88
+ false
90
89
  end
91
90
 
92
- def timestamp
93
- @timestamp ||= (metadata_stamp || filename_stamp)
91
+ def new_timestamp
92
+ @new_timestamp ||= (
93
+ timestamp_from_metadata ||
94
+ timestamp_from_filename ||
95
+ timestamp_from_filesystem
96
+ ) + Photein::Config.timestamp_delta
94
97
  end
95
98
 
96
- def filename_stamp
99
+ def timestamp_from_metadata
100
+ raise NotImplementedError
101
+ end
102
+
103
+ def timestamp_from_filename
97
104
  Time.parse(path.basename(path.extname).to_s)
98
105
  rescue ArgumentError
99
- begin
100
- File.birthtime(path)
101
- rescue NotImplementedError
102
- File.mtime(path)
103
- end
106
+ nil
107
+ end
108
+
109
+ def timestamp_from_filesystem
110
+ File.birthtime(path)
111
+ rescue NotImplementedError
112
+ File.mtime(path)
113
+ end
114
+
115
+ def dest_filename
116
+ @dest_filename ||= new_timestamp.strftime(DATE_FORMAT)
104
117
  end
105
118
 
106
119
  def extname
@@ -114,25 +127,29 @@ module Photein
114
127
 
115
128
  case collisions.length
116
129
  when 0
117
- return pathname
130
+ pathname
118
131
  when 1
119
- return pathname.sub_ext("+1#{pathname.extname}")
132
+ pathname.sub_ext("+1#{pathname.extname}")
120
133
  else # TODO: what to do for heterogeneous suffixes?
121
134
  collisions.tap { |c| c.delete(pathname.to_s) }.max
122
- .slice(/(?<=^#{pathname.to_s.delete_suffix(pathname.extname)}).*(?=#{pathname.extname}$)/)
123
- .tap { |counter| raise 'Unresolved timestamp conflict' unless counter&.match?(/^\+[1-8]$/) }
124
- .then { |counter| pathname.sub_ext("#{counter.next}#{pathname.extname}") }
135
+ .slice(/(?<=^#{pathname.to_s.delete_suffix(pathname.extname)}).*(?=#{pathname.extname}$)/)
136
+ .tap { |counter| raise 'Unresolved timestamp conflict' unless counter&.match?(/^\+[1-8]$/) }
137
+ .then { |counter| pathname.sub_ext("#{counter.next}#{pathname.extname}") }
125
138
  end
126
139
  end
127
140
 
141
+ def update_exif_tags(path)
142
+ raise NotImplementedError
143
+ end
144
+
128
145
  class << self
129
146
  def for(file)
130
147
  file = Pathname(file)
131
148
  raise Errno::ENOENT, "#{file}" unless file.exist?
132
149
 
133
150
  [Image, Video].find { |type| type::SUPPORTED_FORMATS.include?(file.extname.downcase) }
134
- .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
135
- .then { |type| type.new(file) }
151
+ .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
152
+ .then { |type| type.new(file) }
136
153
  end
137
154
  end
138
155
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.1.6'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/photein/video.rb CHANGED
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'json'
4
5
  require 'time'
5
6
 
6
7
  require 'photein/media_file'
8
+ require 'active_support/core_ext/string/zones'
9
+ require 'active_support/core_ext/time/zones'
10
+ require 'active_support/values/time_zone'
7
11
  require 'mediainfo'
12
+ require 'mini_exiftool'
8
13
  require 'streamio-ffmpeg'
14
+ require 'wheretz'
9
15
  require_relative '../../vendor/terminal-size/lib/terminal-size'
10
16
 
11
17
  module Photein
@@ -65,9 +71,10 @@ module Photein
65
71
  raise
66
72
  end
67
73
 
68
- def metadata_stamp
69
- # video timestamps are typically UTC
70
- MediaInfo.from(path.to_s).general.encoded_date&.getlocal
74
+ # Video timestamps are typically UTC, and must be adjusted to local TZ.
75
+ # Look for GPS tags first, then default to system local TZ.
76
+ def timestamp_from_metadata
77
+ MediaInfo.from(path.to_s).general.encoded_date
71
78
  rescue MediaInfo::EnvironmentError
72
79
  Photein.logger.error('mediainfo is required to read timestamp metadata')
73
80
  raise
@@ -75,23 +82,55 @@ module Photein
75
82
 
76
83
  # NOTE: This may be largely unnecessary:
77
84
  # metadata timestamps are generally present in all cases except WhatsApp
78
- def filename_stamp
85
+ def timestamp_from_filename
79
86
  path.basename(path.extname).to_s.then do |filename|
80
87
  case filename
81
88
  when /^LINE_MOVIE_\d{13}$/ # LINE: UNIX time in milliseconds (at download)
82
- Time.strptime(filename[0..-4], 'LINE_MOVIE_%s')
89
+ local_tz.strptime(filename[0..-4], 'LINE_MOVIE_%s')
83
90
  when /^VID-\d{8}-WA\d{4}$/ # WhatsApp: date + counter (at receipt)
84
- Time.strptime(filename, 'VID-%Y%m%d-WA%M%S')
91
+ local_tz.strptime(filename, 'VID-%Y%m%d-WA%M%S')
85
92
  when /^VID_\d{8}_\d{6}_\d{3}$/ # Telegram: datetime in milliseconds (at download)
86
- Time.strptime(filename, 'VID_%Y%m%d_%H%M%S_%L')
93
+ local_tz.strptime(filename, 'VID_%Y%m%d_%H%M%S_%L')
87
94
  when /^signal-\d{4}-\d{2}-\d{2}-\d{6}( \(\d+\))?$/ # Signal: datetime + optional counter (at receipt)
88
- Time.strptime(filename[0, 24], 'signal-%F-%H%M%S')
95
+ local_tz.strptime(filename[0, 24], 'signal-%F-%H%M%S')
89
96
  else
90
- super
97
+ super&.asctime&.in_time_zone(local_tz)
98
+ end
99
+ end&.utc
100
+ end
101
+
102
+ def timestamp_from_filesystem
103
+ super.asctime.in_time_zone(local_tz).utc
104
+ end
105
+
106
+ def dest_filename
107
+ @dest_filename ||= local_tz.tzinfo.to_local(new_timestamp).strftime(DATE_FORMAT)
108
+ end
109
+
110
+ def local_tz
111
+ @local_tz ||= ActiveSupport::TimeZone[
112
+ MiniExiftool.new(path).then(&method(:gps_coords))&.then(&method(:coords_to_tz)) ||
113
+ Photein::Config.local_tz ||
114
+ Time.now.gmt_offset
115
+ ]
116
+ end
117
+
118
+ def gps_coords(exif)
119
+ return nil if exif.gps_latitude.nil? || exif.gps_longitude.nil?
120
+
121
+ [exif.gps_latitude, exif.gps_longitude].map do |str|
122
+ # `str' follows the format %(xx deg xx' xx.xx" x)
123
+ str.split(/[^\d.NESW]+/).then do |deg, min, sec, dir|
124
+ (deg.to_i + (min.to_f / 60) + (sec.to_f / 3600)) * (%(N E).include?(dir) ? 1 : -1)
91
125
  end
92
126
  end
93
127
  end
94
128
 
129
+ def coords_to_tz(coords)
130
+ # WhereTZ::get() can return a single element OR an array -_-'
131
+ Array(WhereTZ.get(coords[0], coords[1])).first
132
+ end
133
+
95
134
  def display_progress_bar(progress)
96
135
  return unless $stdout.tty?
97
136
 
@@ -103,5 +142,19 @@ module Photein
103
142
  progress_bar = "[#{'#' * progress_len}#{'-' * bg_len}]#{percentage}"
104
143
  print "#{progress_bar}\r"
105
144
  end
145
+
146
+ def update_exif_tags(path)
147
+ return if Photein::Config.timestamp_delta.zero? && Photein::Config.local_tz.nil?
148
+
149
+ args = []
150
+ args.push("-AllDates=#{new_timestamp.strftime('%Y:%m:%d\\ %H:%M:%S')}") if Photein::Config.timestamp_delta != 0
151
+
152
+ if (lat, lon = Photein::Config.tz_coordinates)
153
+ args.push("-xmp:GPSLatitude=#{lat}")
154
+ args.push("-xmp:GPSLongitude=#{lon}")
155
+ end
156
+
157
+ system("exiftool -overwrite_original #{args.join(' ')} #{path}", out: File::NULL, err: File::NULL)
158
+ end
106
159
  end
107
160
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: photein
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Lue
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-16 00:00:00.000000000 Z
10
+ date: 2024-12-31 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: logger
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +163,34 @@ dependencies:
150
163
  - - "~>"
151
164
  - !ruby/object:Gem::Version
152
165
  version: '3.0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: tzinfo
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '2.0'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '2.0'
180
+ - !ruby/object:Gem::Dependency
181
+ name: wheretz
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '0.0'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '0.0'
153
194
  - !ruby/object:Gem::Dependency
154
195
  name: pry-remote
155
196
  requirement: !ruby/object:Gem::Requirement
@@ -187,6 +228,7 @@ extra_rdoc_files: []
187
228
  files:
188
229
  - README.md
189
230
  - bin/photein
231
+ - data/tz_coords.json
190
232
  - lib/photein.rb
191
233
  - lib/photein/config.rb
192
234
  - lib/photein/image.rb
@@ -200,7 +242,6 @@ licenses:
200
242
  - MIT
201
243
  metadata:
202
244
  source_code_uri: https://github.com/rlue/photein
203
- post_install_message:
204
245
  rdoc_options: []
205
246
  require_paths:
206
247
  - lib
@@ -215,8 +256,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
256
  - !ruby/object:Gem::Version
216
257
  version: '0'
217
258
  requirements: []
218
- rubygems_version: 3.5.16
219
- signing_key:
259
+ rubygems_version: 3.6.2
220
260
  specification_version: 4
221
261
  summary: Import/rename photos & videos from one directory to another.
222
262
  test_files: []