photein 0.0.9 → 0.1.0

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: 7b3bcb770e66b470a49fd5f486560c447e92ba8df6daa33434c45fdd3564a2e7
4
- data.tar.gz: 83df115282d7cef28bfd2f32a42643a843726ea40c8eda1de5e7908f1b7f4832
3
+ metadata.gz: 90719855955bcdf3585080664753c65528e9edaad99d2646498965c063ae0299
4
+ data.tar.gz: 874d2e1d44defc090968d10f4c000f84c7501d4f5d3a1bff6b2a2145297bf169
5
5
  SHA512:
6
- metadata.gz: 33ceaea508cbe94cc40906364a1094c825c647e0bc7a38a1f9bdc21f1624a63a8b0c7e39351a0dcbc973afd2969af0e5e20615f3404d2b7b2af2f2cc6db1930f
7
- data.tar.gz: 798e0351b09bcb1cbe0cae6875b6731cbfeeffb2cba1d2e09bc6b5e9ded21ebf009af302e2c17841e5b6cf2f025e5b1cbcd819787ce645205c3167e6afbb6ba7
6
+ metadata.gz: 9a70c02ce6b74cd0cf58f887d4bf6ba21bdf0df50651f6acd230aa0e6382d6bdf3669d637f4f112acdfc4c5a6d8417425cf7a87f0b28dd6df5383cd539fd339f
7
+ data.tar.gz: 0a69d825de1c6f7fb4d81add6db960d2a12793b46e1aa6bb277193511dea0a59bcde2b47b97313419a268b6f38e97cc99e5b75642ebef3cbb88a445d4e1de446
data/bin/photein CHANGED
@@ -9,13 +9,9 @@ Photein.logger.open
9
9
 
10
10
  # Setup ------------------------------------------------------------------------
11
11
 
12
- SRC_DIR = Pathname(Photein::Config.source)
13
- DEST_DIR = Pathname(Photein::Config.dest)
14
-
15
12
  begin
16
- raise "#{Photein::Config.source}: no such directory" unless SRC_DIR.exist?
17
- raise "#{Photein::Config.dest}: no such directory" unless DEST_DIR.exist?
18
- raise "#{Photein::Config.source}: no photos or videos found" if Dir.empty?(SRC_DIR)
13
+ raise "#{Photein::Config.source}: no such directory" unless Photein::Config.source.exist?
14
+ raise "#{Photein::Config.source}: no photos or videos found" if Dir.empty?(Photein::Config.source)
19
15
  rescue => e
20
16
  Photein.logger.fatal(e.message)
21
17
  exit 1
@@ -24,7 +20,7 @@ end
24
20
  # Cleanup ----------------------------------------------------------------------
25
21
  at_exit do
26
22
  unless Photein::Config.keep
27
- Dir[SRC_DIR.join('**/')].sort
23
+ Dir[Photein::Config.source.join('**/')].sort
28
24
  .drop(1)
29
25
  .reverse_each { |d| Dir.rmdir(d) if Dir.empty?(d) }
30
26
  end
@@ -8,15 +8,16 @@ module Photein
8
8
  include Singleton
9
9
 
10
10
  OPTIONS = [
11
- ['-v', '--verbose', 'print verbose output'],
12
- ['-s SOURCE', '--source=SOURCE', 'specify the source directory'],
13
- ['-d DESTINATION', '--dest=DESTINATION', 'specify the destination directory'],
14
- ['-r', '--recursive', 'ingest source files recursively'],
15
- ['-k', '--keep', 'do not delete source files'],
16
- ['-i', '--interactive', 'ask whether to import each file found'],
17
- ['-n', '--dry-run', 'perform a "no-op" trial run'],
18
- [ '--safe', 'skip files in use by other processes'],
19
- [ '--optimize-for=TARGET', %i[desktop web], 'compress images/video before importing']
11
+ ['-v', '--verbose', 'print verbose output'],
12
+ ['-s SOURCE', '--source=SOURCE', 'path to the source directory'],
13
+ ['-m MASTER', '--library-master=MASTER', 'path to a destination directory (master)'],
14
+ ['-d DESKTOP', '--library-desktop=DESKTOP', 'path to a destination directory (desktop-optimized)'],
15
+ ['-w WEB', '--library-web=WEB', 'path to a destination directory (web-optimized)'],
16
+ ['-r', '--recursive', 'ingest source files recursively'],
17
+ ['-k', '--keep', 'do not delete source files'],
18
+ ['-i', '--interactive', 'ask whether to import each file found'],
19
+ ['-n', '--dry-run', 'perform a "no-op" trial run'],
20
+ [ '--safe', 'skip files in use by other processes'],
20
21
  ].freeze
