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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 362d9c0535398b17f736549119b3079e52fdc7c22451014d8039b1ff7172914f
4
- data.tar.gz: f2a87140f5c536f5bc36663bf7d40c42314199ec3a3247d88a2b619507a360c6
3
+ metadata.gz: 786766a470966476665ebc0ef22d7cbd939c7b78eb184f0af98037dd1b24f20d
4
+ data.tar.gz: b70c2a242e2ede4206460e2f99c0b612709531baf80edfaa58c5f6e7f6df7376
5
5
  SHA512:
6
- metadata.gz: 93814ee95d41d740892978276ed15863daa8832a4c6d4dcc62cbda22d114781e910d74fdbacbb508a78dce9cbbd1e14c5f9c131215b680c3c11865863dc11d18
7
- data.tar.gz: 31384d711c87386c22b52439985ce4aec5d4d413ab8271e184f0b32cd4dad1f2628d0d88bf74f27e0a248beb304b45d0b33f20961e33b72e343cbe8affdcc7e7
6
+ metadata.gz: cbf4010ed2ed511a248072ce94a7c558203d4b24df56d8c7809c23be55613523543d631cf4c13b4c6be03443e6d3cfb8c42139b02e272d5464eb236c56c9847a
7
+ data.tar.gz: 45c7f373992b3b4a36f0c7d89e041fbf22beef6140dc8ec48b046e318ded6eb85d1529cf3f767129bab45e74979c1a3681778cdde5102d5eab5bada0ba7d7253
@@ -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.6'
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.6
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-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.10'
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.10'
67
+ version: '2.14'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: mini_magick
70
70
  requirement: !ruby/object:Gem::Requirement