mr_eko 0.2.3
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.
- data/Gemfile +5 -0
- data/README.md +2 -0
- data/Rakefile +171 -0
- data/TODO +1 -0
- data/bin/mreko +90 -0
- data/db/migrate/001_add_playlists.rb +16 -0
- data/db/migrate/002_add_songs.rb +36 -0
- data/db/migrate/003_add_useful_song_fields.rb +20 -0
- data/db/migrate/04_add_code_to_songs.rb +13 -0
- data/ext/enmfp/LICENSE +54 -0
- data/ext/enmfp/README +96 -0
- data/ext/enmfp/RELEASE_NOTES +34 -0
- data/ext/enmfp/codegen.Darwin +0 -0
- data/ext/enmfp/codegen.Linux-i686 +0 -0
- data/ext/enmfp/codegen.Linux-x86_64 +0 -0
- data/ext/enmfp/codegen.windows.exe +0 -0
- data/lib/mr_eko/core.rb +16 -0
- data/lib/mr_eko/playlist.rb +100 -0
- data/lib/mr_eko/presets.rb +29 -0
- data/lib/mr_eko/song.rb +122 -0
- data/lib/mr_eko.rb +101 -0
- data/mr_eko.gemspec +97 -0
- data/test/mr_eko_test.rb +25 -0
- data/test/playlist_test.rb +135 -0
- data/test/test.rb +22 -0
- metadata +227 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
class MrEko::Playlist < Sequel::Model
|
2
|
+
|
3
|
+
include MrEko::Core
|
4
|
+
include MrEko::Presets
|
5
|
+
|
6
|
+
class NoSongsError < Exception; end
|
7
|
+
|
8
|
+
plugin :validation_helpers
|
9
|
+
many_to_many :songs
|
10
|
+
FORMATS = [:pls, :m3u, :text]
|
11
|
+
|
12
|
+
# Creates and returns a new Playlist from the passed <tt>options</tt>.
|
13
|
+
# <tt>options</tt> should be finder options you pass to Song plus (optionally) :name.
|
14
|
+
def self.create_from_options(options)
|
15
|
+
# TODO: Is a name (or persisting) even necessary?
|
16
|
+
pl = create(:name => options.delete(:name) || "Playlist #{rand(10000)}")
|
17
|
+
prepare_options!(options)
|
18
|
+
|
19
|
+
songs = MrEko::Song.where(options).all
|
20
|
+
if songs.size > 0
|
21
|
+
songs.each{ |song| pl.add_song(song) }
|
22
|
+
pl.save
|
23
|
+
else
|
24
|
+
pl.delete # TODO: Look into not creating Playlist in the 1st place
|
25
|
+
raise NoSongsError.new("No songs match that criteria!")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Organize and transform!
|
30
|
+
def self.prepare_options!(options)
|
31
|
+
if preset = options.delete(:preset)
|
32
|
+
options.replace load_preset(preset)
|
33
|
+
else
|
34
|
+
unless options[:tempo].is_a? Range
|
35
|
+
min_tempo = options.delete(:min_tempo) || 0
|
36
|
+
max_tempo = options.delete(:max_tempo) || 500
|
37
|
+
options[:tempo] = min_tempo..max_tempo
|
38
|
+
end
|
39
|
+
|
40
|
+
unless options[:duration].is_a? Range
|
41
|
+
min_duration = options.delete(:min_duration) || 10 # worthless jams
|
42
|
+
max_duration = options.delete(:max_duration) || 1200 # 20 min.
|
43
|
+
options[:duration] = min_duration..max_duration
|
44
|
+
end
|
45
|
+
|
46
|
+
if options.has_key?(:mode)
|
47
|
+
options[:mode] = MrEko.mode_lookup(options[:mode])
|
48
|
+
end
|
49
|
+
|
50
|
+
if options.has_key?(:key)
|
51
|
+
options[:key] = MrEko.key_lookup(options[:key])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the formatted playlist.
|
57
|
+
def output(format = :pls)
|
58
|
+
format = format.to_sym
|
59
|
+
raise ArgumentError.new("Format must be one of #{FORMATS.join(', ')}") unless FORMATS.include? format
|
60
|
+
|
61
|
+
case format
|
62
|
+
when :pls
|
63
|
+
create_pls
|
64
|
+
when :m3u
|
65
|
+
create_m3u
|
66
|
+
else
|
67
|
+
create_text
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a text representation of the Playlist.
|
72
|
+
def create_text
|
73
|
+
songs.inject("") do |list, song|
|
74
|
+
list << "#{song.filename}, #{song.title}\n"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns a PLS representation of the Playlist.
|
79
|
+
def create_pls
|
80
|
+
pls = "[playlist]\n"
|
81
|
+
pls << "NumberOfEntries=#{songs.size}\n\n"
|
82
|
+
|
83
|
+
i = 0
|
84
|
+
while i < songs.size do
|
85
|
+
num = i+1
|
86
|
+
pls << "File#{num}=#{songs[i].filename}\n"
|
87
|
+
pls << "Title#{num}=#{songs[i].title || songs[i].filename}\n"
|
88
|
+
pls << "Length#{num}=#{songs[i].duration.round}\n\n"
|
89
|
+
i+=1
|
90
|
+
end
|
91
|
+
|
92
|
+
pls << "Version=2"
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_m3u
|
96
|
+
"TBD"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
MrEko::Playlist.plugin :timestamps
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module MrEko::Presets
|
2
|
+
FACTORY = {
|
3
|
+
:gym => {
|
4
|
+
:tempo => 125..300, # sweat, sweat, sweat!
|
5
|
+
:mode => 'major', # bring the HappyHappy
|
6
|
+
:duration => 180..300, # shorter, poppier tunes
|
7
|
+
:energy => 0.5..1.0,
|
8
|
+
:danceability => 0.4..1.0
|
9
|
+
},
|
10
|
+
:chill => {
|
11
|
+
:tempo => 60..120, # mellow
|
12
|
+
:duration => 180..600, # bring the epic, long-players
|
13
|
+
:energy => 0.2..0.5,
|
14
|
+
:danceability => 0.1..0.5
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def load_preset(name)
|
21
|
+
FACTORY[name.to_sym]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.included(base)
|
27
|
+
base.extend(ClassMethods)
|
28
|
+
end
|
29
|
+
end
|
data/lib/mr_eko/song.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
class MrEko::Song < Sequel::Model
|
2
|
+
include MrEko::Core
|
3
|
+
plugin :validation_helpers
|
4
|
+
many_to_many :playlists
|
5
|
+
|
6
|
+
# IDEA: This probably won't work since it's creating a new file,
|
7
|
+
# but could try uploading a sample of the song (faster).
|
8
|
+
# ffmpeg -y -i mogwai.mp3 -ar 22050 -ac 1 -ss 30 -t 30 output.mp3
|
9
|
+
# or
|
10
|
+
# sox mogwai.mp3 output.mp3 30 60
|
11
|
+
|
12
|
+
# Using the Echonest Musical Fingerprint lib in the hopes
|
13
|
+
# of sidestepping the mp3 upload process.
|
14
|
+
def self.enmfp_data(filename, md5)
|
15
|
+
unless File.exists?(fp_location(md5))
|
16
|
+
log 'Running ENMFP'
|
17
|
+
`#{File.join(MrEko::HOME_DIR, 'ext', 'enmfp', enmfp_binary)} "#{File.expand_path(filename)}" > #{fp_location(md5)}`
|
18
|
+
end
|
19
|
+
|
20
|
+
File.read fp_location(md5)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the file path of the EN fingerprint JSON file
|
24
|
+
def self.fp_location(md5)
|
25
|
+
File.expand_path File.join(MrEko::FINGERPRINTS_DIR, "#{md5}.json")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Use the platform-specific binary.
|
29
|
+
def self.enmfp_binary
|
30
|
+
case RUBY_PLATFORM
|
31
|
+
when /darwin/
|
32
|
+
'codegen.Darwin'
|
33
|
+
when /686/
|
34
|
+
'codegen.Linux-i686'
|
35
|
+
when /x86/
|
36
|
+
'codegen.Linux-x86_64'
|
37
|
+
else
|
38
|
+
'codegen.windows.exe'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the analysis and profile data from Echonest for the given track.
|
43
|
+
def self.get_datapoints_by_filename(filename)
|
44
|
+
analysis = MrEko.nest.track.analysis(filename)
|
45
|
+
profile = MrEko.nest.track.profile(:md5 => MrEko.md5(filename)).body.track
|
46
|
+
|
47
|
+
return [analysis, profile]
|
48
|
+
end
|
49
|
+
|
50
|
+
# TODO: Cleanup - This method is prety ugly now.
|
51
|
+
def self.create_from_file!(filename)
|
52
|
+
md5 = MrEko.md5(filename)
|
53
|
+
existing = where(:md5 => md5).first
|
54
|
+
return existing unless existing.nil?
|
55
|
+
|
56
|
+
fingerprint_data = enmfp_data(filename, md5)
|
57
|
+
fingerprint_json_data = Hashie::Mash.new(JSON.parse(fingerprint_data).first)
|
58
|
+
|
59
|
+
if fingerprint_json_data.keys.include?('error')
|
60
|
+
analysis, profile = get_datapoints_by_filename(filename)
|
61
|
+
else
|
62
|
+
log "Identifying with ENMFP code"
|
63
|
+
identify_options = {:code => fingerprint_data}
|
64
|
+
identify_options[:artist] = fingerprint_json_data.metadata.artist if fingerprint_json_data.metadata.artist
|
65
|
+
identify_options[:title] = fingerprint_json_data.metadata.title if fingerprint_json_data.metadata.title
|
66
|
+
identify_options[:release] = fingerprint_json_data.metadata.release if fingerprint_json_data.metadata.release
|
67
|
+
profile = MrEko.nest.song.identify(identify_options)
|
68
|
+
|
69
|
+
if profile.songs.empty?
|
70
|
+
# ENMFP wasn't recognized, so upload.
|
71
|
+
log "ENMP returned nothing, uploading"
|
72
|
+
analysis, profile = get_datapoints_by_filename(filename)
|
73
|
+
else
|
74
|
+
begin
|
75
|
+
profile = profile.songs.first
|
76
|
+
analysis = MrEko.nest.song.profile(:id => profile.id, :bucket => 'audio_summary').songs.first.audio_summary
|
77
|
+
rescue Exception => e
|
78
|
+
log "Issues using ENMP data, uploading \"(#{e})\""
|
79
|
+
analysis, profile = get_datapoints_by_filename(filename)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO: add ruby-mp3info as fallback for parsing ID3 tags
|
85
|
+
# since Echonest seems a bit flaky in that dept.
|
86
|
+
song = new()
|
87
|
+
song.filename = File.expand_path(filename)
|
88
|
+
song.md5 = md5
|
89
|
+
song.code = fingerprint_json_data.code
|
90
|
+
song.tempo = analysis.tempo
|
91
|
+
song.duration = analysis.duration
|
92
|
+
song.fade_in = analysis.end_of_fade_in
|
93
|
+
song.fade_out = analysis.start_of_fade_out
|
94
|
+
song.key = analysis.key
|
95
|
+
song.mode = analysis.mode
|
96
|
+
song.loudness = analysis.loudness
|
97
|
+
song.time_signature = analysis.time_signature
|
98
|
+
song.echonest_id = profile.id
|
99
|
+
song.bitrate = profile.bitrate
|
100
|
+
song.title = profile.title
|
101
|
+
song.artist = profile.artist || profile.artist_name
|
102
|
+
song.album = profile.release
|
103
|
+
song.danceability = profile.audio_summary? ? profile.audio_summary.danceability : analysis.danceability
|
104
|
+
song.energy = profile.audio_summary? ? profile.audio_summary.energy : analysis.energy
|
105
|
+
|
106
|
+
song.save
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate
|
110
|
+
super
|
111
|
+
set_md5 # no Sequel callback for this?
|
112
|
+
validates_unique :md5
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
def set_md5
|
117
|
+
self.md5 ||= MrEko.md5(filename)
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
MrEko::Song.plugin :timestamps
|
data/lib/mr_eko.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler"
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require "sqlite3"
|
6
|
+
require "sequel"
|
7
|
+
require "logger"
|
8
|
+
require "hashie"
|
9
|
+
require "digest/md5"
|
10
|
+
require "echonest"
|
11
|
+
|
12
|
+
STDOUT.sync = true
|
13
|
+
|
14
|
+
EKO_ENV = ENV['EKO_ENV'] || 'development'
|
15
|
+
Sequel.default_timezone = :utc
|
16
|
+
|
17
|
+
module MrEko
|
18
|
+
VERSION = '0.2.3'
|
19
|
+
USER_DIR = File.join(ENV['HOME'], ".mreko")
|
20
|
+
FINGERPRINTS_DIR = File.join(USER_DIR, 'fingerprints')
|
21
|
+
HOME_DIR = File.join(File.dirname(__FILE__), '..')
|
22
|
+
|
23
|
+
MODES = %w(minor major)
|
24
|
+
CHROMATIC_SCALE = %w(C C# D D# E F F# G G# A A# B).freeze
|
25
|
+
|
26
|
+
class << self
|
27
|
+
attr_accessor :logger
|
28
|
+
|
29
|
+
def env
|
30
|
+
EKO_ENV
|
31
|
+
end
|
32
|
+
|
33
|
+
def connection
|
34
|
+
@connection
|
35
|
+
end
|
36
|
+
|
37
|
+
def nest
|
38
|
+
@nest
|
39
|
+
end
|
40
|
+
|
41
|
+
def md5(filename)
|
42
|
+
Digest::MD5.hexdigest(open(filename).read)
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup!
|
46
|
+
@logger ||= Logger.new(STDOUT)
|
47
|
+
setup_directories!
|
48
|
+
setup_db!
|
49
|
+
setup_echonest!
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_directories!
|
53
|
+
Dir.mkdir(USER_DIR) unless File.directory?(USER_DIR)
|
54
|
+
Dir.mkdir(FINGERPRINTS_DIR) unless File.directory?(FINGERPRINTS_DIR)
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_db!
|
58
|
+
return @connection if @connection
|
59
|
+
@connection = Sequel.sqlite(db_name)
|
60
|
+
@connection.loggers << @logger
|
61
|
+
end
|
62
|
+
|
63
|
+
def setup_echonest!
|
64
|
+
@nest ||= Echonest(File.read(api_key))
|
65
|
+
end
|
66
|
+
|
67
|
+
def db_name
|
68
|
+
env == 'test' ? 'db/eko_test.db' : 'db/eko.db'
|
69
|
+
end
|
70
|
+
|
71
|
+
def api_key
|
72
|
+
[File.join(USER_DIR, 'echonest_api.key'), File.join(HOME_DIR, 'echonest_api.key')].each do |file|
|
73
|
+
return file if File.exists?(file)
|
74
|
+
end
|
75
|
+
raise "You need to create an echonest_api.key file in #{USER_DIR}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Takes 'minor' or 'major' and returns its integer representation.
|
79
|
+
def mode_lookup(mode)
|
80
|
+
MODES.index(mode.downcase)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Takes a chromatic key (eg: G#) and returns its integer representation.
|
84
|
+
def key_lookup(key_letter)
|
85
|
+
CHROMATIC_SCALE.index(key_letter.upcase)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Takes an integer and returns its standard (chromatic) representation.
|
89
|
+
def key_letter(key)
|
90
|
+
CHROMATIC_SCALE[key]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
MrEko.setup!
|
97
|
+
|
98
|
+
require "lib/mr_eko/core"
|
99
|
+
require "lib/mr_eko/presets"
|
100
|
+
require "lib/mr_eko/playlist"
|
101
|
+
require "lib/mr_eko/song"
|
data/mr_eko.gemspec
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
10
|
+
s.rubygems_version = '1.3.5'
|
11
|
+
|
12
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
13
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
14
|
+
## the sub! line in the Rakefile
|
15
|
+
s.name = 'mr_eko'
|
16
|
+
s.version = '0.2.3'
|
17
|
+
s.date = '2011-02-08'
|
18
|
+
s.rubyforge_project = 'mr_eko'
|
19
|
+
|
20
|
+
## Make sure your summary is short. The description may be as long
|
21
|
+
## as you like.
|
22
|
+
s.summary = "Catalogs music file data and exposes a playlist interface"
|
23
|
+
s.description = "Catalogs music file data and exposes a playlist interface"
|
24
|
+
|
25
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
26
|
+
## better to set the email to an email list or something. If you don't have
|
27
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
28
|
+
s.authors = ["Ed Hickey"]
|
29
|
+
s.email = 'bassnode@gmail.com'
|
30
|
+
s.homepage = 'http://github.com/bassnode/mreko'
|
31
|
+
|
32
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
33
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
34
|
+
s.require_paths = %w[lib]
|
35
|
+
|
36
|
+
|
37
|
+
## If your gem includes any executables, list them here.
|
38
|
+
s.executables = ["mreko"]
|
39
|
+
s.default_executable = 'mreko'
|
40
|
+
|
41
|
+
## Specify any RDoc options here. You'll want to add your README and
|
42
|
+
## LICENSE files to the extra_rdoc_files list.
|
43
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
44
|
+
s.extra_rdoc_files = %w[README.md]
|
45
|
+
|
46
|
+
## List your runtime dependencies here. Runtime dependencies are those
|
47
|
+
## that are needed for an end user to actually USE your code.
|
48
|
+
s.add_dependency('sequel', "= 3.15")
|
49
|
+
s.add_dependency('sqlite3-ruby', "~> 1.3")
|
50
|
+
s.add_dependency('hashie')
|
51
|
+
s.add_dependency('httpclient', "~> 2.1")
|
52
|
+
s.add_dependency('json', "= 1.4.6")
|
53
|
+
|
54
|
+
## List your development dependencies here. Development dependencies are
|
55
|
+
## those that are only needed during development
|
56
|
+
s.add_development_dependency('mocha', "= 0.9.8")
|
57
|
+
s.add_development_dependency('shoulda', "~> 2.11")
|
58
|
+
s.add_development_dependency('test-unit', "~> 2.1")
|
59
|
+
s.add_development_dependency("ruby-debug", "~> 0.10.3")
|
60
|
+
|
61
|
+
## Leave this section as-is. It will be automatically generated from the
|
62
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
63
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
64
|
+
# = MANIFEST =
|
65
|
+
s.files = %w[
|
66
|
+
Gemfile
|
67
|
+
README.md
|
68
|
+
Rakefile
|
69
|
+
TODO
|
70
|
+
bin/mreko
|
71
|
+
db/migrate/001_add_playlists.rb
|
72
|
+
db/migrate/002_add_songs.rb
|
73
|
+
db/migrate/003_add_useful_song_fields.rb
|
74
|
+
db/migrate/04_add_code_to_songs.rb
|
75
|
+
ext/enmfp/LICENSE
|
76
|
+
ext/enmfp/README
|
77
|
+
ext/enmfp/RELEASE_NOTES
|
78
|
+
ext/enmfp/codegen.Darwin
|
79
|
+
ext/enmfp/codegen.Linux-i686
|
80
|
+
ext/enmfp/codegen.Linux-x86_64
|
81
|
+
ext/enmfp/codegen.windows.exe
|
82
|
+
lib/mr_eko.rb
|
83
|
+
lib/mr_eko/core.rb
|
84
|
+
lib/mr_eko/playlist.rb
|
85
|
+
lib/mr_eko/presets.rb
|
86
|
+
lib/mr_eko/song.rb
|
87
|
+
mr_eko.gemspec
|
88
|
+
test/mr_eko_test.rb
|
89
|
+
test/playlist_test.rb
|
90
|
+
test/test.rb
|
91
|
+
]
|
92
|
+
# = MANIFEST =
|
93
|
+
|
94
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
95
|
+
## matches what you actually use.
|
96
|
+
s.test_files = s.files.select { |path| path =~ /^test\/*_test\.rb/ }
|
97
|
+
end
|
data/test/mr_eko_test.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
class MrEkoTest < Test::Unit::TestCase
|
2
|
+
|
3
|
+
context "the module" do
|
4
|
+
|
5
|
+
should "return an Echonest API instance for nest" do
|
6
|
+
assert_instance_of Echonest::Api, MrEko.nest
|
7
|
+
end
|
8
|
+
|
9
|
+
should "return a Sequel instance for connection" do
|
10
|
+
assert_instance_of Sequel::SQLite::Database, MrEko.connection
|
11
|
+
end
|
12
|
+
|
13
|
+
# should "raise an error when there is no api.key found" do
|
14
|
+
# File.expects(:exists?).with(File.join(MrEko::USER_DIR, 'echonest_api.key')).returns(false)
|
15
|
+
# File.expects(:exists?).with(File.join(MrEko::HOME_DIR, 'echonest_api.key')).returns(false)
|
16
|
+
# assert_raise(RuntimeError){ MrEko.setup_echonest! }
|
17
|
+
# end
|
18
|
+
|
19
|
+
should "return the MD5 of the passed filename" do
|
20
|
+
md5 = Digest::MD5.hexdigest(open(__FILE__).read)
|
21
|
+
assert_equal md5, MrEko.md5(__FILE__)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
class PlaylistTest < Test::Unit::TestCase
|
2
|
+
|
3
|
+
context "a new playlist" do
|
4
|
+
setup do
|
5
|
+
@playlist = MrEko::Playlist.new
|
6
|
+
end
|
7
|
+
|
8
|
+
should "have no songs" do
|
9
|
+
assert_equal 0, @playlist.songs.size
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context "create_from_options" do
|
14
|
+
|
15
|
+
setup do
|
16
|
+
@options = {:tempo => 100..200}
|
17
|
+
MrEko::Song.delete
|
18
|
+
@playlist_count = MrEko::Playlist.count
|
19
|
+
end
|
20
|
+
|
21
|
+
should "not create a playlist when there no songs found" do
|
22
|
+
assert_equal 0, MrEko::Song.count
|
23
|
+
assert_raise(MrEko::Playlist::NoSongsError){ MrEko::Playlist.create_from_options(@options) }
|
24
|
+
assert_equal @playlist_count, MrEko::Playlist.count
|
25
|
+
end
|
26
|
+
|
27
|
+
should "create a playlist when there are songs found" do
|
28
|
+
assert MrEko::Song.insert( :tempo => @options[:tempo].max,
|
29
|
+
:filename => 'third_eye.mp3',
|
30
|
+
:artist => 'Tool',
|
31
|
+
:title => 'Third Eye',
|
32
|
+
:md5 => Digest::MD5.hexdigest(Time.now.to_s),
|
33
|
+
:created_on => Time.now,
|
34
|
+
:duration => 567
|
35
|
+
)
|
36
|
+
|
37
|
+
assert MrEko::Playlist.create_from_options(@options)
|
38
|
+
assert_equal @playlist_count + 1, MrEko::Playlist.count
|
39
|
+
end
|
40
|
+
|
41
|
+
should "filter out certain options before querying for songs" do
|
42
|
+
unfiltered_options = {:name => "Rock You in Your Face mix #{rand(1000)}", :time_signature => 4}
|
43
|
+
MrEko::Song.expects(:where).with(Not(has_key(:name))).once.returns(sequel_dataset_stub)
|
44
|
+
assert_raise(MrEko::Playlist::NoSongsError){ MrEko::Playlist.create_from_options(unfiltered_options) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "prepare_options!" do
|
49
|
+
|
50
|
+
context "when passed a preset option" do
|
51
|
+
|
52
|
+
should "only use the presets' options, not the others passed" do
|
53
|
+
opts = { :time_signature => 4, :preset => :gym }
|
54
|
+
MrEko::Playlist.prepare_options!(opts)
|
55
|
+
assert !opts.has_key?(:time_signature)
|
56
|
+
assert_equal MrEko::Presets::FACTORY[:gym][:tempo], opts[:tempo]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "for tempo" do
|
61
|
+
|
62
|
+
should "not transform when tempo is a Range" do
|
63
|
+
opts = {:tempo => 160..180}
|
64
|
+
MrEko::Playlist.prepare_options!(opts)
|
65
|
+
assert_equal 160..180, opts[:tempo]
|
66
|
+
end
|
67
|
+
|
68
|
+
should "transform even when there aren't any passed tempo opts" do
|
69
|
+
opts = {:time_signature => 4}
|
70
|
+
MrEko::Playlist.prepare_options!(opts)
|
71
|
+
assert opts.has_key? :tempo
|
72
|
+
end
|
73
|
+
|
74
|
+
should "remove min and max keys" do
|
75
|
+
opts = {:min_tempo => 100, :max_tempo => 200}
|
76
|
+
MrEko::Playlist.prepare_options!(opts)
|
77
|
+
assert !opts.has_key?(:min_tempo)
|
78
|
+
assert !opts.has_key?(:max_tempo)
|
79
|
+
end
|
80
|
+
|
81
|
+
should "create a range with the passed min and max tempos" do
|
82
|
+
opts = {:min_tempo => 100, :max_tempo => 200}
|
83
|
+
MrEko::Playlist.prepare_options!(opts)
|
84
|
+
assert_equal 100..200, opts[:tempo]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "for duration" do
|
89
|
+
|
90
|
+
should "not transform when duration is a Range" do
|
91
|
+
opts = {:duration => 200..2010}
|
92
|
+
MrEko::Playlist.prepare_options!(opts)
|
93
|
+
assert_equal 200..2010, opts[:duration]
|
94
|
+
end
|
95
|
+
|
96
|
+
should "transform even when there aren't any passed duration opts" do
|
97
|
+
opts = {:time_signature => 4}
|
98
|
+
MrEko::Playlist.prepare_options!(opts)
|
99
|
+
assert opts.has_key? :duration
|
100
|
+
end
|
101
|
+
|
102
|
+
should "remove min and max keys" do
|
103
|
+
opts = {:min_duration => 100, :max_duration => 2000}
|
104
|
+
MrEko::Playlist.prepare_options!(opts)
|
105
|
+
assert !opts.has_key?(:min_duration)
|
106
|
+
assert !opts.has_key?(:max_duration)
|
107
|
+
end
|
108
|
+
|
109
|
+
should "create a range with the passed min and max durations" do
|
110
|
+
opts = {:min_duration => 100, :max_duration => 2000}
|
111
|
+
MrEko::Playlist.prepare_options!(opts)
|
112
|
+
assert_equal 100..2000, opts[:duration]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "for mode" do
|
117
|
+
|
118
|
+
should "transform into numeric representation" do
|
119
|
+
opts = {:mode => 'minor'}
|
120
|
+
MrEko::Playlist.prepare_options!(opts)
|
121
|
+
assert_equal 0, opts[:mode]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "for key" do
|
126
|
+
|
127
|
+
should "transform into numeric representation" do
|
128
|
+
opts = {:key => 'C#'}
|
129
|
+
MrEko::Playlist.prepare_options!(opts)
|
130
|
+
assert_equal 1, opts[:key]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
data/test/test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
ENV['EKO_ENV'] = 'test'
|
2
|
+
require "bundler/setup"
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require 'test/unit'
|
6
|
+
require 'shoulda'
|
7
|
+
require 'mocha'
|
8
|
+
require "mr_eko"
|
9
|
+
|
10
|
+
require 'sequel/extensions/migration'
|
11
|
+
Sequel::Migrator.apply(MrEko.connection, File.join(File.dirname(__FILE__), "..", "db", "migrate"))
|
12
|
+
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
|
15
|
+
# Could be fleshed out some more.
|
16
|
+
def sequel_dataset_stub
|
17
|
+
data = mock()
|
18
|
+
data.stubs(:all).returns( [] )
|
19
|
+
data
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|