imagesorter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f5ce17c344a92e399f3654dfef66291a1ab44fe5
4
+ data.tar.gz: aefc58447eb3ae56149cd7a0190889446680dcd0
5
+ SHA512:
6
+ metadata.gz: 5aab53b23f7146ac247d76ca4dd5a8095655363c1277d968522db98677d0a148a696d090c18573cb678c54e5a996549c00009b78bb50ee5d1415b92f933f0d20
7
+ data.tar.gz: cdd1a773adc3a435e68455818f5b59241979b4335d4ccc14a64818fe6e6fb82ee2879b49624ccae648541b08216bca371d844c97589de2e182bf50e47c79f8e6
@@ -0,0 +1,31 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Documentation cache and generated files:
14
+ /.yardoc/
15
+ /_yardoc/
16
+ /doc/
17
+ /rdoc/
18
+
19
+ ## Environment normalization:
20
+ /.bundle/
21
+ /vendor/bundle
22
+ /lib/bundler/man/
23
+
24
+ # for a library or gem, you might want to ignore these files since the code is
25
+ # intended to run in multiple environments; otherwise, check them in:
26
+ Gemfile.lock
27
+ .ruby-version
28
+ .ruby-gemset
29
+
30
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
31
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,14 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.1
5
+
6
+ Metrics/LineLength:
7
+ Max: 160
8
+
9
+ Style/Documentation:
10
+ Enabled: false
11
+
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - 'spec/**/*'
@@ -0,0 +1,33 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2017-08-09 21:58:47 +0300 using RuboCop version 0.49.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ Metrics/AbcSize:
11
+ Max: 38
12
+
13
+ # Offense count: 7
14
+ # Configuration parameters: CountComments.
15
+ Metrics/MethodLength:
16
+ Max: 54
17
+
18
+ # Offense count: 1
19
+ # Configuration parameters: CountKeywordArgs.
20
+ Metrics/ParameterLists:
21
+ Max: 7
22
+
23
+ # Offense count: 6
24
+ Style/Documentation:
25
+ Exclude:
26
+ - 'spec/**/*'
27
+ - 'test/**/*'
28
+ - 'lib/imagesorter/file_batch_processor.rb'
29
+ - 'lib/imagesorter/file_exif_categorizer.rb'
30
+ - 'lib/imagesorter/file_stat_categorizer.rb'
31
+ - 'lib/imagesorter/file_system_processor.rb'
32
+ - 'lib/imagesorter/gem.rb'
33
+ - 'lib/imagesorter/option_parser.rb'
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.1
5
+ - 2.2
6
+ - 2.3
7
+ - ruby-head
8
+
9
+ before_install:
10
+ - gem update --system
11
+ - gem --version
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] - 2017-08-12
8
+ ### Added
9
+ - Initial release
@@ -0,0 +1,46 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ ## Our Standards
8
+
9
+ Examples of behavior that contributes to creating a positive environment include:
10
+
11
+ * Using welcoming and inclusive language
12
+ * Being respectful of differing viewpoints and experiences
13
+ * Gracefully accepting constructive criticism
14
+ * Focusing on what is best for the community
15
+ * Showing empathy towards other community members
16
+
17
+ Examples of unacceptable behavior by participants include:
18
+
19
+ * The use of sexualized language or imagery and unwelcome sexual attention or advances
20
+ * Trolling, insulting/derogatory comments, and personal or political attacks
21
+ * Public or private harassment
22
+ * Publishing others' private information, such as a physical or electronic address, without explicit permission
23
+ * Other conduct which could reasonably be considered inappropriate in a professional setting
24
+
25
+ ## Our Responsibilities
26
+
27
+ Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
+
29
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ ## Scope
32
+
33
+ This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34
+
35
+ ## Enforcement
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at antmanj@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38
+
39
+ Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40
+
41
+ ## Attribution
42
+
43
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44
+
45
+ [homepage]: http://contributor-covenant.org
46
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Joakim Antman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,42 @@
1
+ # imagesorter
2
+
3
+ [![Build Status](https://travis-ci.org/anakinj/imagesorter.svg?branch=master)](https://travis-ci.org/anakinj/imagesorter)
4
+ [![Code Climate](https://codeclimate.com/github/anakinj/imagesorter/badges/gpa.svg)](https://codeclimate.com/github/anakinj/imagesorter)
5
+ [![Gem Version](https://badge.fury.io/rb/imagesorter.svg)](https://badge.fury.io/rb/imagesorter)
6
+
7
+ A command line tool that sorts your photos and videos (actually any files you tell it to sort) based on date it was created or EXIF information from the picture. The tool can do it's magic in parallel.
8
+
9
+ The tool was inspired by [https://github.com/andrewning/sortphotos](https://github.com/andrewning/sortphotos), but I wanted the process to be done in multiple threads to minimize the wait time and my pearl skills are a little rusty.
10
+
11
+
12
+ ## Installation
13
+
14
+ ```gem install imagesorter``` and you're good to go.
15
+
16
+ Currently the tool requires Ruby 2.1 or newer.
17
+
18
+ ## Usage
19
+
20
+ ```imagesorter --help``` gives you all the details
21
+
22
+ ### Destination format
23
+
24
+ The destination format is used to configure the template where into the destination the files will be copied, the template will be populated with metadata extracted from the source file.
25
+
26
+ The source file metadata is referred to with the ```%{key}``` notation. Also directives used for [formatting timestamps](https://ruby-doc.org/stdlib-2.1.1/libdoc/date/rdoc/Date.html#method-i-strftime) are available.
27
+
28
+ Keys that are not found are replaced with a empty string.
29
+
30
+ ```imagesorter -s . -d /my/dest --destination-format "%{exif.make} %{exif.model}/%Y/%m/%d/%{full_name}```
31
+
32
+ #### File metadata
33
+ | Key | Description |
34
+ | --- | --- |
35
+ | name | Name of the source file |
36
+ | extension | Extension of the source file |
37
+ | full_name | Alias for name+extension |
38
+ | exif.* | Data extracted from the image EXIF data |
39
+
40
+ #### Duplicate handling
41
+
42
+ The tool checks for duplicates and identical files are ignored. On conflicting filenames the file to be copied gets an additional tag in the filename, no existing files will be touched.
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'rspec/core/rake_task'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,18 @@
1
+ version: '{build}'
2
+
3
+ skip_tags: true
4
+
5
+ environment:
6
+ matrix:
7
+ - ruby_version: "21"
8
+ - ruby_version: "21-x64"
9
+
10
+ install:
11
+ - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
12
+ - gem install bundler --no-document -v 1.10.5
13
+ - bundle install --retry=3
14
+
15
+ test_script:
16
+ - bundle exec rake
17
+
18
+ build: off
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #--
5
+ # Copyright (c) 2003, 2004, 2005, 2006, 2007 Jim Weirich
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to
9
+ # deal in the Software without restriction, including without limitation the
10
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11
+ # sell copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in
15
+ # all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23
+ # IN THE SOFTWARE.
24
+ #++
25
+
26
+ require 'imagesorter/cmd'
27
+
28
+ Imagesorter::Cmd.new.run
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'imagesorter'
7
+ s.version = File.read(File.expand_path('../version', __FILE__)).strip
8
+ s.description = 'Command line tool for sorting photos and videos'
9
+ s.summary = 'Command line tool for sorting photos and videos'
10
+ s.authors = ['Joakim Antman']
11
+ s.email = 'antmanj@gmail.com'
12
+
13
+ s.homepage = 'https://github.com/anakinj/imagesorter'
14
+ s.license = 'MIT'
15
+ s.require_paths = ['lib']
16
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ s.bindir = 'exe'
20
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+
22
+ s.required_ruby_version = '>= 2.1'
23
+
24
+ s.add_dependency 'exifr'
25
+ s.add_dependency 'progressbar'
26
+ s.add_dependency 'r18n-core'
27
+
28
+ s.add_development_dependency 'rubocop'
29
+ s.add_development_dependency 'rspec'
30
+ s.add_development_dependency 'simplecov'
31
+ s.add_development_dependency 'bundler'
32
+ s.add_development_dependency 'rake'
33
+ s.add_development_dependency 'pry'
34
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'fileutils'
5
+ require 'ostruct'
6
+ require 'r18n-core'
7
+ require 'exifr/jpeg'
8
+
9
+ R18n.set('en')
10
+
11
+ require 'imagesorter/gem'
12
+ require 'imagesorter/sortable_file'
13
+ require 'imagesorter/categorizers/chained_categorizer'
14
+ require 'imagesorter/categorizers/file_exif_categorizer'
15
+ require 'imagesorter/categorizers/file_stat_categorizer'
16
+ require 'imagesorter/file_system_processor'
17
+ require 'imagesorter/file_batch_processor'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ module Categorizers
5
+ # Chains categorizers together, will try in order until gets a successfull result
6
+ class ChainedCategorizer
7
+ def initialize(*categorizers)
8
+ @categorizers = categorizers
9
+ end
10
+
11
+ def process(file)
12
+ result = nil
13
+ @categorizers.each do |categorizer|
14
+ result = categorizer.process(file)
15
+ return result unless result.nil?
16
+ end
17
+ end
18
+
19
+ def step_name
20
+ 'Categorizing'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ module Categorizers
5
+ class FileExifCategorizer
6
+ def process(file)
7
+ exif = EXIFR::JPEG.new(file.file)
8
+
9
+ exif.to_hash.each do |key, value|
10
+ file["exif.#{key}"] = value
11
+ end
12
+
13
+ time = exif.date_time
14
+ return nil if time.nil?
15
+ file.time = time
16
+ rescue
17
+ nil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ module Categorizers
5
+ class FileStatCategorizer
6
+ def initialize(stat)
7
+ @stat = stat
8
+ end
9
+
10
+ def process(file)
11
+ time = file.file.send(@stat)
12
+ return nil if time.nil?
13
+ file.time = time
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'imagesorter/option_parser'
4
+ require 'imagesorter'
5
+ require 'progressbar'
6
+
7
+ module Imagesorter
8
+ class Cmd
9
+ attr_reader :progressbar
10
+
11
+ def initialize
12
+ @options = Imagesorter::OptionParser.parse(ARGV)
13
+ end
14
+
15
+ def run
16
+ print_options
17
+
18
+ setup_logger
19
+ setup_locale
20
+
21
+ Imagesorter::FileBatchProcessor.new(batch_options).execute!
22
+
23
+ finalize
24
+
25
+ puts 'DONE'
26
+ exit 0
27
+ rescue Interrupt
28
+ puts 'FAIL: INTERRUPTED'
29
+ exit 1
30
+ rescue => e
31
+ puts e
32
+ exit 2
33
+ end
34
+
35
+ private
36
+
37
+ def setup_logger
38
+
39
+ if @options.logfile
40
+ Imagesorter.logger = Logger.new(@options.logfile)
41
+ end
42
+
43
+ if @options.verbose == true
44
+ Imagesorter.logger.level = Logger::DEBUG
45
+ elsif @options.silent == true && !@options.logfile
46
+ Imagesorter.logger.level = Logger::FATAL
47
+ else
48
+ Imagesorter.logger.level = Logger::INFO
49
+ end
50
+
51
+ Imagesorter.logger.formatter = proc do |severity, datetime, _progname, msg|
52
+ date_format = datetime.strftime('%Y-%m-%d %H:%M:%S')
53
+ if @options.test
54
+ "[#{date_format}] [#{severity}] TEST - #{msg}\n"
55
+ else
56
+ "[#{date_format}] [#{severity}] #{msg}\n"
57
+ end
58
+ end
59
+ end
60
+
61
+ def setup_locale
62
+ R18n.locale(@options.locale)
63
+ R18n.set(@options.locale)
64
+ end
65
+
66
+ def batch_options
67
+ options = {
68
+ source: @options.source,
69
+ recursive: @options.recursive,
70
+ extensions: @options.extensions,
71
+ processor: Imagesorter::FileSystemProcessor.new(destination: @options.dest,
72
+ destination_fmt: @options.destination_format,
73
+ copy_mode: @options.copy_mode,
74
+ test: @options.test)
75
+ }
76
+
77
+ if progressbar_enabled?
78
+ options[:progress_proc] = proc { |progress_options| progress(progress_options) }
79
+ end
80
+
81
+ options
82
+ end
83
+
84
+ def print_options
85
+ puts "Source: #{@options.source}"
86
+ puts "Destination: #{@options.dest}"
87
+ puts "Threads: #{@options.threads}"
88
+ end
89
+
90
+ def finalize
91
+ @progressbar.finish if @progressbar
92
+ end
93
+
94
+ def progressbar_enabled?
95
+ @options.silent == false && !@options.logfile.nil?
96
+ end
97
+
98
+ def progress(progress_options)
99
+ @progressbar ||= ProgressBar.create(format: '%a %e %P% %t %c of %C',
100
+ autofinish: false)
101
+ @progressbar.title = progress_options[:step]
102
+ @progressbar.total = progress_options[:total_steps]
103
+ @progressbar.increment
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ class FileBatchProcessor
5
+ attr_reader :files
6
+
7
+ def initialize(source:,
8
+ processor:,
9
+ categorizer: nil,
10
+ progress_proc: nil,
11
+ threads: 1,
12
+ extensions: nil,
13
+ recursive: false)
14
+ @dir = source
15
+ @categorizer = categorizer || Categorizers::ChainedCategorizer.new(Categorizers::FileExifCategorizer.new,
16
+ Categorizers::FileStatCategorizer.new(:ctime))
17
+ @processor = processor
18
+ @progress_proc = progress_proc
19
+ @queue = Queue.new
20
+ @files = []
21
+ @threads = threads
22
+ @recursive = recursive
23
+ @extensions = Array(extensions).map(&:upcase)
24
+ @total_steps = nil
25
+
26
+ @skipped_files = 0
27
+ end
28
+
29
+ def execute!
30
+ collect!
31
+
32
+ @total_steps = @skipped_files + @files.size * 3
33
+
34
+ process!
35
+
36
+ if @threads > 1
37
+ start_queue_workers
38
+ else
39
+ work_on_queue
40
+ end
41
+ end
42
+
43
+ def collect!
44
+ @files = []
45
+
46
+ collect_files_from_dir(@dir)
47
+ end
48
+
49
+ def collect_files_from_dir(dir)
50
+ Dir.foreach(dir) do |file|
51
+ collect_file_from_dir(dir, file)
52
+ end
53
+ end
54
+
55
+ def collect_file_from_dir(dir, file)
56
+ return if file =~ /^\.\.?$/
57
+ full_path = File.join(dir, file)
58
+
59
+ if File.directory?(full_path)
60
+ collect_files_from_dir(full_path) if @recursive
61
+ return
62
+ end
63
+
64
+ if include_file?(full_path)
65
+ @files << SortableFile.new(full_path)
66
+ else
67
+ @skipped_files += 1
68
+ end
69
+
70
+ increment('Collecting files')
71
+ end
72
+
73
+ def include_file?(file)
74
+ File.file?(file) &&
75
+ (@extensions.empty? || @extensions.include?(File.extname(file).delete('.').upcase))
76
+ end
77
+
78
+ def process!
79
+ @files.each do |file|
80
+ queue_categorizing(file)
81
+ end
82
+ end
83
+ require 'json'
84
+ def queue_categorizing(file)
85
+ queue_job do
86
+ file.process!(@categorizer)
87
+
88
+ Imagesorter.logger.debug "#{file.file.path} metadata: #{JSON.pretty_generate(file.to_h)}"
89
+
90
+ increment(@categorizer.step_name)
91
+ queue_proceesing(file)
92
+ end
93
+ end
94
+
95
+ def queue_proceesing(file)
96
+ return if @processor.nil?
97
+ queue_job do
98
+ file.process!(@processor)
99
+ increment(@processor.step_name)
100
+ end
101
+ end
102
+
103
+ def increment(step)
104
+ return if @progress_proc.nil?
105
+ @progress_proc.call(step: step.ljust(14, ' '),
106
+ total_steps: @total_steps)
107
+ end
108
+
109
+ def queue_job(&block)
110
+ @queue.push(block)
111
+ end
112
+
113
+ def work_on_queue
114
+ until @queue.empty?
115
+ job = @queue.shift
116
+ job.call
117
+ end
118
+ end
119
+
120
+ def start_queue_workers
121
+ @threads = Array.new(@threads) do
122
+ Thread.new do
123
+ work_on_queue
124
+ end
125
+ end
126
+
127
+ @threads.each(&:join)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ class FileSystemProcessor
5
+ include R18n::Helpers
6
+
7
+ attr_reader :copy_mode
8
+
9
+ def initialize(destination:,
10
+ destination_fmt: '%Y/%m/%d/%<name>s.%<extension>s',
11
+ copy_mode: :copy,
12
+ test: false)
13
+ @destination = destination
14
+ @test = test
15
+ @copy_mode = copy_mode
16
+ @destination_fmt = destination_fmt
17
+ end
18
+
19
+ def step_name
20
+ @copy_mode == :move ? 'Moving' : 'Copying'
21
+ end
22
+
23
+ def process(file)
24
+ source = file.file.path
25
+
26
+ destination_params = {
27
+ full_name: File.basename(source),
28
+ name: File.basename(source, '.*'),
29
+ extension: File.extname(source).delete('.')
30
+ }
31
+
32
+ dest = begin
33
+ File.join(@destination, format(l(file.time, @destination_fmt), file.to_h.merge(destination_params)))
34
+ rescue KeyError => e
35
+ Imagesorter.logger.warn e.message
36
+
37
+ key = /^key<(.*)> not found$/.match(e.message)[1]
38
+
39
+ unless key.nil?
40
+ destination_params[key.to_sym] = ''
41
+ retry
42
+ else
43
+ raise
44
+ end
45
+ end
46
+
47
+ return if dest.nil?
48
+
49
+ dest = handle_duplicate(source, dest)
50
+
51
+ return if dest.nil?
52
+
53
+ dest_dir = File.dirname(dest)
54
+
55
+ Imagesorter.logger.info "#{step_name} #{source} to #{dest}"
56
+
57
+ return if @test
58
+
59
+ FileUtils.mkdir_p(dest_dir) unless File.directory?(dest_dir)
60
+
61
+ if @copy_mode == :move
62
+ FileUtils.mv(source, dest)
63
+ else
64
+ FileUtils.cp(source, dest, preserve: true)
65
+ end
66
+ end
67
+
68
+ def handle_duplicate(source, dest)
69
+ return dest unless File.exist?(dest)
70
+
71
+ return nil if FileUtils.identical?(source, dest) # skip if identical
72
+
73
+ basename = File.basename(dest, '.*')
74
+ extname = File.extname(dest)
75
+ dirname = File.dirname(dest)
76
+
77
+ sequence = 1 # TODO, Figure out current sequence number
78
+
79
+ handle_duplicate(source, File.join(dirname, "#{basename}_#{sequence}#{extname}"))
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Imagesorter
6
+ def self.logger
7
+ @logger ||= Logger.new(STDOUT)
8
+ end
9
+
10
+ def self.logger=(logger)
11
+ @logger = logger
12
+ end
13
+
14
+ # Gem related helpers
15
+ module Gem
16
+ def self.root
17
+ @root ||= File.expand_path('../../..', __FILE__)
18
+ end
19
+
20
+ def self.version
21
+ @version ||= File.read(File.join(root, 'version')).strip
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ module OptionParser
5
+ DEFAULT_OPTIONS = {
6
+ silent: false,
7
+ test: false,
8
+ recursive: false,
9
+ copy_mode: :copy,
10
+ threads: 1,
11
+ locale: 'en-us',
12
+ extensions: %w[JPG JPEG MP4 MOV],
13
+ destination_format: '%Y/%m/%d/%{full_name}' # rubocop:disable Style/FormatStringToken
14
+ }.freeze
15
+
16
+ def self.parse(argv)
17
+ options = OpenStruct.new(DEFAULT_OPTIONS)
18
+
19
+ # rubocop:disable Metrics/BlockLength
20
+ parser = ::OptionParser.new do |opts|
21
+ opts.banner = 'Usage: imagesorter [options]'
22
+
23
+ opts.on('-s', '--source SOURCE_DIR', 'Source directory') do |source|
24
+ options.source = source
25
+ end
26
+
27
+ opts.on('-d', '--dest DESTINATION_DIR', 'Destination directory (will be created if it does not exist)') do |dest|
28
+ options.dest = dest
29
+ end
30
+
31
+ opts.on('-t', '--threads THREADS', "Number of threads to run the processing in (default is #{options.threads})") do |threads|
32
+ options.threads = threads
33
+ end
34
+
35
+ opts.on('-m', '--move', 'Move rather than copy') do
36
+ options.copy_mode = :move
37
+ end
38
+
39
+ opts.on('-e', '--extensions EXTENSIONS', Array, "What extensions to include (default is #{options.extensions.join(',')})") do |extensions|
40
+ options.extensions = extensions
41
+ end
42
+
43
+ opts.on('-r', '--recursive', "Iterate source recursively (default is #{options.recursive})") do |recursive|
44
+ options.recursive = recursive
45
+ end
46
+
47
+ opts.on('-l', '--logfile LOGFILE', 'Write message to given logfile. Default output is STDOUT') do |logfile|
48
+ options.logfile = logfile
49
+ end
50
+
51
+ opts.on('--locale LOCALE', "Locale to use (default is #{options.locale})") do |locale|
52
+ options.locale = locale
53
+ end
54
+
55
+ opts.on('--destination-format DESTINATION_FORMAT', "Destination format (default is #{options.destination_format})") do |destination_format|
56
+ options.destination_format = destination_format
57
+ end
58
+
59
+ opts.on('--[no-]silent', 'Run silently') do |silent|
60
+ options.silent = silent
61
+ end
62
+
63
+ opts.on('--[no-]test', 'Do a test-run without touching any files') do |test|
64
+ options.test = test
65
+ end
66
+
67
+ opts.separator ''
68
+ opts.separator 'Common options:'
69
+ opts.on_tail('-h', '--help', 'Show this message') do
70
+ puts opts
71
+ exit
72
+ end
73
+
74
+ opts.on('--verbose', 'Increase log message verbosity') do |verbose|
75
+ options.verbose = verbose
76
+ end
77
+
78
+ # Another typical switch to print the version.
79
+ opts.on_tail('-v', '--version', 'Show version') do
80
+ puts Imagesorter::Gem.version
81
+ exit
82
+ end
83
+ end
84
+
85
+ parser.parse!(argv)
86
+
87
+ validate_mandatory_options!(options)
88
+
89
+ options
90
+ rescue ::OptionParser::InvalidOption, ::OptionParser::MissingArgument => e
91
+ puts e.to_s
92
+ puts parser
93
+ exit
94
+ end
95
+
96
+ def self.validate_mandatory_options!(options)
97
+ mandatory = %i[source dest]
98
+ missing = mandatory.select { |param| options[param].nil? }
99
+ return if missing.empty?
100
+ raise ::OptionParser::MissingArgument, missing.join(', ')
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imagesorter
4
+ # Base for every sortable entity
5
+ class SortableFile < OpenStruct
6
+ def initialize(path)
7
+ super(file: File.new(path))
8
+ end
9
+
10
+ def process!(processor)
11
+ processor.process(self)
12
+ end
13
+ end
14
+ end
data/version ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: imagesorter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Joakim Antman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-08-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: exifr
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: progressbar
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: r18n-core
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Command line tool for sorting photos and videos
140
+ email: antmanj@gmail.com
141
+ executables:
142
+ - imagesorter
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".rspec"
148
+ - ".rubocop.yml"
149
+ - ".rubocop_todo.yml"
150
+ - ".travis.yml"
151
+ - CHANGELOG.md
152
+ - CODE_OF_CONDUCT.md
153
+ - Gemfile
154
+ - LICENSE
155
+ - README.md
156
+ - Rakefile
157
+ - appveyor.yml
158
+ - exe/imagesorter
159
+ - imagesorter.gemspec
160
+ - lib/imagesorter.rb
161
+ - lib/imagesorter/categorizers/chained_categorizer.rb
162
+ - lib/imagesorter/categorizers/file_exif_categorizer.rb
163
+ - lib/imagesorter/categorizers/file_stat_categorizer.rb
164
+ - lib/imagesorter/cmd.rb
165
+ - lib/imagesorter/file_batch_processor.rb
166
+ - lib/imagesorter/file_system_processor.rb
167
+ - lib/imagesorter/gem.rb
168
+ - lib/imagesorter/option_parser.rb
169
+ - lib/imagesorter/sortable_file.rb
170
+ - version
171
+ homepage: https://github.com/anakinj/imagesorter
172
+ licenses:
173
+ - MIT
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '2.1'
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubyforge_project:
191
+ rubygems_version: 2.4.6
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: Command line tool for sorting photos and videos
195
+ test_files: []