rapgenius 0.1.0 → 1.0.0
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.
- 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
|