audiothority 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b9bc796f3af6115a58308fe0453019106973758
4
+ data.tar.gz: 2f131df987b5bc6f7b4fd33bea7ca7090738a760
5
+ SHA512:
6
+ metadata.gz: ff4b3dc52403c8f70b29cc18f329f37573d0b02f5e346776534bcfda70adc342e2d166e21def694267f6a40944d31c8041f6825c2dbe35a324b8d7c3f291b44c
7
+ data.tar.gz: c9349fe6267217a5d2efa4edf713bb3a8747cdaa3b326e1fabe7ce295fc982cc40e3fbf02774cfceedebd3632cddf724af5455ab9eded8f1f7d50d682f5ae1ec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Mathias Söderberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # audiothority
2
+
3
+ [![Build Status](https://travis-ci.org/mthssdrbrg/audiothority.svg?branch=master)](https://travis-ci.org/mthssdrbrg/audiothority)
4
+ [![Coverage Status](https://img.shields.io/coveralls/mthssdrbrg/audiothority.svg)](https://coveralls.io/r/mthssdrbrg/audiothority?branch=master)
5
+
6
+ Audiothority is a small command-line app for finding albums (or directories if
7
+ you prefer) with inconsistent tags among the tracks, and either enforce some
8
+ basic guidelines or move all inconsistent albums to a new directory.
9
+
10
+ Currently the `artist`, `album`, and `year` tags are checked for uniqueness, and
11
+ the `track` tag is checked for missing tack numbers.
12
+
13
+ ## Installation
14
+
15
+ Audiothority depends on `taglib-ruby`, which requires `taglib`.
16
+ See [taglib-ruby](https://github.com/robinst/taglib-ruby) for instructions on
17
+ how to install `taglib` on a few different operating systems.
18
+
19
+ ```
20
+ gem install audiothority
21
+ ```
22
+
23
+ This will make the `audiothorian` command available.
24
+ For basic usage run `audiothorian --help`, and run any of the subcommands
25
+ without arguments to see their usage (or `audiothorian help <CMD>` if you
26
+ prefer).
27
+
28
+ ## Copyright
29
+
30
+ Released under the [MIT License](http://www.opensource.org/licenses/MIT) :: 2014 Mathias Söderberg.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'audiothority'
6
+
7
+
8
+ Audiothority::Cli.start(ARGV)
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ require 'taglib'
4
+ require 'pathname'
5
+
6
+
7
+ require 'audiothority/change'
8
+ require 'audiothority/cli'
9
+ require 'audiothority/crawler'
10
+ require 'audiothority/custodian'
11
+ require 'audiothority/enforcer'
12
+ require 'audiothority/extract'
13
+ require 'audiothority/inspector'
14
+ require 'audiothority/society'
15
+ require 'audiothority/summary'
16
+ require 'audiothority/tracker'
17
+ require 'audiothority/validation'
18
+ require 'audiothority/validators'
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Change
5
+ def initialize(field, choices, tags)
6
+ @field = field
7
+ @choices = choices
8
+ @tags = tags
9
+ end
10
+
11
+ def perform
12
+ if performable?
13
+ @tags.each do |tag|
14
+ tag.send(tag_setter, value)
15
+ end
16
+ end
17
+ end
18
+
19
+ def present(display)
20
+ if performable?
21
+ ignored = @choices.reject { |k, _| k == value }
22
+ ignored = ignored.map do |v, c|
23
+ %("#{display.set_color(v, :red)}" (#{c}))
24
+ end
25
+ chosen = %("#{display.set_color(value, :green)}" (#{@choices[value]}))
26
+ table = ignored.map do |i|
27
+ [display.set_color(@field, :yellow), i, '~>', chosen]
28
+ end
29
+ display.print_table(table, indent: 2)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def tag_setter
36
+ @tag_setter ||= (@field.to_s + '=').to_sym
37
+ end
38
+
39
+ def value
40
+ @value ||= @choices.keys.last
41
+ end
42
+
43
+ def performable?
44
+ @choices.any? && (@choices.one? || majority?)
45
+ end
46
+
47
+ def majority?
48
+ @choices.values[-2..-1].uniq.size == 2
49
+ end
50
+ end
51
+
52
+ class RewriteChange < Change
53
+ def initialize(field, tags)
54
+ @field = field
55
+ @tags = tags
56
+ end
57
+
58
+ def perform
59
+ @tags.each do |tag|
60
+ tag.send(tag_setter, tag.send(@field))
61
+ end
62
+ end
63
+
64
+ def present(display)
65
+ display.say(%( #{display.set_color(@field, :yellow)} rewrite field))
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+
3
+ require 'thor'
4
+
5
+
6
+ module Audiothority
7
+ class Cli < Thor
8
+ desc 'scan PATHS', 'Scan given paths for inconsistencies'
9
+ method_option :paths_only,
10
+ desc: 'only display paths for inconsistent directories',
11
+ type: :boolean,
12
+ default: false
13
+ method_option :custody,
14
+ desc: 'move inconsistent directories to a custody directory',
15
+ aliases: '-C',
16
+ type: :string
17
+ def scan(*paths)
18
+ if paths.any?
19
+ run_scan_for(paths)
20
+ display_summary
21
+ throw_in_custody
22
+ else
23
+ self.class.task_help(console, 'scan')
24
+ end
25
+ end
26
+
27
+ desc 'enforce PATHS', 'Enforce tagging guidelines'
28
+ method_option :society,
29
+ desc: 'move enforced directories to a `society` directory',
30
+ aliases: '-S',
31
+ type: :string
32
+ def enforce(*paths)
33
+ if paths.any?
34
+ run_scan_for(paths)
35
+ display_summary
36
+ if tracker.suspects.any? && should_enforce?
37
+ execute_enforcement
38
+ end
39
+ else
40
+ self.class.task_help(console, 'enforce')
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def should_enforce?
47
+ console.yes?('enforce audiothority on violations?')
48
+ end
49
+
50
+ def console
51
+ @console ||= Thor::Shell::Color.new
52
+ end
53
+
54
+ def tracker
55
+ @tracker ||= Tracker.new
56
+ end
57
+
58
+ def run_scan_for(paths)
59
+ Inspector.scan(paths, tracker)
60
+ end
61
+
62
+ def display_summary
63
+ s = (options.paths_only? ? PathsOnlySummary : Summary).new(tracker.suspects)
64
+ s.display(console)
65
+ end
66
+
67
+ def throw_in_custody
68
+ if options.custody
69
+ c = Custodian.new(options.custody, tracker.suspects)
70
+ c.throw_in_custody
71
+ end
72
+ end
73
+
74
+ def execute_enforcement
75
+ Enforcer.new(tracker.suspects, console, society: society).enforce
76
+ end
77
+
78
+ def society
79
+ Society.new(options.society) if options.society
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Crawler
5
+ def initialize(dirs, blacklist=[])
6
+ @dirs = dirs
7
+ @blacklist = blacklist
8
+ end
9
+
10
+ def crawl
11
+ @dirs.each do |dir|
12
+ dir.each_child do |path|
13
+ if consider?(path)
14
+ yield path
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def consider?(path)
23
+ path.readable? && path.directory? && path.children.any? && !blacklisted?(path)
24
+ end
25
+
26
+ def blacklisted?(path)
27
+ @blacklist.any? { |r| path.basename.to_s.match(r) }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ require 'fileutils'
4
+
5
+
6
+ module Audiothority
7
+ CustodyTorchedError = Class.new(ArgumentError)
8
+
9
+ class Custodian
10
+ def initialize(custody, suspects, fileutils=FileUtils)
11
+ @custody = Pathname.new(custody)
12
+ @suspects = suspects
13
+ @fileutils = fileutils
14
+ end
15
+
16
+ def throw_in_custody
17
+ if @custody.exist?
18
+ @suspects.each do |path, _|
19
+ @fileutils.copy_entry(path.to_s, @custody.join(path.basename).to_s, true)
20
+ end
21
+ else
22
+ raise CustodyTorchedError, %("#{@custody}" seems to have been torched)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Enforcer
5
+ def initialize(suspects, console, options={})
6
+ @suspects = suspects
7
+ @console = console
8
+ @extract = options[:extract] || Extract.new
9
+ @society = options[:society] || EmptySociety.new
10
+ end
11
+
12
+ def enforce
13
+ @suspects.each do |path, violations|
14
+ violations = violations.select(&:applicable?)
15
+ @extract.as_tags(path.children, save: true) do |tags|
16
+ changes = violations.map do |violation|
17
+ field = violation.field
18
+ values = fields_from(tags, field)
19
+ choices = choices_from(values)
20
+ Change.new(field, choices, tags)
21
+ end
22
+ changes << RewriteChange.new(:track, tags)
23
+ changes << RewriteChange.new(:year, tags)
24
+ @console.say(%(changes for #{path}:))
25
+ changes.each do |change|
26
+ change.present(@console)
27
+ end
28
+ if perform_changes?
29
+ changes.each(&:perform)
30
+ @society.transfer(path)
31
+ @console.say
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def perform_changes?
40
+ action = @console.ask(perform_question)
41
+ action.empty? || action.downcase == 'p'
42
+ end
43
+
44
+ def perform_question
45
+ @perform_question ||= begin
46
+ %([#{@console.set_color('P', :magenta)}]erform or [#{@console.set_color('S', :magenta)}kip]?)
47
+ end
48
+ end
49
+
50
+ def fields_from(tags, field)
51
+ tags.map { |t| t.send(field) }
52
+ end
53
+
54
+ def choices_from(vs)
55
+ f = vs.each_with_object(Hash.new(0)) { |v, s| s[v] += 1 }
56
+ f = Hash[f.sort_by(&:last)]
57
+ f
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ require 'taglib'
4
+
5
+
6
+ module Audiothority
7
+ class Extract
8
+ def initialize(file_ref=TagLib::FileRef)
9
+ @file_ref = file_ref
10
+ end
11
+
12
+ def as_tags(paths, options={})
13
+ file_refs = paths.map { |p| @file_ref.new(p.to_s, false) }
14
+ null_refs = file_refs.select(&:null?)
15
+ if null_refs.any?
16
+ file_refs = file_refs - null_refs
17
+ null_refs.each(&:close)
18
+ end
19
+ if file_refs.empty?
20
+ return
21
+ end
22
+ yield file_refs.map(&:tag)
23
+ ensure
24
+ if file_refs
25
+ file_refs.each(&:save) if options[:save]
26
+ file_refs.each(&:close)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class Inspector
5
+ def self.scan(dirs, tracker, opts={})
6
+ paths = dirs.map { |d| Pathname.new(d) }
7
+ crawler = Crawler.new(paths)
8
+ validators = opts[:validators] || Validators.default
9
+ inspector = new(crawler, validators, tracker)
10
+ inspector.investigate
11
+ end
12
+
13
+ def initialize(crawler, validators, tracker, opts={})
14
+ @crawler = crawler
15
+ @validators = validators
16
+ @tracker = tracker
17
+ @extract = opts[:extract] || Extract.new
18
+ end
19
+
20
+ def investigate
21
+ @crawler.crawl do |path|
22
+ @extract.as_tags(path.children) do |tags|
23
+ violations = @validators.map { |v| v.validate(tags) }.select(&:invalid?)
24
+ if violations.any?
25
+ @tracker.mark(path, violations)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ module Audiothority
4
+ class EmptySociety
5
+ def transfer(*args) ; end
6
+ end
7
+
8
+ class Society
9
+ def initialize(location, fileutils=FileUtils)
10
+ @location = Pathname.new(location)
11
+ @fileutils = fileutils
12
+ end
13
+
14
+ def transfer(enforced)
15
+ @fileutils.move(enforced, @location)
16
+ end
17
+ end
18
+ end