torganiser 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/sergei-matheson/torganiser.svg?branch=master)](https://travis-ci.org/sergei-matheson/torganiser)
|
7
|
+
[![Code Climate](https://codeclimate.com/github/sergei-matheson/torganiser/badges/gpa.svg)](https://codeclimate.com/github/sergei-matheson/torganiser)
|
8
|
+
[![Test Coverage](https://codeclimate.com/github/sergei-matheson/torganiser/badges/coverage.svg)](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
|