photein 0.2.0 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/photein/config.rb +85 -56
- data/lib/photein/image.rb +7 -7
- data/lib/photein/media_file.rb +16 -14
- data/lib/photein/version.rb +1 -1
- data/lib/photein/video.rb +5 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a154956ea4c490bc480572f28c76591a6a6d495ae5d0f3a4558737b27077ba8
|
4
|
+
data.tar.gz: 4f861fde9e1d1002cedb446d4d1e29a82acdded09ac3d3dd7fa1b8d2ddf6afd4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2a54a0d4737106d830f5b98f7dd81ce6b34977e650f49251e486e181a523b6f92764bf345d2ff205e66db3bb418ac95296f762d5d9decd00a1dbb1d04595c917
|
7
|
+
data.tar.gz: 7662e6df25ebf21441f937eda60271611e1106b35096bb207a0325d6ee8d6ffa66fbd9348b2cb339c3664d7794ec72281639049a3f48df9bb2d71c73e0ebad00
|
data/lib/photein/config.rb
CHANGED
@@ -1,15 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
-
require 'singleton'
|
5
4
|
require 'optparse'
|
6
5
|
|
7
6
|
require 'tzinfo'
|
8
7
|
|
9
8
|
module Photein
|
10
9
|
class Config
|
11
|
-
include Singleton
|
12
|
-
|
13
10
|
OPTIONS = [
|
14
11
|
['-v', '--verbose', 'print verbose output'],
|
15
12
|
['-s SOURCE', '--source=SOURCE', 'path to the source directory'],
|
@@ -38,11 +35,75 @@ module Photein
|
|
38
35
|
.then(&JSON.method(:parse))
|
39
36
|
.freeze
|
40
37
|
|
41
|
-
|
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
|
42
103
|
|
43
104
|
class << self
|
44
|
-
def
|
45
|
-
@
|
105
|
+
def base_config
|
106
|
+
@base_config ||= Photein::Config.new
|
46
107
|
end
|
47
108
|
|
48
109
|
def parse_opts!
|
@@ -53,72 +114,40 @@ module Photein
|
|
53
114
|
BANNER
|
54
115
|
|
55
116
|
OPTIONS.each { |opt| opts.on(*opt) }
|
56
|
-
end.tap { |p| p.parse!(into: @params) }
|
57
|
-
|
58
|
-
@params[:verbose] ||= @params[:'dry-run']
|
117
|
+
end.tap { |p| p.parse!(into: base_config.instance_variable_get('@params')) }
|
59
118
|
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
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"
|
70
122
|
end
|
71
123
|
|
72
|
-
|
73
|
-
|
74
|
-
raise "no source directory given" if !@params.key?(:source)
|
75
|
-
(%i[library-master library-desktop library-web] & @params.keys)
|
76
|
-
.then { |dest_dirs| raise "no destination directory given" if dest_dirs.empty? }
|
124
|
+
base_config.validate_params!
|
77
125
|
rescue => e
|
78
126
|
warn("#{parser.program_name}: #{e.message}")
|
79
127
|
warn(parser.help) if e.is_a?(OptionParser::ParseError)
|
80
128
|
exit 1
|
81
129
|
end
|
82
130
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
def method_missing(m, *args, &blk)
|
88
|
-
m.to_s.tr('_', '-').to_sym
|
89
|
-
.then { |key| OPTION_NAMES.include?(key) ? self[key] : super }
|
90
|
-
end
|
91
|
-
|
92
|
-
def respond_to_missing?(m, *args)
|
93
|
-
@params.key?(m.to_s.tr('_', '-').to_sym) || super
|
94
|
-
end
|
95
|
-
|
96
|
-
def source
|
97
|
-
@source ||= Pathname(@params[:source])
|
98
|
-
end
|
99
|
-
|
100
|
-
def destinations
|
101
|
-
@destinations ||= {
|
102
|
-
master: @params[:'library-master'],
|
103
|
-
desktop: @params[:'library-desktop'],
|
104
|
-
web: @params[:'library-web']
|
105
|
-
}.compact.transform_values(&Pathname.method(:new))
|
131
|
+
# Do not remove! Used in https://github.com/rlue/xferase
|
132
|
+
def set(**params)
|
133
|
+
base_config.instance_variable_get('@params').replace(params)
|
106
134
|
end
|
107
135
|
|
108
|
-
def
|
109
|
-
|
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!)
|
110
141
|
end
|
111
142
|
|
112
|
-
def
|
113
|
-
return
|
143
|
+
def method_missing(m, *args, &blk)
|
144
|
+
return super unless m.to_s.tr('_', '-').sub(/=$/, '').to_sym
|
114
145
|
|
115
|
-
|
146
|
+
base_config.send(m, *args)
|
116
147
|
end
|
117
148
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
@tz_coordinates = @params.key?(:'local-tz') ? TZ_GEOCOORDS[@params[:'local-tz']] : nil
|
149
|
+
def respond_to_missing?(m, *args)
|
150
|
+
base_config.respond_to?(m) || super
|
122
151
|
end
|
123
152
|
end
|
124
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
|
42
|
+
end unless config.dry_run
|
43
43
|
when '.png'
|
44
|
-
FileUtils.cp(path, tempfile, noop:
|
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
|
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
|
@@ -102,16 +102,16 @@ module Photein
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def update_exif_tags(path)
|
105
|
-
return if
|
105
|
+
return if config.timestamp_delta.zero? && config.local_tz.nil?
|
106
106
|
|
107
107
|
file = MiniExiftool.new(path)
|
108
|
-
file.all_dates = new_timestamp.strftime('%Y:%m:%d %H:%M:%S') if
|
108
|
+
file.all_dates = new_timestamp.strftime('%Y:%m:%d %H:%M:%S') if config.timestamp_delta != 0
|
109
109
|
|
110
|
-
if !
|
110
|
+
if !config.local_tz.nil?
|
111
111
|
new_timestamp.to_s # "2020-02-14 22:55:30 -0800"
|
112
112
|
.split.tap(&:pop).join(' ').then { |time| time + ' UTC' } # "2020-02-14 22:55:30 UTC"
|
113
113
|
.then(&Time.method(:parse)) # 2020-02-14 22:55:30 UTC
|
114
|
-
.then(&
|
114
|
+
.then(&config.local_tz.method(:to_local)) # 2020-02-14 22:55:30 +0800
|
115
115
|
.strftime('%z').insert(3, ':') # "+08:00"
|
116
116
|
.tap { |offset| file.offset_time = offset }
|
117
117
|
.tap { |offset| file.offset_time_digitized = offset }
|
data/lib/photein/media_file.rb
CHANGED
@@ -13,18 +13,20 @@ module Photein
|
|
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
|
25
|
-
return if
|
26
|
+
return if config.interactive && denied_by_user?
|
27
|
+
return if config.safe && in_use?
|
26
28
|
|
27
|
-
|
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
|
@@ -41,23 +43,23 @@ module Photein
|
|
41
43
|
optimize(tempfile: tempfile, lib_type: lib_type)
|
42
44
|
|
43
45
|
Photein.logger.info(<<~MSG.chomp)
|
44
|
-
#{
|
46
|
+
#{config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
|
45
47
|
MSG
|
46
48
|
|
47
|
-
FileUtils.mkdir_p(dest_path.dirname, noop:
|
49
|
+
FileUtils.mkdir_p(dest_path.dirname, noop: config.dry_run)
|
48
50
|
|
49
51
|
if File.exist?(tempfile)
|
50
|
-
FileUtils.mv(tempfile, dest_path, noop:
|
52
|
+
FileUtils.mv(tempfile, dest_path, noop: config.dry_run)
|
51
53
|
else
|
52
|
-
FileUtils.cp(path, dest_path, noop:
|
53
|
-
FileUtils.chmod('-x', dest_path, noop:
|
54
|
+
FileUtils.cp(path, dest_path, noop: config.dry_run)
|
55
|
+
FileUtils.chmod('-x', dest_path, noop: config.dry_run)
|
54
56
|
end
|
55
57
|
|
56
|
-
update_exif_tags(dest_path.realdirpath.to_s) if !
|
58
|
+
update_exif_tags(dest_path.realdirpath.to_s) if !config.dry_run
|
57
59
|
end
|
58
60
|
end.compact.map(&:join).then do |threads|
|
59
61
|
# e.g.: with --library-web only, .dngs are skipped, so DON'T DELETE!
|
60
|
-
FileUtils.rm(path, noop: threads.empty? ||
|
62
|
+
FileUtils.rm(path, noop: threads.empty? || config.dry_run || config.keep)
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
@@ -93,7 +95,7 @@ module Photein
|
|
93
95
|
timestamp_from_metadata ||
|
94
96
|
timestamp_from_filename ||
|
95
97
|
timestamp_from_filesystem
|
96
|
-
) +
|
98
|
+
) + config.timestamp_delta
|
97
99
|
end
|
98
100
|
|
99
101
|
def timestamp_from_metadata
|
@@ -143,13 +145,13 @@ module Photein
|
|
143
145
|
end
|
144
146
|
|
145
147
|
class << self
|
146
|
-
def for(file)
|
148
|
+
def for(file, opts: {})
|
147
149
|
file = Pathname(file)
|
148
150
|
raise Errno::ENOENT, "#{file}" unless file.exist?
|
149
151
|
|
150
152
|
[Image, Video].find { |type| type::SUPPORTED_FORMATS.include?(file.extname.downcase) }
|
151
153
|
.tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
|
152
|
-
.then { |type| type.new(file) }
|
154
|
+
.then { |type| type.new(file, opts: opts) }
|
153
155
|
end
|
154
156
|
end
|
155
157
|
end
|
data/lib/photein/version.rb
CHANGED
data/lib/photein/video.rb
CHANGED
@@ -44,7 +44,7 @@ module Photein
|
|
44
44
|
return if video.bitrate < BITRATE_THRESHOLD[lib_type]
|
45
45
|
|
46
46
|
Photein.logger.info("transcoding #{tempfile}")
|
47
|
-
return if
|
47
|
+
return if config.dry_run
|
48
48
|
|
49
49
|
video.transcode(
|
50
50
|
tempfile.to_s,
|
@@ -110,7 +110,7 @@ module Photein
|
|
110
110
|
def local_tz
|
111
111
|
@local_tz ||= ActiveSupport::TimeZone[
|
112
112
|
MiniExiftool.new(path).then(&method(:gps_coords))&.then(&method(:coords_to_tz)) ||
|
113
|
-
|
113
|
+
config.local_tz ||
|
114
114
|
Time.now.gmt_offset
|
115
115
|
]
|
116
116
|
end
|
@@ -144,12 +144,12 @@ module Photein
|
|
144
144
|
end
|
145
145
|
|
146
146
|
def update_exif_tags(path)
|
147
|
-
return if
|
147
|
+
return if config.timestamp_delta.zero? && config.local_tz.nil?
|
148
148
|
|
149
149
|
args = []
|
150
|
-
args.push("-AllDates=#{new_timestamp.strftime('%Y:%m:%d\\ %H:%M:%S')}") if
|
150
|
+
args.push("-AllDates=#{new_timestamp.strftime('%Y:%m:%d\\ %H:%M:%S')}") if config.timestamp_delta != 0
|
151
151
|
|
152
|
-
if (lat, lon =
|
152
|
+
if (lat, lon = config.tz_coordinates)
|
153
153
|
args.push("-xmp:GPSLatitude=#{lat}")
|
154
154
|
args.push("-xmp:GPSLongitude=#{lon}")
|
155
155
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: photein
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Lue
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 2025-01-05 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|