rapgenius 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ module RapGenius
2
+ class Media
3
+ attr_reader :type, :url, :provider
4
+
5
+ def initialize(kwargs)
6
+ @type = kwargs.delete(:type)
7
+ @url = kwargs.delete(:url)
8
+ @provider = kwargs.delete(:provider)
9
+ end
10
+ end
11
+ end
@@ -1,65 +1,137 @@
1
1
  # encoding: utf-8
2
2
  module RapGenius
3
3
  class Song
4
- include RapGenius::Scraper
4
+ include RapGenius::Client
5
+ attr_reader :id
5
6
 
6
- def self.find(path)
7
- self.new(path)
7
+ def self.find(id)
8
+ self.new(id: id).tap { |song| song.document }
8
9
  end
9
10
 
10
- # Search for a song
11
- #
12
- # query - Song to search for
13
- #
14
- # Returns an Array of Song objects.
15
- def self.search(query)
16
- results = Client.search(query)
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
- results.split("\n").map do |song|
19
- info, link, id = song.split('|')
20
- artist, title = info.force_encoding('UTF-8').split(' – ')
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
- new(link, artist: artist, title: title)
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 initialize(path, kwargs = {})
27
- self.url = path
40
+ def url
41
+ response["url"]
42
+ end
28
43
 
29
- @artist = kwargs.delete(:artist)
30
- @title = kwargs.delete(:title)
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 artist
34
- @artist ||= document.css('.song_title a').text
54
+ def artists
55
+ [artist] + featured_artists + producer_artists
35
56
  end
36
57
 
37
58
  def title
38
- @title ||= document.css('.edit_song_description i').text
59
+ @title ||= response["title"]
39
60
  end
40
61
 
41
62
  def description
42
- document.css('.description_body').text
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
- document.css('meta[property="og:image"]').
47
- map { |meta| meta.attr('content') }
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 full_artist
51
- document.css('meta[property="og:title"]').attr('content').to_s.
52
- split(" ").first
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 annotations
56
- @annotations ||= document.css('.lyrics a').map do |a|
57
- Annotation.new(
58
- id: a.attr('data-id').to_s,
59
- song: self,
60
- lyric: a.text
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
@@ -1,3 +1,3 @@
1
1
  module RapGenius
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
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+rapgenius@timrogers.co.uk", "rspeicher@gmail.com"]
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 screen-scraping gem,
15
- you can access the wealth of data on the internet Talmud in Ruby.}
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
@@ -2,61 +2,63 @@ require 'spec_helper'
2
2
 
3
3
  module RapGenius
4
4
  describe Song do
5
- context "given Big Sean's Control", vcr: {cassette_name: "big-sean-control-lyrics"} do
6
- subject { described_class.new("Big-sean-control-lyrics") }
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
- it "should include Big Sean's picture" do
20
- subject.images.should include "http://s3.amazonaws.com/rapgenius/1375029260_Big%20Sean.png"
21
- end
22
- end
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
- describe "#annotations" do
25
- it "should be an Array of Annotation objects" do
26
- subject.annotations.should be_an Array
27
- subject.annotations.first.should be_a Annotation
28
- end
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
- it "should be of a valid length" do
31
- # Annotations get added and removed from the live site; we want our
32
- # count to be somewhat accurate, within reason.
33
- subject.annotations.length.should be_within(15).of(130)
34
- end
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
- describe '.find' do
39
- it "returns a new instance at the specified path" do
40
- i = described_class.find("foobar")
41
- i.should be_a Song
42
- i.url.should eq 'http://rapgenius.com/foobar'
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
- describe '.search', vcr: {cassette_name: 'song-search-big-sean-control'} do
47
- let(:results) { described_class.search('Big Sean Control') }
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
- it "returns an Array of Songs" do
50
- results.should be_an Array
51
- results.first.should be_a Song
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
- describe 'assigned attributes' do
55
- subject { results.first }
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
- its(:url) { should eq "http://rapgenius.com/Big-sean-control-lyrics" }
58
- its(:artist) { should eq "Big Sean (Ft. Jay Electronica & Kendrick Lamar)" }
59
- its(:title) { should eq "Control" }
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