phtools 0.3.0 → 0.4.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
  SHA1:
3
- metadata.gz: 921ff0d0c9821c060f63be602f1ac915100cd08c
4
- data.tar.gz: bd7b1d4d522b91cbf0e968660efc2cedafb27e0d
3
+ metadata.gz: 1f645b7d7f323aba0cea04b8a640b3d84d383b20
4
+ data.tar.gz: 0ccb3bb5953014a8cf1f194f35d62be78a6a5828
5
5
  SHA512:
6
- metadata.gz: 8f76752ffec443ad1cfd8eed65ba440d83b2511f8152cf9c7bbf6e16d6cda16529729357189fce04888039e18761aaa5f327f98efefc880030259e0aee0bc29d
7
- data.tar.gz: 87ac9775742fb7840b69e7a71d9c322c7d9f6184f4cb02e48a267728bcf6308b39fa47c183048e65dc17cb6e7808a3983a735dd704e5eadd58e119ea0ca01d0b
6
+ metadata.gz: eec57813519d3cc5f553f92f8e867101e52e218a09bd9396f874b175ad06616555c83c67f09c036b835ff967f50393b239e61e5882ec3692f5997b8cee0b4de7
7
+ data.tar.gz: f4b0f09eb7f01aafb9bef15af263117e442799a6899baa4b5cd8b5e35bd71e3c43c7ad672050418b43f620084edc3e8646c1be68425c6730700d2c18fdbd48c2
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
- --format documentation
1
+ --format progress
2
2
  --color
