photein 0.1.6 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3490aa4f980df5aed87bc73a127ab9908229edb18e5369377e75c459704f91f5
4
- data.tar.gz: 4fac58b8aaaf96eda55b07496e7b0fc99bb347863ffe3d50ad75429f7f9940f8
3
+ metadata.gz: 6a154956ea4c490bc480572f28c76591a6a6d495ae5d0f3a4558737b27077ba8
4
+ data.tar.gz: 4f861fde9e1d1002cedb446d4d1e29a82acdded09ac3d3dd7fa1b8d2ddf6afd4
5
5
  SHA512:
6
- metadata.gz: 8a414155ee73eac20adf275cb57d593f433531b7e7c70390b5cfa887e16b1e2247f39aea0e31cecc1e2e31b8cd0e093e640c3cf3558ced57a3573893540a8e71
7
- data.tar.gz: c8187a8cfa3f54bf5e2e1e1c9ea8ba4ccd02242fee55b220f8528380c77f8f1c7769bac0e497bf129bbd5a895640124360f8b74ec06d7423236ff052dd2d067c
6
+ metadata.gz: 2a54a0d4737106d830f5b98f7dd81ce6b34977e650f49251e486e181a523b6f92764bf345d2ff205e66db3bb418ac95296f762d5d9decd00a1dbb1d04595c917
7
+ data.tar.gz: 7662e6df25ebf21441f937eda60271611e1106b35096bb207a0325d6ee8d6ffa66fbd9348b2cb339c3664d7794ec72281639049a3f48df9bb2d71c73e0ebad00
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,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
3
+ require 'json'
4
4
  require 'optparse'
5
5
 
6
+ require 'tzinfo'
7
+
6
8
  module Photein
7
9
  class Config
8
- include Singleton
9
-
10
10
  OPTIONS = [
11
11
  ['-v', '--verbose', 'print verbose output'],
12
12
  ['-s SOURCE', '--source=SOURCE', 'path to the source directory'],
@@ -17,7 +17,9 @@ module Photein
17
17
  ['-k', '--keep', 'do not delete source files'],
18
18
  ['-i', '--interactive', 'ask whether to import each file found'],
19
19
  ['-n', '--dry-run', 'perform a "no-op" trial run'],
20
- [ '--safe', 'skip files in use by other processes'],
20
+ [ '--shift-timestamp=N', 'adjust metadata timestamps by N hours'],
21
+ [ '--local-tz=TIMEZONE', "backfill missing GPS* metadata on videos\n (to name files in local time instead of UTC)"],
22
+ [ '--safe', 'skip files in use by other processes']
21
23
  ].freeze
22
24
 
23
25
  OPTION_NAMES = OPTIONS
@@ -26,11 +28,82 @@ module Photein
26
28
  .map { |option| option[/\w[a-z\-]+/] }
27
29
  .map(&:to_sym)
28
30
 
