photein 0.1.5 → 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: 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: []