flacky 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/Gemfile +19 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +12 -0
- data/bin/flacky +12 -0
- data/flacky.gemspec +29 -0
- data/lib/flacky.rb +4 -0
- data/lib/flacky/cli.rb +84 -0
- data/lib/flacky/core_ext.rb +15 -0
- data/lib/flacky/metadata_generator.rb +61 -0
- data/lib/flacky/mp3_convertor.rb +47 -0
- data/lib/flacky/scraper.rb +39 -0
- data/lib/flacky/tagger.rb +27 -0
- data/lib/flacky/version.rb +3 -0
- data/spec/fixtures/silence.flac +0 -0
- data/spec/fixtures/vcr_cassettes/audioslave_revelations.yml +2812 -0
- data/spec/fixtures/vcr_cassettes/bend_sinister_small_fame.yml +669 -0
- data/spec/fixtures/vcr_cassettes/rush_moving_pictures.yml +2956 -0
- data/spec/flacky/scraper_spec.rb +80 -0
- data/spec/flacky/tagger_spec.rb +67 -0
- metadata +202 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in flacky.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'rake', '~> 10.0'
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rbconfig'
|
11
|
+
|
12
|
+
if RbConfig::CONFIG['target_os'] =~ /darwin/i
|
13
|
+
gem 'rb-fsevent', '>= 0.3.9'
|
14
|
+
gem 'growl', '~> 1.0.3'
|
15
|
+
end
|
16
|
+
if RbConfig::CONFIG['target_os'] =~ /linux/i
|
17
|
+
gem 'rb-inotify', '>= 0.5.1'
|
18
|
+
gem 'libnotify', '~> 0.1.3'
|
19
|
+
end
|
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Fletcher Nichol
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Flacky
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'flacky'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install flacky
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/flacky
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- encoding: utf-8 -*-
|
3
|
+
|
4
|
+
# Trap interrupts to quit cleanly. See
|
5
|
+
# https://twitter.com/mitchellh/status/283014103189053442
|
6
|
+
Signal.trap("INT") { exit 1 }
|
7
|
+
|
8
|
+
$:.unshift File.join(File.dirname(__FILE__), %w{.. lib})
|
9
|
+
require 'rubygems'
|
10
|
+
require 'flacky/cli'
|
11
|
+
|
12
|
+
Flacky::CLI.start
|
data/flacky.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'flacky/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "flacky"
|
8
|
+
gem.version = Flacky::VERSION
|
9
|
+
gem.authors = ["Fletcher Nichol"]
|
10
|
+
gem.email = ["fnichol@nichol.ca"]
|
11
|
+
gem.summary = %q{Loose collection of CLI commands to sort and process Flac files}
|
12
|
+
gem.description = gem.summary
|
13
|
+
gem.homepage = "https://github.com/fnichol/flacky"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'thor'
|
21
|
+
gem.add_dependency 'nokogiri'
|
22
|
+
gem.add_dependency 'flacinfo-rb'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'minitest'
|
25
|
+
gem.add_development_dependency 'vcr'
|
26
|
+
gem.add_development_dependency 'webmock', '~> 1.8.11'
|
27
|
+
gem.add_development_dependency 'mocha'
|
28
|
+
gem.add_development_dependency 'guard-minitest'
|
29
|
+
end
|
data/lib/flacky.rb
ADDED
data/lib/flacky/cli.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'json'
|
5
|
+
require 'thor'
|
6
|
+
|
7
|
+
require 'flacky'
|
8
|
+
require 'flacky/metadata_generator'
|
9
|
+
require 'flacky/mp3_convertor'
|
10
|
+
|
11
|
+
module Flacky
|
12
|
+
|
13
|
+
class CLI < Thor
|
14
|
+
|
15
|
+
include Thor::Actions
|
16
|
+
|
17
|
+
desc "generate <root_path>", "Generate and populate metadata as JSON"
|
18
|
+
def generate(root_dir = ENV['PWD'])
|
19
|
+
start_dir = File.join(File.expand_path(root_dir), '**/*.flac')
|
20
|
+
Dir.glob(start_dir).map { |f| File.dirname(f) }.uniq.each do |dir|
|
21
|
+
mdf = File.join(dir, "metadata.json")
|
22
|
+
say("Processing <#{dir}>", :cyan)
|
23
|
+
data = Flacky::MetadataGenerator.new(mdf).combined_data
|
24
|
+
IO.write(mdf, JSON.pretty_generate(data))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "missing_urls <root_path>", "List all metadata files with missing URLs"
|
29
|
+
method_option :print0, :aliases => "-0", :type => :boolean
|
30
|
+
def missing_urls(root_dir = ENV['PWD'])
|
31
|
+
start_dir = File.join(File.expand_path(root_dir), '**/metadata.json')
|
32
|
+
files = []
|
33
|
+
|
34
|
+
Dir.glob(start_dir).each do |mdf|
|
35
|
+
attr = JSON.parse(IO.read(mdf))["allmusic_url"]
|
36
|
+
files << mdf if attr.nil? || attr.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
if options[:print0]
|
40
|
+
print files.join("\x0").concat("\x0")
|
41
|
+
else
|
42
|
+
puts files.join("\n") unless files.empty?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "to_mp3 [file ...]|[**/*.flac ...]", "Convert Flac files to MP3 files"
|
47
|
+
method_option :destination, :aliases => "-d",
|
48
|
+
:desc => "Sets optional destination directory"
|
49
|
+
method_option :'lame-opts', :aliases => "-l",
|
50
|
+
:default => "--vbr-new -V 0 -b 320",
|
51
|
+
:desc => "Set the lame encoding arguments"
|
52
|
+
def to_mp3(*args)
|
53
|
+
%w{flac metaflac lame}.each do |cmd|
|
54
|
+
abort "Command #{cmd} must be on your PATH" unless %x{which #{cmd}}
|
55
|
+
end
|
56
|
+
|
57
|
+
mp3izer = Flacky::Mp3Convertor.new(
|
58
|
+
:lame_opts => options[:'lame-opts'],
|
59
|
+
:dest_root => options[:destination]
|
60
|
+
)
|
61
|
+
|
62
|
+
args.each { |glob| convert_files(glob, mp3izer) }
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def convert_files(glob, mp3izer)
|
68
|
+
Dir.glob(glob).each do |file|
|
69
|
+
next unless file =~ /\.flac$/
|
70
|
+
|
71
|
+
say("Processing #{file}...", :cyan)
|
72
|
+
response = mp3izer.convert_file!(file)
|
73
|
+
say("Created #{response.mp3_filename} #{duration(response.elapsed)}",
|
74
|
+
:yellow)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def duration(total)
|
79
|
+
minutes = (total / 60).to_i
|
80
|
+
seconds = (total - (minutes * 60))
|
81
|
+
"(%dm%.2fs)" % [minutes, seconds]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- encoding: UTF-8 -*-
|
2
|
+
|
3
|
+
module DeepMergeHash
|
4
|
+
|
5
|
+
def deep_merge(other_hash)
|
6
|
+
r = {}
|
7
|
+
merge(other_hash) do |key, oldval, newval|
|
8
|
+
r[key] = oldval.class == self.class ? oldval.deep_merge(newval) : newval
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Hash
|
14
|
+
include DeepMergeHash
|
15
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# -*- encoding: UTF-8 -*-
|
2
|
+
|
3
|
+
require 'flacinfo'
|
4
|
+
|
5
|
+
require 'flacky/core_ext'
|
6
|
+
require 'flacky/scraper'
|
7
|
+
|
8
|
+
module Flacky
|
9
|
+
class MetadataGenerator
|
10
|
+
attr_reader :dir, :metadata_file, :file_metadata
|
11
|
+
|
12
|
+
def initialize(metadata_file)
|
13
|
+
@metadata_file = metadata_file
|
14
|
+
@dir = File.dirname(@metadata_file)
|
15
|
+
@file_metadata = load_file_metadata
|
16
|
+
end
|
17
|
+
|
18
|
+
def flac_metadata
|
19
|
+
flacs = Dir.glob(File.join(dir, '*.flac'))
|
20
|
+
return Hash.new if flacs.empty?
|
21
|
+
|
22
|
+
# keep trying flac files until we run out
|
23
|
+
info = begin
|
24
|
+
FlacInfo.new(flacs.shift).tags
|
25
|
+
rescue FlacInfoReadError => ex
|
26
|
+
flacs.size > 0 ? retry : Hash.new
|
27
|
+
end
|
28
|
+
|
29
|
+
info.each_pair { |k,v| v.force_encoding('UTF-8') if v.is_a? String }
|
30
|
+
result = Hash.new
|
31
|
+
common_tags.each { |t| result[t] = info[t] }
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def scraped_metadata
|
36
|
+
url = file_metadata["allmusic_url"]
|
37
|
+
return Hash.new if !url || (url && url.empty?)
|
38
|
+
|
39
|
+
scraper = Flacky::Scraper.new(url)
|
40
|
+
{ 'STYLE' => scraper.styles.join(';'), 'MOOD' => scraper.moods.join(';') }
|
41
|
+
end
|
42
|
+
|
43
|
+
def combined_data
|
44
|
+
result = { "allmusic_url" => "" }
|
45
|
+
result["flac"] = (result["flac"] || Hash.new).merge(flac_metadata)
|
46
|
+
result = result.deep_merge(file_metadata)
|
47
|
+
result["flac"] = result["flac"].merge(scraped_metadata)
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def load_file_metadata
|
54
|
+
File.exists?(metadata_file) ? JSON.parse(IO.read(metadata_file)) : Hash.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def common_tags
|
58
|
+
%w[Artist Album Date Genre TOTALDISCS STYLE MOOD FILEOWNER].freeze
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Flacky
|
4
|
+
|
5
|
+
class Mp3Convertor
|
6
|
+
|
7
|
+
def initialize(opts = {})
|
8
|
+
@lame_opts = opts[:lame_opts]
|
9
|
+
@dest_root = opts[:dest_root]
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert_file!(file)
|
13
|
+
dst = file.sub(/\.flac$/, ".mp3")
|
14
|
+
if dest_root
|
15
|
+
dst = File.join(dest_root, dst)
|
16
|
+
FileUtils.mkdir_p File.dirname(dst)
|
17
|
+
end
|
18
|
+
|
19
|
+
cmd = %{flac -dcs "#{file}" | lame #{lame_opts}}
|
20
|
+
tags.each { |lt, ft| cmd << %{ --#{lt} "#{tag(file, ft)}"} }
|
21
|
+
cmd << %{ - "#{dst}"}
|
22
|
+
|
23
|
+
elapsed = Benchmark.measure do ; %x{#{cmd}} ; end
|
24
|
+
Response.new(dst, elapsed.real)
|
25
|
+
end
|
26
|
+
|
27
|
+
Response = Struct.new(:mp3_filename, :elapsed)
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :lame_opts, :dest_root
|
32
|
+
|
33
|
+
def tags
|
34
|
+
{ :tt => :title, :tl => :album, :ta => :artist, :tn => :tracknumber,
|
35
|
+
:tg => :genre, :tc => :comment, :ty => :date }.freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
def tag(file, tag)
|
39
|
+
r = %x{metaflac --show-tag=#{tag.to_s.upcase} "#{file}"}
|
40
|
+
unless r.nil? || r.empty?
|
41
|
+
r.split("=").last.chomp
|
42
|
+
else
|
43
|
+
""
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'net/http'
|
6
|
+
|
7
|
+
module Flacky
|
8
|
+
class ScraperError < RuntimeError ; end
|
9
|
+
|
10
|
+
class Scraper
|
11
|
+
|
12
|
+
def initialize(url)
|
13
|
+
@url = url
|
14
|
+
end
|
15
|
+
|
16
|
+
def styles
|
17
|
+
doc.css('#sidebar .styles ul a').map { |link| link.content }.sort
|
18
|
+
end
|
19
|
+
|
20
|
+
def moods
|
21
|
+
doc.css('#sidebar .moods ul a').map { |link| link.content }.sort
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
HTTP_ERRORS = [::Timeout::Error, ::Errno::EINVAL, ::Errno::ECONNRESET,
|
27
|
+
::EOFError, ::Net::HTTPBadResponse, ::Net::HTTPHeaderSyntaxError,
|
28
|
+
::Net::ProtocolError, ::SocketError, ::OpenSSL::SSL::SSLError,
|
29
|
+
::Errno::ECONNREFUSED]
|
30
|
+
|
31
|
+
def doc
|
32
|
+
@doc ||= begin
|
33
|
+
Nokogiri::HTML(open(@url))
|
34
|
+
rescue *HTTP_ERRORS => ex
|
35
|
+
raise Flacky::ScraperError, "#{ex.class.name}: #{ex.message}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'flacinfo'
|
4
|
+
|
5
|
+
module Flacky
|
6
|
+
class Tagger
|
7
|
+
def self.update(flac_file, &block)
|
8
|
+
instance = self.new(flac_file)
|
9
|
+
instance.instance_eval(&block)
|
10
|
+
instance.update!
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(flac_file)
|
14
|
+
@flac_file = flac_file
|
15
|
+
@flac = FlacInfo.new(@flac_file)
|
16
|
+
end
|
17
|
+
|
18
|
+
def tag(name, value)
|
19
|
+
@flac.comment_del(name) if @flac.tags.keys.include?(name)
|
20
|
+
@flac.comment_add("#{name}=#{value}")
|
21
|
+
end
|
22
|
+
|
23
|
+
def update!()
|
24
|
+
@flac.update!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|