photein 0.0.9 → 0.1.3

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: 7b3bcb770e66b470a49fd5f486560c447e92ba8df6daa33434c45fdd3564a2e7
4
- data.tar.gz: 83df115282d7cef28bfd2f32a42643a843726ea40c8eda1de5e7908f1b7f4832
3
+ metadata.gz: 34b792399097ed2bb6a0339633d5e99c9a10387a98dcbd8e690f5442fbec4f93
4
+ data.tar.gz: 395dae72589abe9809d1fada0868f28fea3ad26ec5d2260701c92d923bd8119c
5
5
  SHA512:
6
- metadata.gz: 33ceaea508cbe94cc40906364a1094c825c647e0bc7a38a1f9bdc21f1624a63a8b0c7e39351a0dcbc973afd2969af0e5e20615f3404d2b7b2af2f2cc6db1930f
7
- data.tar.gz: 798e0351b09bcb1cbe0cae6875b6731cbfeeffb2cba1d2e09bc6b5e9ded21ebf009af302e2c17841e5b6cf2f025e5b1cbcd819787ce645205c3167e6afbb6ba7
6
+ metadata.gz: a25e497f9076b2d507f01004a790022fe0fcb1511ef9bf1b43bccf6ac1e718dda8f4e047131f3fb3c0f5865f16f51d59ff5616936200fd0ef9a3c2c6ed250fd2
7
+ data.tar.gz: 61982abacad9467d9346799c8f2eb2839cdc5937805f5dd5c85f4edca2d56167418cc399f367e3ec5a11952fc44051bfb20ebfac9f4d60298727ec1c009b9929
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: @params[:'library-master'],
78
+ desktop: @params[:'library-desktop'],
79
+ web: @params[:'library-web']
80
+ }.compact.transform_values(&Pathname.method(:new))
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.compact.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,21 @@ 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}") }
128
+ end
129
+
130
+ class << self
131
+ def for(file)
132
+ file = Pathname(file)
133
+ raise Errno::ENOENT, "#{file}" unless file.exist?
134
+
135
+ [Image, Video].find { |type| type::SUPPORTED_FORMATS.include?(file.extname) }
136
+ .tap { |type| raise ArgumentError, "#{file}: Invalid media file" if type.nil? }
137
+ .then { |type| type.new(file) }
138
+ end
131
139
  end
132
140
  end
133
141
  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.3'
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.3
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-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mediainfo