torganiser 0.0.2
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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +22 -0
- data/bin/torganiser +27 -0
- data/lib/torganiser/arranger.rb +61 -0
- data/lib/torganiser/episode_file.rb +48 -0
- data/lib/torganiser/file_query.rb +59 -0
- data/lib/torganiser/runner.rb +24 -0
- data/lib/torganiser/scanner.rb +55 -0
- data/lib/torganiser/series.rb +19 -0
- data/lib/torganiser/version.rb +3 -0
- data/lib/torganiser.rb +12 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/torganiser/arranger_spec.rb +37 -0
- data/spec/torganiser/episode_file_spec.rb +63 -0
- data/spec/torganiser/file_query_spec.rb +117 -0
- data/spec/torganiser/runner_spec.rb +76 -0
- data/spec/torganiser/scanner_spec.rb +91 -0
- data/spec/torganiser/series_spec.rb +29 -0
- data/spec/torganiser_spec.rb +9 -0
- data/torganiser.gemspec +32 -0
- metadata +205 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 95da8d1d2cb1e354a078274b45135d20fabf096c
|
4
|
+
data.tar.gz: 4e131a6537e63b4443f5c882ab70ef83ec7eaf22
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 079a6dcc32b5176016b5b302038c79c33ad9f80c0870472786b5982429cbcdb4fb5c0c847146c50bbe8e38627980e802fdce1ea747bb5ce6602dbb1397158547
|
7
|
+
data.tar.gz: 32eabd36aaaad7201a196149ddedfd99f4173dc76b8020edebfdc17a907edd617f80f6db2f38af4e383820c5c4d34d096fb638ad0e7ec5c9b04134df5ec97177
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Sergei Matheson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# Torganiser
|
2
|
+
|
3
|
+
Simple utility that organises media files according to filename
|
4
|
+
|
5
|
+
|
6
|
+
[](https://travis-ci.org/sergei-matheson/torganiser)
|
7
|
+
[](https://codeclimate.com/github/sergei-matheson/torganiser)
|
8
|
+
[](https://codeclimate.com/github/sergei-matheson/torganiser)
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'torganiser'
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install torganiser
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
TODO: Write usage instructions here
|
27
|
+
|
28
|
+
## Contributing
|
29
|
+
|
30
|
+
1. Fork it ( http://github.com/<my-github-username>/torganiser/fork )
|
31
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
32
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
33
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
34
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require 'reek/rake/task'
|
4
|
+
|
5
|
+
Reek::Rake::Task.new
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'cane/rake_task'
|
9
|
+
|
10
|
+
desc "Run cane to check quality metrics"
|
11
|
+
Cane::RakeTask.new(:quality) do |cane|
|
12
|
+
cane.abc_max = 10
|
13
|
+
cane.add_threshold 'coverage/.last_run.json', :>=, 100
|
14
|
+
end
|
15
|
+
|
16
|
+
rescue LoadError
|
17
|
+
warn "cane not available, quality task not provided."
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec::Core::RakeTask.new(:spec)
|
21
|
+
|
22
|
+
task :default => [ :spec, :quality, :reek ]
|
data/bin/torganiser
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'torganiser'
|
4
|
+
|
5
|
+
Clamp do
|
6
|
+
|
7
|
+
option ["--version", "-v"], :flag, "Show version" do
|
8
|
+
puts Torganiser::VERSION
|
9
|
+
exit(0)
|
10
|
+
end
|
11
|
+
|
12
|
+
option ["--collection", "-c"], "DIRECTORY",
|
13
|
+
"Root of the collection into which to organise the files",
|
14
|
+
environment_variable: "TORGANISER_COLLECTION",
|
15
|
+
required: true
|
16
|
+
|
17
|
+
option ["--extension", "-e"], "EXTENSION", "Extension to include eg. mp4. May be specifed multiple times", multivalued: true, attribute_name: :extensions
|
18
|
+
|
19
|
+
option "--dry-run", :flag, "If specified, no files will be moved"
|
20
|
+
|
21
|
+
parameter "FILES ...", "Files or directories to organise", attribute_name: :files
|
22
|
+
|
23
|
+
def execute
|
24
|
+
Torganiser::Runner.new(collection, files: files, extensions: extensions, dry_run: dry_run?).run
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
# Handles arranging episode files into a collection
|
5
|
+
class Arranger
|
6
|
+
|
7
|
+
attr_reader :collection, :dry_run
|
8
|
+
|
9
|
+
def initialize collection, dry_run: false
|
10
|
+
@collection = collection
|
11
|
+
@dry_run = dry_run
|
12
|
+
end
|
13
|
+
|
14
|
+
def arrange file
|
15
|
+
episode = EpisodeFile.new(file)
|
16
|
+
move(episode, Destination.new(collection, episode))
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def move episode, destination
|
22
|
+
directory = destination.directory
|
23
|
+
|
24
|
+
file_utils.mkdir_p directory unless File.exists? directory
|
25
|
+
file_utils.mv episode.file, directory
|
26
|
+
end
|
27
|
+
|
28
|
+
def file_utils
|
29
|
+
@file_utils ||= dry_run ? FileUtils::DryRun : FileUtils
|
30
|
+
end
|
31
|
+
|
32
|
+
# Models a destination for an episode file in a collection
|
33
|
+
class Destination
|
34
|
+
|
35
|
+
attr_reader :collection, :episode_file
|
36
|
+
|
37
|
+
def initialize collection, episode_file
|
38
|
+
@collection = collection
|
39
|
+
@episode_file = episode_file
|
40
|
+
end
|
41
|
+
|
42
|
+
def directory
|
43
|
+
@directory ||= File.join(collection, series_dir, season_dir)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def season_dir
|
48
|
+
"Season #{episode_file.season}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def series_dir
|
52
|
+
series.display_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def series
|
56
|
+
episode_file.series
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Torganiser
|
2
|
+
# Models a file that contains a single episode of a TV series.
|
3
|
+
# Attempts to extract episode data, based on the filename.
|
4
|
+
class EpisodeFile
|
5
|
+
|
6
|
+
attr_reader :file
|
7
|
+
|
8
|
+
EPISODE_INFO_MATCHER = %r{^
|
9
|
+
(?<series>.+) # Series name, and possibly year
|
10
|
+
.s(?<season>\d+) # season number
|
11
|
+
e(?<episode>\d+) # episode number
|
12
|
+
\..*$ # everything else
|
13
|
+
}ix
|
14
|
+
|
15
|
+
YEAR_MATCHER = /^\d{4}$/
|
16
|
+
|
17
|
+
def initialize(file)
|
18
|
+
@file = file
|
19
|
+
end
|
20
|
+
|
21
|
+
def basename
|
22
|
+
@basename ||= File.basename(@file)
|
23
|
+
end
|
24
|
+
|
25
|
+
def season
|
26
|
+
@season ||= episode_info[:season].to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def episode
|
30
|
+
@episode ||= episode_info[:episode].to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def series
|
34
|
+
@series ||= begin
|
35
|
+
parts = episode_info[:series].split('.')
|
36
|
+
year = YEAR_MATCHER.match(parts.last) ? parts.pop.to_i : nil
|
37
|
+
Series.new(parts.join(' '), year:year)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def episode_info
|
44
|
+
@episode_info ||= EPISODE_INFO_MATCHER.match(basename)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Torganiser
|
2
|
+
# A file query is specified by adding allowed directories,
|
3
|
+
# and optionally, extensions.
|
4
|
+
# The 'pattern' method returns a Dir.glob style pattern
|
5
|
+
# that can be used to match a set of files.
|
6
|
+
class FileQuery
|
7
|
+
|
8
|
+
attr_reader :directories, :extensions
|
9
|
+
|
10
|
+
def initialize(directories: nil, extensions: nil)
|
11
|
+
@directories = []
|
12
|
+
@extensions = []
|
13
|
+
add_directory(directories) if directories
|
14
|
+
add_extension(extensions) if extensions
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty?
|
18
|
+
@directories.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_extension extensions
|
22
|
+
@extensions.concat([*extensions])
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_directory directories
|
26
|
+
@directories.concat([*directories])
|
27
|
+
end
|
28
|
+
|
29
|
+
def pattern
|
30
|
+
directory_pattern + "/**/" + extension_pattern
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def directory_pattern
|
36
|
+
ItemsPattern.new(directories).to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def extension_pattern
|
40
|
+
"*" + (extensions.count > 0 ? ItemsPattern.new(extensions).to_s : '')
|
41
|
+
end
|
42
|
+
|
43
|
+
# Models a pattern that matchers a series of one or more string items.
|
44
|
+
class ItemsPattern
|
45
|
+
|
46
|
+
attr_reader :items
|
47
|
+
|
48
|
+
def initialize items
|
49
|
+
@items = items
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
items.count > 1 ? "{#{items.join(',')}}" : items.first
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Torganiser
|
2
|
+
# Runs the organisation process for a given array of
|
3
|
+
# files and extensions
|
4
|
+
class Runner
|
5
|
+
|
6
|
+
def initialize(collection, files: [], extensions: [], dry_run: false)
|
7
|
+
@scanner = Scanner.new(files, extensions)
|
8
|
+
@arranger = Arranger.new(collection, dry_run: dry_run)
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
@scanner.each do |episode_file|
|
13
|
+
arrange episode_file
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def arrange episode_file
|
19
|
+
@arranger.arrange episode_file
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Torganiser
|
2
|
+
# Handles scanning a set of directories and files
|
3
|
+
# and returning any found episode files.
|
4
|
+
class Scanner
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(files, extensions)
|
9
|
+
file_query.add_extension extensions
|
10
|
+
add_files files
|
11
|
+
end
|
12
|
+
|
13
|
+
def each
|
14
|
+
ordinary_files.each do |file|
|
15
|
+
yield file
|
16
|
+
end
|
17
|
+
directory_files.each do |file|
|
18
|
+
yield file
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def directory_files
|
25
|
+
if file_query.empty?
|
26
|
+
[]
|
27
|
+
else
|
28
|
+
Dir[file_query.pattern].select { |file| File.file?(file) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_files files
|
33
|
+
files.each do |file|
|
34
|
+
add_file file
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_file file
|
39
|
+
if File.file?(file)
|
40
|
+
ordinary_files << file
|
41
|
+
else
|
42
|
+
file_query.add_directory file
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def ordinary_files
|
47
|
+
@ordinary_files ||= []
|
48
|
+
end
|
49
|
+
|
50
|
+
def file_query
|
51
|
+
@file_query ||= FileQuery.new
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Torganiser
|
2
|
+
|
3
|
+
# Models the series information from an episode file
|
4
|
+
class Series
|
5
|
+
|
6
|
+
attr_reader :name, :year
|
7
|
+
|
8
|
+
def initialize name, year: nil
|
9
|
+
@name = name
|
10
|
+
@year = year
|
11
|
+
end
|
12
|
+
|
13
|
+
def display_name
|
14
|
+
@display_name = year ? "#{name} (#{year})" : name
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/lib/torganiser.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "torganiser/version"
|
2
|
+
require "torganiser/file_query"
|
3
|
+
require "torganiser/series"
|
4
|
+
require "torganiser/episode_file"
|
5
|
+
require "torganiser/scanner"
|
6
|
+
require "torganiser/arranger"
|
7
|
+
require "torganiser/runner"
|
8
|
+
|
9
|
+
require "clamp"
|
10
|
+
|
11
|
+
module Torganiser
|
12
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
# At the top because simplecov needs to watch files being loaded
|
4
|
+
require 'simplecov'
|
5
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter 'spec/'
|
8
|
+
end
|
9
|
+
require 'torganiser'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
|
5
|
+
describe Arranger do
|
6
|
+
|
7
|
+
describe "for a collection" do
|
8
|
+
subject { Arranger.new("/my/media") }
|
9
|
+
|
10
|
+
context "when arranging a file" do
|
11
|
+
|
12
|
+
before do
|
13
|
+
allow(FileUtils).to receive(:mkdir_p)
|
14
|
+
allow(FileUtils).to receive(:mv)
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:file) { "/tmp/stuff/Waffle.Cone.2007.S01E02.HDTV.x264-LOL.mp4" }
|
18
|
+
|
19
|
+
it "creates a destination by series and season" do
|
20
|
+
expect(FileUtils).to receive(:mkdir_p).with(
|
21
|
+
"/my/media/Waffle Cone (2007)/Season 1"
|
22
|
+
)
|
23
|
+
subject.arrange file
|
24
|
+
end
|
25
|
+
|
26
|
+
it "moves the file to the destination" do
|
27
|
+
expect(FileUtils).to receive(:mv).with(
|
28
|
+
file,
|
29
|
+
"/my/media/Waffle Cone (2007)/Season 1"
|
30
|
+
)
|
31
|
+
subject.arrange file
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
|
5
|
+
describe EpisodeFile do
|
6
|
+
|
7
|
+
context "when initialized with an informative filename" do
|
8
|
+
|
9
|
+
let(:file) { "file/path/Hello.S02E01.mp4"}
|
10
|
+
|
11
|
+
subject { EpisodeFile.new(file) }
|
12
|
+
|
13
|
+
it 'extracts the base file name' do
|
14
|
+
expect(subject.basename).to eq 'Hello.S02E01.mp4'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'extracts season number' do
|
18
|
+
expect(subject.season).to eq 2
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'extracts episode number' do
|
22
|
+
expect(subject.episode).to eq 1
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'creates a series with a name and no year' do
|
26
|
+
expect(Series).to receive(:new).with(
|
27
|
+
"Hello", year: nil
|
28
|
+
)
|
29
|
+
subject.series
|
30
|
+
end
|
31
|
+
|
32
|
+
context "that contains year information" do
|
33
|
+
|
34
|
+
let(:file) { "file/path/Hello.2008.S02E01.mp4"}
|
35
|
+
|
36
|
+
it 'creates a series with a name and year' do
|
37
|
+
expect(Series).to receive(:new).with(
|
38
|
+
"Hello", year: 2008
|
39
|
+
)
|
40
|
+
subject.series
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
context "that has a series name in dot-format" do
|
46
|
+
|
47
|
+
let(:file) { "file/path/Goodbye.Hello.Hamburger.2008.S02E01.mp4"}
|
48
|
+
|
49
|
+
it 'creates a series with a name with spaces' do
|
50
|
+
expect(Series).to receive(:new).with(
|
51
|
+
"Goodbye Hello Hamburger",
|
52
|
+
anything
|
53
|
+
)
|
54
|
+
subject.series
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
|
5
|
+
describe FileQuery do
|
6
|
+
|
7
|
+
context "when initialised" do
|
8
|
+
|
9
|
+
context "with no arguments" do
|
10
|
+
|
11
|
+
it "has no directories" do
|
12
|
+
expect(subject.directories).to be_empty
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has no media extensions" do
|
16
|
+
expect(subject.extensions).to be_empty
|
17
|
+
end
|
18
|
+
|
19
|
+
it "is empty" do
|
20
|
+
expect(subject).to be_empty
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with a directory" do
|
26
|
+
subject { FileQuery.new(directories: "woot/waffle") }
|
27
|
+
|
28
|
+
it "is not empty" do
|
29
|
+
expect(subject).not_to be_empty
|
30
|
+
end
|
31
|
+
|
32
|
+
it "adds that directory to the list of search directories" do
|
33
|
+
expect(subject.directories).to eq ['woot/waffle']
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a directory and a media extension" do
|
39
|
+
|
40
|
+
subject { FileQuery.new(directories: 'mydir', extensions: 'mp4') }
|
41
|
+
|
42
|
+
it "adds that directory to the list of search directories" do
|
43
|
+
expect(subject.directories).to eq ['mydir']
|
44
|
+
end
|
45
|
+
|
46
|
+
it "adds that extension to the list of media extensions" do
|
47
|
+
expect(subject.extensions).to eq ['mp4']
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "search pattern" do
|
55
|
+
|
56
|
+
let(:extensions) { nil }
|
57
|
+
|
58
|
+
let(:directories) { 'the-dir' }
|
59
|
+
|
60
|
+
subject do
|
61
|
+
FileQuery.new(
|
62
|
+
directories: directories,
|
63
|
+
extensions: extensions
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with one directory" do
|
68
|
+
|
69
|
+
let(:directories) { "stuff" }
|
70
|
+
|
71
|
+
it "includes only that directory" do
|
72
|
+
expect(subject.pattern).to match(/^stuff\/\*\*/)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
context "with multiple directories" do
|
78
|
+
|
79
|
+
let(:directories) { ["one","two"] }
|
80
|
+
|
81
|
+
it "includes all specified directories" do
|
82
|
+
expect(subject.pattern).to match(/{one,two}\/\*\*/)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with no extensions" do
|
88
|
+
|
89
|
+
it "matches all files" do
|
90
|
+
expect(subject.pattern).to match(/\*\*\/\*/)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
context "with one extension" do
|
96
|
+
|
97
|
+
let(:extensions) { "mp4" }
|
98
|
+
|
99
|
+
it "includes only that extension" do
|
100
|
+
expect(subject.pattern).to match(/\*\*\/\*mp4/)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
context "with multiple extensions" do
|
106
|
+
|
107
|
+
let(:extensions) { ["mp4", "mov", "qt"] }
|
108
|
+
|
109
|
+
it "includes all specified extensions" do
|
110
|
+
expect(subject.pattern).to match(/\*\*\/\*{mp4,mov,qt}/)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
|
5
|
+
describe Runner do
|
6
|
+
|
7
|
+
context "when initialised with a collection, files and extensions" do
|
8
|
+
|
9
|
+
let(:collection) { double("collection") }
|
10
|
+
let(:files) { double("files") }
|
11
|
+
let(:extensions) { double("extensions") }
|
12
|
+
|
13
|
+
subject do
|
14
|
+
Runner.new(
|
15
|
+
collection,
|
16
|
+
files: files, extensions: extensions, dry_run: true
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:scanner) do
|
21
|
+
instance_double("Torganiser::Scanner", each: nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(Scanner).to receive(:new).and_return scanner
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "a scanner" do
|
29
|
+
|
30
|
+
it "is created for the files and extensions" do
|
31
|
+
expect(Scanner).to receive(:new).with(files, extensions)
|
32
|
+
subject.run
|
33
|
+
end
|
34
|
+
|
35
|
+
it "is used to retrieve episode files" do
|
36
|
+
expect(scanner).to receive(:each)
|
37
|
+
subject.run
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
context "if any episode files are found" do
|
43
|
+
|
44
|
+
let(:episode_file) { instance_double("Torganiser::EpisodeFile") }
|
45
|
+
|
46
|
+
before do
|
47
|
+
allow(scanner).to receive(:each).and_yield episode_file
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "an arranger" do
|
51
|
+
|
52
|
+
let(:arranger) do
|
53
|
+
instance_double("Torganiser::Arranger", arrange: nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
before do
|
57
|
+
allow(Arranger).to receive(:new).and_return arranger
|
58
|
+
end
|
59
|
+
|
60
|
+
it "is created for the collection, and dry run status" do
|
61
|
+
expect(Arranger).to receive(:new).with(collection, dry_run: true)
|
62
|
+
subject.run
|
63
|
+
end
|
64
|
+
|
65
|
+
it "is used to arrange episode files found by the scanner" do
|
66
|
+
expect(arranger).to receive(:arrange).with(episode_file)
|
67
|
+
subject.run
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
|
5
|
+
describe Scanner do
|
6
|
+
|
7
|
+
context "when initialised with an array of files" do
|
8
|
+
|
9
|
+
let(:files) { ["/tmp/file1", "/tmp/dir1", "/tmp/file2", "/tmp/dir2"] }
|
10
|
+
|
11
|
+
let(:extensions) { double("extensions") }
|
12
|
+
|
13
|
+
subject { Scanner.new(files, extensions) }
|
14
|
+
|
15
|
+
let(:query_pattern) { double("query pattern") }
|
16
|
+
|
17
|
+
let(:file_query) do
|
18
|
+
instance_double(
|
19
|
+
"Torganiser::FileQuery",
|
20
|
+
pattern: query_pattern,
|
21
|
+
add_directory: nil,
|
22
|
+
add_extension: nil,
|
23
|
+
empty?: false
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:query_results) do
|
28
|
+
["/tmp/dir1", "/tmp/dir1/file3", "/tmp/dir2", "/tmp/dir2/file4"]
|
29
|
+
end
|
30
|
+
|
31
|
+
before do
|
32
|
+
allow(File).to receive(:file?) { |path| path.match(/file/) }
|
33
|
+
allow(FileQuery).to receive(:new).and_return file_query
|
34
|
+
allow(Dir).to receive(:[]).and_return query_results
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "a file query" do
|
38
|
+
it "is created for any non-ordinary files" do
|
39
|
+
expect(file_query).to receive(:add_directory).with("/tmp/dir1")
|
40
|
+
expect(file_query).to receive(:add_directory).with("/tmp/dir2")
|
41
|
+
subject
|
42
|
+
end
|
43
|
+
|
44
|
+
it "is given any extensions" do
|
45
|
+
expect(file_query).to receive(:add_extension).with extensions
|
46
|
+
subject
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when enumerating" do
|
50
|
+
|
51
|
+
context "when given some directories" do
|
52
|
+
it "is used do a directory search" do
|
53
|
+
expect(Dir).to receive(:[]).with query_pattern
|
54
|
+
subject.each { |_| }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when empty" do
|
59
|
+
|
60
|
+
let(:files) { ["/tmp/file1""/tmp/file2"] }
|
61
|
+
|
62
|
+
let(:file_query) do
|
63
|
+
instance_double(
|
64
|
+
"Torganiser::FileQuery",
|
65
|
+
add_extension: nil,
|
66
|
+
empty?: true
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "is not used" do
|
71
|
+
expect(Dir).not_to receive(:[]).with query_pattern
|
72
|
+
subject.each { |_| }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it "enumerates the ordinary files, and files found in the directories" do
|
80
|
+
expect { |b| subject.each(&b) }.to yield_successive_args(
|
81
|
+
"/tmp/file1",
|
82
|
+
"/tmp/file2",
|
83
|
+
"/tmp/dir1/file3",
|
84
|
+
"/tmp/dir2/file4"
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Torganiser
|
4
|
+
|
5
|
+
describe Series do
|
6
|
+
|
7
|
+
context "when initialized with a name" do
|
8
|
+
|
9
|
+
subject { Series.new("Pear Tree") }
|
10
|
+
|
11
|
+
it 'has a display name' do
|
12
|
+
expect(subject.display_name).to eq 'Pear Tree'
|
13
|
+
end
|
14
|
+
|
15
|
+
context "and year" do
|
16
|
+
|
17
|
+
subject { Series.new("Pear Tree", year: 2009) }
|
18
|
+
|
19
|
+
it 'has a display name that includes year' do
|
20
|
+
expect(subject.display_name).to eq 'Pear Tree (2009)'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/torganiser.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'torganiser/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "torganiser"
|
8
|
+
spec.version = Torganiser::VERSION
|
9
|
+
spec.authors = [""]
|
10
|
+
spec.email = [""]
|
11
|
+
spec.summary = %q{Organises episode files according to filename.}
|
12
|
+
spec.description = %q{Organises episode files according to filename.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "clamp"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "reek"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "simplecov"
|
28
|
+
spec.add_development_dependency "cane"
|
29
|
+
|
30
|
+
spec.add_development_dependency "pry"
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: torganiser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ''
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: clamp
|
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: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
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: reek
|
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: cane
|
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: pry
|
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: codeclimate-test-reporter
|
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: Organises episode files according to filename.
|
140
|
+
email:
|
141
|
+
- ''
|
142
|
+
executables:
|
143
|
+
- torganiser
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- .gitignore
|
148
|
+
- .rspec
|
149
|
+
- .ruby-version
|
150
|
+
- .travis.yml
|
151
|
+
- Gemfile
|
152
|
+
- LICENSE.txt
|
153
|
+
- README.md
|
154
|
+
- Rakefile
|
155
|
+
- bin/torganiser
|
156
|
+
- lib/torganiser.rb
|
157
|
+
- lib/torganiser/arranger.rb
|
158
|
+
- lib/torganiser/episode_file.rb
|
159
|
+
- lib/torganiser/file_query.rb
|
160
|
+
- lib/torganiser/runner.rb
|
161
|
+
- lib/torganiser/scanner.rb
|
162
|
+
- lib/torganiser/series.rb
|
163
|
+
- lib/torganiser/version.rb
|
164
|
+
- spec/spec_helper.rb
|
165
|
+
- spec/torganiser/arranger_spec.rb
|
166
|
+
- spec/torganiser/episode_file_spec.rb
|
167
|
+
- spec/torganiser/file_query_spec.rb
|
168
|
+
- spec/torganiser/runner_spec.rb
|
169
|
+
- spec/torganiser/scanner_spec.rb
|
170
|
+
- spec/torganiser/series_spec.rb
|
171
|
+
- spec/torganiser_spec.rb
|
172
|
+
- torganiser.gemspec
|
173
|
+
homepage: ''
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - '>='
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - '>='
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.0.14
|
194
|
+
signing_key:
|
195
|
+
specification_version: 4
|
196
|
+
summary: Organises episode files according to filename.
|
197
|
+
test_files:
|
198
|
+
- spec/spec_helper.rb
|
199
|
+
- spec/torganiser/arranger_spec.rb
|
200
|
+
- spec/torganiser/episode_file_spec.rb
|
201
|
+
- spec/torganiser/file_query_spec.rb
|
202
|
+
- spec/torganiser/runner_spec.rb
|
203
|
+
- spec/torganiser/scanner_spec.rb
|
204
|
+
- spec/torganiser/series_spec.rb
|
205
|
+
- spec/torganiser_spec.rb
|