commit 9df2ffe29154a4fe3cf0769d7d3f954a384381fa Author: David Date: Wed Sep 23 11:30:59 2020 +0200 Initial commit diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fc7a97f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2020 David Álvarez González + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b29982c --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# MusicAdvisor +CLI app using Spotify API to create a personal music advisor that makes preference-based suggestions and even shares links to new releases and featured playlists. + +## How to run +In root of the project: + + > gradle run -q --console=plain + + +## Parameters +`-access` + +Authorization server path. If it isn't set, default server is https://accounts.spotify.com + +`-resource` + +API server path. If it isn't set, default server is https://api.spotify.com + +`-page` + +Number of entries that should be shown on a page. If it isn't set, default value is 5. + +## Commands +`auth` Creates a link to confirm access of the app. + +![](src/advisor/resources/auth.png) + +`featured` Gets a paginated list of Spotify-featured playlists with their links fetched from API. + +![](src/advisor/resources/featured.png) + +`new` Gets a paginated list of new albums with artists and links on Spotify. + +![](src/advisor/resources/new.png) + +`categories` Gets a paginated list of all available categories on Spotify (just their names) + +![](src/advisor/resources/categories.png) + +`playlists C_NAME` Gets a paginated list containing playlists of this category (where C_NAME is the name of category) and their links on Spotify. + +![](src/advisor/resources/playlists.png) + +`next` Goes to next page. + +`prev` Goes to previous page. + +`exit` Exits the program. + +![](src/advisor/resources/exit.png) \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3bd785d --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'java' +apply plugin: 'application' + +group 'advisor' +version '1.0-SNAPSHOT' + +sourceCompatibility = 11 +mainClassName = 'advisor.Main' + +repositories { + mavenCentral() +} + +dependencies { + compile 'com.google.code.gson:gson:+' +} + +jar { + manifest { + attributes 'Main-Class' : 'advisor.Main' + } + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +run{ + standardInput = System.in +} \ No newline at end of file diff --git a/src/advisor/Main.java b/src/advisor/Main.java new file mode 100644 index 0000000..4b92481 --- /dev/null +++ b/src/advisor/Main.java @@ -0,0 +1,97 @@ +package advisor; + +import advisor.auth.Authorization; +import advisor.auth.LocalServer; +import advisor.config.Params; +import advisor.controller.AdvisorController; +import advisor.model.AdvisorModel; +import advisor.view.AdvisorView; + +import java.util.List; +import java.util.Scanner; +import java.util.StringJoiner; + +public class Main { + + public static void main(String[] args) throws Exception { + Scanner sc = new Scanner(System.in); + String accessServer = Params.ACCESS_SERVER; + String resourceServer = Params.RESOURCE_SERVER; + boolean userAuth = false, exit = false, access = false, resource = false, page = false; + + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-access") && !access) { + accessServer = args[i + 1]; + access = true; + i++; + } + if (args[i].equals("-resource") && !resource) { + resourceServer = args[i + 1]; + resource = true; + i++; + } + if (args[i].equals("-page") && !page) { + Params.RESULTS_PER_PAGE = Integer.parseInt(args[i + 1]); + page = true; + i++; + } + } + + AdvisorController advisorController = new AdvisorController(resourceServer); + + while (!exit) { + String[] input = sc.nextLine().split("\\s+"); + if (!userAuth) { + if (input[0].equals("auth")) { + LocalServer localServer = new LocalServer(accessServer); + localServer.startServer(); + localServer.getCode(); + localServer.stopServer(); + Authorization authorization = new Authorization(accessServer); + authorization.getToken(); + userAuth = true; + AdvisorView.printMessage(Params.SUCCESS); + } else { + AdvisorView.printMessage(Params.ANSWER_DENIED_ACCESS); + } + } else { + switch (input[0]) { + case "featured": + AdvisorView.print(advisorController.getFeaturedPlaylists()); + break; + case "new": + AdvisorView.print(advisorController.getNewReleases()); + break; + case "categories": + AdvisorView.print(advisorController.getCategories()); + break; + case "playlists": + StringJoiner sj = new StringJoiner(" "); + for (int i = 1; i < input.length; i++) { + sj.add(input[i]); + } + String categoryName = sj.toString(); + List categoryPlaylists = advisorController.getCategoryPlaylists(categoryName); + if (categoryPlaylists != null) { + AdvisorView.print(categoryPlaylists); + } else { + AdvisorView.printMessage(Params.UNKNOWN_CATEGORY_NAME); + } + break; + case "next": + AdvisorView.printNextPage(); + break; + case "prev": + AdvisorView.printPrevPage(); + break; + case "exit": + AdvisorView.printMessage(Params.GOODBYE); + exit = true; + break; + default: + AdvisorView.printMessage(Params.INCORRECT_COMMAND); + } + } + } + } +} \ No newline at end of file diff --git a/src/advisor/auth/Authorization.java b/src/advisor/auth/Authorization.java new file mode 100644 index 0000000..f041cac --- /dev/null +++ b/src/advisor/auth/Authorization.java @@ -0,0 +1,42 @@ +package advisor.auth; + +import advisor.config.Params; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class Authorization { + + private static String accessServer; + + public Authorization(String accessServer) { + Authorization.accessServer = accessServer; + } + + public void getToken() throws IOException, InterruptedException { + + System.out.println(Params.MAKING_HTTP_REQUEST_FOR_TOKEN); + + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString( + "client_id=" + Params.CLIENT_ID + + "&client_secret=" + Params.CLIENT_SECRET + + "&grant_type=" + Params.GRANT_TYPE + + "&code=" + Params.AUTH_CODE + + "&redirect_uri=" + Params.REDIRECT_URI)) + .header("Content-Type", "application/x-www-form-urlencoded") + .uri(URI.create(accessServer + Params.TOKEN_PART)) + .build(); + + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + JsonObject responseJson = JsonParser.parseString(response.body()).getAsJsonObject(); + Params.TOKEN_CODE = responseJson.get("access_token").getAsString(); + } +} diff --git a/src/advisor/auth/LocalServer.java b/src/advisor/auth/LocalServer.java new file mode 100644 index 0000000..a258d87 --- /dev/null +++ b/src/advisor/auth/LocalServer.java @@ -0,0 +1,61 @@ +package advisor.auth; + +import advisor.config.Params; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public class LocalServer { + + private static String accessServer; + private static HttpServer httpServer; + + public LocalServer(String accessServer) { + LocalServer.accessServer = accessServer; + } + + public void startServer() throws IOException { + httpServer = HttpServer.create(); + httpServer.bind(new InetSocketAddress(8080), 0); + httpServer.start(); + } + + public void getCode() throws InterruptedException { + System.out.println("use this link to request the access code:"); + System.out.println(accessServer + Params.AUTHORIZE_PART + + "?client_id=" + Params.CLIENT_ID + + "&redirect_uri=" + Params.REDIRECT_URI + + "&response_type=" + Params.RESPONSE_TYPE); + System.out.println("waiting for code..."); + + httpServer.createContext("/", + exchange -> { + String code = exchange.getRequestURI().getQuery(); + String result, answer; + + if (code != null && code.contains("code")) { + Params.AUTH_CODE = code.substring(5); + result = "Got the code. Return back to your program."; + answer = "code received"; + } else { + result = "Not found authorization code. Try again."; + answer = "code not received"; + } + + exchange.sendResponseHeaders(200, result.length()); + exchange.getResponseBody().write(result.getBytes()); + exchange.getResponseBody().close(); + + System.out.println(answer); + } + ); + while (Params.AUTH_CODE.equals("")) { + Thread.sleep(10); + } + } + + public void stopServer() { + httpServer.stop(10); + } +} diff --git a/src/advisor/config/Params.java b/src/advisor/config/Params.java new file mode 100644 index 0000000..7271f33 --- /dev/null +++ b/src/advisor/config/Params.java @@ -0,0 +1,27 @@ +package advisor.config; + +public class Params { + public static final String CLIENT_ID = "dd6e7d19d2624ee48db69174ac093a2f"; + public static final String CLIENT_SECRET = "84fcf5eab40a4854a722bd95802cec5c"; + public static final String AUTHORIZE_PART = "/authorize"; + public static final String RESPONSE_TYPE = "code"; + public static final String TOKEN_PART = "/api/token"; + public static final String GRANT_TYPE = "authorization_code"; + public static final String ACCESS_SERVER = "https://accounts.spotify.com"; + public static final String RESOURCE_SERVER = "https://api.spotify.com"; + public static final String NEW_RELEASES= "/v1/browse/new-releases"; + public static final String MAKING_HTTP_REQUEST_FOR_TOKEN= "making http request for access_token..."; + public static final String FEATURED_PLAYLISTS= "/v1/browse/featured-playlists"; + public static final String CATEGORIES = "/v1/browse/categories"; + public static final String PLAYLISTS= "/playlists"; + public static final String REDIRECT_URI = "http://localhost:8080"; + public static final String SUCCESS = "---SUCCESS---"; + public static final String ANSWER_DENIED_ACCESS = "Please, provide access for application."; + public static final String UNKNOWN_CATEGORY_NAME = "Unknown category name."; + public static final String GOODBYE = "---GOODBYE!---"; + public static final String INCORRECT_COMMAND = "Incorrect command. Try again."; + public static final String NO_MORE_PAGES = "No more pages."; + public static String AUTH_CODE = ""; + public static String TOKEN_CODE = ""; + public static int RESULTS_PER_PAGE = 5; +} \ No newline at end of file diff --git a/src/advisor/controller/AdvisorController.java b/src/advisor/controller/AdvisorController.java new file mode 100644 index 0000000..c678a6b --- /dev/null +++ b/src/advisor/controller/AdvisorController.java @@ -0,0 +1,112 @@ +package advisor.controller; + +import advisor.model.AdvisorModel; +import advisor.config.Params; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +public class AdvisorController { + private static String resourceServer; + + public AdvisorController(String resourceServer) { + AdvisorController.resourceServer = resourceServer; + } + + public List getNewReleases() throws IOException, InterruptedException { + List newReleases = new ArrayList<>(); + HttpRequest newReleasesRequest = createRequest(resourceServer + Params.NEW_RELEASES); + for (JsonElement item: Objects.requireNonNull(getJsonItems(newReleasesRequest, "albums"))) { + AdvisorModel album = new AdvisorModel(); + album.setAlbum(item.getAsJsonObject().get("name").toString()); + StringJoiner joiner = new StringJoiner(", ", "[", "]"); + for (JsonElement artist: item.getAsJsonObject().getAsJsonArray("artists")) { + String artistName = artist.getAsJsonObject().get("name").getAsString(); + joiner.add(artistName); + } + album.setArtists(String.valueOf(joiner)); + album.setLink(item.getAsJsonObject().get("external_urls").getAsJsonObject().get("spotify").toString()); + newReleases.add(album); + } + return newReleases; + } + + public List getFeaturedPlaylists() throws IOException, InterruptedException { + List featuredPlaylists = new ArrayList<>(); + HttpRequest featuredPlaylistsRequest = createRequest(resourceServer + Params.FEATURED_PLAYLISTS); + return getPlaylists(featuredPlaylists, featuredPlaylistsRequest); + } + + private List getPlaylists(List categoryPlaylists, HttpRequest categoryPlaylistsRequest) throws IOException, InterruptedException { + for (JsonElement item: Objects.requireNonNull(getJsonItems(categoryPlaylistsRequest, "playlists"))) { + AdvisorModel categoryPlaylist = new AdvisorModel(); + categoryPlaylist.setAlbum(item.getAsJsonObject().get("name").toString()); + categoryPlaylist.setLink(item.getAsJsonObject().get("external_urls").getAsJsonObject().get("spotify").toString()); + categoryPlaylists.add(categoryPlaylist); + } + return categoryPlaylists; + } + + public List getCategories() throws IOException, InterruptedException { + List categories = new ArrayList<>(); + HttpRequest categoriesRequest = createRequest(resourceServer + Params.CATEGORIES); + for (JsonElement item: Objects.requireNonNull(getJsonItems(categoriesRequest, "categories"))) { + AdvisorModel category = new AdvisorModel(); + category.setAlbum(item.getAsJsonObject().get("name").toString()); + categories.add(category); + } + return categories; + } + + public List getCategoryPlaylists(String categoryName) throws IOException, InterruptedException { + List categoryPlaylists = new ArrayList<>(); + String categoryID = getCategoryIdByCategoryName(categoryName); + HttpRequest categoryPlaylistsRequest = + createRequest(resourceServer + Params.CATEGORIES + "/" + categoryID + Params.PLAYLISTS); + return (categoryID == null) ? null : getPlaylists(categoryPlaylists, categoryPlaylistsRequest); + } + + private String getCategoryIdByCategoryName(String categoryName) throws IOException, InterruptedException { + HttpRequest categories = createRequest(resourceServer + Params.CATEGORIES); + JsonArray items = getJsonItems(categories, "categories"); + assert items != null; + for(JsonElement item: items){ + String name = item.getAsJsonObject().get("name").getAsString(); + if(categoryName.equalsIgnoreCase(name)){ + return item.getAsJsonObject().get("id").getAsString(); + } + } + return null; + } + + private JsonArray getJsonItems(HttpRequest request, String element) throws IOException, InterruptedException { + HttpClient client = HttpClient.newBuilder().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + JsonObject responseJson = JsonParser.parseString(response.body()).getAsJsonObject(); + if (responseJson.get("error") == null){ + JsonObject featured = responseJson.getAsJsonObject(element); + return featured.getAsJsonArray("items"); + } + System.out.println(responseJson.getAsJsonObject("error").get("message").getAsString()); + return null; + } + + private static HttpRequest createRequest(String requestedFeatureURL) { + return HttpRequest.newBuilder() + .header("Authorization", "Bearer " + Params.TOKEN_CODE) + .uri(URI.create(requestedFeatureURL)) + .GET() + .build(); + } +} \ No newline at end of file diff --git a/src/advisor/model/AdvisorModel.java b/src/advisor/model/AdvisorModel.java new file mode 100644 index 0000000..e36ae79 --- /dev/null +++ b/src/advisor/model/AdvisorModel.java @@ -0,0 +1,28 @@ +package advisor.model; + +public class AdvisorModel { + String album = null; + String artists = null; + String link = null; + + public void setAlbum(String album) { + this.album = album; + } + + public void setArtists(String authors) { + this.artists = authors; + } + + public void setLink(String link) { + this.link = link; + } + + @Override + public String toString() { + StringBuilder info = new StringBuilder(); + if (album != null) { info.append(album).append("\n"); } + if (artists != null) { info.append(artists).append("\n"); } + if (link != null) { info.append(link).append("\n"); } + return info.toString().replaceAll("\"", ""); + } +} diff --git a/src/advisor/resources/auth.png b/src/advisor/resources/auth.png new file mode 100644 index 0000000..83b7744 Binary files /dev/null and b/src/advisor/resources/auth.png differ diff --git a/src/advisor/resources/categories.png b/src/advisor/resources/categories.png new file mode 100644 index 0000000..be4b2a3 Binary files /dev/null and b/src/advisor/resources/categories.png differ diff --git a/src/advisor/resources/exit.png b/src/advisor/resources/exit.png new file mode 100644 index 0000000..88722b6 Binary files /dev/null and b/src/advisor/resources/exit.png differ diff --git a/src/advisor/resources/featured.png b/src/advisor/resources/featured.png new file mode 100644 index 0000000..3e1327a Binary files /dev/null and b/src/advisor/resources/featured.png differ diff --git a/src/advisor/resources/new.png b/src/advisor/resources/new.png new file mode 100644 index 0000000..58d428f Binary files /dev/null and b/src/advisor/resources/new.png differ diff --git a/src/advisor/resources/playlists.png b/src/advisor/resources/playlists.png new file mode 100644 index 0000000..34eff72 Binary files /dev/null and b/src/advisor/resources/playlists.png differ diff --git a/src/advisor/view/AdvisorView.java b/src/advisor/view/AdvisorView.java new file mode 100644 index 0000000..fcca214 --- /dev/null +++ b/src/advisor/view/AdvisorView.java @@ -0,0 +1,55 @@ +package advisor.view; + +import advisor.model.AdvisorModel; +import advisor.config.Params; + +import java.util.List; + +public class AdvisorView { + static int elem; + static int page; + static List data; + static int pagesCount; + + public static void printMessage(String message) { + System.out.println(message); + } + + public static void print(List data) { + elem -= Params.RESULTS_PER_PAGE; + page = 0; + AdvisorView.data = data; + pagesCount = data.size() / Params.RESULTS_PER_PAGE; + pagesCount += data.size() % Params.RESULTS_PER_PAGE != 0 ? 1 : 0; + + printNextPage(); + } + + public static void printNextPage() { + if (page >= pagesCount) { + System.out.println(Params.NO_MORE_PAGES); + } else { + elem += Params.RESULTS_PER_PAGE; + page++; + print(); + } + } + + public static void printPrevPage() { + if (page == 1) { + System.out.println(Params.NO_MORE_PAGES); + } else { + elem -= Params.RESULTS_PER_PAGE; + page--; + print(); + } + } + + public static void print() { + data.stream() + .skip(elem) + .limit(Params.RESULTS_PER_PAGE) + .forEach(System.out::println); + System.out.printf("---PAGE %d OF %d---\n", page, pagesCount); + } +}