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.
@@ -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