audiothority 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.
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Summary
5
+ def initialize(state)
6
+ @state = state
7
+ end
8
+
9
+ def display(console)
10
+ if @state.any?
11
+ @state.each do |path, violations|
12
+ console.say %(#{path} is inconsistent due to:)
13
+ violations.each do |violation|
14
+ checkmark = console.set_color(%( ✗ ), :red, :bold)
15
+ console.say(checkmark + violation.message)
16
+ end
17
+ end
18
+ else
19
+ checkmark = console.set_color(%( ✓ ), :green, :bold)
20
+ console.say(checkmark + %(All is good))
21
+ end
22
+ end
23
+ end
24
+
25
+ class PathsOnlySummary < Summary
26
+ def display(console)
27
+ @state.each do |path, _|
28
+ console.say(File.expand_path(path))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Tracker
5
+ def initialize
6
+ @suspects = {}
7
+ end
8
+
9
+ def suspects
10
+ @suspects.freeze
11
+ end
12
+
13
+ def mark(path, violations)
14
+ @suspects[path] = violations
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Validation
5
+ def initialize(valid=true)
6
+ @valid = valid
7
+ end
8
+
9
+ def valid?
10
+ !!@valid
11
+ end
12
+
13
+ def invalid?
14
+ !valid?
15
+ end
16
+ end
17
+
18
+ class Violation < Validation
19
+ attr_reader :field, :reason, :message
20
+
21
+ def initialize(field, reason, message, applicable=true)
22
+ super(false)
23
+ @field = field
24
+ @reason = reason
25
+ @message = message
26
+ @applicable = applicable
27
+ end
28
+
29
+ def applicable?
30
+ !!@applicable
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ module Validators
5
+ def self.default
6
+ [Artist, Album, TrackNumber, Year].map(&:new)
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'audiothority/validators/unique'
12
+ require 'audiothority/validators/album'
13
+ require 'audiothority/validators/artist'
14
+ require 'audiothority/validators/track'
15
+ require 'audiothority/validators/year'
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ module Validators
5
+ class Album < Unique
6
+ def initialize
7
+ super(:album)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ module Validators
5
+ class Artist < Unique
6
+ def initialize
7
+ super(:artist)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ module Validators
5
+ class TrackNumber
6
+ def validate(tags)
7
+ missing = tags.map(&:track).select(&:zero?)
8
+ if missing.empty?
9
+ Validation.new
10
+ else
11
+ Violation.new(:track, :missing, 'track(s) without track numbers', false)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ module Validators
5
+ class Unique
6
+ def initialize(thing)
7
+ @thing = thing
8
+ end
9
+
10
+ def validate(tags)
11
+ items = tags.map { |t| t.send(@thing) }.uniq
12
+ if items.one?
13
+ Validation.new
14
+ elsif items.compact.empty?
15
+ Violation.new(@thing, :missing, %(missing #{@thing} field))
16
+ elsif items.size > 1
17
+ Violation.new(@thing, :multiple, %(multiple #{@thing}s: #{items.map(&:inspect).join(', ')}))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ module Validators
5
+ class Year < Unique
6
+ def initialize
7
+ super(:year)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ VERSION = '0.1.0'.freeze
5
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Audiothority
7
+ describe Crawler do
8
+ let :crawler do
9
+ described_class.new([dir], blacklist)
10
+ end
11
+
12
+ let :dir do
13
+ double(:pathname)
14
+ end
15
+
16
+ let :blacklist do
17
+ []
18
+ end
19
+
20
+ let :child do
21
+ double(:child)
22
+ end
23
+
24
+ describe '#crawl' do
25
+ it 'does not yield non-readable paths' do
26
+ non_readable = double(readable?: false)
27
+ allow(dir).to receive(:each_child).and_yield(non_readable)
28
+ expect { |b| crawler.crawl(&b) }.to_not yield_control
29
+ end
30
+
31
+ it 'does not yield empty directories' do
32
+ empty_path = double(readable?: true, directory?: true, children: [])
33
+ allow(dir).to receive(:each_child).and_yield(empty_path)
34
+ expect { |b| crawler.crawl(&b) }.to_not yield_control
35
+ end
36
+
37
+ it 'does not yield single files' do
38
+ single_file = double(readable?: true, directory?: false)
39
+ allow(dir).to receive(:each_child).and_yield(single_file)
40
+ expect { |b| crawler.crawl(&b) }.to_not yield_control
41
+ end
42
+
43
+ it 'yields non-empty directories' do
44
+ valid_dir = double(readable?: true, directory?: true, children: ['non-empty'])
45
+ allow(dir).to receive(:each_child).and_yield(valid_dir)
46
+ expect { |b| crawler.crawl(&b) }.to yield_with_args(valid_dir)
47
+ end
48
+
49
+ context 'with blacklist' do
50
+ let :blacklist do
51
+ [/ignore/]
52
+ end
53
+
54
+ it 'does not yield paths that match the blacklist' do
55
+ blacklisted_dir = double(readable?: true, directory?: true, children: ['non-empty'], basename: 'ignored')
56
+ allow(dir).to receive(:each_child).and_yield(blacklisted_dir)
57
+ expect { |b| crawler.crawl(&b) }.to_not yield_control
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Audiothority
7
+ describe Custodian do
8
+ let :custodian do
9
+ described_class.new(custody, suspects, fileutils)
10
+ end
11
+
12
+ let :custody do
13
+ Dir.mktmpdir
14
+ end
15
+
16
+ let :suspects do
17
+ {
18
+ Pathname.new('suspect1') => double(:violations),
19
+ Pathname.new('suspect2') => double(:violations),
20
+ }
21
+ end
22
+
23
+ let :fileutils do
24
+ double(:fileutils)
25
+ end
26
+
27
+ before do
28
+ allow(fileutils).to receive(:copy_entry)
29
+ end
30
+
31
+ after do
32
+ FileUtils.remove_entry_secure(custody) if File.exists?(custody)
33
+ end
34
+
35
+ describe '#throw_in_custody' do
36
+ context 'when `custody` exists' do
37
+ before do
38
+ custodian.throw_in_custody
39
+ end
40
+
41
+ it 'moves all suspects to custody' do
42
+ expect(fileutils).to have_received(:copy_entry).with('suspect1', %(#{custody}/suspect1), anything)
43
+ expect(fileutils).to have_received(:copy_entry).with('suspect2', %(#{custody}/suspect2), anything)
44
+ end
45
+
46
+ it 'peserves attributes' do
47
+ expect(fileutils).to have_received(:copy_entry).with(anything, anything, true).twice
48
+ end
49
+ end
50
+
51
+ context 'when `custody does not exist' do
52
+ let :custody do
53
+ 'i do not exist, mkay?'
54
+ end
55
+
56
+ it 'assumes that the custody has been torched' do
57
+ expect { custodian.throw_in_custody }.to raise_error(CustodyTorchedError, /seems to have been torched/)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,126 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Audiothority
7
+ describe Extract do
8
+ let :extract do
9
+ described_class.new(file_ref)
10
+ end
11
+
12
+ let :file_ref do
13
+ double(:file_ref)
14
+ end
15
+
16
+ before do
17
+ allow(file_ref).to receive(:new).and_return(*refs)
18
+ end
19
+
20
+ describe '#as_tags' do
21
+ context 'when given a proper set of audio files' do
22
+ let :paths do
23
+ [double(:audio_file)]
24
+ end
25
+
26
+ let :refs do
27
+ [double(:file_ref, null?: false, tag: tags.first, close: nil)]
28
+ end
29
+
30
+ let :tags do
31
+ [double(:tag)]
32
+ end
33
+
34
+ it 'yields extracted tags' do
35
+ expect { |b| extract.as_tags(paths, &b) }.to yield_with_args(tags)
36
+ end
37
+
38
+ it 'closes file refs' do
39
+ extract.as_tags(paths) { }
40
+ refs.each do |ref|
41
+ expect(ref).to have_received(:close)
42
+ end
43
+ end
44
+ end
45
+
46
+ context 'when given a set of files that includes non-audio files' do
47
+ let :paths do
48
+ [double(:audio_file), double(:non_audio_file)]
49
+ end
50
+
51
+ let :refs do
52
+ [
53
+ double(:file_ref, null?: false, tag: tags.first, close: nil),
54
+ double(:file_ref, null?: true, close: nil),
55
+ ]
56
+ end
57
+
58
+ let :tags do
59
+ [double(:tag)]
60
+ end
61
+
62
+ it 'skips `null` tags' do
63
+ expect { |b| extract.as_tags(paths, &b) }.to yield_with_args(tags)
64
+ end
65
+
66
+ it 'closes `null` refs' do
67
+ extract.as_tags(paths) { }
68
+ expect(refs.last).to have_received(:close)
69
+ end
70
+
71
+ it 'closes file refs' do
72
+ extract.as_tags(paths) { }
73
+ refs.each do |ref|
74
+ expect(ref).to have_received(:close)
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'when given a set of non-audio files' do
80
+ let :paths do
81
+ [double(:non_audio_file), double(:non_audio_file)]
82
+ end
83
+
84
+ let :refs do
85
+ [
86
+ double(:file_ref, null?: true, close: nil),
87
+ double(:file_ref, null?: true, close: nil),
88
+ ]
89
+ end
90
+
91
+ it 'does not yield anything' do
92
+ expect { |b| extract.as_tags(paths, &b) }.to_not yield_control
93
+ end
94
+
95
+ it 'closes file refs' do
96
+ extract.as_tags(paths) { }
97
+ refs.each do |ref|
98
+ expect(ref).to have_received(:close)
99
+ end
100
+ end
101
+ end
102
+
103
+ context 'when given `:save` => true' do
104
+ let :paths do
105
+ [double(:audio_file)]
106
+ end
107
+
108
+ let :refs do
109
+ [double(:file_ref, null?: false, tag: tags.first, close: nil, save: nil)]
110
+ end
111
+
112
+ let :tags do
113
+ [double(:tag)]
114
+ end
115
+
116
+ it 'saves file refs before closing them' do
117
+ extract.as_tags(paths, save: true) { }
118
+ refs.each do |ref|
119
+ expect(ref).to have_received(:save).ordered
120
+ expect(ref).to have_received(:close).ordered
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,72 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Audiothority
7
+ describe Inspector do
8
+ let :inspector do
9
+ described_class.new(crawler, [validator], tracker, extract: extract)
10
+ end
11
+
12
+ let :crawler do
13
+ double(:crawler)
14
+ end
15
+
16
+ let :validator do
17
+ double(:validator)
18
+ end
19
+
20
+ let :tracker do
21
+ double(:tracker, mark: nil)
22
+ end
23
+
24
+ let :extract do
25
+ double(:extract)
26
+ end
27
+
28
+ let :path do
29
+ double(:path, children: children)
30
+ end
31
+
32
+ let :children do
33
+ [Pathname.new('file01')]
34
+ end
35
+
36
+ let :violation do
37
+ double(:violation, invalid?: true)
38
+ end
39
+
40
+ let :tags do
41
+ [double(:tag)]
42
+ end
43
+
44
+ before do
45
+ allow(crawler).to receive(:crawl).and_yield(path)
46
+ allow(extract).to receive(:as_tags).with(children).and_yield(tags)
47
+ allow(validator).to receive(:validate).with(tags).and_return(violation)
48
+ end
49
+
50
+ before do
51
+ inspector.investigate
52
+ end
53
+
54
+ describe '#investigate' do
55
+ it 'crawls for paths with violations' do
56
+ expect(crawler).to have_received(:crawl)
57
+ end
58
+
59
+ it 'extracts tags' do
60
+ expect(extract).to have_received(:as_tags).with(children)
61
+ end
62
+
63
+ it 'validates each tag set' do
64
+ expect(validator).to have_received(:validate).with(tags)
65
+ end
66
+
67
+ it 'tracks paths with violations' do
68
+ expect(tracker).to have_received(:mark).with(path, [violation])
69
+ end
70
+ end
71
+ end
72
+ end