1:- module(spotify, [access_token/2, playlist_to_csv/3, run_curl/2, run_curl/4,
    2                    retrieve_all/2, retrieve_all/4, playlist/2, playlist_track/2,
    3                    track_name/2, track_artists/2, track_info/2, playlist_info/2,
    4                    playlist_track_info/3, test/1]).    5
    6:- use_module(library(clpfd)).    7:- use_module(library(filesex)).    8:- use_module(library(achelois)).    9:- use_module(library(http/json)).   10:- use_module(library(url)).   11
   12timestamp(Timestamp) :-
   13    get_time(Temp),
   14    Timestamp is round(Temp).
   15
   16encoded_auth(Auth) :-
   17    client_id(ClientId),
   18    client_secret(ClientSecret),
   19    atomic_list_concat([ClientId, ClientSecret], ':', AuthStr),
   20    base64(AuthStr, AuthStr64),
   21    atom_concat('Authorization: Basic ', AuthStr64, Auth).
   22
   23request_new_access_token(token(AccessToken, TokenType, ExpireTime)) :-
   24    timestamp(Now),
   25    encoded_auth(Auth),
   26    run_curl(_, [auth(Auth), method(post), data('grant_type=client_credentials'), url('https://accounts.spotify.com/api/token')], _, JSON),
   27    JSON = json(['access_token'=AccessToken, 'token_type'=TokenType, 'expires_in'=ExpireOffset|_]),
   28    ExpireTime #= Now + ExpireOffset.
   29
   30access_token(Token, NewToken) :-
   31    Token = token(_AccessToken, _TokenType, ExpireTime),
   32    timestamp(Timestamp),
   33
   34    (
   35        ( var(ExpireTime); ExpireTime #=< Timestamp) ->
   36            request_new_access_token(NewToken);
   37
   38        ExpireTime #> Timestamp -> NewToken = Token
   39    ).
   40
   41access_token_auth(token(AccessToken, _TokenType, _ExpireTime), AuthStr) :-
   42    atom_concat('Authorization: Bearer ', AccessToken, AuthStr).
   43
   44select_or_default(X, Xs, NewXs, Default) :-
   45    select(X, Xs, NewXs) -> true;
   46    X = Default, NewXs = Xs.
   47
   48add_params(Url, Params, BuiltUrl) :-
   49    parse_url(Url, Attributes),
   50    select_or_default(search(CurParams), Attributes, Temp, search([])),
   51    append(CurParams, Params, AllParams),
   52    parse_url(BuiltUrl, [search(AllParams) | Temp]).
   53
   54build_curl_option(endpoint(Endpoint), Options) :-
   55    build_curl_option(endpoint(Endpoint, []), Options).
   56build_curl_option(endpoint(Endpoint, Params), [BuiltUrl]) :-
   57    atom_concat('https://api.spotify.com/v1/', Endpoint, Url),
   58    add_params(Url, Params, BuiltUrl).
   59build_curl_option(url(Url), Options) :-
   60    build_curl_option(url(Url, []), Options).
   61build_curl_option(url(Url, Params), [BuiltUrl]) :-
   62    add_params(Url, Params, BuiltUrl).
   63build_curl_option(method(Method), ['-X', MethodStr]) :-
   64    upcase_atom(Method, MethodStr).
   65build_curl_option(data(Data), ['-d', Data]).
   66build_curl_option(silent, ['-s']).
   67build_curl_option(auth(Auth), ['-H', Auth]).
   68
   69with_option(Option, Options, [Option|Temp]) :-
   70    Option =.. [F|Params],
   71    length(Params, L),
   72    length(NewParams, L),
   73    VarOption =.. [F|NewParams],
   74
   75    (
   76        select(VarOption, Options, Temp) -> true;
   77        Temp = Options
   78    ).
   79
   80add_default(Option, Options, NewOptions) :-
   81    Option =.. [F|Params],
   82    length(Params, L),
   83    length(NewParams, L),
   84    VarOption =.. [F|NewParams],
   85
   86    (
   87        member(VarOption, Options) -> NewOptions = Options;
   88        NewOptions = [Option|Options]
   89    ).
   90
   91add_defaults(Token, Options, NewToken, NewOptions) :-
   92    Defaults = [silent],
   93
   94    (
   95        not(member(auth(_), Options)) ->
   96            access_token(Token, NewToken),
   97            access_token_auth(NewToken, Auth),
   98            append([auth(Auth)], Defaults, AllDefaults);
   99
  100        Token = NewToken, AllDefaults = Defaults
  101    ),
  102
  103    foldl(add_default, AllDefaults, Options, NewOptions).
  104
  105curl_options(Token, Options, NewToken, CurlOptions) :-
  106    add_defaults(Token, Options, NewToken, AllOptions),
  107    maplist(build_curl_option, AllOptions, Temp),
  108    flatten(Temp, CurlOptions).
  109
  110run_curl(Options, Response) :- run_curl(_, Options, _, Response).
  111run_curl(Token, Options, NewToken, Response) :-
  112    curl_options(Token, Options, NewToken, CurlOptions),
  113    process(path(curl), CurlOptions, [output(Output)]),
  114    catch(atom_json_term(Output, Response, []), _Error, Response = atom(Output)).
  115
  116retrieve_all(Options, Result) :- retrieve_all(_, Options, _, Result).
  117retrieve_all(Token, Options, NewToken, Result) :-
  118    run_curl(Token, Options, TempToken, Response),
  119
  120    (
  121        Response = json(JSON) ->
  122        (
  123            member(next='@'(null), JSON), member(items=AllItems, JSON) -> member(Result, AllItems);
  124            member(next=Url, JSON), member(items=Items, JSON) ->
  125                (
  126                    member(Result, Items);
  127                    with_option(url(Url), Options, NewOptions),
  128                    retrieve_all(TempToken, NewOptions, NewToken, Result)
  129                );
  130            Result = Response
  131        );
  132        Result = Response
  133    ).
  134
  135playlist(User, Playlist) :-
  136    atomic_list_concat(['users', User, 'playlists'], '/', Endpoint),
  137    retrieve_all(_, [endpoint(Endpoint)], _, Playlist).
  138
  139playlist_track(PlaylistId, Track) :-
  140    atomic_list_concat(['playlists', PlaylistId, 'tracks'], '/', Endpoint),
  141    retrieve_all(_, [endpoint(Endpoint)], _, Track).
  142
  143track_name(json(Track), Name) :-
  144    member(track=json(T), Track),
  145    member(name=Name, T).
  146
  147track_artists(json(Track), ArtistNames) :-
  148    member(track=json(T), Track),
  149    member(artists=Artists, T),
  150    findall(Name, (member(json(Artist), Artists), member(name=Name, Artist)), ArtistNames).
  151
  152track_info(Track, Name-Artists) :-
  153    track_name(Track, Name),
  154    track_artists(Track, Artists).
  155
  156playlist_info(json(Playlist), Id-Name) :-
  157    member(id=Id, Playlist),
  158    member(name=Name, Playlist).
  159
  160playlist_track_info(User, Id-Name, Info) :-
  161    playlist(User, Playlist),
  162    playlist_info(Playlist, Id-Name),
  163    playlist_track(Id, Track),
  164    track_info(Track, Info).
  165
  166track_to_csv(Name-Artists, row(Name, ArtistsStr)) :-
  167    atomic_list_concat(Artists, ',', ArtistsStr).
  168
  169% TODO: Would be very cool if we could make this bidirectional
  170% (e.g., read from a csv and create a playlist, or write a playlist from Spotify)
  171playlist_to_csv(User, Id-Name, Path) :-
  172    findall(Track,
  173    (
  174        playlist_track_info(User, Id-Name, Track),
  175        format('Retrieved track ~w~n', [Track])
  176    ), Tracks),
  177    maplist(track_to_csv, Tracks, Rows),
  178    csv_write_file(Path, Rows).
  179
  180test(X) :- client_id(X)