Chart builder setup, release details endpoint

This commit is contained in:
simon987 2019-05-08 20:35:30 -04:00
parent 606a95279e
commit 98aae25a72
12 changed files with 502 additions and 11 deletions

View File

@ -122,11 +122,5 @@
<artifactId>sqlite-jdbc</artifactId>
<version>3.27.2.1</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>17.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -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/"),

View File

@ -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<Tag> tags;
public ReleaseDetails() {
this.tags = new ArrayList<>();
}
}

View File

@ -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<String, byte[]> getCovers(List<String> mbids) throws Exception;
}

View File

@ -155,4 +155,49 @@ public class MusicDatabase extends AbstractBinder {
return relation;
}
public ReleaseDetails getReleaseDetails(String mbid) {
Map<String, Object> 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<String, Object> 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>) 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;
}
}
}

View File

@ -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<String, byte[]> getCovers(List<String> mbids) throws SQLException {
HashMap<String, byte[]> 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()) {

View File

@ -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) {

View File

@ -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<String> 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);
}
}

View File

@ -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<String, byte[]> covers;
public List<ReleaseDetails> details;
public int rows;
public int cols;
public String backgroundColor;
public String textColor = "white";
public String font = "Hack-Regular";
public String borderColor = "white";
}

View File

@ -0,0 +1,6 @@
package net.simon987.musicgraph.webapi;
public interface IChartBuilder {
byte[] makeChart(ChartOptions options) throws Exception;
}

View File

@ -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<String> 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<String> releases)
throws InterruptedException, IOException {
ProcessBuilder processBuilder = new ProcessBuilder();
List<String> 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;
}
}

View File

@ -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<Throwable> {
@Override
public Response toResponse(Throwable ex) {
//TODO: logger
return Response.status(500)
.entity(Exceptions.getStackTraceAsString(ex))
.type("text/plain")
.build();
}
}