photein 0.2.0 → 0.2.6
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 +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 +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 786766a470966476665ebc0ef22d7cbd939c7b78eb184f0af98037dd1b24f20d
|
4
|
+
data.tar.gz: b70c2a242e2ede4206460e2f99c0b612709531baf80edfaa58c5f6e7f6df7376
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbf4010ed2ed511a248072ce94a7c558203d4b24df56d8c7809c23be55613523543d631cf4c13b4c6be03443e6d3cfb8c42139b02e272d5464eb236c56c9847a
|
7
|
+
data.tar.gz: 45c7f373992b3b4a36f0c7d89e041fbf22beef6140dc8ec48b046e318ded6eb85d1529cf3f767129bab45e74979c1a3681778cdde5102d5eab5bada0ba7d7253
|
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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Lue
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 2025-05-11 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|
@@ -57,14 +57,14 @@ dependencies:
|
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: '2.
|
60
|
+
version: '2.14'
|
61
61
|
type: :runtime
|
62
62
|
prerelease: false
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: '2.
|
67
|
+
version: '2.14'
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
69
|
name: mini_magick
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|