rapgenius 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +8 -1
- data/LICENSE +1 -1
- data/README.md +95 -72
- data/lib/rapgenius.rb +40 -3
- data/lib/rapgenius/artist.rb +54 -0
- data/lib/rapgenius/client.rb +57 -0
- data/lib/rapgenius/exceptions.rb +1 -1
- data/lib/rapgenius/line.rb +59 -0
- data/lib/rapgenius/media.rb +11 -0
- data/lib/rapgenius/song.rb +105 -33
- data/lib/rapgenius/version.rb +1 -1
- data/rapgenius.gemspec +5 -4
- data/spec/rapgenius/artist_spec.rb +33 -0
- data/spec/rapgenius/client_spec.rb +42 -0
- data/spec/rapgenius/line_spec.rb +23 -0
- data/spec/rapgenius/media_spec.rb +19 -0
- data/spec/rapgenius/song_spec.rb +46 -44
- metadata +27 -46
- data/lib/rapgenius/annotation.rb +0 -37
- data/lib/rapgenius/scraper.rb +0 -82
- data/spec/rapgenius/annotation_spec.rb +0 -41
- data/spec/rapgenius/scraper_spec.rb +0 -54
data/lib/rapgenius/song.rb
CHANGED
@@ -1,65 +1,137 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module RapGenius
|
3
3
|
class Song
|
4
|
-
include RapGenius::
|
4
|
+
include RapGenius::Client
|
5
|
+
attr_reader :id
|
5
6
|
|
6
|
-
def self.find(
|
7
|
-
self.new(
|
7
|
+
def self.find(id)
|
8
|
+
self.new(id: id).tap { |song| song.document }
|
8
9
|
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def initialize(kwargs = {})
|
12
|
+
@id = kwargs.delete(:id)
|
13
|
+
@artist = kwargs.delete(:artist)
|
14
|
+
@title = kwargs.delete(:title)
|
15
|
+
self.url = "songs/#{@id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def response
|
19
|
+
document["response"]["song"]
|
20
|
+
end
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
22
|
+
def artist
|
23
|
+
@artist ||= Artist.new(
|
24
|
+
name: response["primary_artist"]["name"],
|
25
|
+
id: response["primary_artist"]["id"],
|
26
|
+
type: :primary
|
27
|
+
)
|
28
|
+
end
|
21
29
|
|
22
|
-
|
30
|
+
def featured_artists
|
31
|
+
@featured_artists ||= response["featured_artists"].map do |artist|
|
32
|
+
Artist.new(
|
33
|
+
name: artist["name"],
|
34
|
+
id: artist["id"],
|
35
|
+
type: :featured
|
36
|
+
)
|
23
37
|
end
|
24
38
|
end
|
25
39
|
|
26
|
-
def
|
27
|
-
|
40
|
+
def url
|
41
|
+
response["url"]
|
42
|
+
end
|
28
43
|
|
29
|
-
|
30
|
-
@
|
44
|
+
def producer_artists
|
45
|
+
@producer_artists ||= response["producer_artists"].map do |artist|
|
46
|
+
Artist.new(
|
47
|
+
name: artist["name"],
|
48
|
+
id: artist["id"],
|
49
|
+
type: :producer
|
50
|
+
)
|
51
|
+
end
|
31
52
|
end
|
32
53
|
|
33
|
-
def
|
34
|
-
|
54
|
+
def artists
|
55
|
+
[artist] + featured_artists + producer_artists
|
35
56
|
end
|
36
57
|
|
37
58
|
def title
|
38
|
-
@title ||=
|
59
|
+
@title ||= response["title"]
|
39
60
|
end
|
40
61
|
|
41
62
|
def description
|
42
|
-
document.
|
63
|
+
@description ||= document["response"]["song"]["description"]["dom"]["children"].map do |node|
|
64
|
+
parse_description(node)
|
65
|
+
end.flatten.join("")
|
43
66
|
end
|
44
67
|
|
45
68
|
def images
|
46
|
-
|
47
|
-
|
69
|
+
@images ||= keys_with_images.map do |key|
|
70
|
+
node = response[key]
|
71
|
+
if node.is_a? Array
|
72
|
+
node.map { |subnode| subnode["image_url"] }
|
73
|
+
elsif node.is_a? Hash
|
74
|
+
node["image_url"]
|
75
|
+
else
|
76
|
+
return
|
77
|
+
end
|
78
|
+
end.flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
def pyongs
|
82
|
+
response["pyongs_count"]
|
83
|
+
end
|
84
|
+
|
85
|
+
def hot?
|
86
|
+
response["stats"]["hot"]
|
87
|
+
end
|
88
|
+
|
89
|
+
def views
|
90
|
+
response["stats"]["pageviews"]
|
91
|
+
end
|
92
|
+
|
93
|
+
def concurrent_viewers
|
94
|
+
response["stats"]["concurrents"]
|
48
95
|
end
|
49
96
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
97
|
+
def media
|
98
|
+
response["media"].map do |m|
|
99
|
+
Media.new(type: m["type"], provider: m["provider"], url: m["url"])
|
100
|
+
end
|
53
101
|
end
|
54
102
|
|
55
|
-
def
|
56
|
-
@
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
103
|
+
def lines
|
104
|
+
@lines ||= response["lyrics"]["dom"]["children"].map do |node|
|
105
|
+
parse_lines(node)
|
106
|
+
end.flatten.compact
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def parse_lines(node)
|
112
|
+
if node.is_a?(Array)
|
113
|
+
node.map { |subnode| parse_lines(subnode) }
|
114
|
+
elsif node.is_a?(String)
|
115
|
+
Line.new(
|
116
|
+
song: Song.new(id: @id),
|
117
|
+
lyric: node
|
118
|
+
)
|
119
|
+
elsif node.is_a?(Hash) && node["tag"] == "p"
|
120
|
+
parse_lines(node["children"])
|
121
|
+
elsif node.is_a?(Hash) && node["tag"] == "a"
|
122
|
+
Line.new(
|
123
|
+
song: Song.new(id: @id),
|
124
|
+
lyric: node["children"].select {|l| l.is_a? String }.join("\n"),
|
125
|
+
id: node["data"]["id"]
|
61
126
|
)
|
127
|
+
else
|
128
|
+
return
|
62
129
|
end
|
63
130
|
end
|
131
|
+
|
132
|
+
def keys_with_images
|
133
|
+
%w{featured_artists producer_artists primary_artist}
|
134
|
+
end
|
135
|
+
|
64
136
|
end
|
65
137
|
end
|
data/lib/rapgenius/version.rb
CHANGED
data/rapgenius.gemspec
CHANGED
@@ -7,14 +7,14 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = RapGenius::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Tim Rogers", "Robert Speicher"]
|
10
|
-
s.email = ["me
|
10
|
+
s.email = ["me@timrogers.co.uk", "rspeicher@gmail.com"]
|
11
11
|
s.homepage = "https://github.com/timrogers/rapgenius"
|
12
12
|
s.summary = %q{A gem for accessing lyrics and explanations on RapGenius.com}
|
13
13
|
s.description = %q{Up until until now, to quote RapGenius themselves,
|
14
|
-
"working at Rap Genius is the API". With this magical
|
15
|
-
you can access the wealth of data on
|
14
|
+
"working at Rap Genius is the API". With this magical gem using the
|
15
|
+
private API in the 'Genius' iOS app you can access the wealth of data on
|
16
|
+
the internet Talmud in Ruby.}
|
16
17
|
|
17
|
-
s.add_runtime_dependency "nokogiri", "~>1.6.0"
|
18
18
|
s.add_runtime_dependency "httparty", "~>0.11.0"
|
19
19
|
s.add_development_dependency "rspec", "~>2.14.1"
|
20
20
|
s.add_development_dependency "mocha", "~>0.14.0"
|
@@ -25,4 +25,5 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
26
26
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
27
27
|
s.require_paths = ["lib"]
|
28
|
+
s.license = 'MIT'
|
28
29
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RapGenius
|
4
|
+
describe Artist do
|
5
|
+
context "given Drake", vcr: { cassette_name: "artist-130" } do
|
6
|
+
subject(:artist) { described_class.find(130) }
|
7
|
+
|
8
|
+
its(:url) { should eq "http://rapgenius.com/artists/Drake" }
|
9
|
+
its(:name) { should eq "Drake" }
|
10
|
+
its(:image) { should eq "http://images.rapgenius.com/2b3fa8326a5277fa31f2012a7b581e2e.500x319x11.gif" }
|
11
|
+
its(:description) { should include "Drake is part of a generation of new rappers" }
|
12
|
+
|
13
|
+
context "#songs" do
|
14
|
+
subject { artist.songs }
|
15
|
+
|
16
|
+
# The iOS app only loads a certain number, and doesn't (appear to)
|
17
|
+
# support pagination
|
18
|
+
its(:count) { should eq 25}
|
19
|
+
|
20
|
+
its(:last) { should be_a Song }
|
21
|
+
its("last.title") { should eq "Bitch Is Crazy" }
|
22
|
+
end
|
23
|
+
|
24
|
+
context "a non-existent artist ID" do
|
25
|
+
subject(:artist) { described_class.find("bahahaha") }
|
26
|
+
|
27
|
+
it "raises an exception" do
|
28
|
+
expect { artist }.to raise_exception
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ClientTester
|
4
|
+
# On other classes, we don't expose this as we'd rather use this attribute
|
5
|
+
# for the URL of the song itself
|
6
|
+
attr_reader :url
|
7
|
+
|
8
|
+
include RapGenius::Client
|
9
|
+
end
|
10
|
+
|
11
|
+
module RapGenius
|
12
|
+
describe Client do
|
13
|
+
|
14
|
+
let(:client) { ClientTester.new }
|
15
|
+
|
16
|
+
describe "#url=" do
|
17
|
+
it "forms the URL with the base URL, if the current path is relative" do
|
18
|
+
client.url = "foobar"
|
19
|
+
client.url.should include RapGenius::Client::BASE_URL
|
20
|
+
end
|
21
|
+
|
22
|
+
it "leaves the URL as it is if already complete" do
|
23
|
+
client.url = "http://foobar.com/baz"
|
24
|
+
client.url.should eq "http://foobar.com/baz"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#document" do
|
29
|
+
before { client.url = "http://foo.bar" }
|
30
|
+
|
31
|
+
context "with a failed request" do
|
32
|
+
before do
|
33
|
+
stub_request(:get, "http://foo.bar").to_return({body: '', status: 404})
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raises a ScraperError" do
|
37
|
+
expect { client.document }.to raise_error(RapGenius::Error)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RapGenius
|
4
|
+
describe Line, vcr: { cassette_name: "line-2638695" } do
|
5
|
+
|
6
|
+
let(:line) { described_class.find("2638695") }
|
7
|
+
subject { line }
|
8
|
+
|
9
|
+
its(:id) { should eq "2638695" }
|
10
|
+
its(:song) { should be_a Song }
|
11
|
+
its(:lyric) { should eq "Versace, Versace, Medusa head on me like I'm 'luminati" }
|
12
|
+
its("explanations.first") { should include "Versace’s logo is the head of Medusa" }
|
13
|
+
its(:explanations) { should eq line.annotations }
|
14
|
+
|
15
|
+
context "a non-existent referent ID" do
|
16
|
+
let(:line) { described_class.find("bahahaha") }
|
17
|
+
|
18
|
+
it "raises an exception" do
|
19
|
+
expect { line }.to raise_exception
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RapGenius
|
4
|
+
describe Media do
|
5
|
+
|
6
|
+
subject(:media) do
|
7
|
+
Media.new(
|
8
|
+
type: "foo",
|
9
|
+
url: "foo",
|
10
|
+
provider: "foo"
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
its(:type) { should eq "foo" }
|
15
|
+
its(:url) { should eq "foo" }
|
16
|
+
its(:provider) { should eq "foo" }
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/spec/rapgenius/song_spec.rb
CHANGED
@@ -2,61 +2,63 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module RapGenius
|
4
4
|
describe Song do
|
5
|
-
context "given
|
6
|
-
subject { described_class.
|
7
|
-
|
8
|
-
its(:url) { should eq "http://rapgenius.com/Big-sean-control-lyrics" }
|
9
|
-
its(:title) { should eq "Control" }
|
10
|
-
its(:artist) { should eq "Big Sean" }
|
11
|
-
its(:description) { should include "blew up the Internet" }
|
12
|
-
its(:full_artist) { should include "(Ft. Jay Electronica & Kendrick Lamar)"}
|
13
|
-
|
14
|
-
describe "#images" do
|
15
|
-
it "should be an Array" do
|
16
|
-
subject.images.should be_an Array
|
17
|
-
end
|
5
|
+
context "given Migos's Versace", vcr: { cassette_name: "song-176872" } do
|
6
|
+
subject(:song) { described_class.find(176872) }
|
18
7
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
8
|
+
its(:url) { should eq "http://rapgenius.com/Migos-versace-lyrics" }
|
9
|
+
its(:title) { should eq "Versace" }
|
10
|
+
|
11
|
+
its(:description) { should include "they absolutely killed it" }
|
23
12
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
13
|
+
context "#artist" do
|
14
|
+
subject { song.artist }
|
15
|
+
it { should be_a Artist }
|
16
|
+
its(:name) { should eq "Migos" }
|
17
|
+
end
|
29
18
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
19
|
+
context "#featured_artists" do
|
20
|
+
subject { song.featured_artists }
|
21
|
+
its(:length) { should eq 1 }
|
22
|
+
its("first.name") { should eq "Drake" }
|
23
|
+
its(:first) { should be_a Artist }
|
35
24
|
end
|
36
|
-
end
|
37
25
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
26
|
+
context "#producer_artists" do
|
27
|
+
subject { song.producer_artists }
|
28
|
+
its(:length) { should eq 1 }
|
29
|
+
its("first.name") { should eq "Zaytoven" }
|
30
|
+
its(:first) { should be_a Artist }
|
43
31
|
end
|
44
|
-
end
|
45
32
|
|
46
|
-
|
47
|
-
|
33
|
+
context "#media" do
|
34
|
+
subject { song.media }
|
35
|
+
its(:length) { should eq 2 }
|
36
|
+
its(:first) { should be_a Media }
|
37
|
+
its("first.provider") { should eq "soundcloud" }
|
38
|
+
end
|
48
39
|
|
49
|
-
|
50
|
-
|
51
|
-
|
40
|
+
context "#lines" do
|
41
|
+
subject { song.lines }
|
42
|
+
its(:count) { should eq 81 }
|
43
|
+
its(:first) { should be_a Line }
|
44
|
+
its("first.id") { should eq "1983907" }
|
45
|
+
its("first.lyric") { should eq "[Verse 1: Drake]" }
|
46
|
+
its("first.explanations.first") { should include "Versace used his verse in this runway show" }
|
52
47
|
end
|
53
48
|
|
54
|
-
|
55
|
-
|
49
|
+
its(:images) { should include "http://images.rapgenius.com/2b3fa8326a5277fa31f2012a7b581e2e.500x319x11.gif" }
|
50
|
+
its(:pyongs) { should eq 22 }
|
51
|
+
its(:hot?) { should eq false }
|
52
|
+
its(:views) { should eq 1834811 }
|
53
|
+
its(:concurrent_viewers) { should eq 9 }
|
54
|
+
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
context "a non-existent song ID" do
|
57
|
+
subject(:song) { described_class.find("bahahaha") }
|
58
|
+
|
59
|
+
it "raises an exception" do
|
60
|
+
expect { song }.to raise_exception
|
61
|
+
end
|
60
62
|
end
|
61
63
|
end
|
62
64
|
end
|