photein 0.0.5 → 0.0.9
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 +4 -4
- data/README.md +17 -16
- data/bin/photein +3 -24
- data/lib/photein/config.rb +6 -2
- data/lib/photein/image.rb +18 -3
- data/lib/photein/logger.rb +8 -0
- data/lib/photein/media_file.rb +11 -4
- data/lib/photein/version.rb +1 -1
- data/lib/photein/video.rb +11 -1
- data/lib/photein.rb +12 -0
- metadata +5 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b3bcb770e66b470a49fd5f486560c447e92ba8df6daa33434c45fdd3564a2e7
|
4
|
+
data.tar.gz: 83df115282d7cef28bfd2f32a42643a843726ea40c8eda1de5e7908f1b7f4832
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33ceaea508cbe94cc40906364a1094c825c647e0bc7a38a1f9bdc21f1624a63a8b0c7e39351a0dcbc973afd2969af0e5e20615f3404d2b7b2af2f2cc6db1930f
|
7
|
+
data.tar.gz: 798e0351b09bcb1cbe0cae6875b6731cbfeeffb2cba1d2e09bc6b5e9ded21ebf009af302e2c17841e5b6cf2f025e5b1cbcd819787ce645205c3167e6afbb6ba7
|
data/README.md
CHANGED
@@ -14,21 +14,22 @@ no matter where they come from:
|
|
14
14
|
```sh
|
15
15
|
# Before # After
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
17
|
+
~ ~
|
18
|
+
├── Downloads ├── Downloads
|
19
|
+
│ ├── 1619593208911.jpeg └── Pictures
|
20
|
+
│ ├── DCIM ├── 2020
|
21
|
+
│ │ └── 2021_03_26 │ ├── 2020-08-01_113129.heic
|
22
|
+
│ │ ├── R0014285.MOV │ └── 2020-05-20_160209.png
|
23
|
+
│ │ ├── R0014286.DNG └── 2021
|
24
|
+
│ │ ├── R0014286.JPG ├── 2021-02-12_081933a.jpg
|
25
|
+
│ │ ├── R0014287.DNG ├── 2021-02-12_081933b.jpg
|
26
|
+
│ │ └── R0014287.JPG ├── 2021-02-12_081939.mp4
|
27
|
+
│ ├── IMG_20210212_081933_001.jpg ├── 2021-03-26_161245.mp4
|
28
|
+
│ ├── IMG_20210212_081933_002.jpg ├── 2021-03-26_161518.dng
|
29
|
+
│ ├── IMG_8953.HEIC ├── 2021-03-26_161518.jpg
|
30
|
+
│ ├── Screenshot_20200520_160209.png ├── 2021-03-26_170304.dng
|
31
|
+
│ └── VID_20210212_081939.mp4 ├── 2021-03-26_170304.jpg
|
32
|
+
└── Pictures └── 2021-04-28_000008.jpg
|
32
33
|
```
|
33
34
|
|
34
35
|
Photein generates these folders & filenames
|
@@ -38,7 +39,7 @@ based on metadata timestamps, filename timestamps, or file creation times.
|
|
38
39
|
>
|
39
40
|
> If you use a photo management app that decides
|
40
41
|
> where and how your photos should be stored on disk
|
41
|
-
> (
|
42
|
+
> (looking at you, Apple Photos 👀), Photein is not for you.
|
42
43
|
|
43
44
|
It can also optimize photos and videos for reduced file size.
|
44
45
|
|
data/bin/photein
CHANGED
@@ -5,7 +5,7 @@ require 'photein'
|
|
5
5
|
require 'pathname'
|
6
6
|
|
7
7
|
Photein::Config.parse_opts!
|
8
|
-
Photein
|
8
|
+
Photein.logger.open
|
9
9
|
|
10
10
|
# Setup ------------------------------------------------------------------------
|
11
11
|
|
@@ -17,7 +17,7 @@ begin
|
|
17
17
|
raise "#{Photein::Config.dest}: no such directory" unless DEST_DIR.exist?
|
18
18
|
raise "#{Photein::Config.source}: no photos or videos found" if Dir.empty?(SRC_DIR)
|
19
19
|
rescue => e
|
20
|
-
Photein
|
20
|
+
Photein.logger.fatal(e.message)
|
21
21
|
exit 1
|
22
22
|
end
|
23
23
|
|
@@ -33,25 +33,4 @@ at_exit do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Core Logic -------------------------------------------------------------------
|
36
|
-
|
37
|
-
.zip(Photein::Image::SUPPORTED_FORMATS.map(&:upcase))
|
38
|
-
.flatten
|
39
|
-
|
40
|
-
SRC_DIR
|
41
|
-
.join(Photein::Config.recursive ? '**' : '')
|
42
|
-
.join("*{#{image_formats.join(',')}}")
|
43
|
-
.then { |glob| Dir[glob].sort }
|
44
|
-
.map(&Photein::Image.method(:new))
|
45
|
-
.each(&:import)
|
46
|
-
|
47
|
-
# Video compression is time-consuming, so save it for last
|
48
|
-
video_formats = Photein::Video::SUPPORTED_FORMATS
|
49
|
-
.zip(Photein::Video::SUPPORTED_FORMATS.map(&:upcase))
|
50
|
-
.flatten
|
51
|
-
|
52
|
-
SRC_DIR
|
53
|
-
.join(Photein::Config.recursive ? '**' : '')
|
54
|
-
.join("*{#{video_formats.join(',')}}")
|
55
|
-
.then { |glob| Dir[glob].sort }
|
56
|
-
.map(&Photein::Video.method(:new))
|
57
|
-
.each(&:import)
|
36
|
+
Photein.run
|
data/lib/photein/config.rb
CHANGED
@@ -25,10 +25,14 @@ module Photein
|
|
25
25
|
.map { |option| option[/\w[a-z\-]+/] }
|
26
26
|
.map(&:to_sym)
|
27
27
|
|
28
|
+
@params = {}
|
29
|
+
|
28
30
|
class << self
|
29
|
-
def
|
30
|
-
@params
|
31
|
+
def set(**params)
|
32
|
+
@params.replace(params)
|
33
|
+
end
|
31
34
|
|
35
|
+
def parse_opts!
|
32
36
|
parser = OptionParser.new do |opts|
|
33
37
|
opts.version = Photein::VERSION
|
34
38
|
opts.banner = <<~BANNER
|
data/lib/photein/image.rb
CHANGED
@@ -29,7 +29,7 @@ module Photein
|
|
29
29
|
when '.jpg', '.heic'
|
30
30
|
return false if image.dimensions.reduce(&:*) < MAX_RES_WEB
|
31
31
|
|
32
|
-
Photein
|
32
|
+
Photein.logger.info "optimizing #{path}"
|
33
33
|
MiniMagick::Tool::Convert.new do |convert|
|
34
34
|
convert << path
|
35
35
|
convert.colorspace('sRGB')
|
@@ -42,8 +42,13 @@ module Photein
|
|
42
42
|
end unless Photein::Config.dry_run
|
43
43
|
when '.png'
|
44
44
|
FileUtils.cp(path, tempfile, noop: Photein::Config.dry_run)
|
45
|
-
Photein
|
46
|
-
|
45
|
+
Photein.logger.info "optimizing #{path}"
|
46
|
+
begin
|
47
|
+
Optipng.optimize(tempfile, level: 4) unless Photein::Config.dry_run
|
48
|
+
rescue Errno::ENOENT
|
49
|
+
Photein.logger.error('optipng is required to compress PNG images')
|
50
|
+
raise
|
51
|
+
end
|
47
52
|
end
|
48
53
|
end
|
49
54
|
|
@@ -51,10 +56,20 @@ module Photein
|
|
51
56
|
|
52
57
|
def image
|
53
58
|
@image ||= MiniMagick::Image.open(path)
|
59
|
+
rescue MiniMagick::Invalid => e
|
60
|
+
Photein.logger.error(<<~MSG) if e.message.match?(/You must have ImageMagick/)
|
61
|
+
ImageMagick is required to manipulate image files
|
62
|
+
MSG
|
63
|
+
raise
|
54
64
|
end
|
55
65
|
|
56
66
|
def metadata_stamp
|
57
67
|
MiniExiftool.new(path.to_s).date_time_original
|
68
|
+
rescue MiniExiftool::Error => e
|
69
|
+
Photein.logger.error(<<~MSG) if e.message.match?(/exiftool: not found/)
|
70
|
+
exiftool is required to read timestamp metadata
|
71
|
+
MSG
|
72
|
+
raise
|
58
73
|
end
|
59
74
|
|
60
75
|
# NOTE: This may be largely unnecessary:
|
data/lib/photein/logger.rb
CHANGED
data/lib/photein/media_file.rb
CHANGED
@@ -20,6 +20,7 @@ module Photein
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def import
|
23
|
+
return if corrupted?
|
23
24
|
return if Photein::Config.interactive && denied_by_user?
|
24
25
|
return if Photein::Config.safe && in_use?
|
25
26
|
return if Photein::Config.optimize_for && non_optimizable_format?
|
@@ -28,7 +29,7 @@ module Photein
|
|
28
29
|
|
29
30
|
optimize if Photein::Config.optimize_for
|
30
31
|
|
31
|
-
Photein
|
32
|
+
Photein.logger.info(<<~MSG.chomp)
|
32
33
|
#{Photein::Config.keep ? 'copying' : 'moving'} #{path.basename} to #{dest_path}
|
33
34
|
MSG
|
34
35
|
|
@@ -44,6 +45,12 @@ module Photein
|
|
44
45
|
|
45
46
|
private
|
46
47
|
|
48
|
+
def corrupted?(result = false)
|
49
|
+
return result.tap do |r|
|
50
|
+
Photein.logger.error("#{path.basename}: cannot import corrupted file") if r
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
47
54
|
def denied_by_user?
|
48
55
|
$stdout.printf "Import #{path}? [y/N]"
|
49
56
|
(STDIN.getch.downcase != 'y').tap { $stdout.puts }
|
@@ -54,7 +61,7 @@ module Photein
|
|
54
61
|
|
55
62
|
if status.success? # Do open files ALWAYS return exit status 0? (I think so.)
|
56
63
|
cmd, pid = out.lines[1]&.split&.first(2)
|
57
|
-
Photein
|
64
|
+
Photein.logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
|
58
65
|
return true
|
59
66
|
else
|
60
67
|
return false
|
@@ -112,8 +119,8 @@ module Photein
|
|
112
119
|
when 0 # if no files found, no biggie
|
113
120
|
when 1 # if one file found, WITH OR WITHOUT COUNTER, reset counter to a
|
114
121
|
if Dir[collision_glob].first != collision_glob.sub('*', 'a') # don't try if it's already a lone, correctly-countered file
|
115
|
-
Photein
|
116
|
-
FileUtils.mv(Dir[collision_glob].first, collision_glob.sub('*', 'a'))
|
122
|
+
Photein.logger.info('conflicting timestamp found; adding counter to existing file')
|
123
|
+
FileUtils.mv(Dir[collision_glob].first, collision_glob.sub('*', 'a'), noop: Photein::Config.dry_run)
|
117
124
|
end
|
118
125
|
else # TODO: if multiple files found, rectify them?
|
119
126
|
end
|
data/lib/photein/version.rb
CHANGED
data/lib/photein/video.rb
CHANGED
@@ -36,7 +36,7 @@ module Photein
|
|
36
36
|
def optimize
|
37
37
|
return if video.bitrate < BITRATE_THRESHOLD[Photein::Config.optimize_for]
|
38
38
|
|
39
|
-
Photein
|
39
|
+
Photein.logger.info("transcoding #{tempfile}")
|
40
40
|
return if Photein::Config.dry_run
|
41
41
|
|
42
42
|
video.transcode(
|
@@ -53,13 +53,23 @@ module Photein
|
|
53
53
|
|
54
54
|
private
|
55
55
|
|
56
|
+
def corrupted?
|
57
|
+
super(video.bitrate.nil?)
|
58
|
+
end
|
59
|
+
|
56
60
|
def video
|
57
61
|
@video ||= FFMPEG::Movie.new(path.to_s)
|
62
|
+
rescue Errno::ENOENT
|
63
|
+
Photein.logger.error('ffmpeg is required to manipulate video files')
|
64
|
+
raise
|
58
65
|
end
|
59
66
|
|
60
67
|
def metadata_stamp
|
61
68
|
# video timestamps are typically UTC
|
62
69
|
MediaInfo.from(path.to_s).general.encoded_date&.getlocal
|
70
|
+
rescue MediaInfo::EnvironmentError
|
71
|
+
Photein.logger.error('mediainfo is required to read timestamp metadata')
|
72
|
+
raise
|
63
73
|
end
|
64
74
|
|
65
75
|
# NOTE: This may be largely unnecessary:
|
data/lib/photein.rb
CHANGED
@@ -8,4 +8,16 @@ require 'photein/image'
|
|
8
8
|
require 'photein/video'
|
9
9
|
|
10
10
|
module Photein
|
11
|
+
class << self
|
12
|
+
def run
|
13
|
+
[Photein::Image, Photein::Video].each do |media_type|
|
14
|
+
Pathname(Photein::Config.source)
|
15
|
+
.join(Photein::Config.recursive ? '**' : '')
|
16
|
+
.join("*{#{media_type::SUPPORTED_FORMATS.join(',')}}")
|
17
|
+
.then { |glob| Dir.glob(glob, File::FNM_CASEFOLD).sort }
|
18
|
+
.map(&media_type.method(:new))
|
19
|
+
.each(&:import)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
11
23
|
end
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: photein
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
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-
|
11
|
+
date: 2021-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: debouncer
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0.2'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0.2'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: mediainfo
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,20 +80,6 @@ dependencies:
|
|
94
80
|
- - "~>"
|
95
81
|
- !ruby/object:Gem::Version
|
96
82
|
version: '0.2'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rb-inotify
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0.10'
|
104
|
-
type: :runtime
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0.10'
|
111
83
|
- !ruby/object:Gem::Dependency
|
112
84
|
name: streamio-ffmpeg
|
113
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,19 +95,19 @@ dependencies:
|
|
123
95
|
- !ruby/object:Gem::Version
|
124
96
|
version: '3.0'
|
125
97
|
- !ruby/object:Gem::Dependency
|
126
|
-
name: pry
|
98
|
+
name: pry-remote
|
127
99
|
requirement: !ruby/object:Gem::Requirement
|
128
100
|
requirements:
|
129
101
|
- - "~>"
|
130
102
|
- !ruby/object:Gem::Version
|
131
|
-
version: '0.
|
103
|
+
version: '0.1'
|
132
104
|
type: :development
|
133
105
|
prerelease: false
|
134
106
|
version_requirements: !ruby/object:Gem::Requirement
|
135
107
|
requirements:
|
136
108
|
- - "~>"
|
137
109
|
- !ruby/object:Gem::Version
|
138
|
-
version: '0.
|
110
|
+
version: '0.1'
|
139
111
|
- !ruby/object:Gem::Dependency
|
140
112
|
name: rspec
|
141
113
|
requirement: !ruby/object:Gem::Requirement
|