29
- @params = {}
31
+ SECONDS_PER_HOUR = 60 * 60
32
+
33
+ TZ_GEOCOORDS = File.expand_path('../../data/tz_coords.json', File.dirname(__FILE__))
34
+ .then(&File.method(:read))
35
+ .then(&JSON.method(:parse))
36
+ .freeze
37
+
38
+ def initialize(params = {})
39
+ @params = params
40
+ end
41
+
42
+ def validate_params!
43
+ @params[:verbose] ||= @params[:'dry-run']
44
+
45
+ if @params.key?(:'shift-timestamp') && !@params[:'shift-timestamp'].match?(/^-?\d+$/)
46
+ raise "invalid --shift-timestamp option (must be integer)"
47
+ end
48
+
49
+ if @params.key?(:'local-tz')
50
+ if !TZInfo::Timezone.all_identifiers.include?(@params[:'local-tz'])
51
+ raise 'invalid --local-tz option (must be from IANA tz database)'
52
+ end
53
+
54
+ if tz_coordinates.nil?
55
+ raise 'invalid --local-tz option (must reference a location)'
56
+ end
57
+ end
58
+
59
+ @params.freeze
60
+
61
+ (%i[library-master library-desktop library-web] & @params.keys)
62
+ .then { |dest_dirs| raise "no destination directory given" if dest_dirs.empty? }
63
+ end
64
+
65
+ def method_missing(m, *args, &blk)
66
+ case m = m.to_s.tr('_', '-').to_sym
67
+ when *OPTION_NAMES
68
+ @params[m]
69
+ when *OPTION_NAMES.map { |opt| "#{opt}=" }.map(&:to_sym)
70
+ @params[m.sub(/=$/, '')] = args.shift
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def respond_to_missing?(m, *args)
77
+ OPTION_NAMES.include?(m.to_s.tr('_', '-').sub(/=$/, '').to_sym) || super
78
+ end
79
+
80
+ def source
81
+ @source ||= Pathname(@params[:source])
82
+ end
83
+
84
+ def destinations
85
+ @destinations ||= {
86
+ master: @params[:'library-master'],
87
+ desktop: @params[:'library-desktop'],
88
+ web: @params[:'library-web']
89
+ }.compact.transform_values(&Pathname.method(:new))
90
+ end
91
+
92
+ def timestamp_delta
93
+ @timestamp_delta ||= @params[:'shift-timestamp'].to_i * SECONDS_PER_HOUR
94
+ end
95
+
96
+ def local_tz
97
+ @local_tz ||= @params[:'local-tz']&.then(&TZInfo::Timezone.method(:get))
98
+ end
99
+
100
+ def tz_coordinates
101
+ @tz_coordinates ||= TZ_GEOCOORDS[@params[:'local-tz']]
102
+ end
30
103
 
31
104
  class << self
32
- def set(**params)
33
- @params.replace(params)
105
+ def base_config
106
+ @base_config ||= Photein::Config.new
34
107
  end
35
108
 
36
109
  def parse_opts!
@@ -41,43 +114,40 @@ module Photein
41
114
  BANNER
42
115
 
43
116
  OPTIONS.each { |opt| opts.on(*opt) }
44
- end.tap { |p| p.parse!(into: @params) }
117
+ end.tap { |p| p.parse!(into: base_config.instance_variable_get('@params')) }
45
118
 
46
- @params[:verbose] ||= @params[:'dry-run']
47
- @params.freeze
119
+ # This param is only required on the base config
120
+ if !base_config.instance_variable_get('@params').key?(:source)
121
+ raise "no source directory given"
122
+ end
48
123
 
49
- raise "no source directory given" if !@params.key?(:source)
50
- (%i[library-master library-desktop library-web] & @params.keys)
51
- .then { |dest_dirs| raise "no destination directory given" if dest_dirs.empty? }
124
+ base_config.validate_params!
52
125
  rescue => e
53
126
  warn("#{parser.program_name}: #{e.message}")
54
127
  warn(parser.help) if e.is_a?(OptionParser::ParseError)
55
128
  exit 1
56
129
  end
57
130
 
58
- def [](key)
59
- @params[key]
131
+ # Do not remove! Used in https://github.com/rlue/xferase
132
+ def set(**params)
133
+ base_config.instance_variable_get('@params').replace(params)
60
134
  end
61
135
 
62
- def method_missing(m, *args, &blk)
63
- m.to_s.tr('_', '-').to_sym
64
- .then { |key| OPTION_NAMES.include?(key) ? self[key] : super }
136
+ def with(opts)
137
+ opts.transform_keys { |k| k.to_s.tr('_', '-').to_sym }
138
+ .then(&base_config.instance_variable_get('@params').method(:merge))
139
+ .then(&Photein::Config.method(:new))
140
+ .tap(&:validate_params!)
65
141
  end
66
142
 
67
- def respond_to_missing?(m, *args)
68
- @params.key?(m.to_s.tr('_', '-').to_sym) || super
69
- end
143
+ def method_missing(m, *args, &blk)
144
+ return super unless m.to_s.tr('_', '-').sub(/=$/, '').to_sym
70
145
 
71
- def source
72
- @source ||= Pathname(@params[:source])
146
+ base_config.send(m, *args)
73
147
  end
74
148
 
75
- def destinations
76
- @destinations ||= {
77
- master: @params[:'library-master'],
78
- desktop: @params[:'library-desktop'],
79
- web: @params[:'library-web']
80
- }.compact.transform_values(&Pathname.method(:new))
149
+ def respond_to_missing?(m, *args)
150
+ base_config.respond_to?(m) || super
81
151
  end
