genius-api 0.3.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/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +23 -0
- data/.github/workflows/check-source-branch.yml +8 -0
- data/.github/workflows/ci.yml +38 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +106 -0
- data/CONTRIBUTING.md +368 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +174 -0
- data/LICENSE.txt +674 -0
- data/README.md +288 -0
- data/SECURITY.md +14 -0
- data/Steepfile +13 -0
- data/bin/console +15 -0
- data/bin/release +5 -0
- data/bin/setup +21 -0
- data/docscribe.yml +9 -0
- data/exe/genius-api +4 -0
- data/genius-api.gemspec +47 -0
- data/lib/extensions/deep_find.rb +36 -0
- data/lib/extensions/extensions.rb +12 -0
- data/lib/extensions/options_helper.rb +17 -0
- data/lib/extensions/token_ext.rb +12 -0
- data/lib/extensions/unescape.rb +13 -0
- data/lib/genius/api/account.rb +35 -0
- data/lib/genius/api/annotations.rb +90 -0
- data/lib/genius/api/artists.rb +82 -0
- data/lib/genius/api/authorization.rb +47 -0
- data/lib/genius/api/errors.rb +211 -0
- data/lib/genius/api/referents.rb +38 -0
- data/lib/genius/api/search.rb +26 -0
- data/lib/genius/api/songs.rb +84 -0
- data/lib/genius/api/version.rb +8 -0
- data/lib/genius/api/web_pages.rb +26 -0
- data/lib/genius/api.rb +23 -0
- data/rbs_collection.lock.yaml +232 -0
- data/rbs_collection.yaml +14 -0
- data/sig/lib/extensions/deep_find.rbs +30 -0
- data/sig/lib/extensions/options_helper.rbs +3 -0
- data/sig/lib/extensions/token_ext.rbs +3 -0
- data/sig/lib/extensions/unescape.rbs +3 -0
- data/sig/lib/genius/api/account.rbs +23 -0
- data/sig/lib/genius/api/annotations.rbs +13 -0
- data/sig/lib/genius/api/artists.rbs +15 -0
- data/sig/lib/genius/api/authorization.rbs +11 -0
- data/sig/lib/genius/api/errors.rbs +46 -0
- data/sig/lib/genius/api/referents.rbs +7 -0
- data/sig/lib/genius/api/search.rbs +5 -0
- data/sig/lib/genius/api/songs.rbs +15 -0
- data/sig/lib/genius/api/version.rbs +5 -0
- data/sig/lib/genius/api/web_pages.rbs +5 -0
- data/sig/lib/genius/api.rbs +9 -0
- metadata +313 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# An artist is how Genius represents the creator of one or more songs (or other documents hosted on Genius). It's
|
|
5
|
+
# usually a musician or group of musicians.
|
|
6
|
+
module Artists
|
|
7
|
+
class << self
|
|
8
|
+
# Data for a specific artist.
|
|
9
|
+
#
|
|
10
|
+
# @param [String?] token Token to access https://api.genius.com.
|
|
11
|
+
# @param [Integer?] id ID of the artist.
|
|
12
|
+
# @raise [ArgumentError] if +id+ is nil.
|
|
13
|
+
# @return [Hash, nil]
|
|
14
|
+
def artists(token: nil, id: nil)
|
|
15
|
+
Auth.authorized?(method_name: "#{Module.nesting[1].name}.#{__method__}") if token.nil?
|
|
16
|
+
Errors.validate_token(token) unless token.nil?
|
|
17
|
+
raise ArgumentError, "`id` can't be nil!" if id.nil?
|
|
18
|
+
|
|
19
|
+
response = HTTParty.get("#{Api::RESOURCE}/artists/#{id}?access_token=#{token_ext(token)}").body
|
|
20
|
+
JSON.parse(response)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Songs for the artist specified. By default 20 items per request.
|
|
24
|
+
#
|
|
25
|
+
# @param [String?] token Token to access https://api.genius.com.
|
|
26
|
+
# @param [Integer?] id ID of the artist.
|
|
27
|
+
# @param [Hash] options Optional query params: +:sort+, +:per_page+, +:page+.
|
|
28
|
+
# @return [Hash, nil]
|
|
29
|
+
def artists_songs(token: nil, id: nil, options: {})
|
|
30
|
+
return if token.nil? && !Auth.authorized?.nil?
|
|
31
|
+
|
|
32
|
+
Errors.validate_token(token) unless token.nil?
|
|
33
|
+
|
|
34
|
+
sort_values = %w[title popularity]
|
|
35
|
+
validate(sort_values, sort: options[:sort], per_page: options[:per_page], page: options[:page])
|
|
36
|
+
|
|
37
|
+
params = options_helper(options, %i[sort per_page page])
|
|
38
|
+
response = HTTParty.get("#{Api::RESOURCE}/artists/#{id}?access_token=#{token_ext(token)}#{params}").body
|
|
39
|
+
JSON.parse(response)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Validates sort, per_page, and page options for artists endpoint.
|
|
45
|
+
#
|
|
46
|
+
# @private
|
|
47
|
+
# @param [Array] sort_values Allowed sort values.
|
|
48
|
+
# @param [Object] options Options with +:sort+, +:per_page+, +:page+.
|
|
49
|
+
# @return [void]
|
|
50
|
+
def validate(sort_values, **options)
|
|
51
|
+
validate_sort(options[:sort], sort_values)
|
|
52
|
+
validate_page_per_page(options[:per_page])
|
|
53
|
+
validate_page_per_page(options[:page])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validates the sort option against allowed values.
|
|
57
|
+
#
|
|
58
|
+
# @private
|
|
59
|
+
# @param [String?] sort Sort value to validate.
|
|
60
|
+
# @param [Array] sort_values Allowed sort values.
|
|
61
|
+
# @raise [ArgumentError] if +sort+ is not in +sort_values+.
|
|
62
|
+
# @return [void]
|
|
63
|
+
def validate_sort(sort, sort_values)
|
|
64
|
+
return unless sort && !sort_values.include?(sort)
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, "`sort` can't be #{sort}. Possible values: #{sort_values.join(', ')}."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validates that per_page or page is not negative.
|
|
70
|
+
#
|
|
71
|
+
# @private
|
|
72
|
+
# @param [Integer?] page_per_page Value to validate.
|
|
73
|
+
# @raise [ArgumentError] if value is negative.
|
|
74
|
+
# @return [void]
|
|
75
|
+
def validate_page_per_page(page_per_page)
|
|
76
|
+
raise ArgumentError, "`per_page` or `page` can't be negative." if page_per_page&.negative?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Genius::Errors::DynamicRescue.rescue(Module.nesting[1])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# +Genius::Auth+ module is used to authenticate users with their token. It
|
|
5
|
+
# provides initialization of token instance variable.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# Genius::Auth.login="yuiaYqbncErCVwItjQxFspNWUZLhGpXrPbkvgbgHSEKJRAlToamzMfdOeDB"
|
|
9
|
+
module Auth
|
|
10
|
+
class << self
|
|
11
|
+
# Sets the authentication token after validation.
|
|
12
|
+
#
|
|
13
|
+
# @param [String] token Token to access https://api.genius.com.
|
|
14
|
+
# @raise [Genius::Errors::TokenError] if +token+ is invalid.
|
|
15
|
+
# @return [String]
|
|
16
|
+
def token=(token)
|
|
17
|
+
Genius::Errors.validate_token(token)
|
|
18
|
+
@token = token
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Checks if the current token is authorized. Returns +false+ on validation failure.
|
|
22
|
+
#
|
|
23
|
+
# @param [String] token Token to validate.
|
|
24
|
+
# @param [String] method_name Method name for error messages.
|
|
25
|
+
# @raise [Genius::Errors::TokenError]
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def authorized?(token = @token, method_name: "#{Module.nesting[1].name}.#{__method__}")
|
|
28
|
+
Errors.validate_token(token, method_name: method_name)
|
|
29
|
+
rescue Genius::Errors::TokenError
|
|
30
|
+
false
|
|
31
|
+
else
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Revokes the current session by setting the token to +nil+.
|
|
36
|
+
#
|
|
37
|
+
# @return [nil]
|
|
38
|
+
def logout!
|
|
39
|
+
@token = nil unless @token.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias login= token=
|
|
43
|
+
|
|
44
|
+
Genius::Errors::DynamicRescue.rescue(Module.nesting[1])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# +Genius::Errors+ module includes custom exception classes and methods to
|
|
5
|
+
# handle all errors during requests to https://api.genius.com or during
|
|
6
|
+
# the work with library methods.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# module Genius
|
|
10
|
+
# module Foo
|
|
11
|
+
# include Genius::Errors
|
|
12
|
+
# class << self
|
|
13
|
+
# def bar(params)
|
|
14
|
+
# # body
|
|
15
|
+
# rescue TokenError => e
|
|
16
|
+
# puts "Error description: #{e.msg}" #=> Invalid token!....
|
|
17
|
+
# puts "Error description: #{e.exception_type}" #=> token_error
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Exception classes fields provide custom message and error types
|
|
24
|
+
# (+connection_error+, +token_error+, +auth_required+, etc.)
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# begin
|
|
28
|
+
# raise TokenError.new(msg: "Message", error_type: "error_type")
|
|
29
|
+
# rescue TokenError => e
|
|
30
|
+
# puts e.message #=> Message
|
|
31
|
+
# puts e.exception_type #=> error_type
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# There will be a standard output of each exception if there will be no
|
|
35
|
+
# params provided.
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# begin
|
|
39
|
+
# raise TokenError
|
|
40
|
+
# rescue TokenError => e
|
|
41
|
+
# puts e.message #=> Invalid token!....
|
|
42
|
+
# puts e.exception_type #=> token_error
|
|
43
|
+
# end
|
|
44
|
+
module Errors
|
|
45
|
+
# Endpoint for resource.
|
|
46
|
+
ENDPOINT = "#{Api::RESOURCE}/account/?access_token".freeze
|
|
47
|
+
|
|
48
|
+
# Abstract class to store all exception classes in a single object.
|
|
49
|
+
class GeniusExceptionSuperClass < StandardError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A +TokenError+ object provides handling error during token validation.
|
|
53
|
+
# It throws error when +token+ is invalid - expired, revoked or something
|
|
54
|
+
# else. To generate new token you should go to
|
|
55
|
+
# https://genius.com/signup_or_login and login, then you need to create
|
|
56
|
+
# new client via the link below: https://genius.com/api-clients and
|
|
57
|
+
# generate new access token. Fields to create new api client can
|
|
58
|
+
# be filled in as you like - there is no restrictions and standards.
|
|
59
|
+
class TokenError < GeniusExceptionSuperClass
|
|
60
|
+
attr_reader :msg, :exception_type, :method_name
|
|
61
|
+
|
|
62
|
+
# Initializes a token validation error with optional method name hint.
|
|
63
|
+
#
|
|
64
|
+
# @param [String] msg Error message.
|
|
65
|
+
# @param [String] exception_type Error type identifier.
|
|
66
|
+
# @param [String?] method_name Optional method name for user hint.
|
|
67
|
+
# @return [void]
|
|
68
|
+
def initialize(msg: 'Invalid token. The access token provided is expired, revoked, malformed or invalid for ' \
|
|
69
|
+
'other reasons.', exception_type: 'token_error', method_name: nil)
|
|
70
|
+
@msg = if method_name.nil?
|
|
71
|
+
msg
|
|
72
|
+
else
|
|
73
|
+
"#{msg} or type #{method_name}(token: \"YOUR_TOKEN\")"
|
|
74
|
+
end
|
|
75
|
+
@exception_type = exception_type
|
|
76
|
+
super(msg)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A +LyricsNotFoundError+ object handles an exception where JSON with
|
|
81
|
+
# lyrics is not found.
|
|
82
|
+
class LyricsNotFoundError < GeniusExceptionSuperClass
|
|
83
|
+
attr_reader :msg, :exception_type
|
|
84
|
+
|
|
85
|
+
# Initializes a lyrics-not-found error.
|
|
86
|
+
#
|
|
87
|
+
# @param [String] msg Error message.
|
|
88
|
+
# @param [String] exception_type Error type identifier.
|
|
89
|
+
# @return [void]
|
|
90
|
+
def initialize(msg: 'Lyrics not found in current session. Retrying...', exception_type: 'invalid_lyrics')
|
|
91
|
+
@msg = msg
|
|
92
|
+
@exception_type = exception_type
|
|
93
|
+
super(msg)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A +PageNotFound+ object handles an exception where response payload is
|
|
98
|
+
# invalid and Genius itself or its related service returns not found.
|
|
99
|
+
class PageNotFound < GeniusExceptionSuperClass
|
|
100
|
+
attr_reader :msg, :exception_type
|
|
101
|
+
|
|
102
|
+
# Initializes a page-not-found error.
|
|
103
|
+
#
|
|
104
|
+
# @param [String] msg Error message.
|
|
105
|
+
# @param [String] exception_type Error type identifier.
|
|
106
|
+
# @return [void]
|
|
107
|
+
def initialize(msg: 'Page not found. Try again with another response', exception_type: 'page_not_found')
|
|
108
|
+
@msg = msg
|
|
109
|
+
@exception_type = exception_type
|
|
110
|
+
super(msg)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Checks if the HTML response indicates a page-not-found error.
|
|
114
|
+
#
|
|
115
|
+
# @param [Object] html Parsed HTML document.
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def self.page_not_found?(html)
|
|
118
|
+
html.text.include?('Page not found')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# +Genius::Errors::DynamicRescue+ module is used to call dynamically
|
|
123
|
+
# exceptions to each method in module or class, defined in
|
|
124
|
+
# +Genius::Errors+ scope.
|
|
125
|
+
module DynamicRescue
|
|
126
|
+
class << self
|
|
127
|
+
# Wraps singleton methods of +klass+ with exception handling via {DynamicRescue.rescue_from}.
|
|
128
|
+
#
|
|
129
|
+
# @param [Module] klass Module whose singleton methods to wrap.
|
|
130
|
+
# @return [Array]
|
|
131
|
+
def rescue(klass)
|
|
132
|
+
DynamicRescue.rescue_from klass.singleton_methods, klass, GeniusExceptionSuperClass do |e|
|
|
133
|
+
puts "Error description: #{e.msg}\nException type: #{e.exception_type}"
|
|
134
|
+
# @todo make raise ExceptionKlass
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Redefines each method in +meths+ on +klass+ to rescue +exception+ and yield to the block.
|
|
139
|
+
#
|
|
140
|
+
# @param [Array] meths Method names to wrap.
|
|
141
|
+
# @param [Module] klass Module to redefine methods on.
|
|
142
|
+
# @param [Module] exception Exception class to rescue.
|
|
143
|
+
# @raise [StandardError]
|
|
144
|
+
# @return [Array]
|
|
145
|
+
def rescue_from(meths, klass, exception, &)
|
|
146
|
+
meths.each do |meth|
|
|
147
|
+
old = klass.singleton_method(meth)
|
|
148
|
+
klass.define_singleton_method(meth) do |*args, **kwargs|
|
|
149
|
+
old.unbind.bind(klass).call(*args, **kwargs) # steep:ignore
|
|
150
|
+
rescue exception => e
|
|
151
|
+
yield(e)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class << self
|
|
159
|
+
# Validates the access token by checking length and making a test request to the API.
|
|
160
|
+
#
|
|
161
|
+
# @param [String?] token Token to validate.
|
|
162
|
+
# @param [String?] method_name Optional method name for error hints.
|
|
163
|
+
# @raise [StandardError] if +token+ is nil, wrong length, or invalid.
|
|
164
|
+
# @return [void]
|
|
165
|
+
def validate_token(token, method_name: nil)
|
|
166
|
+
raise TokenError.new(method_name: method_name) if token.nil? || token.size != 64
|
|
167
|
+
|
|
168
|
+
response = HTTParty.get("#{ENDPOINT}=#{token}").body
|
|
169
|
+
status = JSON.parse(response).dig('meta', 'status')
|
|
170
|
+
raise TokenError.new(method_name: method_name) unless status == 200
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Validates token and raises on failure. Returns +true+ if valid.
|
|
174
|
+
#
|
|
175
|
+
# @deprecated Use {.validate_token} instead.
|
|
176
|
+
# @param [String?] token Token to validate.
|
|
177
|
+
# @param [String?] method_name Optional method name for error hints.
|
|
178
|
+
# @raise [StandardError] if token is invalid.
|
|
179
|
+
# @return [Boolean]
|
|
180
|
+
def error_handle?(token, method_name: nil)
|
|
181
|
+
if token.nil?
|
|
182
|
+
raise TokenError.new(msg: 'Token is required for this method. Please, add token via ' \
|
|
183
|
+
"`Genius::Auth.login=``token''` method and continue",
|
|
184
|
+
method_name: method_name)
|
|
185
|
+
elsif token.size != 64 || check_status?(token) == false
|
|
186
|
+
raise TokenError.new(method_name: method_name)
|
|
187
|
+
end
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# Checks if the token returns a 200 status from the API.
|
|
194
|
+
#
|
|
195
|
+
# @deprecated Use {.validate_token} instead.
|
|
196
|
+
# @private
|
|
197
|
+
# @param [String] token Token to check.
|
|
198
|
+
# @raise [TokenError] if the response status is not 200.
|
|
199
|
+
# @return [Boolean]
|
|
200
|
+
def check_status?(token)
|
|
201
|
+
return false if token.size != 64 || token.nil?
|
|
202
|
+
|
|
203
|
+
response = HTTParty.get("#{ENDPOINT}=#{token}").body
|
|
204
|
+
raise TokenError unless JSON.parse(response).dig('meta', 'status')
|
|
205
|
+
|
|
206
|
+
status = JSON.parse(response).dig('meta', 'status')
|
|
207
|
+
status == 200
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# Referents are the sections of a piece of content to which annotations are
|
|
5
|
+
# attached. Each referent is associated with a web page or a song and may
|
|
6
|
+
# have one or more annotations. Referents can be searched by the document
|
|
7
|
+
# they are attached to or by the user that created them. When a new
|
|
8
|
+
# annotation is created either a referent is created with it or that
|
|
9
|
+
# annotation is attached to an existing referent.
|
|
10
|
+
module Referents
|
|
11
|
+
class << self
|
|
12
|
+
# Endpoint of the resource
|
|
13
|
+
ENDPOINT = "#{Api::RESOURCE}/referents".freeze
|
|
14
|
+
# Referents by content item or user. Pass only one of +:song_id+ and +:web_page+.
|
|
15
|
+
#
|
|
16
|
+
# @param [String?] token Token to access https://api.genius.com.
|
|
17
|
+
# @param [Hash] options Options: +:created_by_id+, +:text_format+, +:web_page_id+,
|
|
18
|
+
# +:song_id+, +:per_page+, +:page+.
|
|
19
|
+
# @raise [ArgumentError] if both +:song_id+ and +:web_page+ are present.
|
|
20
|
+
# @return [Hash, nil]
|
|
21
|
+
def referents(token: nil, options: {})
|
|
22
|
+
return if token.nil? && !Auth.authorized?.nil?
|
|
23
|
+
|
|
24
|
+
Errors.validate_token(token) unless token.nil?
|
|
25
|
+
if options.key?(:web_page) && options.key?(:song_id)
|
|
26
|
+
raise ArgumentError, 'You may pass only one of song_id and web_page_id, not both!'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
params = options_helper(options, %i[created_by_id text_format per_page page])
|
|
30
|
+
|
|
31
|
+
response = HTTParty.get("#{ENDPOINT}?access_token=#{token_ext(token)}#{params}").body
|
|
32
|
+
JSON.parse(response)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Genius::Errors::DynamicRescue.rescue(Module.nesting[1])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# +Genius::Search+ module provides methods to work with Genius search database
|
|
5
|
+
module Search
|
|
6
|
+
class << self
|
|
7
|
+
# Searches Genius for songs, artists, and other content. Optionally filters results by key using +deep_find+.
|
|
8
|
+
#
|
|
9
|
+
# @param [String?] token Token to access https://api.genius.com.
|
|
10
|
+
# @param [String?] query Search query.
|
|
11
|
+
# @param [String?] search_by Key to filter results with {Hash#deep_find}.
|
|
12
|
+
# @return [Hash, String, nil]
|
|
13
|
+
def search(token: nil, query: nil, search_by: nil)
|
|
14
|
+
return if token.nil? && !Auth.authorized?.nil?
|
|
15
|
+
|
|
16
|
+
Errors.validate_token(token) unless token.nil?
|
|
17
|
+
|
|
18
|
+
response = HTTParty.get("#{Api::RESOURCE}/search?q=#{query}&access_token=#{token_ext(token)}").body
|
|
19
|
+
search = JSON.parse(response)
|
|
20
|
+
search_by ? search.deep_find(search_by) : search
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Genius::Errors::DynamicRescue.rescue(Module.nesting[1])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# +Genius::Songs+ module provides methods to work with songs (lyrics/descriptions/etc.)
|
|
5
|
+
module Songs
|
|
6
|
+
class << self
|
|
7
|
+
include Genius::Errors
|
|
8
|
+
|
|
9
|
+
# Returns song data by ID. Optionally merges lyrics from the Genius page when +combine+ is +true+.
|
|
10
|
+
#
|
|
11
|
+
# @param [String?] token Token to access https://api.genius.com.
|
|
12
|
+
# @param [Integer?] song_id ID of the song.
|
|
13
|
+
# @param [Boolean] combine If +true+, fetches and merges lyrics into the response.
|
|
14
|
+
# @return [Hash, String, nil]
|
|
15
|
+
def songs(token: nil, song_id: nil, combine: false)
|
|
16
|
+
return if token.nil? && !Auth.authorized?.nil?
|
|
17
|
+
|
|
18
|
+
Errors.validate_token(token) unless token.nil?
|
|
19
|
+
|
|
20
|
+
response = HTTParty.get("#{Api::RESOURCE}/songs/#{song_id}?access_token=#{token_ext(token)}").body
|
|
21
|
+
response = JSON.parse response
|
|
22
|
+
combine && song_id ? merge_lyrics(song_id, response) : response
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Fetches the Genius HTML page for a song and merges lyrics into the API response.
|
|
28
|
+
#
|
|
29
|
+
# @private
|
|
30
|
+
# @param [Integer] song_id ID of the song.
|
|
31
|
+
# @param [Hash] response Original API response hash.
|
|
32
|
+
# @raise [Errors::PageNotFound] if the song page is not found.
|
|
33
|
+
# @raise [Errors::LyricsNotFoundError] if lyrics cannot be parsed.
|
|
34
|
+
# @return [Hash, String]
|
|
35
|
+
def merge_lyrics(song_id, response)
|
|
36
|
+
output_html = Nokogiri::HTML(HTTParty.get("https://genius.com/songs/#{song_id}"))
|
|
37
|
+
raise Errors::PageNotFound if Errors::PageNotFound.page_not_found?(output_html)
|
|
38
|
+
|
|
39
|
+
response['lyrics'] = parse_preloaded_state(output_html)
|
|
40
|
+
response
|
|
41
|
+
rescue Errors::LyricsNotFoundError
|
|
42
|
+
retry
|
|
43
|
+
rescue Errors::PageNotFound => e
|
|
44
|
+
"Error description: #{e.msg}\nException type: #{e.exception_type}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Extracts the preloaded state JSON from the Genius page HTML.
|
|
48
|
+
#
|
|
49
|
+
# @private
|
|
50
|
+
# @param [Object] output_html Parsed Nokogiri HTML document.
|
|
51
|
+
# @raise [Errors::LyricsNotFoundError] if the preloaded state script is not found.
|
|
52
|
+
# @return [Hash]
|
|
53
|
+
def parse_preloaded_state(output_html)
|
|
54
|
+
unformed_json = output_html.css('script')[17]
|
|
55
|
+
.text.match(/window\.__PRELOADED_STATE__\s=\sJSON.parse\('(?<json>(?:.+?))'\);/)
|
|
56
|
+
raise Errors::LyricsNotFoundError if unformed_json.nil?
|
|
57
|
+
|
|
58
|
+
JSON.parse(unformed_json[:json].unescape)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
public
|
|
62
|
+
|
|
63
|
+
# Extracts lyrics as plain text from the Genius song page.
|
|
64
|
+
#
|
|
65
|
+
# @param [Integer] song_id ID of the song.
|
|
66
|
+
# @raise [ArgumentError] if +song_id+ is nil.
|
|
67
|
+
# @raise [NoMethodError]
|
|
68
|
+
# @return [String]
|
|
69
|
+
def get_lyrics(song_id)
|
|
70
|
+
raise ArgumentError, '`song_id` should be not blank!' if song_id.nil?
|
|
71
|
+
|
|
72
|
+
response = HTTParty.get("https://genius.com/songs/#{song_id}")
|
|
73
|
+
document = Nokogiri::HTML(response)
|
|
74
|
+
# @todo: something wrong with lyrics attribute value
|
|
75
|
+
lyrics_path = document.xpath("//*[@class='Lyrics__Container-sc-1ynbvzw-6 YYrds']")
|
|
76
|
+
lyrics_path.at_css('p').content
|
|
77
|
+
rescue NoMethodError
|
|
78
|
+
retry
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Genius::Errors::DynamicRescue.rescue(Module.nesting[1])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Genius
|
|
4
|
+
# A web page is a single, publicly accessible page to which annotations may
|
|
5
|
+
# be attached. Web pages map 1-to-1 with unique, canonical URLs.
|
|
6
|
+
module WebPages
|
|
7
|
+
class << self
|
|
8
|
+
# Looks up a web page by URL variants and returns Genius metadata.
|
|
9
|
+
#
|
|
10
|
+
# @param [String?] token Token to access https://api.genius.com.
|
|
11
|
+
# @param [Hash] options URL variants: +:raw_annotatable_url+, +:canonical_url+, +:og_url+.
|
|
12
|
+
# @return [Hash, nil]
|
|
13
|
+
def lookup(token: nil, options: {})
|
|
14
|
+
return if token.nil? && !Auth.authorized?.nil?
|
|
15
|
+
|
|
16
|
+
Errors.validate_token(token) unless token.nil?
|
|
17
|
+
|
|
18
|
+
params = options_helper(options, %i[raw_annotatable_url canonical_url og_url])
|
|
19
|
+
response = HTTParty.get("#{Api::RESOURCE}/?access_token=#{token_ext(token)}#{params}").body
|
|
20
|
+
JSON.parse(response)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Genius::Errors::DynamicRescue.rescue(Module.nesting[1])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/genius/api.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Base module which contains all of other methods/classes/etc.
|
|
4
|
+
module Genius
|
|
5
|
+
# +Genius::Api+ is a base module with different constants.
|
|
6
|
+
module Api
|
|
7
|
+
# +Genius::Api::RESOURCE+ constant contains reference to
|
|
8
|
+
# {Genius API}[https://api.genius.com] resource.
|
|
9
|
+
RESOURCE = 'https://api.genius.com'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require 'extensions/extensions'
|
|
14
|
+
require_relative 'api/errors'
|
|
15
|
+
require_relative 'api/version'
|
|
16
|
+
require_relative 'api/authorization'
|
|
17
|
+
require_relative 'api/account'
|
|
18
|
+
require_relative 'api/search'
|
|
19
|
+
require_relative 'api/songs'
|
|
20
|
+
require_relative 'api/annotations'
|
|
21
|
+
require_relative 'api/referents'
|
|
22
|
+
require_relative 'api/artists'
|
|
23
|
+
require_relative 'api/web_pages'
|