Class: MrEko::Song

Inherits:
Sequel::Model
  • Object
show all
Includes:
Core
Defined in:
lib/mr_eko/song.rb

Overview

TODO: Refactor this so everything’s not in class methods

Constant Summary collapse

REQUIRED_ID3_TAGS =
[:artist, :title]

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Core

#before_create, #before_update, included

Class Method Details

.catalog_via_enmfp(filename, opts = {}) ⇒ MrEko::Song, NilClass

Run local analysis (ENMFP) on the passed file, send that identifier code to EN and store the returned details in our DB. If the local analysis fails, upload the MP3 to EN for server-side analysis.

Parameters:

  • location (String)

    of the audio file

  • opts (Hash) (defaults to: {})

Options Hash (opts):

  • :md5 (String)

    pre-calculated MD5 of file

Returns:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/mr_eko/song.rb', line 38

def self.catalog_via_enmfp(filename, opts={})
  md5 = opts[:md5] || MrEko.md5(filename)
  existing = where(:md5 => md5).first
  return existing unless existing.nil?

  return if file_too_big? filename

  log "Identifying with ENMFP code #{filename}"

  begin
    fingerprint_json = enmfp_data(filename, md5)
    profile = identify_from_enmfp_data(fingerprint_json)
  rescue MrEko::EnmfpError => e
    log %Q{Issues using ENMFP data "(#{e})" #{e.backtrace.join("\n")}}
    analysis, profile = get_datapoints_by_upload(filename)
  end

  create do |song|
    song.filename       = File.expand_path(filename)
    song.md5            = md5
    song.code           = fingerprint_json ? fingerprint_json.code : nil
    song.tempo          = profile.audio_summary.tempo
    song.duration       = profile.audio_summary.duration
    song.key            = profile.audio_summary[:key]
    song.mode           = profile.audio_summary.mode
    song.loudness       = profile.audio_summary.loudness
    song.time_signature = profile.audio_summary.time_signature
    song.echonest_id    = profile.id
    song.bitrate        = fingerprint_json ? fingerprint_json..bitrate : nil
    song.title          = profile.title
    song.artist         = profile.artist || profile.artist_name
    song.album          = fingerprint_json ? fingerprint_json..release : profile.release
    song.danceability   = profile.audio_summary.danceability
    song.energy         = profile.audio_summary.energy
  end
end

.catalog_via_tags(filename, opts = {}) ⇒ MrEko::Song

Uses ID3 tags to query Echonest and then store the resultant data.

Returns:

See Also:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/mr_eko/song.rb', line 90

def self.catalog_via_tags(filename, opts={})
  tags = parse_id3_tags(filename)

  return unless tags.reject{ |k, v| v.blank? }.size >= REQUIRED_ID3_TAGS.size

  md5 = opts[:md5] || MrEko.md5(filename)
  begin
    analysis = MrEko.nest.song.search(:artist => tags.artist,
                                      :title => tags.title,
                                      :bucket => 'audio_summary',
                                      :limit => 1).songs.first
  rescue => e
    log "BAD TAGS? #{tags.inspect} #{filename}"
    log e.message
    log e.backtrace.join("\n")
    return
  end

  create do |song|
    song.filename       = File.expand_path(filename)
    song.md5            = md5
    song.tempo          = analysis.audio_summary.tempo
    song.duration       = analysis.audio_summary.duration
    song.key            = analysis.audio_summary['key']
    song.mode           = analysis.audio_summary.mode
    song.loudness       = analysis.audio_summary.loudness
    song.time_signature = analysis.audio_summary.time_signature
    song.echonest_id    = analysis.id
    song.title          = tags.title
    song.artist         = tags.artist
    song.album          = tags.album
    song.danceability   = analysis.audio_summary.danceability
    song.energy         = analysis.audio_summary.energy
    # XXX: Won't have these from tags - worth getting from EN?
    # song.code           = fingerprint_json.code
    # XXX: ID3Lib doesn't return these - worth parsing?
    # song.bitrate        =  profile.bitrate
  end if analysis
end

.create_from_file!(filename, opts = {}) ⇒ MrEko::Song

A wrapper which gets called by the bin file. By default will try to extract the needed song info from the ID3 tags and if fails, will analyze via ENMFP/upload.

Parameters:

  • file (String)

    path of the MP3

  • options (Hash)

    hash

Returns:



17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/mr_eko/song.rb', line 17

def self.create_from_file!(filename, opts={})
  md5 = MrEko.md5(filename)
  existing = where(:md5 => md5).first
  return existing unless existing.nil?

  if song = catalog_via_tags(filename, :md5 => md5)
    song
  elsif !opts[:tags_only]
    catalog_via_enmfp(filename, :md5 => md5)
  end

end

.enmfp_data(filename, md5) ⇒ Hash

Using the Echonest Musical Fingerprint lib in the hopes of sidestepping the mp3 upload process.

Parameters:

  • file (String)

    path of the MP3

  • MD5 (String)

    hash of the file

Returns:

  • (Hash)

    data from the ENMFP process



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/mr_eko/song.rb', line 137

def self.enmfp_data(filename, md5)
  unless File.exists?(fp_location(md5))
    log 'Waiting for ENMFP binary...'
    `#{MrEko.enmfp_binary} "#{File.expand_path(filename)}" > #{fp_location(md5)}`
  end

  begin
    raw_json = File.read fp_location(md5)
    hash = Hashie::Mash.new(JSON.parse(raw_json).first)
    hash.raw_data = raw_json

    if hash.keys.include?('error')
      raise MrEko::EnmfpError, "Errors returned in the ENMFP fingerprint data: #{hash.error.inspect}"
    end

  rescue JSON::ParserError => e
    raise MrEko::EnmfpError, e.message
  end

  hash
end

.get_datapoints_by_upload(filename) ⇒ Array

Returns the analysis and profile data from Echonest for the given track.

Parameters:

  • file (String)

    path of the MP3

Returns:

  • (Array)

    Analysis and profile data from EN



163
164
165
166
167
168
169
# File 'lib/mr_eko/song.rb', line 163

def self.get_datapoints_by_upload(filename)
  log "Uploading data to EN for analysis"
  analysis = MrEko.nest.track.analysis(filename)
  profile  = MrEko.nest.track.profile(:md5 => MrEko.md5(filename), :bucket => 'audio_summary').body.track

  return [analysis, profile]
end

.parse_id3_tags(filename) ⇒ MrEko::TagParser::Result

Parses the file’s ID3 tags

Parameters:

  • The (String)

    file path

Returns:



79
80
81
82
83
# File 'lib/mr_eko/song.rb', line 79

def self.parse_id3_tags(filename)
  log "Parsing ID3 tags"

  MrEko::TagParser.parse(filename)
end

Instance Method Details

#validateObject



171
172
173
174
175
# File 'lib/mr_eko/song.rb', line 171

def validate
  super
  set_md5 # no Sequel callback for this?
  validates_unique :md5
end