photein 0.0.5 → 0.0.9

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: f2e520a4d9508ba94604273c88b9f0d3bb1052aae63cef56c856595e5b0b3866
4
- data.tar.gz: 14633d71731ff23c3350c861bfe33d289bfb53777d6369a6245c203de93dacde
3
+ metadata.gz: 7b3bcb770e66b470a49fd5f486560c447e92ba8df6daa33434c45fdd3564a2e7
4
+ data.tar.gz: 83df115282d7cef28bfd2f32a42643a843726ea40c8eda1de5e7908f1b7f4832
5
5
  SHA512:
6
- metadata.gz: cf796a670cc45ce45238b4e9b0e23641ce1dc21aa67008bb016e9507b0470bebdd4790620bfd5325eabdcbe833165588c32811b244b5f8271ff8fa05d5b2d473
7
- data.tar.gz: deaea8ad0c1ad541c5e160ea927dbfef115902a7f25b96205e67c780e45477a0def030fe1dc4ba2063cc3c936fb7298ccb9f7f4dca70069e4e7ac4ec7fdd586f
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
- ~/Pictures ~/Pictures
18
- └── _inbox ├── _inbox
19
- ├── 1619593208911.jpeg ├── 2020
20
- ├── DCIM├── 2020-08-01_113129.heic
21
- │ └── 2021_03_26 └── 2020-05-20_160209.png
22
- │ ├── R0014285.MOV └── 2021
23
- │ ├── R0014286.DNG ├── 2021-02-12_081933a.jpg
24
- │ ├── R0014286.JPG ├── 2021-02-12_081933b.jpg
25
- │ ├── R0014287.DNG ├── 2021-02-12_081939.mp4
26
- │ └── R0014287.JPG ├── 2021-03-26_161245.mp4
27
- ├── IMG_20210212_081933_001.jpg ├── 2021-03-26_161518.dng
28
- ├── IMG_20210212_081933_002.jpg ├── 2021-03-26_161518.jpg
29
- ├── IMG_8953.HEIC ├── 2021-03-26_170304.dng
30
- ├── Screenshot_20200520_160209.png ├── 2021-03-26_170304.jpg
31
- └── VID_20210212_081939.mp4 └── 2021-04-28_000008.jpg
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
- > (👀 looking at you, Apple Photos), Photein is not for you.
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::Logger.open
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::Logger.fatal(e.message)
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
- image_formats = Photein::Image::SUPPORTED_FORMATS
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
@@ -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 parse_opts!
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::Logger.info "optimizing #{path}"
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::Logger.info "optimizing #{path}"
46
- Optipng.optimize(tempfile, level: 4) unless Photein::Config.dry_run
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:
@@ -4,6 +4,14 @@ require 'logger'
4
4
  require 'singleton'
5
5
 
6
6
  module Photein
7
+ class << self
8
+ attr_writer :logger
9
+
10
+ def logger
11
+ @logger ||= Photein::Logger
12
+ end
13
+ end
14
+
7
15
  class Logger
8
16
  include Singleton
9
17
 
@@ -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::Logger.info(<<~MSG.chomp)
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::Logger.fatal("skipping #{path}: file in use by #{cmd} (PID #{pid})")
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::Logger.info('conflicting timestamp found; adding counter to existing file')
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.9'
5
5
  end
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::Logger.info("transcoding #{tempfile}")
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.5
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-05-28 00:00:00.000000000 Z
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.14'
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.14'
110
+ version: '0.1'
139
111
  - !ruby/object:Gem::Dependency
140
112
  name: rspec
141
113
  requirement: !ruby/object:Gem::Requirement