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 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