data/README.md CHANGED
@@ -1,22 +1,31 @@
1
1
  [![Version ](https://img.shields.io/gem/v/phtools.svg?style=flat)](https://rubygems.org/gems/phtools)
2
2
  # PHTOOLS by ANB
3
- A bundle of small CLI tools for arranging, renaming, tagging of the photo and video files. Helps keep photo-video assets in order.
3
+ A bundle of small CLI tools for arranging, renaming, tagging of the photo and video files. Helps to keep photo-video assets in order.
4
4
 
5
- ##Installation
5
+ ## Rationale
6
+ PHTOOLS is an instrument intended for photographers\photo enthusiasts who:
7
+ * own tons of photo-video files and want to keep it in order
8
+ * really don't like the way how digital cameras name the files: P1193691.JPG, IMP_1409.JPG, _DSC1459.ARW etc.
9
+ * for photo storage prefer usage of traditional File System (folder structure) instead of "black box" databases of media managers (like iPhoto, Photoshop etc.)
10
+ * would like to have date-time-original info in the name of the file
11
+ * expects that sorting folder content "by name" will arrange photo-video assets in chronological order
12
+ * for some events (wedding, holydays etc.) have photos from different authors and would like to keep visible author name (nik) in the file name
13
+ * appreciate the use of internal metadata (EXIF, XMP etc.) beleiving it is the best way to keep context info of the picture
14
+ * are Ok with the use of Command Line tools
15
+
16
+ ## Installation
6
17
  ### Install for usage
7
- Get the latest [ruby](https://www.ruby-lang.org/) (>= 2.3) installed.
18
+ 1. Get the latest [ruby](https://www.ruby-lang.org/) (>= 2.3) installed.
19
+ 2. Install ExifTool by Phil Harvey (http://www.sno.phy.queensu.ca/~phil/exiftool/).
20
+ 3. `gem install phtools`
21
+ 4. Get list of phtools: `phtools`
22
+ 5. Get usage info for particular command: `phls -h`
8
23
 
9
- Install ExifTool by Phil Harvey (http://www.sno.phy.queensu.ca/~phil/exiftool/)
10
- ```
11
- gem install phtools
12
- ```
13
24
  ### Install for development
14
- Fork or download from GitHub.
15
-
16
- ```sh
17
- bundle install
18
- ```
19
- Develop, test:
25
+ 1. Fork or download from GitHub.
26
+ 2. Install dependencies: `bundle install`
27
+ 3. Develop.
28
+ 4. Test:
20
29
  ```sh
21
30
  bundle exec rspec
22
31
  bundle exec cucumber
@@ -29,21 +38,34 @@ bundle exec guard
29
38
  ## PHTOOLS Use cases
30
39
  ### Use Case 1. Collect photos, videos, raw-photos from different sources into one place (for further processing)
31
40
 
32
- ####Given
33
- I have copies of SD Cards with photos, videos taken with DSLR camera on my Hard Disk in `~/path/to/copy/SDCard1` and in `~/path/to/copy/SDCard2`.
41
+ #### Given
42
+ I have copies of SD Cards with photos, videos taken with DSLR camera on my Hard Disk in `~/Desktop/SDCard1` and in `~/Desktop/SDCard2`.
34
43
 
35
44
  And I have empty folder `~/Desktop/assets_staging` I would like to collect all the photo-files to.
36
45
 
37
- ####When
46
+ #### When
38
47
  I run:
39
48
  ```sh
40
- cd ~/Desktop/assets_staging
41
- phls -R ~/path/to/copy/SDCard1 ~/path/to/copy/SDCard2 | phmove -a
49
+ phls -R ~/Desktop/SDCard1 ~/Desktop/SDCard2 | phmove -a ~/Desktop/assets_staging
42
50
  ```
43
51
 
44
- ####Then
52
+ #### Then
45
53
  I get all photos moved to `~/Desktop/assets_staging`.
46
54
 
47
55
  And all videos are moved to `~/Desktop/assets_staging/VIDEO`.
48
56
 
49
57
  And all raw photo-files are moved to `~/Desktop/assets_staging/RAW`.
58
+
59
+ ## PHTOOLS concepts
60
+ ### PHTOOLS Standard file name
61
+ PHTOOLS standard file name looks like this: **`YYYYmmdd-HHMMSS_AAA ORIGINAL.EXT`**, where
62
+
63
+ **YYYYmmdd-HHMMSS** - photo creation datestamp (year-month-day-hours-minutes-seconds). By default PHTOOLS use the value of EXIF tag `DateTimeOriginal` or `CreateDate` for this purpose.
64
+
65
+ **AAA** - author nikname. 3 character long, only latin alphabet supported.
66
+
67
+ **ORIGINAL.EXT** - original file name, created by digital camera.
68
+
69
+ For example, the digital camera photo file `P1193691.JPG`, taken by AndrewBiz (aka ANB), after PHTOOLS processing will look like:
70
+ `20160902-174939_ANB P1193691.JPG`
71
+
data/TODO.md CHANGED
@@ -1,6 +1,9 @@
1
1
  - [x] core - runner.rb: print class instance variables in debug mode
2
2
  - [x] phls: use init method to initialize variables
3
3
  - [x] phls: change -r to -R
4
+ - [ ] phls: make it work with .folders (like ftls did)
4
5
  - [x] phmove: create phmove tool based on ftarrange code (see ftools repo)
5
- - [ ] phmove: make parameters to set photo, video, raw folder names
6
- - [ ] phmove: -a (--arrange) parameter means to put photo, video, raw files into separate folders inside target. If -a is not set all files are moved to target directory (plain collection of files)
6
+ - [x] phmove: make target_folder as parameter not an option
7
+ - [x] phmove: -a (--arrange) parameter means to put photo, video, raw files into separate folders inside target. If -a is not set all files are moved to root of target directory (plain collection of files)
8
+ - [ ] phmove: delete unused empty RAW and VIDEO folders
9
+ - [ ] phmove: make options to set video, raw folder names
data/exe/phmove CHANGED
@@ -11,22 +11,22 @@ module PhTools
11
11
  ***************************************************
12
12
  phtools - *Keep Your Photos In Order* (c) ANB
13
13
  ***************************************************
14
- #{tool_name} moves input file(s) into WORKING_FOLDER
15
- separating photo files, RAW photo files and VIDEO files in corresponding
16
- subfolders.
14
+ #{tool_name} moves input file(s) into TARGET_FOLDER
15
+ If --arrange option is set it separates photo files, RAW photo files and VIDEO files
16
+ to corresponding subfolders.
17
17
  phtools friendly files: #{file_type * ','}
18
18
 
19
19
  Optimized to be used with other *phtools* via pipes.
20
- Example: phls | phrename -a anb | #{tool_name}
20
+ Example: phls | phrename -a anb | #{tool_name} -a target/folder
21
21
 
22
22
  Usage:
23
- #{tool_name} [-w WORKING_FOLDER] [-D]
23
+ #{tool_name} [-D] [-a] TARGET_FOLDER
24
24
  #{tool_name} -h | --help
25
25
  #{tool_name} -v | --version
26
26
 
27
27
  Options:
28
- -w FLD --working_folder=FLD Folder the input files to be
29
- moved to [default: .]
28
+ -a --arrange Move photos to TARGET_FOLDER, videos to TARGET_FOLDER/VIDEO
29
+ raw-files to TARGET_FOLDER/RAW
30
30
  -D --debug Turn on debugging (verbose) mode
31
31
  -h --help Show this screen.
32
32
  -v --version Show version.
data/exe/phrename ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # (c) ANB Andrew Bizyaev
4
+
5
+ module PhTools
6
+ tool_name = File.basename(__FILE__)
7
+ require "#{tool_name}"
8
+
9
+ file_type = FILE_TYPE_IMAGE + FILE_TYPE_VIDEO + FILE_TYPE_AUDIO
10
+ usage = <<DOCOPT
11
+ ***************************************************
12
+ phtools - *Keep Your Photos In Order* (c) ANB
13
+ ***************************************************
14
+ #{tool_name} renames the input files based on EXIF
15
+ DateTimeOriginal (or CreateDate) tag and author NICKNAME provided.
16
+ The target file name format is YYYYmmdd-HHMMSS_AAA ORIGINAL.EXT, where:
17
+ YYYYmmdd-HHMMSS - Date-Time of photo creation (DateTimeOriginal tag),
18
+ AAA - the author nickname,
19
+ ORIGINAL.EXT - the photo name given by digital camera.
20
+ Input file should be one of the types: #{file_type * ','}
21
+ Example: input file DSC03455.JPG will be renamed to 20130108-124145_ANB DSC03455.JPG
22
+ #{tool_name} is optimized to be used with other *phtools* via pipes, e.g.:
23
+ phls | #{tool_name} -a anb
24
+
25
+ This program uses external utility ExifTool by Phil Harvey
26
+ (http://www.sno.phy.queensu.ca/~phil/exiftool/). Make sure you have exiftool installed.
27
+
28
+ Usage:
29
+ #{tool_name} -a NICKNAME [-D] [-t TAG]
30
+ #{tool_name} -h | --help
31
+ #{tool_name} -v | --version
32
+
33
+ Options:
34
+ -a NICKNAME --author=NICKNAME Author nickname size should be #{PhFile::NICKNAME_SIZE} chars,
35
+ have no spaces and other non-word chars,
36
+ have no digits,
37
+ have only ASCII chars (e.g. ANB)
38
+ -t TAG --tag_date=TAG Set the TAG name containing Date_Time creation
39
+ info. The TAG value will be used instead of
40
+ standard DateTimeOriginal tag.
41
+ All existing tags and tag names you can get
42
+ using command `phls filename|phmtags -f`
43
+ -D --debug Turn on debugging (verbose) mode
44
+ -h --help Show this screen.
45
+ -v --version Show version.
46
+ DOCOPT
47
+
48
+ PhTools.const_get(tool_name.capitalize).new(usage, file_type).run!
49
+ end
data/lib/phmove.rb CHANGED
@@ -7,43 +7,49 @@ require 'phtools/runner'
7
7
  module PhTools
8
8
  class Phmove < Runner
9
9
  def self.about
10
- "moves input files into working folder"
10
+ "moves input files to target folder"
11
11
  end
12
12
  private
13
13
 
14
14
  def validate_options
15
- @working_folder = @options_cli['--working_folder'] || ''
16
- @raw_folder = File.join(@working_folder, 'RAW')
17
- @video_folder = File.join(@working_folder, 'VIDEO')
15
+ @target_folder = @options_cli['TARGET_FOLDER']
16
+ @arrange = @options_cli['--arrange']
17
+ if @arrange
18
+ @raw_folder = File.join(@target_folder, 'RAW')
19
+ @video_folder = File.join(@target_folder, 'VIDEO')
20
+ else
21
+ @raw_folder = @target_folder
22
+ @video_folder = @target_folder
23
+ end
18
24
  end
19
25
 
20
26
  def process_before
21
- fail PhTools::Error, "#{@working_folder} does not exist" unless File.exist?(@working_folder)
22
- fail PhTools::Error, "#{@working_folder} is not a directory" unless File.directory?(@working_folder)
27
+ fail PhTools::Error, "#{@target_folder} does not exist" unless File.exist?(@target_folder)
28
+ fail PhTools::Error, "#{@target_folder} is not a directory" unless File.directory?(@target_folder)
23
29
  begin
24
30
  Dir.mkdir @raw_folder unless Dir.exist?(@raw_folder)
25
31
  Dir.mkdir @video_folder unless Dir.exist?(@video_folder)
26
32
  rescue
27
- raise PhTools::Error, "Unable to make dir inside '#{@working_folder}'"
33
+ raise PhTools::Error, "Unable to make dir inside '#{@target_folder}'"
28
34
  end
29
35
  end
30
36
 
31
- def process_file(ftfile)
32
- ftfile_out = ftfile.clone
33
- file_type = ftfile.extname.slice(1..-1).downcase
37
+ def process_file(phfile)
38
+ phfile_out = phfile.clone
39
+ file_type = phfile.extname.slice(1..-1).downcase
34
40
  case
35
41
  when FILE_TYPE_IMAGE_NORMAL.include?(file_type)
36
- ftfile_out.dirname = @working_folder
42
+ phfile_out.dirname = @target_folder
37
43
  when FILE_TYPE_IMAGE_RAW.include?(file_type)
38
- ftfile_out.dirname = @raw_folder
44
+ phfile_out.dirname = @raw_folder
39
45
  when FILE_TYPE_VIDEO.include?(file_type)
40
- ftfile_out.dirname = @video_folder
46
+ phfile_out.dirname = @video_folder
41
47
  when FILE_TYPE_AUDIO.include?(file_type)
42
- ftfile_out.dirname = @working_folder
48
+ phfile_out.dirname = @target_folder
43
49
  end
44
50
 
45
- FileUtils.mv(ftfile.filename, ftfile_out.filename) unless ftfile == ftfile_out
46
- ftfile_out
51
+ FileUtils.mv(phfile.filename, phfile_out.filename) unless phfile == phfile_out
52
+ phfile_out
47
53
  rescue SystemCallError => e
48
54
  raise PhTools::Error, 'file moving - ' + e.message
49
55
  end
data/lib/phrename.rb CHANGED
@@ -3,11 +3,43 @@
3
3
  # (c) ANB Andrew Bizyaev
4
4
 
5
5
  require 'phtools/runner'
6
+ require 'phtools/mini_exiftool-2.3.0anb'
6
7
 
7
8
  module PhTools
8
9
  class Phrename < Runner
9
10
  def self.about
10
- "!UNDER CONSTRUCTION!"
11
+ "renames input files to phtools standard"
12
+ end
13
+
14
+ private
15
+
16
+ def validate_options
17
+ @author = @options_cli['--author'].upcase || ''
18
+ ok, msg = PhFile.validate_author(@author)
19
+ fail PhTools::Error, msg unless ok
20
+ @user_tag_date = @options_cli['--tag_date'] || ''
21
+ end
22
+
23
+ def process_file(phfile)
24
+ phfile_out = phfile.clone
25
+ begin
26
+ tag = MiniExiftool.new(phfile.filename, timestamps: DateTime)
27
+ rescue
28
+ raise PhTools::Error, 'EXIF tags reading'
29
+ end
30
+ if @user_tag_date.empty?
31
+ dto = tag.date_time_original || tag.create_date || PhFile::ZERO_DATE
32
+ else
33
+ fail PhTools::Error, "tag #{@user_tag_date} is not found" unless tag[@user_tag_date]
34
+ fail PhTools::Error, "tag #{@user_tag_date} is not a DateTime type" unless tag[@user_tag_date].kind_of?(DateTime)
35
+ dto = tag[@user_tag_date] || PhFile::ZERO_DATE
36
+ end
37
+ phfile_out.standardize!(date_time: dto, author: @author)
38
+ FileUtils.mv(phfile.filename, phfile_out.filename) unless
39
+ phfile == phfile_out
40
+ phfile_out
41
+ rescue => e
42
+ raise PhTools::Error, 'file renaming - ' + e.message
11
43
  end
12
44
  end
13
45
  end
@@ -0,0 +1,703 @@
1
+ # -- encoding: utf-8 --
2
+ #
3
+ # MiniExiftool
4
+ #
5
+ # This library is wrapper for the Exiftool command-line
6
+ # application (http://www.sno.phy.queensu.ca/~phil/exiftool/)
7
+ # written by Phil Harvey.
8
+ # Read and write access is done in a clean OO manner.
9
+ #
10
+ # Author: Jan Friedrich
11
+ # Copyright (c) 2007-2013 by Jan Friedrich
12
+ # Licensed under the GNU LESSER GENERAL PUBLIC LICENSE,
13
+ # Version 2.1, February 1999
14
+ #
15
+
16
+ require 'fileutils'
17
+ require 'json'
18
+ require 'pstore'
19
+ require 'rational'
20
+ require 'rbconfig'
21
+ require 'set'
22
+ require 'tempfile'
23
+ require 'time'
24
+ require 'nesty' # ANB
25
+
26
+ # Simple OO access to the Exiftool command-line application.
27
+ class MiniExiftool
28
+
29
+ VERSION = '2.3.0'
30
+
31
+ # Name of the Exiftool command-line application
32
+ @@cmd = 'exiftool'
33
+
34
+ # Hash of the standard options used when call MiniExiftool.new
35
+ @@opts = { :numerical => false, :composite => true, :ignore_minor_errors => false,
36
+ :replace_invalid_chars => false, :timestamps => Time }
37
+
38
+ # Encoding of the filesystem (filenames in command line)
39
+ @@fs_enc = Encoding.find('filesystem')
40
+
41
+ def self.opts_accessor *attrs
42
+ attrs.each do |a|
43
+ define_method a do
44
+ @opts[a]
45
+ end
46
+ define_method "#{a}=" do |val|
47
+ @opts[a] = val
48
+ end
49
+ end
50
+ end
51
+
52
+ attr_reader :filename, :errors
53
+
54
+ opts_accessor :numerical, :composite, :ignore_minor_errors,
55
+ :replace_invalid_chars, :timestamps
56
+
57
+ @@encoding_types = %w(exif iptc xmp png id3 pdf photoshop quicktime aiff mie vorbis)
58
+
59
+ def self.encoding_opt enc_type
60
+ (enc_type.to_s + '_encoding').to_sym
61
+ end
62
+
63
+ @@encoding_types.each do |enc_type|
64
+ opts_accessor encoding_opt(enc_type)
65
+ end
66
+
67
+ # +opts+ support at the moment
68
+ # * <code>:numerical</code> for numerical values, default is +false+
69
+ # * <code>:composite</code> for including composite tags while loading,
70
+ # default is +true+
71
+ # * <code>:ignore_minor_errors</code> ignore minor errors (See -m-option
72
+ # of the exiftool command-line application, default is +false+)
73
+ # * <code>:coord_format</code> set format for GPS coordinates (See
74
+ # -c-option of the exiftool command-line application, default is +nil+
75
+ # that means exiftool standard)
76
+ # * <code>:replace_invalid_chars</code> replace string for invalid
77
+ # UTF-8 characters or +false+ if no replacing should be done,
78
+ # default is +false+
79
+ # * <code>:timestamps</code> generating DateTime objects instead of
80
+ # Time objects if set to <code>DateTime</code>, default is +Time+
81
+ #
82
+ # <b>ATTENTION:</b> Time objects are created using <code>Time.local</code>
83
+ # therefore they use <em>your local timezone</em>, DateTime objects instead
84
+ # are created <em>without timezone</em>!
85
+ # * <code>:exif_encoding</code>, <code>:iptc_encoding</code>,
86
+ # <code>:xmp_encoding</code>, <code>:png_encoding</code>,
87
+ # <code>:id3_encoding</code>, <code>:pdf_encoding</code>,
88
+ # <code>:photoshop_encoding</code>, <code>:quicktime_encoding</code>,
89
+ # <code>:aiff_encoding</code>, <code>:mie_encoding</code>,
90
+ # <code>:vorbis_encoding</code> to set this specific encoding (see
91
+ # -charset option of the exiftool command-line application, default is
92
+ # +nil+: no encoding specified)
93
+ def initialize filename=nil, opts={}
94
+ @opts = @@opts.merge opts
95
+ if @opts[:convert_encoding]
96
+ warn 'Option :convert_encoding is not longer supported!'
97
+ warn 'Please use the String#encod* methods.'
98
+ end
99
+ @values = TagHash.new
100
+ @changed_values = TagHash.new
101
+ @errors = TagHash.new
102
+ load filename unless filename.nil?
103
+ end
104
+
105
+ def initialize_from_hash hash # :nodoc:
106
+ set_values hash
107
+ set_opts_by_heuristic
108
+ self
109
+ end
110
+
111
+ def initialize_from_json json # :nodoc:
112
+ @output = json
113
+ @errors.clear
114
+ parse_output
115
+ self
116
+ end
117
+
118
+ # Load the tags of filename.
119
+ def load filename
120
+ MiniExiftool.setup
121
+ unless filename && File.exist?(filename)
122
+ raise MiniExiftool::Error.new("File '#{filename}' does not exist.")
123
+ end
124
+ if File.directory?(filename)
125
+ raise MiniExiftool::Error.new("'#{filename}' is a directory.")
126
+ end
127
+ @filename = filename
128
+ @values.clear
129
+ @changed_values.clear
130
+ params = '-j '
131
+ params << (@opts[:numerical] ? '-n ' : '')
132
+ params << (@opts[:composite] ? '' : '-e ')
133
+ params << (@opts[:coord_format] ? "-c \"#{@opts[:coord_format]}\"" : '')
134
+ @@encoding_types.each do |enc_type|
135
+ if enc_val = @opts[MiniExiftool.encoding_opt(enc_type)]
136
+ params << "-charset #{enc_type}=#{enc_val} "
137
+ end
138
+ end
139
+ if run(cmd_gen(params, @filename))
140
+ parse_output
141
+ else
142
+ raise MiniExiftool::Error.new(@error_text)
143
+ end
144
+ self
145
+ end
146
+
147
+ # Reload the tags of an already read file.
148
+ def reload
149
+ load @filename
150
+ end
151
+
152
+ # Returns the value of a tag.
153
+ def [] tag
154
+ @changed_values[tag] || @values[tag]
155
+ end
156
+
157
+ # Set the value of a tag.
158
+ def []= tag, val
159
+ @changed_values[tag] = val
160
+ end
161
+
162
+ # Returns true if any tag value is changed or if the value of a
163
+ # given tag is changed.
164
+ def changed? tag=false
165
+ if tag
166
+ @changed_values.include? tag
167
+ else
168
+ !@changed_values.empty?
169
+ end
170
+ end
171
+
172
+ # Revert all changes or the change of a given tag.
173
+ def revert tag=nil
174
+ if tag
175
+ val = @changed_values.delete(tag)
176
+ res = val != nil
177
+ else
178
+ res = @changed_values.size > 0
179
+ @changed_values.clear
180
+ end
181
+ res
182
+ end
183
+
184
+ # Returns an array of the tags (original tag names) of the read file.
185
+ def tags
186
+ @values.keys.map { |key| MiniExiftool.original_tag(key) }
187
+ end
188
+
189
+ # Returns an array of all changed tags.
190
+ def changed_tags
191
+ @changed_values.keys.map { |key| MiniExiftool.original_tag(key) }
192
+ end
193
+
194
+ # Save the changes to the file.
195
+ def save
196
+ MiniExiftool.setup
197
+ return false if @changed_values.empty?
198
+ @errors.clear
199
+ temp_file = Tempfile.new('mini_exiftool')
200
+ temp_file.close
201
+ temp_filename = temp_file.path
202
+ FileUtils.cp filename.encode(@@fs_enc), temp_filename
203
+ all_ok = true
204
+ @changed_values.each do |tag, val|
205
+ original_tag = MiniExiftool.original_tag(tag)
206
+ arr_val = val.kind_of?(Array) ? val : [val]
207
+ arr_val.map! {|e| convert_before_save(e)}
208
+ params = '-q -P -overwrite_original '
209
+ params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '')
210
+ params << (@opts[:ignore_minor_errors] ? '-m ' : '')
211
+ arr_val.each do |v|
212
+ params << %Q(-#{original_tag}=#{escape(v)} )
213
+ end
214
+ result = run(cmd_gen(params, temp_filename))
215
+ unless result
216
+ all_ok = false
217
+ @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp
218
+ end
219
+ end
220
+ if all_ok
221
+ FileUtils.cp temp_filename, filename.encode(@@fs_enc)
222
+ reload
223
+ end
224
+ temp_file.delete
225
+ all_ok
226
+ end
227
+
228
+ def save!
229
+ unless save
230
+ err = []
231
+ @errors.each do |key, value|
232
+ err << "(#{key}) #{value}"
233
+ end
234
+ raise MiniExiftool::Error.new("MiniExiftool couldn't save. The following errors occurred: #{err.empty? ? "None" : err.join(", ")}")
235
+ end
236
+ end
237
+
238
+ # Returns a hash of the original loaded values of the MiniExiftool
239
+ # instance.
240
+ def to_hash
241
+ result = {}
242
+ @values.each do |k,v|
243
+ result[MiniExiftool.original_tag(k)] = v
244
+ end
245
+ result
246
+ end
247
+
248
+ # Returns a YAML representation of the original loaded values of the
249
+ # MiniExiftool instance.
250
+ def to_yaml
251
+ to_hash.to_yaml
252
+ end
253
+
254
+ # Create a MiniExiftool instance from a hash. Default value
255
+ # conversions will be applied if neccesary.
256
+ def self.from_hash hash, opts={}
257
+ instance = MiniExiftool.new nil, opts
258
+ instance.initialize_from_hash hash
259
+ instance
260
+ end
261
+
262
+ # Create a MiniExiftool instance from JSON data. Default value
263
+ # conversions will be applied if neccesary.
264
+ def self.from_json json, opts={}
265
+ instance = MiniExiftool.new nil, opts
266
+ instance.initialize_from_json json
267
+ instance
268
+ end
269
+
270
+ # Create a MiniExiftool instance from YAML data created with
271
+ # MiniExiftool#to_yaml
272
+ def self.from_yaml yaml, opts={}
273
+ MiniExiftool.from_hash YAML.load(yaml), opts
274
+ end
275
+
276
+ # Returns the command name of the called Exiftool application.
277
+ def self.command
278
+ @@cmd
279
+ end
280
+
281
+ # Setting the command name of the called Exiftool application.
282
+ def self.command= cmd
283
+ @@cmd = cmd
284
+ end
285
+
286
+ # Returns the options hash.
287
+ def self.opts
288
+ @@opts
289
+ end
290
+
291
+ # Returns a set of all known tags of Exiftool.
292
+ def self.all_tags
293
+ unless defined? @@all_tags
294
+ @@all_tags = pstore_get :all_tags
295
+ end
296
+ @@all_tags
297
+ end
298
+
299
+ # Returns a set of all possible writable tags of Exiftool.
300
+ def self.writable_tags
301
+ unless defined? @@writable_tags
302
+ @@writable_tags = pstore_get :writable_tags
303
+ end
304
+ @@writable_tags
305
+ end
306
+
307
+ # Returns the original Exiftool name of the given tag
308
+ def self.original_tag tag
309
+ unless defined? @@all_tags_map
310
+ @@all_tags_map = pstore_get :all_tags_map
311
+ end
312
+ @@all_tags_map[tag]
313
+ end
314
+
315
+ # Returns the version of the Exiftool command-line application.
316
+ def self.exiftool_version
317
+ output = `#{MiniExiftool.command} -ver 2>&1`
318
+ unless $?.exitstatus == 0
319
+ raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found")
320
+ end
321
+ output.chomp!
322
+ end
323
+
324
+ def self.unify tag
325
+ tag.to_s.gsub(/[-_]/,'').downcase
326
+ end
327
+
328
+ # Exception class
329
+ class MiniExiftool::Error < Nesty::NestedStandardError; end # ANB
330
+
331
+ ############################################################################
332
+ private
333
+ ############################################################################
334
+
335
+ @@setup_done = false
336
+ def self.setup
337
+ return if @@setup_done
338
+ @@error_file = Tempfile.new 'errors'
339
+ @@error_file.close
340
+ @@setup_done = true
341
+ end
342
+
343
+ def cmd_gen arg_str='', filename
344
+ [@@cmd, arg_str.encode('UTF-8'), escape(filename.encode(@@fs_enc))].map {|s| s.force_encoding('UTF-8')}.join(' ')
345
+ end
346
+
347
+ def run cmd
348
+ if $DEBUG
349
+ $stderr.puts cmd
350
+ end
351
+ @output = `#{cmd} 2>#{@@error_file.path}`
352
+ @status = $?
353
+ unless @status.exitstatus == 0
354
+ @error_text = File.readlines(@@error_file.path).join
355
+ @error_text.force_encoding('UTF-8')
356
+ return false
357
+ else
358
+ @error_text = ''
359
+ return true
360
+ end
361
+ end
362
+
363
+ def convert_before_save val
364
+ case val
365
+ when Time
366
+ val = val.strftime('%Y:%m:%d %H:%M:%S')
367
+ end
368
+ val
369
+ end
370
+
371
+ def method_missing symbol, *args
372
+ tag_name = symbol.id2name
373
+ if tag_name.sub!(/=$/, '')
374
+ self[tag_name] = args.first
375
+ else
376
+ self[tag_name]
377
+ end
378
+ end
379
+
380
+ def parse_output
381
+ adapt_encoding
382
+ set_values JSON.parse(@output).first
383
+ end
384
+
385
+ def adapt_encoding
386
+ @output.force_encoding('UTF-8')
387
+ if @opts[:replace_invalid_chars] && !@output.valid_encoding?
388
+ @output.encode!('UTF-16le', invalid: :replace, replace: @opts[:replace_invalid_chars]).encode!('UTF-8')
389
+ end
390
+ end
391
+
392
+ def convert_after_load tag, value
393
+ return value unless value.kind_of?(String)
394
+ return value unless value.valid_encoding?
395
+ case value
396
+ when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d/
397
+ s = value.sub(/^(\d+):(\d+):/, '\1-\2-')
398
+ begin
399
+ if @opts[:timestamps] == Time
400
+ value = Time.parse(s)
401
+ elsif @opts[:timestamps] == DateTime
402
+ value = DateTime.parse(s)
403
+ else
404
+ raise MiniExiftool::Error.new("Value #{@opts[:timestamps]} not allowed for option timestamps.")
405
+ end
406
+ rescue ArgumentError
407
+ value = false
408
+ end
409
+ when /^\+\d+\.\d+$/
410
+ value = value.to_f
411
+ when /^0+[1-9]+$/
412
+ # nothing => String
413
+ when /^-?\d+$/
414
+ value = value.to_i
415
+ when %r(^(\d+)/(\d+)$)
416
+ value = Rational($1.to_i, $2.to_i)
417
+ when /^[\d ]+$/
418
+ # nothing => String
419
+ end
420
+ value
421
+ end
422
+
423
+ def set_values hash
424
+ hash.each_pair do |tag,val|
425
+ @values[tag] = convert_after_load(tag, val)
426
+ end
427
+ # Remove filename specific tags use attr_reader
428
+ # MiniExiftool#filename instead
429
+ # Cause: value of tag filename and attribute
430
+ # filename have different content, the latter
431
+ # holds the filename with full path (like the
432
+ # sourcefile tag) and the former the basename
433
+ # of the filename also there is no official
434
+ # "original tag name" for sourcefile
435
+ %w(directory filename sourcefile).each do |t|
436
+ @values.delete(t)
437
+ end
438
+ end
439
+
440
+ def set_opts_by_heuristic
441
+ @opts[:composite] = tags.include?('ImageSize')
442
+ @opts[:numerical] = self.file_size.kind_of?(Integer)
443
+ @opts[:timestamps] = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time
444
+ end
445
+
446
+ def self.pstore_get attribute
447
+ load_or_create_pstore unless defined? @@pstore
448
+ result = nil
449
+ @@pstore.transaction(true) do |ps|
450
+ result = ps[attribute]
451
+ end
452
+ result
453
+ end
454
+
455
+ @@running_on_windows = /mswin|mingw|cygwin/ === RbConfig::CONFIG['host_os']
456
+
457
+ def self.load_or_create_pstore
458
+ # This will hopefully work on *NIX and Windows systems
459
+ home = ENV['HOME'] || ENV['HOMEDRIVE'] + ENV['HOMEPATH'] || ENV['USERPROFILE']
460
+ subdir = @@running_on_windows ? '_mini_exiftool' : '.mini_exiftool'
461
+ FileUtils.mkdir_p(File.join(home, subdir))
462
+ pstore_filename = File.join(home, subdir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore')
463
+ @@pstore = PStore.new pstore_filename
464
+ if !File.exist?(pstore_filename) || File.size(pstore_filename) == 0
465
+ @@pstore.transaction do |ps|
466
+ ps[:all_tags] = all_tags = determine_tags('list')
467
+ ps[:writable_tags] = determine_tags('listw')
468
+ map = {}
469
+ all_tags.each { |k| map[unify(k)] = k }
470
+ ps[:all_tags_map] = map
471
+ end
472
+ end
473
+ end
474
+
475
+ def self.determine_tags arg
476
+ output = `#{@@cmd} -#{arg}`
477
+ lines = output.split(/\n/)
478
+ tags = Set.new
479
+ lines.each do |line|
480
+ next unless line =~ /^\s/
481
+ tags |= line.chomp.split
482
+ end
483
+ tags
484
+ end
485
+
486
+ if @@running_on_windows
487
+ def escape val
488
+ '"' << val.to_s.gsub(/([\\"])/, "\\\\\\1") << '"'
489
+ end
490
+ else
491
+ def escape val
492
+ '"' << val.to_s.gsub(/([\\"$])/, "\\\\\\1") << '"'
493
+ end
494
+ end
495
+
496
+ # Hash with indifferent access:
497
+ # DateTimeOriginal == datetimeoriginal == date_time_original
498
+ class TagHash < Hash # :nodoc:
499
+ def[] k
500
+ super(unify(k))
501
+ end
502
+ def []= k, v
503
+ super(unify(k), v)
504
+ end
505
+ def delete k
506
+ super(unify(k))
507
+ end
508
+
509
+ def unify tag
510
+ MiniExiftool.unify tag
511
+ end
512
+ end
513
+ end
514
+
515
+ # ANB - dump of real @values:
516
+ # exiftoolversion=9.41
517
+ # filesize=128 kB
518
+ # filemodifydate=2014-04-02T23:00:50+04:00
519
+ # fileaccessdate=2014-04-02T23:00:51+04:00
520
+ # fileinodechangedate=2014-04-02T23:00:50+04:00
521
+ # filepermissions=rw-r--r--
522
+ # filetype=JPEG
523
+ # mimetype=image/jpeg
524
+ # jfifversion=1.01
525
+ # exifbyteorder=Little-endian (Intel, II)
526
+ # imagedescription=
527
+ # make=SONY
528
+ # model=SLT-A65V
529
+ # xresolution=350
530
+ # yresolution=350
531
+ # resolutionunit=inches
532
+ # software=SLT-A65V v1.05
533
+ # modifydate=2014-04-02T20:50:32+00:00
534
+ # artist=Andrey Bizyaev (photographer); Andrey Bizyaev (camera owner)
535
+ # ycbcrpositioning=Co-sited
536
+ # copyright=2013 (c) Andrey Bizyaev. All Rights Reserved.
537
+ # exposuretime=1/100
538
+ # fnumber=5.6
539
+ # exposureprogram=Program AE
540
+ # iso=160
541
+ # sensitivitytype=Recommended Exposure Index
542
+ # recommendedexposureindex=160
543
+ # exifversion=230
544
+ # datetimeoriginal=2013-01-03T15:39:08+00:00
545
+ # createdate=2013-01-03T15:39:08+00:00
546
+ # componentsconfiguration=Y, Cb, Cr, -
547
+ # compressedbitsperpixel=2
548
+ # brightnessvalue=6.3375
549
+ # exposurecompensation=0
550
+ # maxaperturevalue=5.6
551
+ # meteringmode=Multi-segment
552
+ # lightsource=Unknown
553
+ # flash=Off, Did not fire
554
+ # focallength=55.0 mm
555
+ # quality=Fine
556
+ # flashexposurecomp=0
557
+ # teleconverter=None
558
+ # whitebalancefinetune=0
559
+ # rating=0
560
+ # brightness=0
561
+ # longexposurenoisereduction=On (unused)
562
+ # highisonoisereduction=Normal
563
+ # hdr=Off; Uncorrected image
564
+ # multiframenoisereduction=Off
565
+ # pictureeffect=Off
566
+ # softskineffect=Off
567
+ # vignettingcorrection=Auto
568
+ # lateralchromaticaberration=Auto
569
+ # distortioncorrection=Off
570
+ # wbshiftabgm=0 0
571
+ # faceinfooffset=94
572
+ # sonydatetime=2013-01-03T15:39:08+00:00
573
+ # sonyimagewidth=6000
574
+ # facesdetected=0
575
+ # faceinfolength=37
576
+ # metaversion=DC7303320222000
577
+ # maxaperture=5.3
578
+ # minaperture=33
579
+ # flashstatus=Built-in Flash present
580
+ # imagecount=8330
581
+ # lensmount=A-Mount
582
+ # lensformat=APS-C
583
+ # sequenceimagenumber=1
584
+ # sequencefilenumber=1
585
+ # releasemode2=Normal
586
+ # shotnumbersincepowerup=2
587
+ # sequencelength=1 shot
588
+ # cameraorientation=Horizontal (normal)
589
+ # quality2=JPEG
590
+ # sonyimageheight=4000
591
+ # modelreleaseyear=2011
592
+ # batterylevel=18%
593
+ # afpointsselected=(all)
594
+ # fileformat=ARW 2.3
595
+ # sonymodelid=SLT-A65 / SLT-A65V
596
+ # creativestyle=Standard
597
+ # colortemperature=Auto
598
+ # colorcompensationfilter=0
599
+ # scenemode=Auto
600
+ # zonematching=ISO Setting Used
601
+ # dynamicrangeoptimizer=Auto
602
+ # imagestabilization=On
603
+ # lenstype=Sony DT 16-105mm F3.5-5.6 (SAL16105)
604
+ # colormode=Standard
605
+ # lensspec=DT 16-105mm F3.5-5.6
606
+ # fullimagesize=6000x4000
607
+ # previewimagesize=1616x1080
608
+ # flashlevel=Normal
609
+ # releasemode=Normal
610
+ # sequencenumber=Single
611
+ # antiblur=On (Shooting)
612
+ # intelligentauto=On
613
+ # whitebalance=Auto
614
+ # usercomment=
615
+ # flashpixversion=100
616
+ # colorspace=sRGB
617
+ # exifimagewidth=800
618
+ # exifimageheight=534
619
+ # interopindex=R98 - DCF basic file (sRGB)
620
+ # interopversion=100
621
+ # relatedimagewidth=6000
622
+ # relatedimageheight=4000
623
+ # filesource=Digital Camera
624
+ # scenetype=Directly photographed
625
+ # customrendered=Normal
626
+ # exposuremode=Auto
627
+ # focallengthin35mmformat=82 mm
628
+ # scenecapturetype=Standard
629
+ # contrast=Normal
630
+ # saturation=Normal
631
+ # sharpness=Normal
632
+ # imageuniqueid=20140402-205030-0001
633
+ # lensinfo=16-105mm f/3.5-5.6
634
+ # lensmodel=DT 16-105mm F3.5-5.6
635
+ # gpsversionid=2.3.0.0
636
+ # gpslatituderef=North
637
+ # gpslongituderef=East
638
+ # gpsaltituderef=Above Sea Level
639
+ # gpstimestamp=11:39:09.588
640
+ # gpsstatus=Measurement Active
641
+ # gpsmeasuremode=3-Dimensional Measurement
642
+ # gpsdop=2.0026
643
+ # gpsspeedref=km/h
644
+ # gpsspeed=1.097
645
+ # gpstrackref=True North
646
+ # gpstrack=357.15
647
+ # gpsmapdatum=WGS-84
648
+ # gpsdatestamp=2013:01:03
649
+ # gpsdifferential=No Correction
650
+ # printimversion=300
651
+ # compression=JPEG (old-style)
652
+ # orientation=Horizontal (normal)
653
+ # thumbnailoffset=21840
654
+ # thumbnaillength=5859
655
+ # xmptoolkit=Image::ExifTool 9.41
656
+ # location=Дворцовая пл.
657
+ # locationshowncity=Санкт-Петербург
658
+ # locationshowncountrycode=RU
659
+ # locationshowncountryname=Russia
660
+ # locationshownprovincestate=Санкт-Петербург
661
+ # locationshownsublocation=Дворцовая пл.
662
+ # locationshownworldregion=Europe
663
+ # creator=["Andrey Bizyaev (photographer)", "Andrey Bizyaev (camera owner)"]
664
+ # rights=2013 (c) Andrey Bizyaev. All Rights Reserved.
665
+ # subject=["before-what-travel", "before-who-Andrew", "before-where-Baltic", "before-when-day", "before-why-vacation", "before-how-fine", "before-method-digicam"]
666
+ # collectionname=S-Peterburg Travel
667
+ # collectionuri=anblab.net
668
+ # country=Russia
669
+ # state=Санкт-Петербург
670
+ # iptcdigest=1569c0bffab4b64cb1107134254cf97d
671
+ # currentiptcdigest=7be9172b29568717f9bc0976c93ba53d
672
+ # codedcharacterset=UTF8
673
+ # enveloperecordversion=4
674
+ # keywords=["before-what-travel", "before-who-Andrew", "before-where-Baltic", "before-when-day", "before-why-vacation", "before-how-fine", "before-method-digicam"]
675
+ # byline=["Andrey Bizyaev (photographer)", "Andrey Bizyaev (camera owner)"]
676
+ # city=Санкт-Петербург
677
+ # sublocation=Дворцовая пл.
678
+ # provincestate=Санкт-Петербург
679
+ # countryprimarylocationname=Russia
680
+ # copyrightnotice=2013 (c) Andrey Bizyaev. All Rights Reserved.
681
+ # applicationrecordversion=4
682
+ # imagewidth=800
683
+ # imageheight=534
684
+ # encodingprocess=Baseline DCT, Huffman coding
685
+ # bitspersample=8
686
+ # colorcomponents=3
687
+ # ycbcrsubsampling=YCbCr4:4:4 (1 1)
688
+ # aperture=5.6
689
+ # gpsaltitude=0.5 m Above Sea Level
690
+ # gpsdatetime=2013-01-03T11:39:09+00:00
691
+ # gpslatitude=60 deg 0' 0.00" N
692
+ # gpslongitude=25 deg 0' 0.00" E
693
+ # gpsposition=60 deg 0' 0.00" N, 25 deg 0' 0.00" E
694
+ # imagesize=800x534
695
+ # lensid=Sony DT 16-105mm F3.5-5.6 (SAL16105)
696
+ # scalefactor35efl=1.5
697
+ # shutterspeed=1/100
698
+ # thumbnailimage=(Binary data 5859 bytes)
699
+ # circleofconfusion=0.020 mm
700
+ # fov=24.8 deg
701
+ # focallength35efl=55.0 mm (35 mm equivalent: 82.0 mm)
702
+ # hyperfocaldistance=26.80 m
703
+ # lightvalue=10.9
@@ -16,7 +16,7 @@ module PhTools
16
16
  FILE_TYPE_AUDIO = %w{wav}
17
17
 
18
18
  # phtools file name operations
19
- class FTFile
19
+ class PhFile
20
20
  include Comparable
21
21
 
22
22
  # filename constants
@@ -51,8 +51,8 @@ module PhTools
51
51
  ARGF.each_line do |line|
52
52
  filename = line.chomp
53
53
  begin
54
- FTFile.validate_file!(filename, @file_type)
55
- ftfile = FTFile.new(filename)
54
+ PhFile.validate_file!(filename, @file_type)
55
+ ftfile = PhFile.new(filename)
56
56
  @os.output process_file(ftfile)
57
57
  rescue PhTools::Error => e
58
58
  PhTools.puts_error "ERROR: '#{filename}' - #{e.message}", e
@@ -1,4 +1,4 @@
1
1
 
2
2
  module PhTools
3
- VERSION = '0.3.0'
3
+ VERSION = '0.4.0'
4
4
  end
data/lib/phtools.rb CHANGED
@@ -27,7 +27,7 @@ Please run phtools in a terminal via CLI commands:
27
27
  phfixfmd\t(#{Phfixfmd::about}),
28
28
  phls\t(#{Phls::about}),
29
29
  phmtags\t(#{Phmtags::about}),
30
- phrename \t(#{Phrename::about}),
30
+ phrename\t(#{Phrename::about}),
31
31
  phtagset\t(#{Phtagset::about}).
32
32
  For more information run these commands with -h option.
33
33
  General info about phtools usage see at https://github.com/AndrewBiz/phtools.git
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phtools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Bizyaev
@@ -241,6 +241,7 @@ email:
241
241
  executables:
242
242
  - phls
243
243
  - phmove
244
+ - phrename
244
245
  - phtools
245
246
  extensions: []
246
247
  extra_rdoc_files: []
@@ -260,6 +261,7 @@ files:
260
261
  - bin/stmux
261
262
  - exe/phls
262
263
  - exe/phmove
264
+ - exe/phrename
263
265
  - exe/phtools
264
266
  - lib/phbackup.rb
265
267
  - lib/phclname.rb
@@ -273,6 +275,7 @@ files:
273
275
  - lib/phtagset.rb
274
276
  - lib/phtools.rb
275
277
  - lib/phtools/error.rb
278
+ - lib/phtools/mini_exiftool-2.3.0anb.rb
276
279
  - lib/phtools/ph_file.rb
277
280
  - lib/phtools/runner.rb
278
281
  - lib/phtools/utils/os.rb