photein 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a6e9370cd73bb024486ffb10e0a2aa17dce4262d7602e6b978e4019aeb23f55
4
- data.tar.gz: e523c18ea04dfa84afa666b9a928a0ea761e5aca98b5dbd8a634f4fe5edd72ae
3
+ metadata.gz: 362d9c0535398b17f736549119b3079e52fdc7c22451014d8039b1ff7172914f
4
+ data.tar.gz: f2a87140f5c536f5bc36663bf7d40c42314199ec3a3247d88a2b619507a360c6
5
5
  SHA512:
6
- metadata.gz: 702e7a3f7c626ec9d57e2c41bd2ba04076b769c4aca27ae0be29d2807a738307cca34ed315eca62356a7991ac68e847994e096b0671c81587eef66d200623e16
7
- data.tar.gz: b19a618fc185ceb0aa52399fe60c87d13437bcc2edc919738f002a2266fd9f4fd203d32aeabb4ebd475d291805c8f77ddd040cf9c1fd36524c2848b8cc420c60
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
@@ -85,8 +85,8 @@ module Photein
85
85
  Time.strptime(filename, 'IMG-%Y%m%d-WA%M%S')
86
86
  when /^IMG_\d{8}_\d{6}_\d{3}$/ # Telegram: datetime in milliseconds (at download)
87
87
  Time.strptime(filename, 'IMG_%Y%m%d_%H%M%S_%L')
88
- when /^signal-\d{4}(-\d{2}){5}[-\d]*$/ # Signal: datetime + optional counter (at receipt)
89
- Time.strptime(filename[0, 26], 'signal-%F-%H-%M-%S')
88
+ when /^signal-\d{4}-\d{2}-\d{2}-\d{6}( \(\d+\))?$/ # Signal: datetime + optional counter (at receipt)
89
+ Time.strptime(filename[0, 24], 'signal-%F-%H%M%S')
90
90
  when /^\d{13}$/ # LINE: UNIX time in milliseconds (at download)
91
91
  Time.strptime(filename[0..-4], '%s')
92
92
  else
@@ -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,16 +52,19 @@ 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
56
- end
57
- end.compact.each(&:join)
58
55
 
59
- FileUtils.rm(path, noop: Photein::Config.dry_run || Photein::Config.keep)
56
+ update_exif_tags(dest_path.realdirpath.to_s) if !Photein::Config.dry_run
57
+ end
58
+ end.compact.map(&:join).then do |threads|
59
+ # e.g.: with --library-web only, .dngs are skipped, so DON'T DELETE!
60
+ FileUtils.rm(path, noop: threads.empty? || Photein::Config.dry_run || Photein::Config.keep)
61
+ end
60
62
  end
61
63
 
62
64
  private
63
65
 
64
66
  def corrupted?(result = false)
65
- return result.tap do |r|
67
+ result.tap do |r|
66
68
  Photein.logger.error("#{path.basename}: cannot import corrupted file") if r
67
69
  end
68
70
  end
@@ -75,56 +77,69 @@ module Photein
75
77
  def in_use?
76
78
  out, _err, status = Open3.capture3("lsof '#{path}'")
77
79
 
78
- if status.success? # Do open files ALWAYS return exit status 0? (I think so.)
79
- cmd, pid = out.lines[1]&.split&.first(2)
80
- Photein.logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
81
- return true
82
- else
83
- return false
84
- 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
85
+ end
86
+
87
+ def non_optimizable_format?(_lib_type = :master) # may be overridden by subclasses
88
+ false
85
89
  end
86
90
 
87
- def non_optimizable_format?(lib_type = :master) # may be overridden by subclasses
88
- return false
91
+ def new_timestamp
92
+ @new_timestamp ||= (
93
+ timestamp_from_metadata ||
94
+ timestamp_from_filename ||
95
+ timestamp_from_filesystem
96
+ ) + Photein::Config.timestamp_delta
89
97
  end
90
98
 
91
- def timestamp
92
- @timestamp ||= (metadata_stamp || filename_stamp)
99
+ def timestamp_from_metadata
100
+ raise NotImplementedError
93
101
  end
94
102
 
95
- def filename_stamp
103
+ def timestamp_from_filename
96
104
  Time.parse(path.basename(path.extname).to_s)
97
105
  rescue ArgumentError
98
- begin
99
- File.birthtime(path)
100
- rescue NotImplementedError
101
- File.mtime(path)
102
- end
106
+ nil
103
107
  end
104
108
 
105
- def extname
106
- @extname ||= NORMAL_EXTNAME_MAP[path.extname.downcase] || path.extname.downcase
109
+ def timestamp_from_filesystem
110
+ File.birthtime(path)
111
+ rescue NotImplementedError
112
+ File.mtime(path)
107
113
  end
