This repository has been archived by the owner on Sep 28, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 211
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added an example that talks to the Spotify API.
- Loading branch information
Showing
11 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
## A simple example processing data from the Spotify API | ||
|
||
### Build | ||
`mvn package` | ||
|
||
### Run | ||
`java -jar target/spotify-api-example-service.jar` | ||
|
||
### Call | ||
``` | ||
$ http :8080/albums/new | ||
HTTP/1.1 200 OK | ||
Content-Length: 1364 | ||
Date: Thu, 03 Dec 2015 16:30:18 GMT | ||
Server: Jetty(9.3.4.v20151007) | ||
[{"name":"Hör vad du säger men jag har glömt vad du sa","artist":{"name":"Danny Saucedo"}},{"name":"The Only One (Kleerup Remix)","artist":{"name":"Miriam Bryant"}},{"name":"Broken Arrows (Remixes)","artist":{"name":"Avicii"}},{"name":"Handwritten (Revisited)","artist":{"name":"Shawn Mendes"}},{"name":"Jag går nu","artist":{"name":"Melissa Horn"}},{"name":"Bang My Head (feat. Sia & Fetty Wap)","artist":{"name":"David Guetta"}},{"name":"Handwritten (Revisited)","artist":{"name":"Shawn Mendes"}},{"name":"Everglow","artist":{"name":"Coldplay"}},{"name":"Jag hör vad du säger men glömt vad du sa","artist":{"name":"Danny Saucedo"}},{"name":"Regissören","artist":{"name":"Dani M"}},{"name":"Peace Is The Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Stay","artist":{"name":"Kygo"}},{"name":"Fine By Me","artist":{"name":"Chris Brown"}},{"name":"Be Together","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission (Extended)","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Peace is the Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission : Extended","artist":{"name":"Major Lazer"}},{"name":"Adventure Of A Lifetime (Radio Edit)","artist":{"name":"Coldplay"}}] | ||
``` | ||
|
||
``` | ||
$ http :8080/artists/toptracks/se?q=elvis | ||
HTTP/1.1 200 OK | ||
Content-Length: 1110 | ||
Date: Thu, 03 Dec 2015 16:31:29 GMT | ||
Server: Jetty(9.3.4.v20151007) | ||
[{"name":"Can't Help Falling in Love","album":{"name":"Blue Hawaii","artist":{"name":"Elvis Presley"}}},{"name":"Blue Christmas","album":{"name":"Elvis' Christmas Album","artist":{"name":"Elvis Presley"}}},{"name":"Jailhouse Rock","album":{"name":"Elvis' Golden Records","artist":{"name":"Elvis Presley"}}},{"name":"Suspicious Minds","album":{"name":"Back In Memphis","artist":{"name":"Elvis Presley"}}},{"name":"A Little Less Conversation - JXL Radio Edit Remix","album":{"name":"Elvis 75 - Good Rockin' Tonight","artist":{"name":"Elvis Presley"}}},{"name":"Always on My Mind - Remastered","album":{"name":"The Essential Elvis Presley","artist":{"name":"Elvis Presley"}}},{"name":"Here Comes Santa Claus (Right Down Santa Claus Lane)","album":{"name":"Elvis' Christmas Album","artist":{"name":"Elvis Presley"}}},{"name":"In the Ghetto","album":{"name":"From Elvis In Memphis","artist":{"name":"Elvis Presley"}}},{"name":"Hound Dog","album":{"name":"Elvis' Golden Records","artist":{"name":"Elvis Presley"}}},{"name":"Don't Be Cruel","album":{"name":"Elvis' Golden Records","artist":{"name":"Elvis Presley"}}}] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
<?xml version="1.0"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<name>Spotify API Example</name> | ||
<description>An Apollo example application using the Spotify API</description> | ||
<groupId>com.spotify</groupId> | ||
<artifactId>spotify-api-example-service</artifactId> | ||
<version>0.0.1-SNAPSHOT</version> | ||
<packaging>jar</packaging> | ||
|
||
<dependencyManagement> | ||
<dependencies> | ||
<dependency> | ||
<groupId>com.spotify</groupId> | ||
<artifactId>apollo-bom</artifactId> | ||
<version>1.0.0</version> | ||
<type>pom</type> | ||
<scope>import</scope> | ||
</dependency> | ||
</dependencies> | ||
</dependencyManagement> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>com.spotify</groupId> | ||
<artifactId>apollo-http-service</artifactId> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>ch.qos.logback</groupId> | ||
<artifactId>logback-classic</artifactId> | ||
<version>1.1.3</version> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<finalName>${project.artifactId}</finalName> | ||
<plugins> | ||
<plugin> | ||
<artifactId>maven-enforcer-plugin</artifactId> | ||
<version>1.4.1</version> | ||
<executions> | ||
<execution> | ||
<id>enforce</id> | ||
<configuration> | ||
<rules> | ||
<requireUpperBoundDeps /> | ||
</rules> | ||
</configuration> | ||
<goals> | ||
<goal>enforce</goal> | ||
</goals> | ||
</execution> | ||
</executions> | ||
</plugin> | ||
|
||
<plugin> | ||
<artifactId>maven-compiler-plugin</artifactId> | ||
<version>3.1</version> | ||
<configuration> | ||
<source>1.8</source> | ||
<target>1.8</target> | ||
<compilerArgs> | ||
<compilerArg>-Xlint:all</compilerArg> | ||
</compilerArgs> | ||
</configuration> | ||
</plugin> | ||
|
||
<plugin> | ||
<artifactId>maven-dependency-plugin</artifactId> | ||
<version>2.10</version> | ||
<executions> | ||
<execution> | ||
<phase>prepare-package</phase> | ||
<goals> | ||
<goal>copy-dependencies</goal> | ||
</goals> | ||
</execution> | ||
</executions> | ||
<configuration> | ||
<useBaseVersion>false</useBaseVersion> | ||
<overWriteReleases>false</overWriteReleases> | ||
<overWriteSnapshots>true</overWriteSnapshots> | ||
<includeScope>runtime</includeScope> | ||
<outputDirectory>${project.build.directory}/lib</outputDirectory> | ||
</configuration> | ||
</plugin> | ||
|
||
<plugin> | ||
<artifactId>maven-jar-plugin</artifactId> | ||
<version>2.6</version> | ||
<configuration> | ||
<archive> | ||
<addMavenDescriptor>true</addMavenDescriptor> | ||
<manifest> | ||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries> | ||
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> | ||
<addClasspath>true</addClasspath> | ||
<classpathPrefix>lib/</classpathPrefix> | ||
<mainClass>com.spotify.apollo.example.SpotifyApiExample</mainClass> | ||
</manifest> | ||
</archive> | ||
</configuration> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
109 changes: 109 additions & 0 deletions
109
examples/spotify-api-example/src/main/java/com/spotify/apollo/example/AlbumResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package com.spotify.apollo.example; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.spotify.apollo.Client; | ||
import com.spotify.apollo.Request; | ||
import com.spotify.apollo.RequestContext; | ||
import com.spotify.apollo.Response; | ||
import com.spotify.apollo.Status; | ||
import com.spotify.apollo.example.data.Album; | ||
import com.spotify.apollo.example.data.Artist; | ||
import com.spotify.apollo.route.AsyncHandler; | ||
import com.spotify.apollo.route.Middlewares; | ||
import com.spotify.apollo.route.Route; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.StringJoiner; | ||
import java.util.concurrent.CompletionStage; | ||
import java.util.stream.Stream; | ||
|
||
import okio.ByteString; | ||
|
||
/** | ||
* The album resource demonstrates how to use asynchronous routes in Apollo. | ||
*/ | ||
public class AlbumResource { | ||
|
||
private static final String SPOTIFY_API = "https://api.spotify.com"; | ||
private static final String SEARCH_API = SPOTIFY_API + "/v1/search"; | ||
private static final String ALBUM_API = SPOTIFY_API + "/v1/albums"; | ||
|
||
public Stream<Route<AsyncHandler<Response<ByteString>>>> routes() { | ||
// The album resource has two routes. Both are using the autoserializer. | ||
return Stream.of( | ||
Route.async("GET", "/albums/new", this::getAlbums), | ||
Route.async("GET", "/albums/hipster", this::getAlbums) | ||
) | ||
.map(route -> route.withMiddleware(Middlewares::autoSerialize)); | ||
} | ||
|
||
public CompletionStage<Response<ArrayList<Album>>> getAlbums(RequestContext requestContext) { | ||
String uri = requestContext.request().uri(); | ||
String tag = uri.substring(uri.lastIndexOf("/") + 1); | ||
|
||
// We need to first query the search API, parse the result, then query the album API. | ||
Request searchRequest = Request.forUri(SEARCH_API + "?type=album&q=tag%3A" + tag); | ||
Client client = requestContext.requestScopedClient(); | ||
return client | ||
.send(searchRequest) | ||
.thenComposeAsync(response -> { | ||
String ids = parseResponseAlbumIds(response.payload().get().utf8()); | ||
return client.send(Request.forUri(ALBUM_API + "?ids=" + ids)); | ||
}) | ||
.thenApplyAsync(response -> Response.ok() | ||
.withPayload(parseAlbumData(response.payload().get().utf8()))) | ||
.exceptionally(throwable -> Response | ||
.forStatus(Status.INTERNAL_SERVER_ERROR.withReasonPhrase("Something went wrong"))); | ||
} | ||
|
||
/** | ||
* Parses an album response from a | ||
* <a href="https://developer.spotify.com/web-api/album-endpoints/">Spotify API album query</a>. | ||
* | ||
* @param json The json response | ||
* @return A list of albums with artist information | ||
*/ | ||
private ArrayList<Album> parseAlbumData(String json) { | ||
ArrayList<Album> albums = new ArrayList<>(); | ||
try { | ||
ObjectMapper mapper = new ObjectMapper(); | ||
JsonNode jsonNode = mapper.readTree(json); | ||
for (JsonNode albumNode : jsonNode.get("albums")) { | ||
JsonNode artistsNode = albumNode.get("artists"); | ||
// Exclude albums with 0 artists | ||
if (artistsNode.size() >= 1) { | ||
// Only keeping the first artist for simplicity | ||
Artist artist = new Artist(artistsNode.get(0).get("name").asText()); | ||
Album album = new Album(albumNode.get("name").asText(), artist); | ||
albums.add(album); | ||
} | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to parse JSON", e); | ||
} | ||
return albums; | ||
} | ||
|
||
/** | ||
* Parses the album ids from a JSON response from a | ||
* <a href="https://developer.spotify.com/web-api/search-item/">Spotify API search query</a>. | ||
* | ||
* @param json The JSON response | ||
* @return A comma-separated list of album ids from the response | ||
*/ | ||
private String parseResponseAlbumIds(String json) { | ||
StringJoiner sj = new StringJoiner(","); | ||
try { | ||
ObjectMapper mapper = new ObjectMapper(); | ||
JsonNode jsonNode = mapper.readTree(json); | ||
for (JsonNode node : jsonNode.get("albums").get("items")) { | ||
sj.add(node.get("id").asText()); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to parse JSON", e); | ||
} | ||
return sj.toString(); | ||
} | ||
} |
113 changes: 113 additions & 0 deletions
113
examples/spotify-api-example/src/main/java/com/spotify/apollo/example/ArtistResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package com.spotify.apollo.example; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.spotify.apollo.Client; | ||
import com.spotify.apollo.Request; | ||
import com.spotify.apollo.RequestContext; | ||
import com.spotify.apollo.Response; | ||
import com.spotify.apollo.Status; | ||
import com.spotify.apollo.example.data.Album; | ||
import com.spotify.apollo.example.data.Artist; | ||
import com.spotify.apollo.example.data.Track; | ||
import com.spotify.apollo.route.AsyncHandler; | ||
import com.spotify.apollo.route.Middlewares; | ||
import com.spotify.apollo.route.Route; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Optional; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.CompletionStage; | ||
import java.util.stream.Stream; | ||
|
||
import okio.ByteString; | ||
|
||
/** | ||
* The artist resource demonstrates how to use asynchronous routes in Apollo with query arguments. | ||
*/ | ||
public class ArtistResource { | ||
|
||
private static final String SPOTIFY_API = "https://api.spotify.com"; | ||
private static final String SEARCH_API = SPOTIFY_API + "/v1/search"; | ||
private static final String ARTIST_API = SPOTIFY_API + "/v1/artists"; | ||
|
||
public Stream<Route<AsyncHandler<Response<ByteString>>>> routes() { | ||
// The artist resource has one parameterized route. | ||
return Stream.of( | ||
Route.async("GET", "/artists/toptracks/<country>", this::getArtistTopTracks) | ||
.withMiddleware(Middlewares::autoSerialize) | ||
); | ||
} | ||
|
||
public CompletionStage<Response<ArrayList<Track>>> getArtistTopTracks(RequestContext requestContext) { | ||
// Validate request | ||
Optional<String> query = requestContext.request().parameter("q"); | ||
if (!query.isPresent()) { | ||
return CompletableFuture.completedFuture( | ||
Response.forStatus(Status.BAD_REQUEST.withReasonPhrase("No search query"))); | ||
} | ||
String country = requestContext.pathArgs().get("country"); | ||
|
||
// We need to first query the search API, parse the result, then query top-tracks. | ||
Request searchRequest = Request.forUri(SEARCH_API + "?type=artist&q=" + query.get()); | ||
Client client = requestContext.requestScopedClient(); | ||
return client | ||
.send(searchRequest) | ||
.thenComposeAsync(response -> { | ||
String topArtistId = parseFirstArtistId(response.payload().get().utf8()); | ||
Request topTracksRequest = Request.forUri( | ||
String.format("%s/%s/top-tracks?country=%s", ARTIST_API, topArtistId, country)); | ||
return client.send(topTracksRequest); | ||
}).thenApplyAsync(response -> Response.ok() | ||
.withPayload(parseTopTracks(response.payload().get().utf8()))) | ||
.exceptionally(throwable -> Response | ||
.forStatus(Status.INTERNAL_SERVER_ERROR.withReasonPhrase("Something went wrong"))); | ||
} | ||
|
||
/** | ||
* Parses an artist top tracks response from the | ||
* <a href="https://developer.spotify.com/web-api/get-artists-top-tracks/">Spotify API</a> | ||
* | ||
* @param json The json response | ||
* @return A list of top tracks | ||
*/ | ||
private ArrayList<Track> parseTopTracks(String json) { | ||
ArrayList<Track> tracks = new ArrayList<>(); | ||
try { | ||
ObjectMapper mapper = new ObjectMapper(); | ||
JsonNode jsonNode = mapper.readTree(json); | ||
for (JsonNode trackNode : jsonNode.get("tracks")) { | ||
JsonNode albumNode = trackNode.get("album"); | ||
String albumName = albumNode.get("name").asText(); | ||
String artistName = trackNode.get("artists").get(0).get("name").asText(); | ||
String trackName = trackNode.get("name").asText(); | ||
|
||
tracks.add(new Track(trackName, new Album(albumName, new Artist(artistName)))); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to parse JSON", e); | ||
} | ||
return tracks; | ||
} | ||
|
||
/** | ||
* Parses the first artist id from a JSON response from a | ||
* <a href="https://developer.spotify.com/web-api/search-item/">Spotify API search query</a>. | ||
* | ||
* @param json The json response | ||
* @return The id of the first artist in the response. null if response was empty. | ||
*/ | ||
private String parseFirstArtistId(String json) { | ||
try { | ||
ObjectMapper mapper = new ObjectMapper(); | ||
JsonNode jsonNode = mapper.readTree(json); | ||
for (JsonNode node : jsonNode.get("artists").get("items")) { | ||
return node.get("id").asText(); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to parse JSON", e); | ||
} | ||
return null; | ||
} | ||
} |
Oops, something went wrong.