flacky 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rvmrc
19
+ .rbenv-verion
20
+ .ruby-version
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
@@ -0,0 +1,5 @@
1
+ guard 'minitest' do
2
+ # with Minitest::Spec
3
+ watch(%r|^spec/(.*)_spec\.rb|)
4
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
5
+ end
@@ -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.
@@ -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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs.push 'lib'
8
+ t.test_files = FileList['spec/**/*_spec.rb']
9
+ t.verbose = true
10
+ end
11
+
12
+ task :default => 'test'
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ require "flacky/version"
2
+
3
+ module Flacky
4
+ end
@@ -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