108
114
 
109
- def resolve_name_collision(filename)
110
- raise ArgumentError, 'Invalid filename' if filename.to_s.include?('*')
115
+ def dest_filename
116
+ @dest_filename ||= new_timestamp.strftime(DATE_FORMAT)
117
+ end
111
118
 
112
- collision_glob = Pathname(filename).sub_ext("*#{filename.extname}")
119
+ def extname
120
+ @extname ||= NORMAL_EXTNAME_MAP[path.extname.downcase] || path.extname.downcase
121
+ end
113
122
 
114
- case Dir[collision_glob].length
115
- when 0 # if no files found, no biggie
116
- when 1 # if one file found, WITH OR WITHOUT COUNTER, reset counter to a
117
- if Dir[collision_glob].first != collision_glob.sub('*', 'a') # don't try if it's already a lone, correctly-countered file
118
- Photein.logger.info('conflicting timestamp found; adding counter to existing file')
119
- FileUtils.mv(Dir[collision_glob].first, collision_glob.sub('*', 'a'), noop: Photein::Config.dry_run)
120
- end
121
- else # TODO: if multiple files found, rectify them?
123
+ def resolve_name_collision(pathname)
124
+ raise ArgumentError, 'Invalid filename' if pathname.to_s.include?('*')
125
+
126
+ collisions = Dir[pathname.sub_ext("*#{pathname.extname}")]
127
+
128
+ case collisions.length
129
+ when 0
130
+ pathname
131
+ when 1
132
+ pathname.sub_ext("+1#{pathname.extname}")
133
+ else # TODO: what to do for heterogeneous suffixes?
134
+ collisions.tap { |c| c.delete(pathname.to_s) }.max
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}") }
122
138
  end
139
+ end
123
140
 
124
- # return the next usable filename
125
- Dir[collision_glob].max&.slice(/.(?=#{Regexp.escape(collision_glob.extname)})/)&.next
126
- .tap { |counter| raise 'Unresolved timestamp conflict' unless [*Array('a'..'z'), nil].include?(counter) }
127
- .then { |counter| filename.sub_ext("#{counter}#{filename.extname}") }
141
+ def update_exif_tags(path)
142
+ raise NotImplementedError
128
143
  end
129
144
 
130
145
  class << self
@@ -133,8 +148,8 @@ module Photein
133
148
  raise Errno::ENOENT, "#{file}" unless file.exist?
134
149
 
135
150
  [Image, Video].find { |type| type::SUPPORTED_FORMATS.include?(file.extname.downcase) }
136
- .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
137
- .then { |type| type.new(file) }
151
+ .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
152
+ .then { |type| type.new(file) }
138
153
  end
139
154
  end
140
155
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.1.5'
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,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: photein
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
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: 2022-09-06 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'
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.6'
13
40
  - !ruby/object:Gem::Dependency
14
41
  name: mediainfo
15
42
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +107,48 @@ dependencies:
80
107
  - - "~>"
81
108
  - !ruby/object:Gem::Version
82
109
  version: '0.2'
110
+ - !ruby/object:Gem::Dependency
111
+ name: ostruct
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.6'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.6'
124
+ - !ruby/object:Gem::Dependency
125
+ name: pstore
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.1'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.1'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rexml
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '3.4'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '3.4'
83
152
  - !ruby/object:Gem::Dependency
84
153
  name: streamio-ffmpeg
85
154
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +163,34 @@ dependencies:
94
163
  - - "~>"
95
164
  - !ruby/object:Gem::Version
96
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'
97
194
  - !ruby/object:Gem::Dependency
98
195
  name: pry-remote
99
196
  requirement: !ruby/object:Gem::Requirement
@@ -131,6 +228,7 @@ extra_rdoc_files: []
131
228
  files:
132
229
  - README.md
133
230
  - bin/photein
231
+ - data/tz_coords.json
134
232
  - lib/photein.rb
135
233
  - lib/photein/config.rb
136
234
  - lib/photein/image.rb
@@ -144,7 +242,6 @@ licenses:
144
242
  - MIT
145
243
  metadata:
146
244
  source_code_uri: https://github.com/rlue/photein
147
- post_install_message:
148
245
  rdoc_options: []
149
246
  require_paths:
150
247
  - lib
@@ -159,8 +256,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
256
  - !ruby/object:Gem::Version
160
257
  version: '0'
161
258
  requirements: []
162
- rubygems_version: 3.3.7
163
- signing_key:
259
+ rubygems_version: 3.6.2
164
260
  specification_version: 4
165
261
  summary: Import/rename photos & videos from one directory to another.
166
262
  test_files: []