spotify-ruby 0.1.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 +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +146 -0
- data/.ruby-version +1 -0
- data/.rvm-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +72 -0
- data/Rakefile +30 -0
- data/SPEC.md +18 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/html/README_md.html +189 -0
- data/html/Spotify.html +115 -0
- data/html/Spotify/Auth.html +281 -0
- data/html/Spotify/Errors.html +103 -0
- data/html/Spotify/Errors/AuthClientCredentialsError.html +106 -0
- data/html/created.rid +5 -0
- data/html/css/fonts.css +167 -0
- data/html/css/rdoc.css +590 -0
- data/html/fonts/Lato-Light.ttf +0 -0
- data/html/fonts/Lato-LightItalic.ttf +0 -0
- data/html/fonts/Lato-Regular.ttf +0 -0
- data/html/fonts/Lato-RegularItalic.ttf +0 -0
- data/html/fonts/SourceCodePro-Bold.ttf +0 -0
- data/html/fonts/SourceCodePro-Regular.ttf +0 -0
- data/html/images/add.png +0 -0
- data/html/images/arrow_up.png +0 -0
- data/html/images/brick.png +0 -0
- data/html/images/brick_link.png +0 -0
- data/html/images/bug.png +0 -0
- data/html/images/bullet_black.png +0 -0
- data/html/images/bullet_toggle_minus.png +0 -0
- data/html/images/bullet_toggle_plus.png +0 -0
- data/html/images/date.png +0 -0
- data/html/images/delete.png +0 -0
- data/html/images/find.png +0 -0
- data/html/images/loadingAnimation.gif +0 -0
- data/html/images/macFFBgHack.png +0 -0
- data/html/images/package.png +0 -0
- data/html/images/page_green.png +0 -0
- data/html/images/page_white_text.png +0 -0
- data/html/images/page_white_width.png +0 -0
- data/html/images/plugin.png +0 -0
- data/html/images/ruby.png +0 -0
- data/html/images/tag_blue.png +0 -0
- data/html/images/tag_green.png +0 -0
- data/html/images/transparent.png +0 -0
- data/html/images/wrench.png +0 -0
- data/html/images/wrench_orange.png +0 -0
- data/html/images/zoom.png +0 -0
- data/html/index.html +189 -0
- data/html/js/darkfish.js +161 -0
- data/html/js/jquery.js +4 -0
- data/html/js/navigation.js +142 -0
- data/html/js/navigation.js.gz +0 -0
- data/html/js/search.js +109 -0
- data/html/js/search_index.js +1 -0
- data/html/js/search_index.js.gz +0 -0
- data/html/js/searcher.js +229 -0
- data/html/js/searcher.js.gz +0 -0
- data/html/table_of_contents.html +83 -0
- data/lib/spotify.rb +10 -0
- data/lib/spotify/auth.rb +119 -0
- data/lib/spotify/sdk/.keep +0 -0
- data/lib/spotify/version.rb +13 -0
- data/spotify-ruby.gemspec +38 -0
- metadata +232 -0
Binary file
|
data/html/js/search.js
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
Search = function(data, input, result) {
|
2
|
+
this.data = data;
|
3
|
+
this.$input = $(input);
|
4
|
+
this.$result = $(result);
|
5
|
+
|
6
|
+
this.$current = null;
|
7
|
+
this.$view = this.$result.parent();
|
8
|
+
this.searcher = new Searcher(data.index);
|
9
|
+
this.init();
|
10
|
+
}
|
11
|
+
|
12
|
+
Search.prototype = $.extend({}, Navigation, new function() {
|
13
|
+
var suid = 1;
|
14
|
+
|
15
|
+
this.init = function() {
|
16
|
+
var _this = this;
|
17
|
+
var observer = function(e) {
|
18
|
+
switch(e.originalEvent.keyCode) {
|
19
|
+
case 38: // Event.KEY_UP
|
20
|
+
case 40: // Event.KEY_DOWN
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
_this.search(_this.$input[0].value);
|
24
|
+
};
|
25
|
+
this.$input.keyup(observer);
|
26
|
+
this.$input.click(observer); // mac's clear field
|
27
|
+
|
28
|
+
this.searcher.ready(function(results, isLast) {
|
29
|
+
_this.addResults(results, isLast);
|
30
|
+
})
|
31
|
+
|
32
|
+
this.initNavigation();
|
33
|
+
this.setNavigationActive(false);
|
34
|
+
}
|
35
|
+
|
36
|
+
this.search = function(value, selectFirstMatch) {
|
37
|
+
value = jQuery.trim(value).toLowerCase();
|
38
|
+
if (value) {
|
39
|
+
this.setNavigationActive(true);
|
40
|
+
} else {
|
41
|
+
this.setNavigationActive(false);
|
42
|
+
}
|
43
|
+
|
44
|
+
if (value == '') {
|
45
|
+
this.lastQuery = value;
|
46
|
+
this.$result.empty();
|
47
|
+
this.$result.attr('aria-expanded', 'false');
|
48
|
+
this.setNavigationActive(false);
|
49
|
+
} else if (value != this.lastQuery) {
|
50
|
+
this.lastQuery = value;
|
51
|
+
this.$result.attr('aria-busy', 'true');
|
52
|
+
this.$result.attr('aria-expanded', 'true');
|
53
|
+
this.firstRun = true;
|
54
|
+
this.searcher.find(value);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
this.addResults = function(results, isLast) {
|
59
|
+
var target = this.$result.get(0);
|
60
|
+
if (this.firstRun && (results.length > 0 || isLast)) {
|
61
|
+
this.$current = null;
|
62
|
+
this.$result.empty();
|
63
|
+
}
|
64
|
+
|
65
|
+
for (var i=0, l = results.length; i < l; i++) {
|
66
|
+
var item = this.renderItem.call(this, results[i]);
|
67
|
+
item.setAttribute('id', 'search-result-' + target.childElementCount);
|
68
|
+
target.appendChild(item);
|
69
|
+
};
|
70
|
+
|
71
|
+
if (this.firstRun && results.length > 0) {
|
72
|
+
this.firstRun = false;
|
73
|
+
this.$current = $(target.firstChild);
|
74
|
+
this.$current.addClass('search-selected');
|
75
|
+
}
|
76
|
+
if (jQuery.browser.msie) this.$element[0].className += '';
|
77
|
+
|
78
|
+
if (isLast) this.$result.attr('aria-busy', 'false');
|
79
|
+
}
|
80
|
+
|
81
|
+
this.move = function(isDown) {
|
82
|
+
if (!this.$current) return;
|
83
|
+
var $next = this.$current[isDown ? 'next' : 'prev']();
|
84
|
+
if ($next.length) {
|
85
|
+
this.$current.removeClass('search-selected');
|
86
|
+
$next.addClass('search-selected');
|
87
|
+
this.$input.attr('aria-activedescendant', $next.attr('id'));
|
88
|
+
this.scrollIntoView($next[0], this.$view[0]);
|
89
|
+
this.$current = $next;
|
90
|
+
this.$input.val($next[0].firstChild.firstChild.text);
|
91
|
+
this.$input.select();
|
92
|
+
}
|
93
|
+
return true;
|
94
|
+
}
|
95
|
+
|
96
|
+
this.hlt = function(html) {
|
97
|
+
return this.escapeHTML(html).
|
98
|
+
replace(/\u0001/g, '<em>').
|
99
|
+
replace(/\u0002/g, '</em>');
|
100
|
+
}
|
101
|
+
|
102
|
+
this.escapeHTML = function(html) {
|
103
|
+
return html.replace(/[&<>]/g, function(c) {
|
104
|
+
return '&#' + c.charCodeAt(0) + ';';
|
105
|
+
});
|
106
|
+
}
|
107
|
+
|
108
|
+
});
|
109
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
var search_data = {"index":{"searchIndex":["spotify","auth","errors","authclientcredentialserror","authorize_url()","new()","readme"],"longSearchIndex":["spotify","spotify::auth","spotify::errors","spotify::errors::authclientcredentialserror","spotify::auth#authorize_url()","spotify::auth::new()",""],"info":[["Spotify","","Spotify.html","",""],["Spotify::Auth","","Spotify/Auth.html","","<p>Spotify::Auth inherits from OAuth2::Client based on the “oauth-2” gem.\n"],["Spotify::Errors","","Spotify/Errors.html","",""],["Spotify::Errors::AuthClientCredentialsError","","Spotify/Errors/AuthClientCredentialsError.html","","<p>A Error class for when authentication client credentials are empty or\nincorrectly formatted.\n"],["authorize_url","Spotify::Auth","Spotify/Auth.html#method-i-authorize_url","(override_params={})","<p>Get a HTTP URL to send user for authorizing with Spotify.\n<p>@example\n\n<pre>@auth = Spotify::Auth.new({\n client_id: ...</pre>\n"],["new","Spotify::Auth","Spotify/Auth.html#method-c-new","(config)","<p>Initialize the Spotify Auth object.\n<p>@example\n\n<pre>@auth = Spotify::Auth.new({\n client_id: "[client id goes ...</pre>\n"],["README","","README_md.html","","<p>bih/spotify-ruby\n<p>A modern, opinionated and <em>unofficial</em> Ruby SDK for the Spotify Web\nAPI to help developers …\n"]]}}
|
Binary file
|
data/html/js/searcher.js
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
Searcher = function(data) {
|
2
|
+
this.data = data;
|
3
|
+
this.handlers = [];
|
4
|
+
}
|
5
|
+
|
6
|
+
Searcher.prototype = new function() {
|
7
|
+
// search is performed in chunks of 1000 for non-blocking user input
|
8
|
+
var CHUNK_SIZE = 1000;
|
9
|
+
// do not try to find more than 100 results
|
10
|
+
var MAX_RESULTS = 100;
|
11
|
+
var huid = 1;
|
12
|
+
var suid = 1;
|
13
|
+
var runs = 0;
|
14
|
+
|
15
|
+
this.find = function(query) {
|
16
|
+
var queries = splitQuery(query);
|
17
|
+
var regexps = buildRegexps(queries);
|
18
|
+
var highlighters = buildHilighters(queries);
|
19
|
+
var state = { from: 0, pass: 0, limit: MAX_RESULTS, n: suid++};
|
20
|
+
var _this = this;
|
21
|
+
|
22
|
+
this.currentSuid = state.n;
|
23
|
+
|
24
|
+
if (!query) return;
|
25
|
+
|
26
|
+
var run = function() {
|
27
|
+
// stop current search thread if new search started
|
28
|
+
if (state.n != _this.currentSuid) return;
|
29
|
+
|
30
|
+
var results =
|
31
|
+
performSearch(_this.data, regexps, queries, highlighters, state);
|
32
|
+
var hasMore = (state.limit > 0 && state.pass < 4);
|
33
|
+
|
34
|
+
triggerResults.call(_this, results, !hasMore);
|
35
|
+
if (hasMore) {
|
36
|
+
setTimeout(run, 2);
|
37
|
+
}
|
38
|
+
runs++;
|
39
|
+
};
|
40
|
+
runs = 0;
|
41
|
+
|
42
|
+
// start search thread
|
43
|
+
run();
|
44
|
+
}
|
45
|
+
|
46
|
+
/* ----- Events ------ */
|
47
|
+
this.ready = function(fn) {
|
48
|
+
fn.huid = huid;
|
49
|
+
this.handlers.push(fn);
|
50
|
+
}
|
51
|
+
|
52
|
+
/* ----- Utilities ------ */
|
53
|
+
function splitQuery(query) {
|
54
|
+
return jQuery.grep(query.split(/(\s+|::?|\(\)?)/), function(string) {
|
55
|
+
return string.match(/\S/);
|
56
|
+
});
|
57
|
+
}
|
58
|
+
|
59
|
+
function buildRegexps(queries) {
|
60
|
+
return jQuery.map(queries, function(query) {
|
61
|
+
return new RegExp(query.replace(/(.)/g, '([$1])([^$1]*?)'), 'i');
|
62
|
+
});
|
63
|
+
}
|
64
|
+
|
65
|
+
function buildHilighters(queries) {
|
66
|
+
return jQuery.map(queries, function(query) {
|
67
|
+
return jQuery.map(query.split(''), function(l, i) {
|
68
|
+
return '\u0001$' + (i*2+1) + '\u0002$' + (i*2+2);
|
69
|
+
}).join('');
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
// function longMatchRegexp(index, longIndex, regexps) {
|
74
|
+
// for (var i = regexps.length - 1; i >= 0; i--){
|
75
|
+
// if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false;
|
76
|
+
// };
|
77
|
+
// return true;
|
78
|
+
// }
|
79
|
+
|
80
|
+
|
81
|
+
/* ----- Mathchers ------ */
|
82
|
+
|
83
|
+
/*
|
84
|
+
* This record matches if the index starts with queries[0] and the record
|
85
|
+
* matches all of the regexps
|
86
|
+
*/
|
87
|
+
function matchPassBeginning(index, longIndex, queries, regexps) {
|
88
|
+
if (index.indexOf(queries[0]) != 0) return false;
|
89
|
+
for (var i=1, l = regexps.length; i < l; i++) {
|
90
|
+
if (!index.match(regexps[i]) && !longIndex.match(regexps[i]))
|
91
|
+
return false;
|
92
|
+
};
|
93
|
+
return true;
|
94
|
+
}
|
95
|
+
|
96
|
+
/*
|
97
|
+
* This record matches if the longIndex starts with queries[0] and the
|
98
|
+
* longIndex matches all of the regexps
|
99
|
+
*/
|
100
|
+
function matchPassLongIndex(index, longIndex, queries, regexps) {
|
101
|
+
if (longIndex.indexOf(queries[0]) != 0) return false;
|
102
|
+
for (var i=1, l = regexps.length; i < l; i++) {
|
103
|
+
if (!longIndex.match(regexps[i]))
|
104
|
+
return false;
|
105
|
+
};
|
106
|
+
return true;
|
107
|
+
}
|
108
|
+
|
109
|
+
/*
|
110
|
+
* This record matches if the index contains queries[0] and the record
|
111
|
+
* matches all of the regexps
|
112
|
+
*/
|
113
|
+
function matchPassContains(index, longIndex, queries, regexps) {
|
114
|
+
if (index.indexOf(queries[0]) == -1) return false;
|
115
|
+
for (var i=1, l = regexps.length; i < l; i++) {
|
116
|
+
if (!index.match(regexps[i]) && !longIndex.match(regexps[i]))
|
117
|
+
return false;
|
118
|
+
};
|
119
|
+
return true;
|
120
|
+
}
|
121
|
+
|
122
|
+
/*
|
123
|
+
* This record matches if regexps[0] matches the index and the record
|
124
|
+
* matches all of the regexps
|
125
|
+
*/
|
126
|
+
function matchPassRegexp(index, longIndex, queries, regexps) {
|
127
|
+
if (!index.match(regexps[0])) return false;
|
128
|
+
for (var i=1, l = regexps.length; i < l; i++) {
|
129
|
+
if (!index.match(regexps[i]) && !longIndex.match(regexps[i]))
|
130
|
+
return false;
|
131
|
+
};
|
132
|
+
return true;
|
133
|
+
}
|
134
|
+
|
135
|
+
|
136
|
+
/* ----- Highlighters ------ */
|
137
|
+
function highlightRegexp(info, queries, regexps, highlighters) {
|
138
|
+
var result = createResult(info);
|
139
|
+
for (var i=0, l = regexps.length; i < l; i++) {
|
140
|
+
result.title = result.title.replace(regexps[i], highlighters[i]);
|
141
|
+
result.namespace = result.namespace.replace(regexps[i], highlighters[i]);
|
142
|
+
};
|
143
|
+
return result;
|
144
|
+
}
|
145
|
+
|
146
|
+
function hltSubstring(string, pos, length) {
|
147
|
+
return string.substring(0, pos) + '\u0001' + string.substring(pos, pos + length) + '\u0002' + string.substring(pos + length);
|
148
|
+
}
|
149
|
+
|
150
|
+
function highlightQuery(info, queries, regexps, highlighters) {
|
151
|
+
var result = createResult(info);
|
152
|
+
var pos = 0;
|
153
|
+
var lcTitle = result.title.toLowerCase();
|
154
|
+
|
155
|
+
pos = lcTitle.indexOf(queries[0]);
|
156
|
+
if (pos != -1) {
|
157
|
+
result.title = hltSubstring(result.title, pos, queries[0].length);
|
158
|
+
}
|
159
|
+
|
160
|
+
result.namespace = result.namespace.replace(regexps[0], highlighters[0]);
|
161
|
+
for (var i=1, l = regexps.length; i < l; i++) {
|
162
|
+
result.title = result.title.replace(regexps[i], highlighters[i]);
|
163
|
+
result.namespace = result.namespace.replace(regexps[i], highlighters[i]);
|
164
|
+
};
|
165
|
+
return result;
|
166
|
+
}
|
167
|
+
|
168
|
+
function createResult(info) {
|
169
|
+
var result = {};
|
170
|
+
result.title = info[0];
|
171
|
+
result.namespace = info[1];
|
172
|
+
result.path = info[2];
|
173
|
+
result.params = info[3];
|
174
|
+
result.snippet = info[4];
|
175
|
+
result.badge = info[6];
|
176
|
+
return result;
|
177
|
+
}
|
178
|
+
|
179
|
+
/* ----- Searching ------ */
|
180
|
+
function performSearch(data, regexps, queries, highlighters, state) {
|
181
|
+
var searchIndex = data.searchIndex;
|
182
|
+
var longSearchIndex = data.longSearchIndex;
|
183
|
+
var info = data.info;
|
184
|
+
var result = [];
|
185
|
+
var i = state.from;
|
186
|
+
var l = searchIndex.length;
|
187
|
+
var togo = CHUNK_SIZE;
|
188
|
+
var matchFunc, hltFunc;
|
189
|
+
|
190
|
+
while (state.pass < 4 && state.limit > 0 && togo > 0) {
|
191
|
+
if (state.pass == 0) {
|
192
|
+
matchFunc = matchPassBeginning;
|
193
|
+
hltFunc = highlightQuery;
|
194
|
+
} else if (state.pass == 1) {
|
195
|
+
matchFunc = matchPassLongIndex;
|
196
|
+
hltFunc = highlightQuery;
|
197
|
+
} else if (state.pass == 2) {
|
198
|
+
matchFunc = matchPassContains;
|
199
|
+
hltFunc = highlightQuery;
|
200
|
+
} else if (state.pass == 3) {
|
201
|
+
matchFunc = matchPassRegexp;
|
202
|
+
hltFunc = highlightRegexp;
|
203
|
+
}
|
204
|
+
|
205
|
+
for (; togo > 0 && i < l && state.limit > 0; i++, togo--) {
|
206
|
+
if (info[i].n == state.n) continue;
|
207
|
+
if (matchFunc(searchIndex[i], longSearchIndex[i], queries, regexps)) {
|
208
|
+
info[i].n = state.n;
|
209
|
+
result.push(hltFunc(info[i], queries, regexps, highlighters));
|
210
|
+
state.limit--;
|
211
|
+
}
|
212
|
+
};
|
213
|
+
if (searchIndex.length <= i) {
|
214
|
+
state.pass++;
|
215
|
+
i = state.from = 0;
|
216
|
+
} else {
|
217
|
+
state.from = i;
|
218
|
+
}
|
219
|
+
}
|
220
|
+
return result;
|
221
|
+
}
|
222
|
+
|
223
|
+
function triggerResults(results, isLast) {
|
224
|
+
jQuery.each(this.handlers, function(i, fn) {
|
225
|
+
fn.call(this, results, isLast)
|
226
|
+
})
|
227
|
+
}
|
228
|
+
}
|
229
|
+
|
Binary file
|
@@ -0,0 +1,83 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
|
3
|
+
<html>
|
4
|
+
<head>
|
5
|
+
<meta charset="UTF-8">
|
6
|
+
|
7
|
+
<title>Table of Contents - RDoc Documentation</title>
|
8
|
+
|
9
|
+
<script type="text/javascript">
|
10
|
+
var rdoc_rel_prefix = "./";
|
11
|
+
var index_rel_prefix = "./";
|
12
|
+
</script>
|
13
|
+
|
14
|
+
<script src="./js/jquery.js"></script>
|
15
|
+
<script src="./js/darkfish.js"></script>
|
16
|
+
|
17
|
+
<link href="./css/fonts.css" rel="stylesheet">
|
18
|
+
<link href="./css/rdoc.css" rel="stylesheet">
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
<body id="top" class="table-of-contents">
|
23
|
+
<main role="main">
|
24
|
+
<h1 class="class">Table of Contents - RDoc Documentation</h1>
|
25
|
+
|
26
|
+
<h2 id="pages">Pages</h2>
|
27
|
+
<ul>
|
28
|
+
<li class="file">
|
29
|
+
<a href="README_md.html">README</a>
|
30
|
+
|
31
|
+
<ul>
|
32
|
+
<li><a href="README_md.html#label-bih-2Fspotify-ruby">bih/spotify-ruby</a>
|
33
|
+
<li><a href="README_md.html#label-Installation">Installation</a>
|
34
|
+
<li><a href="README_md.html#label-Setup">Setup</a>
|
35
|
+
<li><a href="README_md.html#label-Authentication">Authentication</a>
|
36
|
+
<li><a href="README_md.html#label-Usage">Usage</a>
|
37
|
+
<li><a href="README_md.html#label-Development">Development</a>
|
38
|
+
<li><a href="README_md.html#label-Contributing">Contributing</a>
|
39
|
+
<li><a href="README_md.html#label-License">License</a>
|
40
|
+
<li><a href="README_md.html#label-Code+of+Conduct">Code of Conduct</a>
|
41
|
+
</ul>
|
42
|
+
</li>
|
43
|
+
|
44
|
+
</ul>
|
45
|
+
|
46
|
+
<h2 id="classes">Classes and Modules</h2>
|
47
|
+
<ul>
|
48
|
+
<li class="module">
|
49
|
+
<a href="Spotify.html">Spotify</a>
|
50
|
+
</li>
|
51
|
+
<li class="class">
|
52
|
+
<a href="Spotify/Auth.html">Spotify::Auth</a>
|
53
|
+
</li>
|
54
|
+
<li class="class">
|
55
|
+
<a href="Spotify/Errors.html">Spotify::Errors</a>
|
56
|
+
</li>
|
57
|
+
<li class="class">
|
58
|
+
<a href="Spotify/Errors/AuthClientCredentialsError.html">Spotify::Errors::AuthClientCredentialsError</a>
|
59
|
+
</li>
|
60
|
+
</ul>
|
61
|
+
|
62
|
+
<h2 id="methods">Methods</h2>
|
63
|
+
<ul>
|
64
|
+
|
65
|
+
<li class="method">
|
66
|
+
<a href="Spotify/Auth.html#method-c-new">::new</a>
|
67
|
+
—
|
68
|
+
<span class="container">Spotify::Auth</span>
|
69
|
+
|
70
|
+
<li class="method">
|
71
|
+
<a href="Spotify/Auth.html#method-i-authorize_url">#authorize_url</a>
|
72
|
+
—
|
73
|
+
<span class="container">Spotify::Auth</span>
|
74
|
+
</ul>
|
75
|
+
</main>
|
76
|
+
|
77
|
+
|
78
|
+
<footer id="validator-badges" role="contentinfo">
|
79
|
+
<p><a href="http://validator.w3.org/check/referer">Validate</a>
|
80
|
+
<p>Generated by <a href="https://rdoc.github.io/rdoc">RDoc</a> 5.1.0.
|
81
|
+
<p>Based on <a href="http://deveiate.org/projects/Darkfish-RDoc/">Darkfish</a> by <a href="http://deveiate.org">Michael Granger</a>.
|
82
|
+
</footer>
|
83
|
+
|
data/lib/spotify.rb
ADDED
data/lib/spotify/auth.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "oauth2"
|
4
|
+
|
5
|
+
module Spotify
|
6
|
+
##
|
7
|
+
# Spotify::Auth inherits from OAuth2::Client based on the "oauth-2" gem.
|
8
|
+
#
|
9
|
+
class Auth < OAuth2::Client
|
10
|
+
##
|
11
|
+
# An entire list of Spotify's OAuth scopes. Stored
|
12
|
+
# in the form of a symbolized array.
|
13
|
+
# Example: `[:scope1, :scope2]`
|
14
|
+
#
|
15
|
+
# @see https://developer.spotify.com/web-api/using-scopes/
|
16
|
+
#
|
17
|
+
# Last updated: 21 October 2017
|
18
|
+
#
|
19
|
+
SCOPES = %i[
|
20
|
+
playlist-read-private playlist-read-collaborative
|
21
|
+
playlist-modify-public playlist-modify-private
|
22
|
+
ugc-image-upload user-follow-modify user-follow-read
|
23
|
+
user-library-read user-library-modify user-read-private
|
24
|
+
user-read-birthdate user-read-email user-top-read
|
25
|
+
user-read-playback-state user-modify-playback-state
|
26
|
+
user-read-currently-playing user-read-recently-played
|
27
|
+
streaming
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
##
|
31
|
+
# Error-related translations we're using.
|
32
|
+
#
|
33
|
+
OAUTH_I18N = {
|
34
|
+
must_be_hash: "Must be a Hash. Example: Spotify::Auth.new({ client_id: '', ... })",
|
35
|
+
require_attr: "Expecting a '%s' key. You can obtain from https://developer.spotify.com"
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
##
|
39
|
+
# Initialize the Spotify Auth object.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
#
|
43
|
+
# @auth = Spotify::Auth.new({
|
44
|
+
# client_id: "[client id goes here]",
|
45
|
+
# client_secret: "[client secret goes here]",
|
46
|
+
# redirect_uri: "http://localhost"
|
47
|
+
# })
|
48
|
+
#
|
49
|
+
# @param [Hash] config OAuth configuration containing the Client ID, secret and redirect URL.
|
50
|
+
# The redirect URL can be overriden later.
|
51
|
+
#
|
52
|
+
# @see https://developer.spotify.com/my-applications/
|
53
|
+
#
|
54
|
+
def initialize(config)
|
55
|
+
opts = {
|
56
|
+
site: "https://api.spotify.com",
|
57
|
+
authorize_url: "https://accounts.spotify.com/oauth/authorize"
|
58
|
+
}
|
59
|
+
validate_initialized_input(config)
|
60
|
+
@redirect_uri = config[:redirect_uri]
|
61
|
+
super(config[:client_id], config[:client_secret], opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Get a HTTP URL to send user for authorizing with Spotify.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
#
|
69
|
+
# @auth = Spotify::Auth.new({
|
70
|
+
# client_id: "[client id goes here]",
|
71
|
+
# client_secret: "[client secret goes here]",
|
72
|
+
# redirect_uri: "http://localhost"
|
73
|
+
# })
|
74
|
+
#
|
75
|
+
# @auth.authorize_url
|
76
|
+
#
|
77
|
+
# @param [Hash] override_params Optional hash containing any overriding values for parameters.
|
78
|
+
# Parameters used are client_id, redirect_uri, response_type and scope.
|
79
|
+
#
|
80
|
+
# @see https://developer.spotify.com/web-api/authorization-guide/
|
81
|
+
#
|
82
|
+
def authorize_url(override_params={})
|
83
|
+
super({
|
84
|
+
client_id: id,
|
85
|
+
redirect_uri: redirect_uri,
|
86
|
+
response_type: "code",
|
87
|
+
scope: SCOPES.join(" ")
|
88
|
+
}.merge(override_params))
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
##
|
94
|
+
# OAuth2::Client does not support redirect_uri at initialization, so we store
|
95
|
+
# it in the instance and call it later. We think it makes things clearer.
|
96
|
+
#
|
97
|
+
attr_accessor :redirect_uri
|
98
|
+
|
99
|
+
##
|
100
|
+
# Validate initialization configuration and raise errors.
|
101
|
+
#
|
102
|
+
# @param [Hash] config OAuth configuration containing the Client ID, secret and redirect URL.
|
103
|
+
#
|
104
|
+
def validate_initialized_input(config)
|
105
|
+
raise Errors::AuthClientCredentialsError.new(OAUTH_I18N[:must_be_hash]) unless config.is_a?(Hash)
|
106
|
+
|
107
|
+
%i[client_id client_secret redirect_uri].each do |key|
|
108
|
+
raise Errors::AuthClientCredentialsError.new(OAUTH_I18N[:require_attr] % key) unless config.has_key?(key)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Errors
|
114
|
+
##
|
115
|
+
# A Error class for when authentication client credentials are empty or incorrectly formatted.
|
116
|
+
#
|
117
|
+
class AuthClientCredentialsError < StandardError; end
|
118
|
+
end
|
119
|
+
end
|