Skip to content
This repository has been archived by the owner on Sep 28, 2021. It is now read-only.

Commit

Permalink
Added an example that talks to the Spotify API.
Browse files Browse the repository at this point in the history
  • Loading branch information
Yarin78 committed Dec 4, 2015
1 parent 585fe27 commit bec6a8e
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/spotify-api-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
28 changes: 28 additions & 0 deletions examples/spotify-api-example/README.md
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"}}}]
```
113 changes: 113 additions & 0 deletions examples/spotify-api-example/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?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>com.spotify</groupId>
<artifactId>apollo-extra</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>
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.fasterxml.jackson.databind.ObjectWriter;
import com.spotify.apollo.Client;
import com.spotify.apollo.Request;
import com.spotify.apollo.RequestContext;
import com.spotify.apollo.Response;
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.JsonSerializerMiddlewares;
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";

private final ObjectMapper objectMapper;
private final ObjectWriter objectWriter;

public AlbumResource(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectWriter = objectMapper.writer();
}

public Stream<Route<AsyncHandler<Response<ByteString>>>> routes() {
// The album resource has two routes. Since both are returning the same type,
// we can map them to the same middleware
// Note that this could also have been set up as a single route to "/albums/<tag>".
return Stream.of(
Route.async("GET", "/albums/new", context -> getAlbums(context, "new")),
Route.async("GET", "/albums/hipster", context -> getAlbums(context, "hipster"))
)
.map(route -> route.withMiddleware(
JsonSerializerMiddlewares.jsonSerializeResponse(objectWriter)));
}

public CompletionStage<Response<ArrayList<Album>>> getAlbums(RequestContext requestContext, String tag) {
// 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())));
}

/**
* 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 {
JsonNode jsonNode = this.objectMapper.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 {
JsonNode jsonNode = this.objectMapper.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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.spotify.apollo.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
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.JsonSerializerMiddlewares;
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";

private final ObjectMapper objectMapper;
private final ObjectWriter objectWriter;

public ArtistResource(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectWriter = objectMapper.writer();
}

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(JsonSerializerMiddlewares.jsonSerializeResponse(objectWriter))
);
}

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())))
// This .exceptionally doesn't provide any additional value,
// but it shows how you could handle exceptions
.exceptionally(throwable -> Response
.forStatus(Status.INTERNAL_SERVER_ERROR.withReasonPhrase("Something failed")));
}

/**
* 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 {
JsonNode jsonNode = this.objectMapper.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 {
JsonNode jsonNode = this.objectMapper.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;
}
}

0 comments on commit bec6a8e

Please sign in to comment.