21
22
 
22
23
  OPTION_NAMES = OPTIONS
@@ -46,7 +47,8 @@ module Photein
46
47
  @params.freeze
47
48
 
48
49
  raise "no source directory given" if !@params.key?(:source)
49
- raise "no destination directory given" if !@params.key?(:dest)
50
+ (%i[library-master library-desktop library-web] & @params.keys)
51
+ .then { |dest_dirs| raise "no destination directory given" if dest_dirs.empty? }
50
52
  rescue => e
51
53
  warn("#{parser.program_name}: #{e.message}")
52
54
  warn(parser.help) if e.is_a?(OptionParser::ParseError)
@@ -65,6 +67,18 @@ module Photein
65
67
  def respond_to_missing?(m, *args)
66
68
  @params.key?(m.to_s.tr('_', '-').to_sym) || super
67
69
  end
70
+
71
+ def source
72
+ @source ||= Pathname(@params[:source])
73
+ end
74
+
75
+ def destinations
76
+ @destinations ||= {
77
+ master: Pathname(@params[:'library-master']),
78
+ desktop: Pathname(@params[:'library-desktop']),
79
+ web: Pathname(@params[:'library-web'])
80
+ }.compact
81
+ end
68
82
  end
69
83
  end
70
84
  end
data/lib/photein/image.rb CHANGED
@@ -22,8 +22,8 @@ module Photein
22
22
  }.freeze
23
23
  MAX_RES_WEB = 2097152 # 2MP
24
24
 
25
- def optimize
26
- return if Photein::Config.optimize_for == :desktop
25
+ def optimize(tempfile:, lib_type:)
26
+ return unless lib_type == :web
27
27
 
28
28
  case extname
29
29
  when '.jpg', '.heic'
@@ -95,10 +95,8 @@ module Photein
95
95
  end
96
96
  end
97
97
 
98
- def non_optimizable_format?
99
- return false if !Photein::Config.optimize_for
100
- return false if Photein::Config.optimize_for == :desktop
101
- return true if extname == '.dng'
98
+ def non_optimizable_format?(lib_type)
99
+ return true if lib_type == :web && extname == '.dng'
102
100
 
103
101
  return false
104
102
  end
@@ -23,22 +23,38 @@ module Photein
23
23
  return if corrupted?
24
24
  return if Photein::Config.interactive && denied_by_user?
25
25
  return if Photein::Config.safe && in_use?
26
- return if Photein::Config.optimize_for && non_optimizable_format?
27
26
 
28
- FileUtils.mkdir_p(parent_dir, noop: Photein::Config.dry_run)
29
-
30
- optimize if Photein::Config.optimize_for
31
-
32
- Photein.logger.info(<<~MSG.chomp)
33
- #{Photein::Config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
34
- MSG
35
-
36
- if File.exist?(tempfile)
37
- FileUtils.mv(tempfile, dest_path, noop: Photein::Config.dry_run)
38
- else
39
- FileUtils.cp(path, dest_path, noop: Photein::Config.dry_run)
40
- FileUtils.chmod('-x', dest_path, noop: Photein::Config.dry_run)
41
- end
27
+ Photein::Config.destinations.map do |lib_type, lib_path|
28
+ next if non_optimizable_format?(lib_type)
29
+
30
+ Thread.new do
31
+ dest_basename = timestamp.strftime(DATE_FORMAT)
32
+ dest_extname = self.class::OPTIMIZATION_FORMAT_MAP.dig(lib_type, extname) || extname
33
+ dest_path = lib_path
34
+ .join(timestamp.strftime('%Y'))
35
+ .join("#{dest_basename}#{dest_extname}")
36
+ .then(&method(:resolve_name_collision))
37
+ tempfile = Pathname(Dir.tmpdir)
38
+ .join('photein').join(lib_type.to_s)
39
+ .tap(&FileUtils.method(:mkdir_p))
40
+ .join(dest_path.basename)
41
+
42
+ optimize(tempfile: tempfile, lib_type: lib_type)
43
+
44
+ Photein.logger.info(<<~MSG.chomp)
45
+ #{Photein::Config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
46
+ MSG
47
+
48
+ FileUtils.mkdir_p(dest_path.dirname, noop: Photein::Config.dry_run)
49
+
50
+ if File.exist?(tempfile)
51
+ FileUtils.mv(tempfile, dest_path, noop: Photein::Config.dry_run)
52
+ else
53
+ FileUtils.cp(path, dest_path, noop: Photein::Config.dry_run)
54
+ FileUtils.chmod('-x', dest_path, noop: Photein::Config.dry_run)
55
+ end
56
+ end
57
+ end.each(&:join)
42
58
 
