mr_eko 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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