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 +4 -4
- data/bin/photein +3 -7
- data/lib/photein/config.rb +24 -10
- data/lib/photein/image.rb +4 -6
- data/lib/photein/media_file.rb +50 -42
- data/lib/photein/version.rb +1 -1
- data/lib/photein/video.rb +4 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34b792399097ed2bb6a0339633d5e99c9a10387a98dcbd8e690f5442fbec4f93
|
4
|
+
data.tar.gz: 395dae72589abe9809d1fada0868f28fea3ad26ec5d2260701c92d923bd8119c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
17
|
-
raise "#{Photein::Config.
|
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[
|
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
|
data/lib/photein/config.rb
CHANGED
@@ -8,15 +8,16 @@ module Photein
|
|
8
8
|
include Singleton
|
9
9
|
|
10
10
|
OPTIONS = [
|
11
|
-
['-v', '--verbose',
|
12
|
-
['-s SOURCE', '--source=SOURCE',
|
13
|
-
['-
|
14
|
-
['-
|
15
|
-
['-
|
16
|
-
['-
|
17
|
-
['-
|
18
|
-
[
|
19
|
-
[
|
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
|
-
|
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
|
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
|
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
|
data/lib/photein/media_file.rb
CHANGED
@@ -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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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(
|
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
|
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
|
data/lib/photein/version.rb
CHANGED
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
|
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[
|
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.
|
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-
|
11
|
+
date: 2021-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mediainfo
|