82
152
  end
83
153
  end
data/lib/photein/image.rb CHANGED
@@ -39,12 +39,12 @@ module Photein
39
39
  convert.resize("#{MAX_RES_WEB}@>")
40
40
  convert.sampling_factor('4:2:0')
41
41
  convert << tempfile
42
- end unless Photein::Config.dry_run
42
+ end unless config.dry_run
43
43
  when '.png'
44
- FileUtils.cp(path, tempfile, noop: Photein::Config.dry_run)
44
+ FileUtils.cp(path, tempfile, noop: config.dry_run)
45
45
  Photein.logger.info "optimizing #{path}"
46
46
  begin
47
- Optipng.optimize(tempfile, level: 4) unless Photein::Config.dry_run
47
+ Optipng.optimize(tempfile, level: 4) unless config.dry_run
48
48
  rescue Errno::ENOENT
49
49
  Photein.logger.error('optipng is required to compress PNG images')
50
50
  raise
@@ -63,7 +63,7 @@ module Photein
63
63
  raise
64
64
  end
65
65
 
66
- def metadata_stamp
66
+ def timestamp_from_metadata
67
67
  MiniExiftool.new(path.to_s).date_time_original
68
68
  rescue MiniExiftool::Error => e
69
69
  Photein.logger.error(<<~MSG) if e.message.match?(/exiftool: not found/)
@@ -74,7 +74,7 @@ module Photein
74
74
 
75
75
  # NOTE: This may be largely unnecessary:
76
76
  # metadata timestamps are generally present in all cases except WhatsApp
77
- def filename_stamp
77
+ def timestamp_from_filename
78
78
  path.basename(path.extname).to_s.then do |filename|
79
79
  case filename
80
80
  when /^IMG_\d{8}_\d{6}(_\d{3})?$/ # Android DCIM: datetime + optional counter
@@ -100,5 +100,25 @@ module Photein
100
100
 
101
101
  return false
102
102
  end
103
+
104
+ def update_exif_tags(path)
105
+ return if config.timestamp_delta.zero? && config.local_tz.nil?
106
+
107
+ file = MiniExiftool.new(path)
108
+ file.all_dates = new_timestamp.strftime('%Y:%m:%d %H:%M:%S') if config.timestamp_delta != 0
109
+
110
+ if !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(&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,63 +7,66 @@ 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'
14
14
  }.freeze
15
15
 
16
+ attr_reader :config
16
17
  attr_reader :path
17
18
 
18
- def initialize(path)
19
+ def initialize(path, opts: {})
19
20
  @path = Pathname(path)
21
+ @config = Photein::Config.with(opts)
20
22
  end
21
23
 
22
24
  def import
23
25
  return if corrupted?
24
- return if Photein::Config.interactive && denied_by_user?
25
- return if Photein::Config.safe && in_use?
26
+ return if config.interactive && denied_by_user?
27
+ return if config.safe && in_use?
26
28
 
27
- Photein::Config.destinations.map do |lib_type, lib_path|
29
+ config.destinations.map do |lib_type, lib_path|
28
30
  next if non_optimizable_format?(lib_type)
29
31
 
30
32
  Thread.new do
31
- dest_basename = timestamp.strftime(DATE_FORMAT)
32
33
  dest_extname = self.class::OPTIMIZATION_FORMAT_MAP.dig(lib_type, extname) || extname
33
34
  dest_path = lib_path
34
- .join(timestamp.strftime('%Y'))
35
- .join("#{dest_basename}#{dest_extname}")
36
- .then(&method(:resolve_name_collision))
35
+ .join(Time.parse(dest_filename).strftime('%Y'))
36
+ .join("#{dest_filename}#{dest_extname}")
37
+ .then(&method(:resolve_name_collision))
37
38
  tempfile = Pathname(Dir.tmpdir)
38
- .join('photein').join(lib_type.to_s)
39
- .tap(&FileUtils.method(:mkdir_p))
40
- .join(dest_path.basename)
39
+ .join('photein').join(lib_type.to_s)
40
+ .tap(&FileUtils.method(:mkdir_p))
41
+ .join(dest_path.basename)
41
42
 
