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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 362d9c0535398b17f736549119b3079e52fdc7c22451014d8039b1ff7172914f
4
- data.tar.gz: f2a87140f5c536f5bc36663bf7d40c42314199ec3a3247d88a2b619507a360c6
3
+ metadata.gz: 6a154956ea4c490bc480572f28c76591a6a6d495ae5d0f3a4558737b27077ba8
4
+ data.tar.gz: 4f861fde9e1d1002cedb446d4d1e29a82acdded09ac3d3dd7fa1b8d2ddf6afd4
5
5
  SHA512:
6
- metadata.gz: 93814ee95d41d740892978276ed15863daa8832a4c6d4dcc62cbda22d114781e910d74fdbacbb508a78dce9cbbd1e14c5f9c131215b680c3c11865863dc11d18
7
- data.tar.gz: 31384d711c87386c22b52439985ce4aec5d4d413ab8271e184f0b32cd4dad1f2628d0d88bf74f27e0a248beb304b45d0b33f20961e33b72e343cbe8affdcc7e7
6
+ metadata.gz: 2a54a0d4737106d830f5b98f7dd81ce6b34977e650f49251e486e181a523b6f92764bf345d2ff205e66db3bb418ac95296f762d5d9decd00a1dbb1d04595c917
7
+ data.tar.gz: 7662e6df25ebf21441f937eda60271611e1106b35096bb207a0325d6ee8d6ffa66fbd9348b2cb339c3664d7794ec72281639049a3f48df9bb2d71c73e0ebad00
@@ -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
- @params = {}
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 set(**params)
45
- @params.replace(params)
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
- 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
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
- @params.freeze
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
- def [](key)
84
- @params[key]
85
- end
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 timestamp_delta
109
- @timestamp_delta ||= @params[:'shift-timestamp'].to_i * SECONDS_PER_HOUR
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 local_tz
113
- return @local_tz if defined? @local_tz
143
+ def method_missing(m, *args, &blk)
144
+ return super unless m.to_s.tr('_', '-').sub(/=$/, '').to_sym
114
145
 
115
- @local_tz = @params.key?(:'local-tz') ? TZInfo::Timezone.get(@params[:'local-tz']) : nil
146
+ base_config.send(m, *args)
116
147
  end
117
148
 
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
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 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
@@ -102,16 +102,16 @@ module Photein
102
102
  end
103
103
 
104
104
  def update_exif_tags(path)
105
- return if Photein::Config.timestamp_delta.zero? && Photein::Config.local_tz.nil?
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 Photein::Config.timestamp_delta != 0
108
+ file.all_dates = new_timestamp.strftime('%Y:%m:%d %H:%M:%S') if config.timestamp_delta != 0
109
109
 
110
- if !Photein::Config.local_tz.nil?
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(&Photein::Config.local_tz.method(:to_local)) # 2020-02-14 22:55:30 +0800
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 }
@@ -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 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
@@ -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
- #{Photein::Config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
46
+ #{config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
45
47
  MSG
46
48
 
47
- FileUtils.mkdir_p(dest_path.dirname, noop: Photein::Config.dry_run)
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: Photein::Config.dry_run)
52
+ FileUtils.mv(tempfile, dest_path, noop: config.dry_run)
51
53
  else
52
- FileUtils.cp(path, dest_path, noop: Photein::Config.dry_run)
53
- 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)
54
56
  end
55
57
 
56
- update_exif_tags(dest_path.realdirpath.to_s) if !Photein::Config.dry_run
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? || Photein::Config.dry_run || Photein::Config.keep)
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
- ) + Photein::Config.timestamp_delta
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.2.0'
4
+ VERSION = '0.2.5'
5
5
  end
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 Photein::Config.dry_run
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
- Photein::Config.local_tz ||
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 Photein::Config.timestamp_delta.zero? && Photein::Config.local_tz.nil?
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 Photein::Config.timestamp_delta != 0
150
+ args.push("-AllDates=#{new_timestamp.strftime('%Y:%m:%d\\ %H:%M:%S')}") if config.timestamp_delta != 0
151
151
 
152
- if (lat, lon = Photein::Config.tz_coordinates)
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.0
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: 2024-12-31 00:00:00.000000000 Z
10
+ date: 2025-01-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport