lastfm-path-finder 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "lastfm"
4
+ gem "rspec", "2.11"
5
+ gem "spork"
6
+ gem "vcr"
7
+ gem "webmock"
8
+ gem "redis-objects"
9
+ gem 'rspec-redis_helper'
10
+ gem "settingslogic"
11
+ gem "commander"
12
+ gem "jeweler"
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 David J. Brenes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,15 @@
1
+ This project is the implementation of a conversation between César Alvarez Doval and myself, a program which finds a path between two (apparently) unrelated artists.
2
+
3
+ == How to use it?
4
+
5
+ First, donwolad this repository and install the dependant gems:
6
+
7
+ gem install bundler
8
+
9
+ and then:
10
+
11
+ bundle install
12
+
13
+ and then run the command:
14
+
15
+ ./lastfm-path-finder.rb "Pink Floyd" "Franz Ferdinand"
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,3 @@
1
+ lastfm:
2
+ api_key: --API KEY HERE --
3
+ api_secret: --API SECRET HERE --
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'commander/import'
5
+ require 'lib/lastfm_path_finder'
6
+
7
+ program :version, "1.0"
8
+ program :description, 'Finding paths between artists in Lastfm since 1888'
9
+
10
+ default_command :find
11
+
12
+ command :find do |c|
13
+ c.syntax = 'lastfm-path-finder find [options]'
14
+ c.summary = 'Find paths between two artists'
15
+ c.description = 'Find paths between two artists in Last.fm'
16
+ c.example 'Search the path between Pink Floyd and Franz Ferdinand', 'lastfm-path-finder find "Pink Floyd" "Franz Ferdinand"'
17
+ c.action do |args, options|
18
+
19
+ from_name = args.first || ask("One artist: ")
20
+ to_name = args[1] || ask("Another artist: ")
21
+
22
+ from = LastfmPathFinder::Artist.new(:name => from_name)
23
+ to = LastfmPathFinder::Artist.new(:name => to_name)
24
+ path = LastfmPathFinder::Finder.find from, to
25
+
26
+ if path.found?
27
+ say "Path found! #{path.artists.values.join(" --> ")}"
28
+ else
29
+ say "No path found"
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,42 @@
1
+ require 'active_support/core_ext'
2
+ class LastfmPathFinder::Artist
3
+
4
+ include Redis::Objects
5
+
6
+ attr_accessor :id
7
+
8
+ value :name
9
+ sorted_set :related_artists
10
+
11
+ def initialize params
12
+ params.symbolize_keys!
13
+ self.id = params [:id] || (params[:name]).parameterize
14
+ self.name = params[:name]
15
+ end
16
+
17
+ alias_method :related_artists_without_lastfm, :related_artists
18
+
19
+ def related_artists
20
+ related = related_artists_without_lastfm
21
+ related.blank? ? related_artists_in_lastfm : related
22
+ end
23
+
24
+ def related_artists_in_lastfm
25
+ artists = LastfmPathFinder::Settings.lastfm_api.artist.get_similar(self.name.value)
26
+ related_artists_without_lastfm.clear
27
+ artists.each do |artist|
28
+ related_artists_without_lastfm[artist["name"]] = artist["match"].to_f
29
+ end
30
+ related_artists_without_lastfm
31
+ end
32
+
33
+ def self.find_in_lastfm name
34
+ begin
35
+ LastfmPathFinder::Artist.new(LastfmPathFinder::Settings.lastfm_api.artist.get_info(name))
36
+ rescue Lastfm::ApiError => e
37
+ nil
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,119 @@
1
+ class LastfmPathFinder::Finder
2
+
3
+ # This method finds a way between the two artists. How?
4
+ # 0) We ensure that we don't already have a path for these artists
5
+ # 1) We get the related to From
6
+ # 2) We check if To is in this related list
7
+ # 3) If it's not, we get the related to To
8
+ # 4) We check if From is in this list
9
+ # 5) We check if both related lists share (at least, one artist)
10
+ # 6) We update the score tables and try in a recursive way to find a way between any pair of nodes of the relateds lists
11
+ def self.find from, to, options = {}
12
+
13
+ default_options = {max_length: 7, threshold: 0.4}
14
+ options.merge! default_options
15
+
16
+ currently_included = options[:currently_included] || []
17
+ currently_included = currently_included.dup
18
+
19
+ # 0)
20
+
21
+ path = LastfmPathFinder::Path.new from, to
22
+ return path if path.found?
23
+
24
+ currently_included.push from.name.value
25
+ currently_included.push to.name.value
26
+
27
+ # 1)
28
+ related_from = from.related_artists
29
+
30
+ # 2)
31
+ if related_from.member?(to.name.value)
32
+ score = related_from.score(to.name.value)
33
+ path = LastfmPathFinder::Path.new from, to
34
+ path.score = score
35
+ path.artists << from.name.value
36
+ path.artists << to.name.value
37
+ return path
38
+ end
39
+
40
+ # 3)
41
+ related_to = to.related_artists
42
+
43
+ # 4)
44
+ if related_from.member?(from.name.value)
45
+ score = related_to.score(from.name.value)
46
+ path = LastfmPathFinder::Path.new from, to
47
+ path.score = score
48
+ path.artists << from.name.value
49
+ path.artists << to.name.value
50
+ return path
51
+ end
52
+
53
+ # 5)
54
+ related_from_members = related_from.members
55
+ related_to_members = related_to.members
56
+
57
+ # We create an array of shared relates were we are gonna insert those artists present on both lists
58
+ # with an score wquals to the product of both scores, so we can find the most related to both artists
59
+ shared_relate = []
60
+ related_from_members.each do |name|
61
+
62
+ if related_to.member? name
63
+ score = related_from.score(name)
64
+ other_score = related_to.score(name)
65
+ shared_relate << {:name => name, :score => score*other_score}
66
+ end
67
+
68
+ end
69
+
70
+ # Now we get the first one and return a path with these three nodes
71
+ unless shared_relate.blank?
72
+ shared_contact = shared_relate.sort_by{|r|r[:score]}.last
73
+
74
+ path = LastfmPathFinder::Path.new from, to
75
+ path.score = shared_contact[:score]
76
+
77
+ path.artists << from.name.value
78
+ path.artists << shared_contact[:name]
79
+ path.artists << to.name.value
80
+
81
+ return path
82
+ end
83
+
84
+ #6)
85
+
86
+ # If the path is too long we don't look for longer paths
87
+
88
+ return path if currently_included.size > (options[:max_length]-2)
89
+
90
+ # We store the artists for the score
91
+ related_from_members.reverse!
92
+ related_to_members.reverse!
93
+
94
+ (related_from_members - currently_included).reject{|from_name|related_from.score(from_name) < options[:threshold] }.each do |from_name|
95
+
96
+ from_score = related_from.score(from_name)
97
+
98
+ (related_to_members - currently_included).reject{|to_name|related_to.score(to_name) < options[:threshold] }.each do |to_name|
99
+
100
+ to_score = related_to.score(to_name)
101
+
102
+ path = self.find(LastfmPathFinder::Artist.new(:name => from_name), LastfmPathFinder::Artist.new(:name => to_name), options.merge(currently_included: currently_included))
103
+ if path.found?
104
+ new_path = LastfmPathFinder::Path.new from, to
105
+ new_path.score = path.score * from_score * to_score
106
+ new_path.artists << from.name.value
107
+ path.artists.each {|a| new_path.artists << a }
108
+ new_path.artists << to.name.value
109
+ return new_path
110
+ end
111
+
112
+ end
113
+ end
114
+
115
+ path
116
+
117
+ end
118
+
119
+ end
@@ -0,0 +1,22 @@
1
+ class LastfmPathFinder::Path
2
+
3
+ include Redis::Objects
4
+
5
+ attr_accessor :id
6
+ attr_accessor :artist_from, :artist_to
7
+
8
+ list :artists
9
+ value :score
10
+
11
+ def initialize from, to
12
+ self.artist_from = from
13
+ self.artist_to = to
14
+ self.id = "#{artist_from.id}_#{artist_to.id}"
15
+ end
16
+
17
+ def found?
18
+ !artists.values.empty?
19
+ end
20
+
21
+
22
+ end
@@ -0,0 +1,14 @@
1
+ require 'settingslogic'
2
+ require 'lastfm'
3
+
4
+ class LastfmPathFinder::Settings < Settingslogic
5
+ source "config/settings.yml"
6
+
7
+ def self.lastfm_api
8
+ @@lastfm ||= Lastfm.new(self["lastfm"]["api_key"], self["lastfm"]["api_secret"])
9
+ end
10
+
11
+ def self.redis_connection
12
+ Redis.current ||= Redis.new self["redis"]
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module LastfmPathFinder
2
+ require 'rubygems'
3
+ require 'lastfm'
4
+ require 'redis'
5
+ require 'redis/objects'
6
+
7
+ require_relative 'lastfm_path_finder/settings'
8
+ Settings.redis_connection
9
+ require_relative 'lastfm_path_finder/artist'
10
+ require_relative 'lastfm_path_finder/path'
11
+ require_relative 'lastfm_path_finder/finder'
12
+ end
@@ -0,0 +1,45 @@
1
+ require 'lib/lastfm_path_finder'
2
+
3
+ describe LastfmPathFinder::Artist do
4
+
5
+ it "should have no data when not requested to LastFM" do
6
+ artist = LastfmPathFinder::Artist.new(:id => "pink-floyd")
7
+ artist.name.should be_nil
8
+ end
9
+ it "gets data for existing artists from Last.FM" do
10
+
11
+ VCR.use_cassette('lastfm', :record => :new_episodes) do
12
+ artist = LastfmPathFinder::Artist.find_in_lastfm "pink floyd"
13
+ artist.should_not be_nil
14
+ artist.id.should be_eql("pink-floyd")
15
+ artist.name.should == "Pink Floyd"
16
+ end
17
+
18
+ end
19
+
20
+ it "handles non-existing artists when retrieving info from Last.FM" do
21
+
22
+ VCR.use_cassette('lastfm', :record => :new_episodes) do
23
+ artist = LastfmPathFinder::Artist.find_in_lastfm "who is pink?"
24
+ artist.should be_nil
25
+ end
26
+
27
+ end
28
+
29
+ it "gets data about related artists on LastFM" do
30
+
31
+ VCR.use_cassette('lastfm', :record => :new_episodes) do
32
+ artist = LastfmPathFinder::Artist.new :name => "pink floyd"
33
+ related = artist.related_artists
34
+ related.should_not be_nil
35
+
36
+ related.members.last.should be_eql("David Gilmour")
37
+ related.score("David Gilmour").should be_eql(1.0)
38
+ related.members[-2].should be_eql("Roger Waters")
39
+ related.score("Roger Waters").should be_eql(0.778598)
40
+ end
41
+
42
+ end
43
+
44
+
45
+ end
@@ -0,0 +1,49 @@
1
+ require 'lib/lastfm_path_finder'
2
+
3
+ describe LastfmPathFinder::Path do
4
+
5
+ let(:from) {LastfmPathFinder::Artist.new(:name => from_name)}
6
+ let(:to) {LastfmPathFinder::Artist.new(:name => to_name)}
7
+ let(:path) do
8
+ VCR.use_cassette('lastfm', :record => :new_episodes) do
9
+ LastfmPathFinder::Finder.find from, to
10
+ end
11
+ end
12
+
13
+ context "when there's a direct path between artists" do
14
+
15
+ let(:from_name) {"Pink Floyd"}
16
+ let(:to_name) {"David Gilmour"}
17
+
18
+ subject { path }
19
+ it { should be_found }
20
+ it { path.artists.count.should be_eql(2) }
21
+
22
+ end
23
+
24
+ context "when two artists share a common related artist" do
25
+
26
+ let(:from_name) {"Pink Floyd"}
27
+ let(:to_name) {"B.B. King & Eric Clapton"}
28
+
29
+ subject { path }
30
+ it { should be_found }
31
+ it { path.artists.count.should be_eql(3) }
32
+ it { path.artists.values.should == (["Pink Floyd","Eric Clapton","B.B. King & Eric Clapton"]) }
33
+
34
+ end
35
+
36
+
37
+ context "when two artists not share a common related artist" do
38
+
39
+ let(:from_name) {"Pink Floyd"}
40
+ let(:to_name) {"The Sunday Drivers"}
41
+
42
+ subject { path }
43
+ it { should be_found }
44
+ it { path.artists.count.should be_eql(7) }
45
+ it { path.artists.values.should == ( ["Pink Floyd", "David Gilmour", "Nick Mason", "Serú Girán", "Ismael Serrano", "Quique González", "The Sunday Drivers"]) }
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require "bundler/setup"
3
+ require 'spork'
4
+ require 'rspec'
5
+ require 'rspec-redis_helper'
6
+ #uncomment the following line to use spork with the debugger
7
+ #require 'spork/ext/ruby-debug'
8
+
9
+ Spork.prefork do
10
+
11
+ require 'vcr'
12
+
13
+ VCR.configure do |c|
14
+ c.allow_http_connections_when_no_cassette = true
15
+ c.cassette_library_dir = 'spec/vcr'
16
+ c.hook_into :webmock # or :fakeweb
17
+ end
18
+
19
+
20
+ RSpec.configure do |spec|
21
+ spec.include RSpec::RedisHelper
22
+
23
+ # slightly modified from RSpec::RedisHelper gem site https://github.com/mlanett/rspec-redis_helper
24
+ spec.around(:each) do |example|
25
+ with_clean_redis do
26
+ example.run
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ Spork.each_run do
34
+ # This code will be run each time you run your specs.
35
+ end
36
+
37
+ # --- Instructions ---
38
+ # Sort the contents of this file into a Spork.prefork and a Spork.each_run
39
+ # block.
40
+ #
41
+ # The Spork.prefork block is run only once when the spork server is started.
42
+ # You typically want to place most of your (slow) initializer code in here, in
43
+ # particular, require'ing any 3rd-party gems that you don't normally modify
44
+ # during development.
45
+ #
46
+ # The Spork.each_run block is run each time you run your specs. In case you
47
+ # need to load files that tend to change during development, require them here.
48
+ # With Rails, your application modules are loaded automatically, so sometimes
49
+ # this block can remain empty.
50
+ #
51
+ # Note: You can modify files loaded *from* the Spork.each_run block without
52
+ # restarting the spork server. However, this file itself will not be reloaded,
53
+ # so if you change any of the code inside the each_run block, you still need to
54
+ # restart the server. In general, if you have non-trivial code in this file,
55
+ # it's advisable to move it into a separate file so you can easily edit it
56
+ # without restarting spork. (For example, with RSpec, you could move
57
+ # non-trivial code into a file spec/support/my_helper.rb, making sure that the
58
+ # spec/support/* files are require'd from inside the each_run block.)
59
+ #
60
+ # Any code that is left outside the two blocks will be run during preforking
61
+ # *and* during each_run -- that's probably not what you want.
62
+ #
63
+ # These instructions should self-destruct in 10 seconds. If they don't, feel
64
+ # free to delete them.
65
+
66
+
67
+
68
+
69
+
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lastfm-path-finder
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.0.0
6
+ platform: ruby
7
+ authors:
8
+ - David J. Brenes
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-07-28 00:00:00 +02:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: lastfm
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis-objects
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: settingslogic
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: commander
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :runtime
59
+ version_requirements: *id004
60
+ description: Gem for finding paths between artists in Last.fm
61
+ email: davidjbrenes@gmail.com
62
+ executables: []
63
+
64
+ extensions: []
65
+
66
+ extra_rdoc_files:
67
+ - LICENSE.txt
68
+ - README
69
+ files:
70
+ - Gemfile
71
+ - README
72
+ - VERSION
73
+ - config/settings.example.yml
74
+ - lastfm-path-finder.rb
75
+ - lib/lastfm_path_finder.rb
76
+ - lib/lastfm_path_finder/artist.rb
77
+ - lib/lastfm_path_finder/finder.rb
78
+ - lib/lastfm_path_finder/path.rb
79
+ - lib/lastfm_path_finder/settings.rb
80
+ - spec/models/artist.rb
81
+ - spec/models/path.rb
82
+ - spec/spec_helper.rb
83
+ - LICENSE.txt
84
+ has_rdoc: true
85
+ homepage: http://github.com/brenes/lastfm-path-finder
86
+ licenses:
87
+ - MIT
88
+ post_install_message:
89
+ rdoc_options: []
90
+
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: "0"
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: "0"
105
+ requirements: []
106
+
107
+ rubyforge_project:
108
+ rubygems_version: 1.6.2
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Finding paths between artists in Lastfm since 1888
112
+ test_files: []
113
+