From 98aae25a72cac218d308b82d00c99d195aa21b55 Mon Sep 17 00:00:00 2001 From: simon987 Date: Wed, 8 May 2019 20:35:30 -0400 Subject: [PATCH] Chart builder setup, release details endpoint --- pom.xml | 6 - .../java/net/simon987/musicgraph/Main.java | 9 +- .../musicgraph/entities/ReleaseDetails.java | 17 + .../musicgraph/io/ICoverArtDatabase.java | 5 + .../simon987/musicgraph/io/MusicDatabase.java | 45 +++ .../musicgraph/io/SQLiteCoverArtDatabase.java | 29 ++ .../logging/MusicGraphFormatter.java | 2 +- .../musicgraph/webapi/ChartController.java | 48 +++ .../musicgraph/webapi/ChartOptions.java | 18 ++ .../musicgraph/webapi/IChartBuilder.java | 6 + .../musicgraph/webapi/MagickChartBuilder.java | 306 ++++++++++++++++++ .../musicgraph/webapi/MyExceptionMapper.java | 22 ++ 12 files changed, 502 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/simon987/musicgraph/entities/ReleaseDetails.java create mode 100644 src/main/java/net/simon987/musicgraph/webapi/ChartController.java create mode 100644 src/main/java/net/simon987/musicgraph/webapi/ChartOptions.java create mode 100644 src/main/java/net/simon987/musicgraph/webapi/IChartBuilder.java create mode 100644 src/main/java/net/simon987/musicgraph/webapi/MagickChartBuilder.java create mode 100644 src/main/java/net/simon987/musicgraph/webapi/MyExceptionMapper.java diff --git a/pom.xml b/pom.xml index c2cc454..5fb9a88 100644 --- a/pom.xml +++ b/pom.xml @@ -122,11 +122,5 @@ sqlite-jdbc 3.27.2.1 - - org.jetbrains - annotations - 17.0.0 - compile - \ No newline at end of file diff --git a/src/main/java/net/simon987/musicgraph/Main.java b/src/main/java/net/simon987/musicgraph/Main.java index b572185..25ad897 100644 --- a/src/main/java/net/simon987/musicgraph/Main.java +++ b/src/main/java/net/simon987/musicgraph/Main.java @@ -2,9 +2,7 @@ package net.simon987.musicgraph; import net.simon987.musicgraph.io.*; import net.simon987.musicgraph.logging.LogManager; -import net.simon987.musicgraph.webapi.ArtistController; -import net.simon987.musicgraph.webapi.CoverController; -import net.simon987.musicgraph.webapi.Index; +import net.simon987.musicgraph.webapi.*; import org.glassfish.grizzly.http.server.HttpServer; import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; import org.glassfish.jersey.jackson.JacksonFeature; @@ -18,7 +16,6 @@ public class Main { private final static Logger logger = LogManager.getLogger(); public static void main(String[] args) { - startHttpServer(); } @@ -28,12 +25,16 @@ public class Main { rc.registerInstances(new MusicDatabase()); rc.registerInstances(new SQLiteCoverArtDatabase("covers.db")); + rc.registerInstances(new MagickChartBuilder("/dev/shm/im_chart/")); rc.registerClasses(Index.class); rc.registerClasses(ArtistController.class); rc.registerClasses(CoverController.class); + rc.registerClasses(ChartController.class); rc.registerClasses(JacksonFeature.class); + rc.registerClasses(MyExceptionMapper.class); + try { HttpServer server = GrizzlyHttpServerFactory.createHttpServer( new URI("http://localhost:3030/"), diff --git a/src/main/java/net/simon987/musicgraph/entities/ReleaseDetails.java b/src/main/java/net/simon987/musicgraph/entities/ReleaseDetails.java new file mode 100644 index 0000000..83fe4db --- /dev/null +++ b/src/main/java/net/simon987/musicgraph/entities/ReleaseDetails.java @@ -0,0 +1,17 @@ +package net.simon987.musicgraph.entities; + +import java.util.ArrayList; +import java.util.List; + +public class ReleaseDetails { + + public String name; + public String mbid; + public String artist; + public long year; + public List tags; + + public ReleaseDetails() { + this.tags = new ArrayList<>(); + } +} diff --git a/src/main/java/net/simon987/musicgraph/io/ICoverArtDatabase.java b/src/main/java/net/simon987/musicgraph/io/ICoverArtDatabase.java index 2347444..8fda6d4 100644 --- a/src/main/java/net/simon987/musicgraph/io/ICoverArtDatabase.java +++ b/src/main/java/net/simon987/musicgraph/io/ICoverArtDatabase.java @@ -1,5 +1,8 @@ package net.simon987.musicgraph.io; +import java.util.HashMap; +import java.util.List; + public interface ICoverArtDatabase { /** @@ -8,4 +11,6 @@ public interface ICoverArtDatabase { * @throws Exception if unexpected error */ byte[] getCover(String mbid) throws Exception; + + HashMap getCovers(List mbids) throws Exception; } diff --git a/src/main/java/net/simon987/musicgraph/io/MusicDatabase.java b/src/main/java/net/simon987/musicgraph/io/MusicDatabase.java index dc056b5..4e78064 100644 --- a/src/main/java/net/simon987/musicgraph/io/MusicDatabase.java +++ b/src/main/java/net/simon987/musicgraph/io/MusicDatabase.java @@ -155,4 +155,49 @@ public class MusicDatabase extends AbstractBinder { return relation; } + public ReleaseDetails getReleaseDetails(String mbid) { + + Map params = new HashMap<>(); + params.put("mbid", mbid); + + try (Session session = driver.session()) { + + StatementResult result = query(session, + "MATCH (release:Release {id: $mbid})-[:CREDITED_FOR]-(a:Artist)\n" + + "OPTIONAL MATCH (release)-[r:IS_TAGGED]->(t:Tag)\n" + + "RETURN release {name:release.name, year:release.year," + + "tags:collect({weight:r.weight, name:t.name}), artist: a.name}\n" + + "LIMIT 1", + params); + + ReleaseDetails details = new ReleaseDetails(); + + try { + + if (result.hasNext()) { + Map map = result.next().get("release").asMap(); + + details.mbid = mbid; + details.name = (String) map.get("name"); + details.artist = (String) map.get("artist"); + details.year = (long) map.get("year"); + details.tags.addAll( + ((List) map.get("tags")) + .stream() + .filter(x -> x.get("name") != null) + .map(x -> new Tag( + (String) x.get("name"), + (Long) x.get("weight") + )) + .collect(Collectors.toList()) + ); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return details; + } + } + } diff --git a/src/main/java/net/simon987/musicgraph/io/SQLiteCoverArtDatabase.java b/src/main/java/net/simon987/musicgraph/io/SQLiteCoverArtDatabase.java index 5888e77..e5ef4dc 100644 --- a/src/main/java/net/simon987/musicgraph/io/SQLiteCoverArtDatabase.java +++ b/src/main/java/net/simon987/musicgraph/io/SQLiteCoverArtDatabase.java @@ -4,6 +4,8 @@ import net.simon987.musicgraph.logging.LogManager; import org.glassfish.jersey.internal.inject.AbstractBinder; import java.sql.*; +import java.util.HashMap; +import java.util.List; import java.util.logging.Logger; public class SQLiteCoverArtDatabase extends AbstractBinder implements ICoverArtDatabase { @@ -47,6 +49,33 @@ public class SQLiteCoverArtDatabase extends AbstractBinder implements ICoverArtD } } + public HashMap getCovers(List mbids) throws SQLException { + + HashMap covers = new HashMap<>(mbids.size()); + + try { + setupConn(); + + PreparedStatement stmt = connection.prepareStatement( + //TODO: sqlite injection + String.format("SELECT id, cover FROM covers WHERE id IN ('%s')", + String.join("','", mbids)) + ); + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + covers.put(rs.getString(1), rs.getBytes(2)); + } + + return covers; + + } catch (SQLException e) { + logger.severe(String.format("Exception during cover art query mbid=%s ex=%s", + mbids, e.getMessage())); + throw e; + } + } + private void setupConn() throws SQLException { try { if (connection == null || connection.isClosed()) { diff --git a/src/main/java/net/simon987/musicgraph/logging/MusicGraphFormatter.java b/src/main/java/net/simon987/musicgraph/logging/MusicGraphFormatter.java index 7d2593a..b0532fb 100644 --- a/src/main/java/net/simon987/musicgraph/logging/MusicGraphFormatter.java +++ b/src/main/java/net/simon987/musicgraph/logging/MusicGraphFormatter.java @@ -9,7 +9,7 @@ import java.util.logging.LogRecord; public class MusicGraphFormatter extends Formatter { - private static final DateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss"); + private static final DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); @Override public String format(LogRecord record) { diff --git a/src/main/java/net/simon987/musicgraph/webapi/ChartController.java b/src/main/java/net/simon987/musicgraph/webapi/ChartController.java new file mode 100644 index 0000000..af07229 --- /dev/null +++ b/src/main/java/net/simon987/musicgraph/webapi/ChartController.java @@ -0,0 +1,48 @@ +package net.simon987.musicgraph.webapi; + +import net.simon987.musicgraph.io.ICoverArtDatabase; +import net.simon987.musicgraph.io.MusicDatabase; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Path("/chart") +public class ChartController { + + @Inject + IChartBuilder chartBuilder; + + @Inject + ICoverArtDatabase coverArtDatabase; + + @Inject + private MusicDatabase musicDatabase; + + @GET + @Produces("image/png") + public byte[] makeChart() throws Exception { + var opt = new ChartOptions(); + + throw new NotFoundException(); + +// List releases = new ArrayList<>(); +// +// opt.cols = 4; +// opt.rows = 5; +// opt.covers = coverArtDatabase.getCovers(releases); +// opt.backgroundColor = "blue"; +// opt.details = releases +// .parallelStream() // Thread-safe? +// .map(r -> musicDatabase.getReleaseDetails(r)) +// .collect(Collectors.toList()); +// +// return chartBuilder.makeChart(opt); + } +} diff --git a/src/main/java/net/simon987/musicgraph/webapi/ChartOptions.java b/src/main/java/net/simon987/musicgraph/webapi/ChartOptions.java new file mode 100644 index 0000000..0a5d500 --- /dev/null +++ b/src/main/java/net/simon987/musicgraph/webapi/ChartOptions.java @@ -0,0 +1,18 @@ +package net.simon987.musicgraph.webapi; + +import net.simon987.musicgraph.entities.ReleaseDetails; + +import java.util.HashMap; +import java.util.List; + +public class ChartOptions { + + public HashMap covers; + public List details; + public int rows; + public int cols; + public String backgroundColor; + public String textColor = "white"; + public String font = "Hack-Regular"; + public String borderColor = "white"; +} diff --git a/src/main/java/net/simon987/musicgraph/webapi/IChartBuilder.java b/src/main/java/net/simon987/musicgraph/webapi/IChartBuilder.java new file mode 100644 index 0000000..eb6b4f8 --- /dev/null +++ b/src/main/java/net/simon987/musicgraph/webapi/IChartBuilder.java @@ -0,0 +1,6 @@ +package net.simon987.musicgraph.webapi; + +public interface IChartBuilder { + + byte[] makeChart(ChartOptions options) throws Exception; +} diff --git a/src/main/java/net/simon987/musicgraph/webapi/MagickChartBuilder.java b/src/main/java/net/simon987/musicgraph/webapi/MagickChartBuilder.java new file mode 100644 index 0000000..259b27d --- /dev/null +++ b/src/main/java/net/simon987/musicgraph/webapi/MagickChartBuilder.java @@ -0,0 +1,306 @@ +package net.simon987.musicgraph.webapi; + +import net.simon987.musicgraph.entities.ReleaseDetails; +import org.glassfish.jersey.internal.inject.AbstractBinder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class MagickChartBuilder extends AbstractBinder implements IChartBuilder { + + private String workspacePath; + private File workspace; + + private static final String[] letters = new String[]{ + "A", "B", "C", "D", "E", "F", "G", "H", "I" + }; + + @Override + protected void configure() { + bind(this).to(IChartBuilder.class); + } + + public MagickChartBuilder(String workspacePath) { + this.workspacePath = workspacePath; + this.workspace = new File(workspacePath); + } + + @Override + public byte[] makeChart(ChartOptions options) throws Exception { + + ProcessBuilder processBuilder = new ProcessBuilder(); + List releases = options.details.stream().map(o -> o.mbid).collect(Collectors.toList()); + + File chartWorkspace = makeChartWorkspace(); + + saveCoversToDisk(options, chartWorkspace); + makeGrid(options, chartWorkspace, releases); + File header = makeHeader(options, chartWorkspace); + File side = makeSide(options, chartWorkspace); + + // Side + grid + processBuilder + .directory(chartWorkspace) + .command( + "convert", + "+append", + side.toString(), + "grid.png", + "grid_with_side.png" + ) + .start() + .waitFor(); + + // Side + grid + processBuilder + .directory(chartWorkspace) + .command( + "convert", + "-append", + "-gravity", "east", + header.toString(), + "grid_with_side.png", + "final_grid.png" + ) + .start() + .waitFor(); + + makeDescription(options, chartWorkspace); + + // + description + processBuilder + .directory(chartWorkspace) + .command( + "convert", + "+append", + "-gravity", "south", + "final_grid.png", + "description.png", + "final.png" + ) + .start() + .waitFor(); + + + FileInputStream file = new FileInputStream(new File(chartWorkspace, "final.png")); + byte[] chart = file.readAllBytes(); + file.close(); + + cleanup(chartWorkspace); + + return chart; + } + + private void cleanup(File chartWorkspace) { + String[] entries = chartWorkspace.list(); + if (entries != null) { + for (String s : entries) { + File currentFile = new File(chartWorkspace, s); + currentFile.delete(); + } + } + chartWorkspace.delete(); + } + + private File makeChartWorkspace() throws IOException { + String chartId = UUID.randomUUID().toString(); + + File chartWorkspace = new File(workspacePath, chartId); + if (!chartWorkspace.mkdir()) { + throw new IOException(String.format("Could not create chart workspace: %s", chartWorkspace.getAbsolutePath())); + } + return chartWorkspace; + } + + private void saveCoversToDisk(ChartOptions options, File chartWorkspace) throws IOException { + for (ReleaseDetails releaseDetails : options.details) { + if (options.covers.containsKey(releaseDetails.mbid)) { + FileOutputStream file = new FileOutputStream(new File(chartWorkspace, releaseDetails.mbid)); + file.write(options.covers.get(releaseDetails.mbid)); + file.close(); + } + } + } + + private void makeDescription(ChartOptions options, File chartWorkspace) + throws InterruptedException, IOException { + + ProcessBuilder processBuilder = new ProcessBuilder(); + + for (int i = 0; i < options.rows; i++) { + + String desc = formatDescription(options, i); + + processBuilder + .directory(chartWorkspace) + .command( + "convert", + "-size", "620x316", + "xc:" + options.backgroundColor, + "-fill", options.textColor, + "-pointsize", "20", + "-font", options.font, + "-annotate", "+0+40", desc, + String.format("d_%d.png", i) + ) + .start() + .waitFor(); + processBuilder + .directory(chartWorkspace) + .command( + "convert", + "-append", + "d_*.png", + "description.png" + ) + .start() + .waitFor(); + } + } + + private static String formatDescription(ChartOptions options, int row) { + + StringBuilder sb = new StringBuilder(); + + for (int col = 0; col < options.cols; col++) { + + int releaseIndex = row * options.cols + col; + if (releaseIndex >= options.details.size()) { + break; + } + + ReleaseDetails release = options.details.get(releaseIndex); + + String year = release.year != 0 ? String.format("(%s)", release.year) : ""; + + if (release.name.length() + release.artist.length() < 40) { + sb.append(String.format("%s%d %s - %s %s\n", + letters[row], col+1, + release.name, release.artist, year)); + } else { + sb.append(String.format("%s%d %s -\n\t\t\t %s %s\n", + letters[row], col+1, + release.name, release.artist, year)); + } + } + return sb.toString(); + } + + private void makeGrid(ChartOptions options, File chartWorkspace, List releases) + throws InterruptedException, IOException { + + ProcessBuilder processBuilder = new ProcessBuilder(); + + List cmd = new ArrayList<>(); + cmd.add("montage"); + cmd.addAll(releases); + cmd.add("-tile"); + cmd.add(String.format("%dx%d", options.cols, options.rows)); + cmd.add("-background"); + cmd.add(options.backgroundColor); + cmd.add("-border"); + cmd.add("1"); + cmd.add("-bordercolor"); + cmd.add(options.borderColor); + cmd.add("-geometry"); + cmd.add("256x256+30+30"); + cmd.add("grid.png"); + + processBuilder + .directory(chartWorkspace) + .command(cmd) + .start() + .waitFor(); + } + + private File makeSide(ChartOptions options, File chartWorkSpace) + throws InterruptedException, IOException { + + File sideFile = new File(chartWorkSpace, "side.png"); + + ProcessBuilder processBuilder = new ProcessBuilder(); + + for (int i = 0; i < options.rows; i++) { + processBuilder + .directory(chartWorkSpace) + .command( + "convert", + "-background", options.backgroundColor, + "-gravity", "east", + "-size", "60x256", + "-fill", options.textColor, + "-pointsize", "64", + "-font", options.font, + "-strip", + String.format("label:%s", letters[i]), + String.format("s_%d.png", i + 1) + ) + .start() + .waitFor(); + } + + processBuilder + .directory(chartWorkSpace) + .command( + "montage", + "-tile", "1x", + "-background", options.backgroundColor, + "-geometry", "+0+31", + "s_*.png", + sideFile.toString() + ) + .start() + .waitFor(); + + return sideFile; + } + + private File makeHeader(ChartOptions options, File chartWorkspace) + throws InterruptedException, IOException { + + File headerFile = new File(chartWorkspace, "header.png"); + ProcessBuilder processBuilder = new ProcessBuilder(); + + // Header + for (int i = 1; i <= options.cols; i++) { + processBuilder + .directory(chartWorkspace) + .command( + "convert", + "-background", options.backgroundColor, + "-gravity", "south", + "-size", "256x80", + "-fill", options.textColor, + "-chop", "0x12", + "-pointsize", "64", + "-font", options.font, + "-strip", + String.format("label:%d", i), + String.format("h_%d.png", i) + ) + .start() + .waitFor(); + } + + processBuilder + .directory(chartWorkspace) + .command( + "montage", + "-tile", "x1", + "h_*.png", + "-background", options.backgroundColor, + "-geometry", "+31+0", + headerFile.toString() + ) + .start() + .waitFor(); + + return headerFile; + } +} diff --git a/src/main/java/net/simon987/musicgraph/webapi/MyExceptionMapper.java b/src/main/java/net/simon987/musicgraph/webapi/MyExceptionMapper.java new file mode 100644 index 0000000..f24da64 --- /dev/null +++ b/src/main/java/net/simon987/musicgraph/webapi/MyExceptionMapper.java @@ -0,0 +1,22 @@ +package net.simon987.musicgraph.webapi; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.glassfish.grizzly.utils.Exceptions; + +@Provider +public class MyExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(Throwable ex) { + + //TODO: logger + + return Response.status(500) + .entity(Exceptions.getStackTraceAsString(ex)) + .type("text/plain") + .build(); + } +} +