phtools 0.3.0 → 0.4.0

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
  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