torganiser 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fa62b57bdb8c2d67d0068a7c999f21bbd0d5cd13
4
- data.tar.gz: 876710a0c443c89d88ecc420133d1687c44b2ab1
3
+ metadata.gz: 4a6e95d03cf82ff1426755d92efe2f603e7a504d
4
+ data.tar.gz: cfa305bdc2b3fb04ecba975f6a921e0e7c87fa1f
5
5
  SHA512:
6
- metadata.gz: 6488c31821632f93787d9e1e2a25e3aa0bc0e2f8b0531b55677991a29577a2e215e8acdf2accb1561ffc1e9ee1e2ad76372ef2725ad6d98c8aa072d21ff3d4a7
7
- data.tar.gz: 4d89ba9e9a1a12944a8f21c775880273e5315028bd6702cd36d34d930802fdee939961af758d399aa900be0b43a087da1da4a0bcc4f7734ef195b53a1cf9f911
6
+ metadata.gz: 545ffd98cbdf6fd662f542a6350c45ecd7797cfb2857c36aaa6d1d5f113502a1dd82c52efa863ac8ba3d782a76274cd0c0c09c431f03e7cc97bdec953a524938
7
+ data.tar.gz: 1bc8e5eb7a7ac3d0fd475c2302482514c4da7d0725ed688b777aec05789a165dce2585cc1dd101479126407f34dc4e3cd6ba6b668deec073d7293f87e6f948df
data/Rakefile CHANGED
@@ -1,22 +1,27 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
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 "Run cane to check quality metrics"
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 "cane not available, quality task not provided."
18
+ warn 'cane not available, quality task not provided.'
18
19
  end
19
20
 
20
21
  RSpec::Core::RakeTask.new(:spec)
21
22
 
22
- task :default => [ :spec, :quality, :reek ]
23
+ RuboCop::RakeTask.new(:rubocop) do |task|
24
+ task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
25
+ end
26
+
27
+ task default: [:spec, :quality, :reek, :rubocop]
@@ -4,34 +4,44 @@ require 'torganiser'
4
4
 
5
5
  Clamp do
6
6
 
7
- option ["--version", "-v"], :flag, "Show version" do
7
+ option ['--version', '-v'], :flag, 'Show version' do
8
8
  puts Torganiser::VERSION
9
9
  exit(0)
10
10
  end
11
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
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 ["--extension", "-e"], "EXTENSION",
18
- "Extension to include eg. mp4. May be specifed multiple times",
19
- multivalued: true, attribute_name: :extensions
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 ["--ignore", "-i"], "PATTERN",
22
- "Ignore files whose path matches pattern eg. May be specifed multiple times.",
23
- multivalued: true, attribute_name: :ignore
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 "--dry-run", :flag, "If specified, no files will be moved"
25
+ option '--dry-run', :flag, 'If specified, no files will be moved'
26
26
 
27
- parameter "FILES ...", "Files or directories to organise",
28
- attribute_name: :files
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(collection,
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
@@ -1,12 +1,15 @@
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"
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 "clamp"
11
+ require 'clamp'
10
12
 
13
+ # Main module and entry point.
11
14
  module Torganiser
12
15
  end
@@ -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
- attr_reader :collection, :dry_run
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 file
14
+ def arrange(file)
15
15
  episode = EpisodeFile.new(file)
16
- move(episode, Destination.new(collection, episode))
16
+ arrange_episode(episode)
17
17
  end
18
18
 
19
19
  private
20
20
 
21
- def move episode, destination
22
- directory = destination.directory
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
- file_utils.mkdir_p directory unless File.exists? directory
25
- file_utils.mv episode.file, directory
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 collection, episode_file
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 = year.to_i if year = episode_info[: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) or raise(
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 extensions
20
+ def add_extension(extensions)
22
21
  @extensions.concat([*extensions])
23
22
  end
24
23
 
25
- def add_directory directories
24
+ def add_directory(directories)
26
25
  @directories.concat([*directories])
27
26
  end
28
27
 
29
28
  def pattern
30
- directory_pattern + "/**/" + extension_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
- "*" + (extensions.count > 0 ? ItemsPattern.new(extensions).to_s : '')
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 items
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,12 @@
1
+ module Torganiser
2
+ # Creates a regex-ready string for matching one of a set of alternatives
3
+ class MatchOne
4
+ def initialize(alternatives)
5
+ @alternatives = alternatives
6
+ end
7
+
8
+ def to_s
9
+ "(#{@alternatives.join('|')})"
10
+ end
11
+ end
12
+ 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
@@ -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
- def initialize(
7
- collection, files: [], extensions: [], ignored: [], dry_run: false
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
- def arrange episode_file
17
+
18
+ def arrange(episode_file)
24
19
  @arranger.arrange episode_file
25
20
  end
26
-
27
21
  end
28
-
29
22
  end
@@ -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? file
28
+ def ignored?(file)
30
29
  @ignored_patterns.any? { |pattern| pattern.match file }
31
30
  end
32
31
 
33
- def ignore ignored
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 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 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