Daniel@0
|
1 /* Part of DML (Digital Music Laboratory)
|
Daniel@0
|
2 Copyright 2014-2015 Samer Abdallah, University of London
|
Daniel@0
|
3
|
Daniel@0
|
4 This program is free software; you can redistribute it and/or
|
Daniel@0
|
5 modify it under the terms of the GNU General Public License
|
Daniel@0
|
6 as published by the Free Software Foundation; either version 2
|
Daniel@0
|
7 of the License, or (at your option) any later version.
|
Daniel@0
|
8
|
Daniel@0
|
9 This program is distributed in the hope that it will be useful,
|
Daniel@0
|
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
Daniel@0
|
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
Daniel@0
|
12 GNU General Public License for more details.
|
Daniel@0
|
13
|
Daniel@0
|
14 You should have received a copy of the GNU General Public
|
Daniel@0
|
15 License along with this library; if not, write to the Free Software
|
Daniel@0
|
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
Daniel@0
|
17 */
|
Daniel@0
|
18
|
Daniel@0
|
19 :- module(spotify,
|
Daniel@0
|
20 [ spotify_authorise/1
|
Daniel@0
|
21 , spotify_app/2
|
Daniel@0
|
22 , spotify/2
|
Daniel@0
|
23 , spotify_user/2
|
Daniel@0
|
24 , spotify_search/4
|
Daniel@0
|
25 , spotify_player//2
|
Daniel@0
|
26 , spotify_player_url/3
|
Daniel@0
|
27 ]).
|
Daniel@0
|
28
|
Daniel@0
|
29 /** <module> Interface to Spotify Web API
|
Daniel@0
|
30
|
Daniel@0
|
31 This module provides access to the Spotify web API.
|
Daniel@0
|
32 It uses dicts, but does not require the .field syntax to be enabled.
|
Daniel@0
|
33
|
Daniel@0
|
34 https://developer.spotify.com/web-api/
|
Daniel@0
|
35
|
Daniel@0
|
36 */
|
Daniel@0
|
37
|
Daniel@0
|
38 :- use_module(library(http/http_dispatch)).
|
Daniel@0
|
39 :- use_module(library(http/thread_httpd)).
|
Daniel@0
|
40 :- use_module(library(http/http_parameters)).
|
Daniel@0
|
41 :- use_module(library(http/http_ssl_plugin)).
|
Daniel@0
|
42 :- use_module(library(http/html_write)).
|
Daniel@0
|
43 :- use_module(library(base64)).
|
Daniel@0
|
44 :- use_module(library(dcg_core)).
|
Daniel@0
|
45 :- use_module(library(webby)).
|
Daniel@0
|
46 :- use_module(library(insist)).
|
Daniel@0
|
47 :- use_module(library(state)).
|
Daniel@0
|
48
|
Daniel@0
|
49 :- multifile app_facet/2.
|
Daniel@0
|
50 :- dynamic state/2.
|
Daniel@0
|
51 :- set_prolog_flag(double_quotes,string).
|
Daniel@0
|
52
|
Daniel@0
|
53 :- http_handler(root(authorise), authorise, []).
|
Daniel@0
|
54
|
Daniel@0
|
55 %% spotify_app(+App:ground, +Spec:list) is det.
|
Daniel@0
|
56 %
|
Daniel@0
|
57 % Declares App to be a registered Spotify application. Spec must contain contain
|
Daniel@0
|
58 % the following elements containing information obtained during the application
|
Daniel@0
|
59 % registration process. They can be atoms or strings.
|
Daniel@0
|
60 % * id(ClientID:text)
|
Daniel@0
|
61 % * secret(Secret:text)
|
Daniel@0
|
62 % * redirect(RedirectURL:text)
|
Daniel@0
|
63 % * scope(Scopes:text)
|
Daniel@0
|
64 % This can only be used as a directive, as the expansion process included declaring
|
Daniel@0
|
65 % an HTTP handler using http_handler/3.%
|
Daniel@0
|
66 spotify_app(_,_) :- throw(directive(spotify_app/2)).
|
Daniel@0
|
67
|
Daniel@0
|
68 user:term_expansion((:- spotify_app(App,Props)), Clauses) :- !,
|
Daniel@0
|
69 Info=[id(_),secret(_),redirect(Redirect),scope(_)],
|
Daniel@0
|
70 maplist(spotify:option_inv(Props),Info),
|
Daniel@0
|
71 insist(url:parse_url(Redirect,Parts),bad_redirect(Redirect)),
|
Daniel@0
|
72 member(path(Path),Parts),
|
Daniel@0
|
73 seqmap(spotify:app_clause(App),Info,Clauses,[(:-http_handler(Path, spotify:callback(App), []))]).
|
Daniel@0
|
74
|
Daniel@0
|
75 option_inv(Props,Prop) :- insist(option(Prop,Props),missing_app_facet(Prop)).
|
Daniel@0
|
76 app_clause(App,Prop) --> [spotify:app_facet(App,Prop)].
|
Daniel@0
|
77
|
Daniel@0
|
78 app_port(App,Port) :-
|
Daniel@0
|
79 app_facet(App,redirect(Redirect)),
|
Daniel@0
|
80 parse_url(Redirect,Parts),
|
Daniel@0
|
81 member(port(Port),Parts).
|
Daniel@0
|
82
|
Daniel@0
|
83
|
Daniel@0
|
84 %% spotify_authorise(+App) is det.
|
Daniel@0
|
85 %
|
Daniel@0
|
86 % Get authorisation for current app from a Spotify user.
|
Daniel@0
|
87 % This requires a working www_open_url to initiate an interaction with
|
Daniel@0
|
88 % the user to handle the Spotify login process. If this doesn't
|
Daniel@0
|
89 % work, consider changing the 'browser' Prolog flag. For example,
|
Daniel@0
|
90 % on Mac OS X, add the following to your ~/.plrc
|
Daniel@0
|
91 % ==
|
Daniel@0
|
92 % :- set_prolog_flag(browser,open).
|
Daniel@0
|
93 % ==
|
Daniel@0
|
94 % After logging in (if necessary) and authorising the app, the browser
|
Daniel@0
|
95 % should show a page confirming that the process succeeded and giving information
|
Daniel@0
|
96 % about the current user. This predicate (spotify_authorise/0) will wait
|
Daniel@0
|
97 % until the confirmation page has been shown. It may hang if something
|
Daniel@0
|
98 % goes wrong.
|
Daniel@0
|
99 spotify_authorise(App) :-
|
Daniel@0
|
100 get_time(Time), variant_sha1(App-Time,Hash),
|
Daniel@0
|
101 login_url(App,Hash,URL),
|
Daniel@0
|
102 app_port(App,Port),
|
Daniel@0
|
103 with_message_queue(spotify,[max_size(5)],
|
Daniel@0
|
104 with_http_server(Port,
|
Daniel@0
|
105 ( thread_send_message(spotify,login(App,Hash,URL)),
|
Daniel@0
|
106 print_message(information,spotify:auth_opening_browser(App,Port,URL)),
|
Daniel@0
|
107 www_open_url(URL),
|
Daniel@0
|
108 print_message(information,spotify:auth_waiting(App)),
|
Daniel@0
|
109 thread_get_message(spotify,cont(App,Hash,RC),[timeout(300)]),
|
Daniel@0
|
110 (RC=error(Ex) -> throw(Ex); true),
|
Daniel@0
|
111 print_message(information,spotify:auth_complete(App))
|
Daniel@0
|
112 ))).
|
Daniel@0
|
113
|
Daniel@0
|
114
|
Daniel@0
|
115 with_http_server(Port,Goal) :-
|
Daniel@0
|
116 setup_call_cleanup(
|
Daniel@0
|
117 http_server(http_dispatch, [port(Port)]), Goal,
|
Daniel@0
|
118 http_stop_server(Port,[])).
|
Daniel@0
|
119
|
Daniel@0
|
120 with_message_queue(Alias,Opts,Goal) :-
|
Daniel@0
|
121 setup_call_cleanup(
|
Daniel@0
|
122 message_queue_create(_,[alias(Alias)|Opts]), Goal,
|
Daniel@0
|
123 message_queue_destroy(Alias)).
|
Daniel@0
|
124
|
Daniel@0
|
125 %% login_url(+App,+Hash,-URL) is det.
|
Daniel@0
|
126 login_url(App,Hash,URL) :-
|
Daniel@0
|
127 maplist(app_facet(App),[id(ID),redirect(Redirect),scope(Scope)]),
|
Daniel@0
|
128 Params=[ response_type=code,client_id=ID,scope=Scope, redirect_uri=Redirect, state=Hash ],
|
Daniel@0
|
129 parse_url(URL,[ protocol(https), host('accounts.spotify.com'), path('/authorize'), search(Params)]).
|
Daniel@0
|
130
|
Daniel@0
|
131
|
Daniel@0
|
132 %% authorise(Request) is det.
|
Daniel@0
|
133 %
|
Daniel@0
|
134 % Authorisation process via a web interface.
|
Daniel@0
|
135 authorise(Request) :-
|
Daniel@0
|
136 ( thread_peek_message(spotify,login(_,_,URL))
|
Daniel@0
|
137 -> http_redirect(see_other,URL,Request)
|
Daniel@0
|
138 ; reply_html_page(default, [title("SWI Prolog Spotify client")],
|
Daniel@0
|
139 center(p("Nothing to see here. Move on.")))).
|
Daniel@0
|
140
|
Daniel@0
|
141
|
Daniel@0
|
142 %% callback(+App,+Request) is det.
|
Daniel@0
|
143 %
|
Daniel@0
|
144 % Handle the callback from Spotify as part of the authorisation process.
|
Daniel@0
|
145 % This handler must be registered with library(http_dispatch) associated
|
Daniel@0
|
146 % with 'redirect' URL property of the current app.
|
Daniel@0
|
147 callback(App,Request) :-
|
Daniel@0
|
148 http_parameters(Request,[ code(Code, [string, optional(true)])
|
Daniel@0
|
149 , error(Error, [string, optional(true)])
|
Daniel@0
|
150 , error_description(ErrorDesc, [string, optional(true)])
|
Daniel@0
|
151 , state(Hash, [atom, optional(false)]) ]),
|
Daniel@0
|
152 debug(spotify,"Got callback (~w)",[Hash]),
|
Daniel@0
|
153 insist(thread_get_message(spotify,login(App,Hash,_),[timeout(0)]),no_matching_state),
|
Daniel@0
|
154
|
Daniel@0
|
155 catch(
|
Daniel@0
|
156 ( insist(var(Error), auth_error(Error,ErrorDesc)),
|
Daniel@0
|
157 insist(nonvar(Code),no_code_received),
|
Daniel@0
|
158 maplist(app_facet(App),[id(ID),secret(Secret),redirect(Redirect)]),
|
Daniel@0
|
159 get_authorisation([ grant_type='authorization_code'
|
Daniel@0
|
160 , code=Code, redirect_uri=Redirect
|
Daniel@0
|
161 , client_id=ID, client_secret=Secret
|
Daniel@0
|
162 ], [], Reply),
|
Daniel@0
|
163 debug(spotify,"Received access and refresh tokens.",[]),
|
Daniel@0
|
164 debug(spotify,"Posting success to login queue ~w...",[Hash]),
|
Daniel@0
|
165 Status=ok(Reply)
|
Daniel@0
|
166 ), Ex, Status=error(Ex)),
|
Daniel@0
|
167 handle_auth_reply(Status,App,Hash).
|
Daniel@0
|
168
|
Daniel@0
|
169 handle_auth_reply(ok(Reply),App,Hash) :-
|
Daniel@0
|
170 set_tokens(App,Reply),
|
Daniel@0
|
171 debug(spotify,"Posting success to login queue ~w...",[Hash]),
|
Daniel@0
|
172 thread_send_message(spotify,cont(App,Hash,ok)),
|
Daniel@0
|
173 spotify(App,me(Me)), is_dict(Me,user),
|
Daniel@0
|
174 set_state(user(App),Me),
|
Daniel@0
|
175 reply_html_page(default,
|
Daniel@0
|
176 [title("SWI-Prolog Spotify Client > Login ok")],
|
Daniel@0
|
177 [ \html_post(head,style(
|
Daniel@0
|
178 [ "table.json table.json { border: thin solid gray }"
|
Daniel@0
|
179 , "table.json { border-collapse: collapse }"
|
Daniel@0
|
180 , "table.json td:first-child { font-weight: bold; text-align: right; padding-right:0.5em }"
|
Daniel@0
|
181 , "table.json td { vertical-align:top; padding-left:0.5em }"
|
Daniel@0
|
182 , "body { margin:2em; }"
|
Daniel@0
|
183 ]))
|
Daniel@0
|
184 , center(div([h3("SWI Prolog library app '~w' authorised as user"-App), \json(Me) ]))
|
Daniel@0
|
185 ]).
|
Daniel@0
|
186
|
Daniel@0
|
187 handle_auth_reply(error(Ex),App,Hash) :-
|
Daniel@0
|
188 debug(spotify,"Posting error to login queue ~w...",[Hash]),
|
Daniel@0
|
189 thread_send_message(spotify,cont(App,Hash,error(Ex))),
|
Daniel@0
|
190 ( Ex=http_bad_status(_,Doc), phrase("<!DOCTYPE html>",Doc,_)
|
Daniel@0
|
191 -> format("Content-type: text/html; charset=UTF-8~n~n~s",[Doc])
|
Daniel@0
|
192 ; throw(Ex)
|
Daniel@0
|
193 ).
|
Daniel@0
|
194
|
Daniel@0
|
195 %% refresh is det.
|
Daniel@0
|
196 % Refresh the access token for the current app.
|
Daniel@0
|
197 refresh(App) :-
|
Daniel@0
|
198 app_facet(App,id(ID)),
|
Daniel@0
|
199 app_facet(App,secret(Secret)),
|
Daniel@0
|
200 get_state(refresh_token(App),Token),
|
Daniel@0
|
201 format(codes(IDCodes),"~w:~w", [ID,Secret]),
|
Daniel@0
|
202 phrase(("Basic ",base64(IDCodes)),AuthCodes),
|
Daniel@0
|
203 string_codes(Auth,AuthCodes),
|
Daniel@0
|
204 % debug(spotify,"Refreshing access tokens...",[]),
|
Daniel@0
|
205 get_authorisation([grant_type=refresh_token, refresh_token=Token],
|
Daniel@0
|
206 [request_header('Authorization'=Auth)], Reply),
|
Daniel@0
|
207 debug(spotify,"Received new access tokens.",[]),
|
Daniel@0
|
208 set_tokens(App,Reply).
|
Daniel@0
|
209
|
Daniel@0
|
210 %% set_tokens(+App,+Dict) is det.
|
Daniel@0
|
211 % Updates the record of the current access and refresh tokens,
|
Daniel@0
|
212 % and their new expiry time.
|
Daniel@0
|
213 set_tokens(App,Dict) :-
|
Daniel@0
|
214 _{expires_in:Expiry,access_token:Access} :< Dict,
|
Daniel@0
|
215 get_time(Now), ExpirationTime is Now+Expiry,
|
Daniel@0
|
216 set_state(access_token(App),Access-ExpirationTime),
|
Daniel@0
|
217 ( _{refresh_token:Refresh} :< Dict
|
Daniel@0
|
218 -> set_state(refresh_token(App),Refresh)
|
Daniel@0
|
219 ; true
|
Daniel@0
|
220 ).
|
Daniel@0
|
221
|
Daniel@0
|
222 %% usable_token(+App,-Token) is det.
|
Daniel@0
|
223 % Gets a usable access token, refreshing if the current one expired.
|
Daniel@0
|
224 usable_token(App,Token) :-
|
Daniel@0
|
225 get_state(access_token(App),Token0-ExpiryDate),
|
Daniel@0
|
226 get_time(Now),
|
Daniel@0
|
227 ( ExpiryDate>Now -> Token=Token0
|
Daniel@0
|
228 ; refresh(App), usable_token(App,Token)
|
Daniel@0
|
229 ).
|
Daniel@0
|
230
|
Daniel@0
|
231 %% spotify_player(+URI:atom,+Opts:options) is det.
|
Daniel@0
|
232 % HTML component for showing the Spotify web widget.
|
Daniel@0
|
233 spotify_player_url(track(URI),Opts,URL) :-
|
Daniel@0
|
234 option(theme(Th),Opts,white), must_be(oneof([white,black]),Th),
|
Daniel@0
|
235 option(view(Vw),Opts,list), must_be(oneof([list,coverart]),Vw),
|
Daniel@0
|
236 parse_url(URL, [ protocol(https),host('embed.spotify.com'),path(/)
|
Daniel@0
|
237 , search([uri=URI, theme=Th, view=Vw]) ]).
|
Daniel@0
|
238
|
Daniel@0
|
239 spotify_player_url(tracks(Title,URIs),Opts,URL) :-
|
Daniel@0
|
240 option(theme(Th),Opts,white), must_be(oneof([white,black]),Th),
|
Daniel@0
|
241 option(view(Vw),Opts,list), must_be(oneof([list,coverart]),Vw),
|
Daniel@0
|
242 maplist(string_concat('spotify:track:'),IDs,URIs),
|
Daniel@0
|
243 atomics_to_string(IDs,',',IDList),
|
Daniel@0
|
244 atomics_to_string([spotify,trackset,Title,IDList],':',URI),
|
Daniel@0
|
245 parse_url(URL, [ protocol(https),host('embed.spotify.com'),path(/)
|
Daniel@0
|
246 , search([uri=URI, theme=Th, view=Vw]) ]).
|
Daniel@0
|
247
|
Daniel@0
|
248 spotify_player(Spec,Opts) -->
|
Daniel@0
|
249 { spotify_player_url(Spec,Opts,URL),
|
Daniel@0
|
250 option(width(W),Opts,300), must_be(between(250,640),W),
|
Daniel@0
|
251 option(height(H),Opts,300), must_be(between(80,720),H)
|
Daniel@0
|
252 },
|
Daniel@0
|
253 html(iframe([ src=URL,width=W,height=H,frameborder=0,allowtransparency=true],[])).
|
Daniel@0
|
254
|
Daniel@0
|
255 % multiple tracks:
|
Daniel@0
|
256 % uri='spotify:trackset:PREFEREDTITLE:5Z7ygHQo02SUrFmcgpwsKW,1x6ACsKV4UdWS2FMuPFUiT'
|
Daniel@0
|
257 json(Dict) -->
|
Daniel@0
|
258 {is_dict(Dict), !, dict_pairs(Dict,_,Pairs)},
|
Daniel@0
|
259 html(table(class=json,
|
Daniel@0
|
260 [colgroup([col(style="border-right:thin solid black"),col([])]), \seqmap(pair_row,Pairs)])).
|
Daniel@0
|
261 json(List) --> {is_list(List)}, !, html(ol(\seqmap(val_li,List))).
|
Daniel@0
|
262 json(Val) --> html("~w"-Val).
|
Daniel@0
|
263 val_li(Val) --> html(li(\json(Val))).
|
Daniel@0
|
264 pair_row(Name-Val) --> html(tr( [ td(Name), td(\json(Val))])).
|
Daniel@0
|
265
|
Daniel@0
|
266 %% spotify(+App,+Request) is nondet.
|
Daniel@0
|
267 %
|
Daniel@0
|
268 % This is the main predicate for making calls to the Spotify web API. The nature of
|
Daniel@0
|
269 % the call, its parameters, and its results are encoded in the term Request.
|
Daniel@0
|
270 % See request/4 for information about what requests are recognised.
|
Daniel@0
|
271 % This predicate will formulate the call URL, refresh the access token if necessary,
|
Daniel@0
|
272 % make the call, and read the results.
|
Daniel@0
|
273 spotify(App,Req) :-
|
Daniel@0
|
274 request(Req,PathPhrase,Method,Reader), !,
|
Daniel@0
|
275 phrase(PathPhrase,PathParts),
|
Daniel@0
|
276 parts_path([v1|PathParts],Path),
|
Daniel@0
|
277 usable_token(App,Token),
|
Daniel@0
|
278 string_concat("Bearer ", Token, Auth),
|
Daniel@0
|
279 spotify(Method,Reader,'api.spotify.com',Path,
|
Daniel@0
|
280 [ request_header('Authorization'=Auth) ]).
|
Daniel@0
|
281
|
Daniel@0
|
282 % just for internal use
|
Daniel@0
|
283 get_authorisation(Params,Opts,Reply) :-
|
Daniel@0
|
284 spotify(post(form(Params)), json(Reply), 'accounts.spotify.com', '/api/token', Opts).
|
Daniel@0
|
285
|
Daniel@0
|
286 % All web calls come through here eventually.
|
Daniel@0
|
287 spotify(Method,Reader,Host,Path,Opts) :-
|
Daniel@0
|
288 restcall(Method, Reader,
|
Daniel@0
|
289 [ protocol(https), host(Host), path(Path) ],
|
Daniel@0
|
290 [ request_header('Accept'='application/json')
|
Daniel@0
|
291 , cert_verify_hook(spotify:verify)
|
Daniel@0
|
292 | Opts ]).
|
Daniel@0
|
293
|
Daniel@0
|
294 verify(_, Problem, _All, _First, Error) :-
|
Daniel@0
|
295 debug(ssl,"Accepting problem certificate (~w)\n~w\n",[Error,Problem]).
|
Daniel@0
|
296
|
Daniel@0
|
297 %% request(-Req:spotify_request, -PathPhrase:phrase(atom), -Method:web_method, -Reader:web_reader) is nondet.
|
Daniel@0
|
298 %
|
Daniel@0
|
299 % Database of mappings from requests to end-points.
|
Daniel@0
|
300 request( me(Me), [me], get([]), json(Me)).
|
Daniel@0
|
301 request( track(TID,T), [tracks,TID], get([]), json(T)).
|
Daniel@0
|
302 request( playlists(UID,Ls), playlists(UID), get([]), json(Ls)).
|
Daniel@0
|
303 request( playlist(UID,PID,L), playlist(UID,PID), get([]), json(L)).
|
Daniel@0
|
304 request( playlist_tracks(UID,PID,Ts), tracks(UID,PID), get([]), json(Ts)).
|
Daniel@0
|
305 request( search(Type,Term,L), [search], get([(type)=Type,q=Term]), json(L)).
|
Daniel@0
|
306 request( create_playlist(UID,Name,PL), playlists(UID), post(json(_{name:Name})), json(PL)).
|
Daniel@0
|
307 request( add_tracks(UID,PID,URIs), tracks(UID,PID), post(json(URIs)), nil).
|
Daniel@0
|
308 request( set_tracks(UID,PID,URIs), tracks(UID,PID), put(json(_{uris:URIs})), nil).
|
Daniel@0
|
309 request( del_tracks(UID,PID,TIDs), tracks(UID,PID), delete(json(TIDs)), json(_)).
|
Daniel@0
|
310
|
Daniel@0
|
311 % DCG for building paths as a list of atoms.
|
Daniel@0
|
312 playlist(UID,PID) --> playlists(UID), [PID].
|
Daniel@0
|
313 playlists(UID) --> [users,UID,playlists].
|
Daniel@0
|
314 tracks(UID,PID) --> playlist(UID,PID), [tracks].
|
Daniel@0
|
315
|
Daniel@0
|
316 spotify_user(App,Me) :- get_state(user(App),Me).
|
Daniel@0
|
317
|
Daniel@0
|
318 spotify_search(App,Type,Term,Item) :-
|
Daniel@0
|
319 spotify(App,search(Type,Term,Response)),
|
Daniel@0
|
320 type_field(Type,Field),
|
Daniel@0
|
321 get_dict(Field,Response,Results),
|
Daniel@0
|
322 get_dict(items,Results,Items),
|
Daniel@0
|
323 member(Item,Items).
|
Daniel@0
|
324
|
Daniel@0
|
325 type_field(artist,artists).
|
Daniel@0
|
326 type_field(track,tracks).
|
Daniel@0
|
327 type_field(album,albums).
|
Daniel@0
|
328
|
Daniel@0
|
329 user:portray(Dict) :-
|
Daniel@0
|
330 is_dict(Dict),
|
Daniel@0
|
331 get_dict(uri,Dict,URI),
|
Daniel@0
|
332 ( get_dict(name,Dict,Name)
|
Daniel@0
|
333 -> format("<~s|~s>",[URI,Name])
|
Daniel@0
|
334 ; format("<~s>",[URI])
|
Daniel@0
|
335 ).
|
Daniel@0
|
336
|
Daniel@0
|
337 prolog:message(directive(Pred)) --> ["~w can only be used as a directive."-[Pred]].
|
Daniel@0
|
338 prolog:message(spotify:refreshing_token) --> ["Refreshing Spotify access token..."].
|
Daniel@0
|
339 prolog:message(spotify:bad_redirect(R)) --> ["Malformed callback URI '~w'"-[R]].
|
Daniel@0
|
340 prolog:message(spotify:missing_app_facet(P)) --> ["spotify_app declaration missing property ~w"-[P]].
|
Daniel@0
|
341 prolog:message(spotify:no_matching_state) --> ["Authorisation synchronisation broken"].
|
Daniel@0
|
342 prolog:message(spotify:no_code_received) --> ["No authorisation code received"].
|
Daniel@0
|
343 prolog:message(spotify:auth_waiting(App)) --> ["Waiting for authorisation of ~w..."-[App]].
|
Daniel@0
|
344 prolog:message(spotify:auth_complete(App)) --> ["Authorisation of ~w complete."-[App]].
|
Daniel@0
|
345 prolog:message(spotify:auth_opening_browser(App,Port,_)) -->
|
Daniel@0
|
346 ["Authorising Spotify app ~w. "-[App]],
|
Daniel@0
|
347 ["Your browser should open automatically. If not, please open it manually and"],
|
Daniel@0
|
348 [" navigate to < http://localhost:~w/authorise >"-[Port]].
|