42
43
  optimize(tempfile: tempfile, lib_type: lib_type)
43
44
 
44
45
  Photein.logger.info(<<~MSG.chomp)
45
- #{Photein::Config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
46
+ #{config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
46
47
  MSG
47
48
 
48
- FileUtils.mkdir_p(dest_path.dirname, noop: Photein::Config.dry_run)
49
+ FileUtils.mkdir_p(dest_path.dirname, noop: config.dry_run)
49
50
 
50
51
  if File.exist?(tempfile)
51
- FileUtils.mv(tempfile, dest_path, noop: Photein::Config.dry_run)
52
+ FileUtils.mv(tempfile, dest_path, noop: config.dry_run)
52
53
  else
53
- FileUtils.cp(path, dest_path, noop: Photein::Config.dry_run)
54
- FileUtils.chmod('-x', dest_path, noop: Photein::Config.dry_run)
54
+ FileUtils.cp(path, dest_path, noop: config.dry_run)
55
+ FileUtils.chmod('-x', dest_path, noop: config.dry_run)
55
56
  end
57
+
58
+ update_exif_tags(dest_path.realdirpath.to_s) if !config.dry_run
56
59
  end
57
60
  end.compact.map(&:join).then do |threads|
58
61
  # e.g.: with --library-web only, .dngs are skipped, so DON'T DELETE!
59
- FileUtils.rm(path, noop: threads.empty? || Photein::Config.dry_run || Photein::Config.keep)
62
+ FileUtils.rm(path, noop: threads.empty? || config.dry_run || config.keep)
60
63
  end
61
64
  end
62
65
 
63
66
  private
64
67
 
65
68
  def corrupted?(result = false)
66
- return result.tap do |r|
69
+ result.tap do |r|
67
70
  Photein.logger.error("#{path.basename}: cannot import corrupted file") if r
68
71
  end
69
72
  end
@@ -76,31 +79,43 @@ module Photein
76
79
  def in_use?
77
80
  out, _err, status = Open3.capture3("lsof '#{path}'")
78
81
 
79
- if status.success? # Do open files ALWAYS return exit status 0? (I think so.)
80
- cmd, pid = out.lines[1]&.split&.first(2)
81
- Photein.logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
82
- return true
83
- else
84
- return false
85
- end
82
+ return false unless status.success? # Do open files ALWAYS return exit status 0? (I think so.)
83
+
84
+ cmd, pid = out.lines[1]&.split&.first(2)
85
+ Photein.logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
86
+ true
86
87
  end
87
88
 
88
- def non_optimizable_format?(lib_type = :master) # may be overridden by subclasses
89
- return false
89
+ def non_optimizable_format?(_lib_type = :master) # may be overridden by subclasses
90
+ false
90
91
  end
91
92
 
92
- def timestamp
93
- @timestamp ||= (metadata_stamp || filename_stamp)
93
+ def new_timestamp
94
+ @new_timestamp ||= (
95
+ timestamp_from_metadata ||
96
+ timestamp_from_filename ||
97
+ timestamp_from_filesystem
98
+ ) + config.timestamp_delta
94
99
  end
95
100
 
96
- def filename_stamp
101
+ def timestamp_from_metadata
102
+ raise NotImplementedError
103
+ end
104
+
105
+ def timestamp_from_filename
97
106
  Time.parse(path.basename(path.extname).to_s)
98
107
  rescue ArgumentError
99
- begin
100
- File.birthtime(path)
101
- rescue NotImplementedError
102
- File.mtime(path)
103
- end
108
+ nil
109
+ end
110
+
111
+ def timestamp_from_filesystem
112
+ File.birthtime(path)
113
+ rescue NotImplementedError
114
+ File.mtime(path)
115
+ end
116
+
117
+ def dest_filename
118
+ @dest_filename ||= new_timestamp.strftime(DATE_FORMAT)
104
119
  end
105
120
 
106
121
  def extname
@@ -114,25 +129,29 @@ module Photein
114
129
 
115
130
  case collisions.length
116
131
  when 0
117
- return pathname
132
+ pathname
118
133
  when 1
119
- return pathname.sub_ext("+1#{pathname.extname}")
134
+ pathname.sub_ext("+1#{pathname.extname}")
120
135
  else # TODO: what to do for heterogeneous suffixes?
