torganiser 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +10 -5
- data/bin/torganiser +28 -18
- data/lib/torganiser.rb +11 -8
- data/lib/torganiser/arranger.rb +18 -12
- data/lib/torganiser/episode_file.rb +3 -42
- data/lib/torganiser/file_query.rb +5 -9
- data/lib/torganiser/match_one.rb +12 -0
- data/lib/torganiser/matcher.rb +63 -0
- data/lib/torganiser/runner.rb +5 -12
- data/lib/torganiser/scanner.rb +4 -6
- data/lib/torganiser/series.rb +1 -5
- data/lib/torganiser/version.rb +2 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/torganiser/arranger_spec.rb +26 -9
- data/spec/torganiser/episode_file_spec.rb +46 -105
- data/spec/torganiser/file_query_spec.rb +28 -28
- data/spec/torganiser/match_one_spec.rb +10 -0
- data/spec/torganiser/matcher_spec.rb +142 -0
- data/spec/torganiser/runner_spec.rb +13 -49
- data/spec/torganiser/scanner_spec.rb +34 -35
- data/spec/torganiser/series_spec.rb +5 -6
- data/torganiser.gemspec +19 -18
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a6e95d03cf82ff1426755d92efe2f603e7a504d
|
4
|
+
data.tar.gz: cfa305bdc2b3fb04ecba975f6a921e0e7c87fa1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 545ffd98cbdf6fd662f542a6350c45ecd7797cfb2857c36aaa6d1d5f113502a1dd82c52efa863ac8ba3d782a76274cd0c0c09c431f03e7cc97bdec953a524938
|
7
|
+
data.tar.gz: 1bc8e5eb7a7ac3d0fd475c2302482514c4da7d0725ed688b777aec05789a165dce2585cc1dd101479126407f34dc4e3cd6ba6b668deec073d7293f87e6f948df
|
data/Rakefile
CHANGED
@@ -1,22 +1,27 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
3
|
require 'reek/rake/task'
|
4
|
+
require 'rubocop/rake_task'
|
4
5
|
|
5
6
|
Reek::Rake::Task.new
|
6
7
|
|
7
8
|
begin
|
8
9
|
require 'cane/rake_task'
|
9
10
|
|
10
|
-
desc
|
11
|
+
desc 'Run cane to check quality metrics'
|
11
12
|
Cane::RakeTask.new(:quality) do |cane|
|
12
13
|
cane.abc_max = 10
|
13
14
|
cane.add_threshold 'coverage/.last_run.json', :>=, 100
|
14
15
|
end
|
15
16
|
|
16
17
|
rescue LoadError
|
17
|
-
warn
|
18
|
+
warn 'cane not available, quality task not provided.'
|
18
19
|
end
|
19
20
|
|
20
21
|
RSpec::Core::RakeTask.new(:spec)
|
21
22
|
|
22
|
-
|
23
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
24
|
+
task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
task default: [:spec, :quality, :reek, :rubocop]
|
data/bin/torganiser
CHANGED
@@ -4,34 +4,44 @@ require 'torganiser'
|
|
4
4
|
|
5
5
|
Clamp do
|
6
6
|
|
7
|
-
option [
|
7
|
+
option ['--version', '-v'], :flag, 'Show version' do
|
8
8
|
puts Torganiser::VERSION
|
9
9
|
exit(0)
|
10
10
|
end
|
11
11
|
|
12
|
-
option [
|
13
|
-
|
14
|
-
|
15
|
-
|
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
16
|
|
17
|
-
option [
|
18
|
-
|
19
|
-
|
17
|
+
option ['--extension', '-e'], 'EXTENSION',
|
18
|
+
'Extension to include eg. mp4. May be specified multiple times',
|
19
|
+
multivalued: true, attribute_name: :extensions
|
20
20
|
|
21
|
-
option [
|
22
|
-
|
23
|
-
|
21
|
+
option ['--ignore', '-i'], 'PATTERN',
|
22
|
+
'Ignore files whose path matches pattern eg. May be specified multiple times.',
|
23
|
+
multivalued: true, attribute_name: :ignore
|
24
24
|
|
25
|
-
option
|
25
|
+
option '--dry-run', :flag, 'If specified, no files will be moved'
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
option '--copy', :flag, 'If specified, files will be copied instead of moved'
|
28
|
+
|
29
|
+
parameter 'FILES ...', 'Files or directories to organise',
|
30
|
+
attribute_name: :files
|
31
|
+
|
32
|
+
def scanner
|
33
|
+
Torganiser::Scanner.new(
|
34
|
+
files, extensions,
|
35
|
+
ignore.map { |string| Regexp.new(string) }
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def arranger
|
40
|
+
Torganiser::Arranger.new(collection, dry_run: dry_run?, copy: copy?)
|
41
|
+
end
|
29
42
|
|
30
43
|
def execute
|
31
|
-
Torganiser::Runner.new(
|
32
|
-
files: files, extensions: extensions,
|
33
|
-
ignored: ignore, dry_run: dry_run?
|
34
|
-
).run
|
44
|
+
Torganiser::Runner.new(scanner: scanner, arranger: arranger).run
|
35
45
|
end
|
36
46
|
|
37
47
|
end
|
data/lib/torganiser.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
1
|
+
require 'torganiser/version'
|
2
|
+
require 'torganiser/file_query'
|
3
|
+
require 'torganiser/series'
|
4
|
+
require 'torganiser/match_one'
|
5
|
+
require 'torganiser/matcher'
|
6
|
+
require 'torganiser/episode_file'
|
7
|
+
require 'torganiser/scanner'
|
8
|
+
require 'torganiser/arranger'
|
9
|
+
require 'torganiser/runner'
|
8
10
|
|
9
|
-
require
|
11
|
+
require 'clamp'
|
10
12
|
|
13
|
+
# Main module and entry point.
|
11
14
|
module Torganiser
|
12
15
|
end
|
data/lib/torganiser/arranger.rb
CHANGED
@@ -3,26 +3,33 @@ require 'fileutils'
|
|
3
3
|
module Torganiser
|
4
4
|
# Handles arranging episode files into a collection
|
5
5
|
class Arranger
|
6
|
+
attr_reader :collection, :dry_run, :copy
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
def initialize collection, dry_run: false
|
8
|
+
def initialize(collection, dry_run: false, copy: false)
|
10
9
|
@collection = collection
|
11
10
|
@dry_run = dry_run
|
11
|
+
@copy = copy
|
12
12
|
end
|
13
13
|
|
14
|
-
def arrange
|
14
|
+
def arrange(file)
|
15
15
|
episode = EpisodeFile.new(file)
|
16
|
-
|
16
|
+
arrange_episode(episode)
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
def
|
22
|
-
|
21
|
+
def arrange_episode(episode)
|
22
|
+
arrange_method.call episode.file, ensure_directory_for(episode)
|
23
|
+
end
|
24
|
+
|
25
|
+
def ensure_directory_for(episode)
|
26
|
+
Destination.new(collection, episode).directory.tap do |dir|
|
27
|
+
file_utils.mkdir_p dir unless File.exist? dir
|
28
|
+
end
|
29
|
+
end
|
23
30
|
|
24
|
-
|
25
|
-
file_utils.
|
31
|
+
def arrange_method
|
32
|
+
@arrange_method ||= file_utils.method(@copy ? :cp : :mv)
|
26
33
|
end
|
27
34
|
|
28
35
|
def file_utils
|
@@ -31,10 +38,9 @@ module Torganiser
|
|
31
38
|
|
32
39
|
# Models a destination for an episode file in a collection
|
33
40
|
class Destination
|
34
|
-
|
35
41
|
attr_reader :collection, :episode_file
|
36
42
|
|
37
|
-
def initialize
|
43
|
+
def initialize(collection, episode_file)
|
38
44
|
@collection = collection
|
39
45
|
@episode_file = episode_file
|
40
46
|
end
|
@@ -44,6 +50,7 @@ module Torganiser
|
|
44
50
|
end
|
45
51
|
|
46
52
|
private
|
53
|
+
|
47
54
|
def season_dir
|
48
55
|
"Season #{episode_file.season}"
|
49
56
|
end
|
@@ -55,7 +62,6 @@ module Torganiser
|
|
55
62
|
def series
|
56
63
|
episode_file.series
|
57
64
|
end
|
58
|
-
|
59
65
|
end
|
60
66
|
end
|
61
67
|
end
|
@@ -2,7 +2,6 @@ module Torganiser
|
|
2
2
|
# Models a file that contains a single episode of a TV series.
|
3
3
|
# Attempts to extract episode data, based on the filename.
|
4
4
|
class EpisodeFile
|
5
|
-
|
6
5
|
attr_reader :file
|
7
6
|
|
8
7
|
def initialize(file)
|
@@ -23,7 +22,8 @@ module Torganiser
|
|
23
22
|
|
24
23
|
def series
|
25
24
|
@series ||= begin
|
26
|
-
year =
|
25
|
+
year = episode_info[:year]
|
26
|
+
year = year.to_i if year
|
27
27
|
Series.new(series_name, year: year)
|
28
28
|
end
|
29
29
|
end
|
@@ -35,48 +35,9 @@ module Torganiser
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def episode_info
|
38
|
-
@episode_info ||= Matcher.match(basename)
|
38
|
+
@episode_info ||= Matcher.match(basename) || fail(
|
39
39
|
"Unable to parse #{file}"
|
40
40
|
)
|
41
41
|
end
|
42
|
-
|
43
|
-
# A matcher that can extract semantic information from a
|
44
|
-
# properly named file.
|
45
|
-
module Matcher
|
46
|
-
separator = '(\.|\s|\s?\-\s?)'
|
47
|
-
|
48
|
-
long_format = [
|
49
|
-
's(?<season>\d+)', # season number
|
50
|
-
'e(?<episode>\d+)', # episode number
|
51
|
-
'(e\d+)?' # optional second episode number, ignored
|
52
|
-
].join('\s?') # optionally space separated
|
53
|
-
|
54
|
-
# season number and episode number together, optionally with an 'x'
|
55
|
-
short_format = '\[?(?<season>\d+)x?(?<episode>\d{2})\]?'
|
56
|
-
|
57
|
-
# specials don't fit nicely into the season/episode model.
|
58
|
-
special = "s(?<season>0)(?<episode>0)|s(?<season>\\d+)#{separator}special"
|
59
|
-
|
60
|
-
season_info = "(#{long_format}|#{short_format}|#{special})"
|
61
|
-
|
62
|
-
# Series name, and possibly year
|
63
|
-
series_with_year = '(?<name>.*)\.(?<year>\d{4})'
|
64
|
-
series_without_year = '(?<name>.*)'
|
65
|
-
|
66
|
-
# Series without year takes precedence
|
67
|
-
series = "(#{series_with_year}|#{series_without_year})"
|
68
|
-
|
69
|
-
PATTERN = %r{^
|
70
|
-
#{series} # Series name, and possibly year
|
71
|
-
#{separator}#{season_info} # season info
|
72
|
-
#{separator}.*$ # stuff we don't care about
|
73
|
-
}ix
|
74
|
-
|
75
|
-
def self.match basename
|
76
|
-
PATTERN.match basename
|
77
|
-
end
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
42
|
end
|
82
43
|
end
|
@@ -4,7 +4,6 @@ module Torganiser
|
|
4
4
|
# The 'pattern' method returns a Dir.glob style pattern
|
5
5
|
# that can be used to match a set of files.
|
6
6
|
class FileQuery
|
7
|
-
|
8
7
|
attr_reader :directories, :extensions
|
9
8
|
|
10
9
|
def initialize(directories: nil, extensions: nil)
|
@@ -18,16 +17,16 @@ module Torganiser
|
|
18
17
|
@directories.empty?
|
19
18
|
end
|
20
19
|
|
21
|
-
def add_extension
|
20
|
+
def add_extension(extensions)
|
22
21
|
@extensions.concat([*extensions])
|
23
22
|
end
|
24
23
|
|
25
|
-
def add_directory
|
24
|
+
def add_directory(directories)
|
26
25
|
@directories.concat([*directories])
|
27
26
|
end
|
28
27
|
|
29
28
|
def pattern
|
30
|
-
directory_pattern +
|
29
|
+
directory_pattern + '/**/' + extension_pattern
|
31
30
|
end
|
32
31
|
|
33
32
|
private
|
@@ -37,23 +36,20 @@ module Torganiser
|
|
37
36
|
end
|
38
37
|
|
39
38
|
def extension_pattern
|
40
|
-
|
39
|
+
'*' + (extensions.count > 0 ? ItemsPattern.new(extensions).to_s : '')
|
41
40
|
end
|
42
41
|
|
43
42
|
# Models a pattern that matchers a series of one or more string items.
|
44
43
|
class ItemsPattern
|
45
|
-
|
46
44
|
attr_reader :items
|
47
45
|
|
48
|
-
def initialize
|
46
|
+
def initialize(items)
|
49
47
|
@items = items
|
50
48
|
end
|
51
49
|
|
52
50
|
def to_s
|
53
51
|
items.count > 1 ? "{#{items.join(',')}}" : items.first
|
54
52
|
end
|
55
|
-
|
56
53
|
end
|
57
|
-
|
58
54
|
end
|
59
55
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Torganiser
|
2
|
+
# A matcher that can extract semantic information from a
|
3
|
+
# properly named file.
|
4
|
+
class Matcher
|
5
|
+
class << self
|
6
|
+
def match(basename)
|
7
|
+
pattern.match basename
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def pattern
|
13
|
+
@pattern ||= /^
|
14
|
+
#{series} # Series name, and possibly year
|
15
|
+
#{separator}#{season_info} # season info
|
16
|
+
#{separator}.*$ # stuff we don't care about
|
17
|
+
/ix
|
18
|
+
end
|
19
|
+
|
20
|
+
def separator
|
21
|
+
@separator ||= one_of '\.', '\s', '\s?\-\s?'
|
22
|
+
end
|
23
|
+
|
24
|
+
def series
|
25
|
+
# Series with year takes precedence
|
26
|
+
one_of series_with_year, series_without_year
|
27
|
+
end
|
28
|
+
|
29
|
+
def series_without_year
|
30
|
+
'(?<name>.*)'
|
31
|
+
end
|
32
|
+
|
33
|
+
def series_with_year
|
34
|
+
'(?<name>.*)\.(?<year>\d{4})'
|
35
|
+
end
|
36
|
+
|
37
|
+
def season_info
|
38
|
+
one_of long_format, short_format, special
|
39
|
+
end
|
40
|
+
|
41
|
+
def long_format
|
42
|
+
[
|
43
|
+
's(?<season>\d+)', # season number
|
44
|
+
'e(?<episode>\d+)', # episode number
|
45
|
+
'(e\d+)?' # optional second episode number, ignored
|
46
|
+
].join('\s?') # optionally space separated
|
47
|
+
end
|
48
|
+
|
49
|
+
def short_format
|
50
|
+
'\[?(?<season>\d+)x?(?<episode>\d{2})\]?'
|
51
|
+
end
|
52
|
+
|
53
|
+
def special
|
54
|
+
# specials don't fit nicely into the season/episode model.
|
55
|
+
"s(?<season>0)0|s(?<season>\\d+)#{separator}special"
|
56
|
+
end
|
57
|
+
|
58
|
+
def one_of(*args)
|
59
|
+
MatchOne.new(args)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/torganiser/runner.rb
CHANGED
@@ -2,15 +2,9 @@ module Torganiser
|
|
2
2
|
# Runs the organisation process for a given array of
|
3
3
|
# files, extensions, and ignored files
|
4
4
|
class Runner
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
)
|
9
|
-
@scanner = Scanner.new(
|
10
|
-
files, extensions,
|
11
|
-
ignored.map { |string| Regexp.new(string) }
|
12
|
-
)
|
13
|
-
@arranger = Arranger.new(collection, dry_run: dry_run)
|
5
|
+
def initialize(scanner: scanner, arranger: arranger)
|
6
|
+
@scanner = scanner
|
7
|
+
@arranger = arranger
|
14
8
|
end
|
15
9
|
|
16
10
|
def run
|
@@ -20,10 +14,9 @@ module Torganiser
|
|
20
14
|
end
|
21
15
|
|
22
16
|
private
|
23
|
-
|
17
|
+
|
18
|
+
def arrange(episode_file)
|
24
19
|
@arranger.arrange episode_file
|
25
20
|
end
|
26
|
-
|
27
21
|
end
|
28
|
-
|
29
22
|
end
|
data/lib/torganiser/scanner.rb
CHANGED
@@ -2,7 +2,6 @@ module Torganiser
|
|
2
2
|
# Handles scanning a set of directories and files
|
3
3
|
# and returning any found episode files.
|
4
4
|
class Scanner
|
5
|
-
|
6
5
|
include Enumerable
|
7
6
|
|
8
7
|
def initialize(files, extensions, ignored)
|
@@ -26,11 +25,11 @@ module Torganiser
|
|
26
25
|
end
|
27
26
|
end
|
28
27
|
|
29
|
-
def ignored?
|
28
|
+
def ignored?(file)
|
30
29
|
@ignored_patterns.any? { |pattern| pattern.match file }
|
31
30
|
end
|
32
31
|
|
33
|
-
def ignore
|
32
|
+
def ignore(ignored)
|
34
33
|
ignored_patterns.concat([*ignored])
|
35
34
|
end
|
36
35
|
|
@@ -46,13 +45,13 @@ module Torganiser
|
|
46
45
|
end
|
47
46
|
end
|
48
47
|
|
49
|
-
def add_files
|
48
|
+
def add_files(files)
|
50
49
|
files.each do |file|
|
51
50
|
add_file file
|
52
51
|
end
|
53
52
|
end
|
54
53
|
|
55
|
-
def add_file
|
54
|
+
def add_file(file)
|
56
55
|
if File.file?(file)
|
57
56
|
ordinary_files << file
|
58
57
|
else
|
@@ -67,6 +66,5 @@ module Torganiser
|
|
67
66
|
def file_query
|
68
67
|
@file_query ||= FileQuery.new
|
69
68
|
end
|
70
|
-
|
71
69
|
end
|
72
70
|
end
|