43
59
  FileUtils.rm(path, noop: Photein::Config.dry_run || Photein::Config.keep)
44
60
  end
@@ -68,29 +84,10 @@ module Photein
68
84
  end
69
85
  end
70
86
 
71
- def non_optimizable_format? # may be overridden by subclasses
87
+ def non_optimizable_format?(lib_type = :master) # may be overridden by subclasses
72
88
  return false
73
89
  end
74
90
 
75
- def parent_dir
76
- Pathname(Photein::Config.dest).join(timestamp.strftime('%Y'))
77
- end
78
-
79
- def tempfile
80
- Pathname(Dir.tmpdir).join('photein')
81
- .tap(&FileUtils.method(:mkdir_p))
82
- .join(dest_path.basename)
83
- end
84
-
85
- def dest_path
86
- @dest_path ||= begin
87
- base_path = parent_dir.join("#{timestamp.strftime(DATE_FORMAT)}#{dest_extname}")
88
- counter = resolve_name_collision(base_path.sub_ext("*#{dest_extname}"))
89
-
90
- base_path.sub_ext("#{counter}#{dest_extname}")
91
- end
92
- end
93
-
94
91
  def timestamp
95
92
  @timestamp ||= (metadata_stamp || filename_stamp)
96
93
  end
@@ -105,16 +102,15 @@ module Photein
105
102
  end
106
103
  end
107
104
 
108
- def dest_extname
109
- self.class::OPTIMIZATION_FORMAT_MAP
110
- .dig(Photein::Config.optimize_for, extname) || extname
111
- end
112
-
113
105
  def extname
114
106
  @extname ||= NORMAL_EXTNAME_MAP[path.extname.downcase] || path.extname.downcase
115
107
  end
116
108
 
117
- def resolve_name_collision(collision_glob)
109
+ def resolve_name_collision(filename)
110
+ raise ArgumentError, 'Invalid filename' if filename.to_s.include?('*')
111
+
112
+ collision_glob = Pathname(filename).sub_ext("*#{filename.extname}")
113
+
118
114
  case Dir[collision_glob].length
119
115
  when 0 # if no files found, no biggie
120
116
  when 1 # if one file found, WITH OR WITHOUT COUNTER, reset counter to a
@@ -125,9 +121,10 @@ module Photein
125
121
  else # TODO: if multiple files found, rectify them?
126
122
  end
127
123
 
128
- # return the next usable counter
124
+ # return the next usable filename
129
125
  Dir[collision_glob].max&.slice(/.(?=#{Regexp.escape(collision_glob.extname)})/)&.next
130
126
  .tap { |counter| raise 'Unresolved timestamp conflict' unless [*Array('a'..'z'), nil].include?(counter) }
127
+ .then { |counter| filename.sub_ext("#{counter}#{filename.extname}") }
131
128
  end
132
129
  end
133
130
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.0.9'
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/photein/video.rb CHANGED
@@ -33,8 +33,9 @@ module Photein
33
33
  web: '35',
34
34
  }.freeze
35
35
 
36
- def optimize
37
- return if video.bitrate < BITRATE_THRESHOLD[Photein::Config.optimize_for]
36
+ def optimize(tempfile:, lib_type:)
37
+ return if lib_type == :master
38
+ return if video.bitrate < BITRATE_THRESHOLD[lib_type]
38
39
 
39
40
  Photein.logger.info("transcoding #{tempfile}")
40
41
  return if Photein::Config.dry_run
@@ -45,7 +46,7 @@ module Photein
45
46
  '-map_metadata', '0', # https://video.stackexchange.com/a/26076
46
47
  '-movflags', 'use_metadata_tags',
47
48
  '-c:v', 'libx264',
48
- '-crf', TARGET_CRF[Photein::Config.optimize_for],
49
+ '-crf', TARGET_CRF[lib_type],
49
50
  ],
50
51
  &method(:display_progress_bar)
51
52
  )
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: photein
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Lue
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-07 00:00:00.000000000 Z
11
+ date: 2021-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mediainfo