121
136
  collisions.tap { |c| c.delete(pathname.to_s) }.max
122
- .slice(/(?<=^#{pathname.to_s.delete_suffix(pathname.extname)}).*(?=#{pathname.extname}$)/)
123
- .tap { |counter| raise 'Unresolved timestamp conflict' unless counter&.match?(/^\+[1-8]$/) }
124
- .then { |counter| pathname.sub_ext("#{counter.next}#{pathname.extname}") }
137
+ .slice(/(?<=^#{pathname.to_s.delete_suffix(pathname.extname)}).*(?=#{pathname.extname}$)/)
138
+ .tap { |counter| raise 'Unresolved timestamp conflict' unless counter&.match?(/^\+[1-8]$/) }
139
+ .then { |counter| pathname.sub_ext("#{counter.next}#{pathname.extname}") }
125
140
  end
126
141
  end
127
142
 
143
+ def update_exif_tags(path)
144
+ raise NotImplementedError
145
+ end
146
+
128
147
  class << self
129
- def for(file)
148
+ def for(file, opts: {})
130
149
  file = Pathname(file)
131
150
  raise Errno::ENOENT, "#{file}" unless file.exist?
132
151
 
133
152
  [Image, Video].find { |type| type::SUPPORTED_FORMATS.include?(file.extname.downcase) }
134
- .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
135
- .then { |type| type.new(file) }
153
+ .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
154
+ .then { |type| type.new(file, opts: opts) }
136
155
  end
137
156
  end
138
157
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.1.6'
4
+ VERSION = '0.2.5'
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
@@ -38,7 +44,7 @@ module Photein
38
44
  return if video.bitrate < BITRATE_THRESHOLD[lib_type]
39
45
 
40
46
  Photein.logger.info("transcoding #{tempfile}")
41
- return if Photein::Config.dry_run
47
+ return if config.dry_run
42
48
 
43
49
  video.transcode(
44
50
  tempfile.to_s,
@@ -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
+ 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 config.timestamp_delta.zero? && config.local_tz.nil?
148
+
149
+ args = []
150
+ args.push("-AllDates=#{new_timestamp.strftime('%Y:%m:%d\\ %H:%M:%S')}") if config.timestamp_delta != 0
151
+
152
+ if (lat, lon = config.tz_coordinates)
153
+ args.push("-xmp:GPSLatitude=#{lat}")
154
+ args.push("-xmp:GPSLongitude=#{lon}")
155
+ end
156
+
157
+ system("exiftool -overwrite_original #{args.join(' ')} #{path}", out: File::NULL, err: File::NULL)
158
+ end
106
159
  end
107
160
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: photein
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Lue
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-16 00:00:00.000000000 Z
10
+ date: 2025-01-05 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: logger
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +163,34 @@ dependencies:
150
163
  - - "~>"
151
164
  - !ruby/object:Gem::Version
152
165
  version: '3.0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: tzinfo
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '2.0'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '2.0'
180
+ - !ruby/object:Gem::Dependency
181
+ name: wheretz
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '0.0'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '0.0'
153
194
  - !ruby/object:Gem::Dependency
154
195
  name: pry-remote
155
196
  requirement: !ruby/object:Gem::Requirement
@@ -187,6 +228,7 @@ extra_rdoc_files: []
187
228
  files:
188
229
  - README.md
189
230
  - bin/photein
231
+ - data/tz_coords.json
190
232
  - lib/photein.rb
191
233
  - lib/photein/config.rb
192
234
  - lib/photein/image.rb
@@ -200,7 +242,6 @@ licenses:
200
242
  - MIT
201
243
  metadata:
202
244
  source_code_uri: https://github.com/rlue/photein
203
- post_install_message:
204
245
  rdoc_options: []
205
246
  require_paths:
206
247
  - lib
@@ -215,8 +256,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
256
  - !ruby/object:Gem::Version
216
257
  version: '0'
217
258
  requirements: []
218
- rubygems_version: 3.5.16
219
- signing_key:
259
+ rubygems_version: 3.6.2
220
260
  specification_version: 4
221
261
  summary: Import/rename photos & videos from one directory to another.
222
262
  test_files: []