Docker support, switch to pgsql, refactor, update neo4j

This commit is contained in:
simon987 2020-08-06 20:15:35 -04:00
parent e6bbdb393c
commit ded142c201
22 changed files with 116 additions and 515 deletions

View File

@ -7,6 +7,9 @@ WORKDIR /app/target
FROM openjdk:11-jre-slim
ENV NEO4J_ADDR "localhost:7687"
ENV PG_URL "localhost:5432/musicbrainz_db"
ENV PG_USERNAME "musicbrainz"
ENV PG_PASSWORD "musicbrainz"
COPY --from=build /app/target/music-graph-0.1-jar-with-dependencies.jar /app/
WORKDIR /app/

View File

@ -1,11 +1,9 @@
# music-graph
[![CodeFactor](https://www.codefactor.io/repository/github/simon987/music-graph-api/badge)](https://www.codefactor.io/repository/github/simon987/music-graph-api)
[![Build Status](https://ci.simon987.net/buildStatus/icon?job=music_graph_api)](https://ci.simon987.net/job/music_graph_api/)
API for music-graph.
This is a read-only wrapper for the data in the Neo4j database, it also serves the cover art, which is stored in a SQLite database.
This project is WIP, as such, it does not implement any kind of rate-limitting or configuration for ports and is mostly undocumented.
This is a read-only wrapper for the data in the Neo4j database,
it also serves the album cover art, which is stored in a PostgreSQL database.

15
pom.xml
View File

@ -59,7 +59,7 @@
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>1.7.3</version>
<version>4.0.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.containers/jersey-container-grizzly2-http -->
@ -89,12 +89,14 @@
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.xml.bind/jaxb-osgi -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-osgi</artifactId>
<version>2.4.0-b180830.0438</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.sun.xml.bind/jaxb-core -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
@ -114,14 +116,15 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
<version>29.0-jre</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.27.2.1</version>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.14</version>
</dependency>
</dependencies>
</project>

View File

@ -23,9 +23,13 @@ public class Main {
ResourceConfig rc = new ResourceConfig();
String pgUrl = System.getenv("PG_URL");
String pgUsername = System.getenv("PG_USERNAME");
String pgPassword = System.getenv("PG_PASSWORD");
rc.registerInstances(new MusicDatabase());
rc.registerInstances(new SQLiteCoverArtDatabase("covers.db"));
rc.registerInstances(new MagickChartBuilder("/dev/shm/im_chart/"));
rc.registerInstances(new PostgreSQLCoverArtDatabase(pgUrl, pgUsername, pgPassword));
// rc.registerInstances(new MagickChartBuilder("/dev/shm/im_chart/"));
rc.registerClasses(Index.class);
rc.registerClasses(ArtistController.class);
@ -33,7 +37,6 @@ public class Main {
rc.registerClasses(CoverController.class);
rc.registerClasses(TagController.class);
rc.registerClasses(ReleaseController.class);
rc.registerClasses(ChartController.class);
rc.registerClasses(JacksonFeature.class);
rc.registerClasses(MyExceptionMapper.class);

View File

@ -1,4 +1,4 @@
package net.simon987.musicgraph.webapi;
package net.simon987.musicgraph.entities;
public class AutocompleteLine {
public String name;

View File

@ -2,7 +2,7 @@ package net.simon987.musicgraph.entities;
public class Relation {
public float weight;
public double weight;
public long source;
public long target;
}

View File

@ -11,6 +11,4 @@ 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

@ -5,11 +5,11 @@ import com.google.common.collect.ImmutableList;
import net.simon987.musicgraph.entities.*;
import net.simon987.musicgraph.logging.LogManager;
import net.simon987.musicgraph.webapi.AutoCompleteData;
import net.simon987.musicgraph.webapi.AutocompleteLine;
import net.simon987.musicgraph.entities.AutocompleteLine;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.neo4j.driver.v1.*;
import org.neo4j.driver.v1.types.Node;
import org.neo4j.driver.v1.types.Relationship;
import org.neo4j.driver.*;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Relationship;
import javax.inject.Singleton;
import javax.validation.constraints.Max;
@ -37,10 +37,10 @@ public class MusicDatabase extends AbstractBinder {
AuthTokens.basic("neo4j", "neo4j"));
}
private StatementResult query(Session session, String query, Map<String, Object> args) {
private Result query(Session session, String query, Map<String, Object> args) {
long start = System.nanoTime();
StatementResult result = session.run(query, args);
Result result = session.run(query, args);
long end = System.nanoTime();
long took = (end - start) / 1000000;
@ -57,7 +57,7 @@ public class MusicDatabase extends AbstractBinder {
Map<String, Object> params = new HashMap<>();
params.put("mbid", mbid);
StatementResult result = query(session,
Result result = query(session,
"MATCH (a:Artist)-[r:IS_MEMBER_OF]-(b:Artist) " +
"WHERE a.id = $mbid " +
"RETURN a as artist, a {rels: collect(DISTINCT r), nodes: collect(DISTINCT b)} as rank1 " +
@ -82,7 +82,7 @@ public class MusicDatabase extends AbstractBinder {
try (Session session = driver.session()) {
StatementResult result = query(session,
Result result = query(session,
"MATCH (a:Artist {id: $mbid})" +
"WITH a OPTIONAL MATCH (a)-[:CREDITED_FOR]->(r:Release) " +
"WITH collect({id: ID(r), mbid:r.id, name:r.name, year:r.year, labels:labels(r)}) as releases, a " +
@ -163,7 +163,7 @@ public class MusicDatabase extends AbstractBinder {
Map<String, Object> params = new HashMap<>();
params.put("mbid", mbid);
StatementResult result = query(session,
Result result = query(session,
"MATCH (a:Artist) " +
"WHERE a.id = $mbid " +
"WITH a OPTIONAL MATCH (a)-[r:IS_RELATED_TO]-(b) " +
@ -191,7 +191,7 @@ public class MusicDatabase extends AbstractBinder {
params.put("idFrom", idFrom);
params.put("idTo", idTo);
StatementResult result = query(session,
Result result = query(session,
"MATCH p = allShortestPaths(" +
"(:Artist {id: $idFrom})-[r:IS_RELATED_TO*..10]-(:Artist {id:$idTo})) " +
"WHERE ALL (rel in r WHERE rel.weight > 0.10) " +
@ -220,7 +220,7 @@ public class MusicDatabase extends AbstractBinder {
Map<String, Object> params = new HashMap<>();
params.put("tag_id", id);
StatementResult result = query(session,
Result result = query(session,
"MATCH (t:Tag)-[r:IS_TAGGED]-(a:Artist) " +
"WHERE ID(t) = $tag_id " +
// Is rels really necessary?
@ -250,7 +250,7 @@ public class MusicDatabase extends AbstractBinder {
Map<String, Object> params = new HashMap<>();
params.put("label_id", id);
StatementResult result = query(session,
Result result = query(session,
"MATCH (l:Label)-[]-(:Release)-[:CREDITED_FOR]-(a:Artist) " +
"WHERE ID(l) = $label_id " +
"RETURN l, {nodes: collect(DISTINCT a)} as rank1 " +
@ -279,7 +279,7 @@ public class MusicDatabase extends AbstractBinder {
Map<String, Object> params = new HashMap<>();
params.put("label_id", id);
StatementResult result = query(session,
Result result = query(session,
"MATCH (l:Label)-[r:IS_RELATED_TO]-(l2:Label) " +
"WHERE ID(t) = $tag_id " +
"RETURN {rels: collect(DISTINCT r), nodes: collect(DISTINCT t2)} as rank1 " +
@ -307,7 +307,7 @@ public class MusicDatabase extends AbstractBinder {
Map<String, Object> params = new HashMap<>();
params.put("tag_id", id);
StatementResult result = query(session,
Result result = query(session,
"MATCH (t:Tag)-[r:IS_RELATED_TO]-(t2:Tag) " +
"WHERE ID(t) = $tag_id " +
"RETURN {rels: collect(DISTINCT r), nodes: collect(DISTINCT t2)} as rank1 " +
@ -329,7 +329,7 @@ public class MusicDatabase extends AbstractBinder {
}
private void parseRelatedResult(StatementResult result, SearchResult out) {
private void parseRelatedResult(Result result, SearchResult out) {
long start = System.nanoTime();
if (result.hasNext()) {
Record row = result.next();
@ -416,7 +416,7 @@ public class MusicDatabase extends AbstractBinder {
relation.source = rel.startNodeId();
relation.target = rel.endNodeId();
if (rel.containsKey("weight")) {
relation.weight = rel.get("weight").asFloat();
relation.weight = rel.get("weight").asDouble();
}
return relation;
@ -429,7 +429,7 @@ public class MusicDatabase extends AbstractBinder {
try (Session session = driver.session()) {
StatementResult result = query(session,
Result result = query(session,
"MATCH (release:Release {id: $mbid})-[:CREDITED_FOR]-(a:Artist) " +
"OPTIONAL MATCH (release)-[r:IS_TAGGED]->(t:Tag) " +
"RETURN release {name:release.name, year:release.year," +
@ -478,7 +478,7 @@ public class MusicDatabase extends AbstractBinder {
AutoCompleteData data = new AutoCompleteData();
StatementResult result = query(session,
Result result = query(session,
"MATCH (a:Artist) " +
"WHERE a.sortname STARTS WITH $prefix " +
"RETURN a ORDER BY a.listeners DESC " +
@ -499,7 +499,7 @@ public class MusicDatabase extends AbstractBinder {
}
params.put("prefix", prefix.toLowerCase());
StatementResult tagResult = query(session,
Result tagResult = query(session,
"MATCH (t:Tag)-[:IS_TAGGED]-(:Artist) " +
"WHERE t.name STARTS WITH $prefix " +
"RETURN DISTINCT t ORDER BY t.occurrences DESC " +

View File

@ -0,0 +1,66 @@
package net.simon987.musicgraph.io;
import net.simon987.musicgraph.logging.LogManager;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import java.sql.*;
import java.util.logging.Logger;
public class PostgreSQLCoverArtDatabase extends AbstractBinder implements ICoverArtDatabase {
private String url;
private String username;
private String password;
private Connection connection;
private static final Logger logger = LogManager.getLogger();
public PostgreSQLCoverArtDatabase(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
@Override
protected void configure() {
bind(this).to(ICoverArtDatabase.class);
}
public byte[] getCover(String mbid) throws SQLException {
try {
setupConn();
PreparedStatement stmt = connection.prepareStatement("SELECT tn FROM mg.covers WHERE mbid::text=?");
stmt.setString(1, mbid);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
byte[] bytes = rs.getBytes(1);
rs.close();
return bytes;
} else {
rs.close();
return null;
}
} catch (SQLException e) {
logger.severe(String.format("Exception during cover art query mbid=%s ex=%s",
mbid, e.getMessage()));
throw e;
}
}
private void setupConn() throws SQLException {
try {
if (connection == null || connection.isClosed()) {
logger.fine("Connecting to cover art DB");
connection = DriverManager.getConnection("jdbc:postgresql://" + url, username, password);
}
} catch (SQLException e) {
logger.fine("Connecting to cover art DB");
connection = DriverManager.getConnection("jdbc:postgresql://" + url, username, password);
}
}
}

View File

@ -1,94 +0,0 @@
package net.simon987.musicgraph.io;
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.Properties;
import java.util.logging.Logger;
public class SQLiteCoverArtDatabase extends AbstractBinder implements ICoverArtDatabase {
private String dbFile;
private Connection connection;
private static Logger logger = LogManager.getLogger();
public SQLiteCoverArtDatabase(String dbFile) {
this.dbFile = dbFile;
}
@Override
protected void configure() {
bind(this).to(ICoverArtDatabase.class);
}
public byte[] getCover(String mbid) throws SQLException {
try {
setupConn();
PreparedStatement stmt = connection.prepareStatement("SELECT cover FROM covers WHERE id=?");
stmt.setString(1, mbid);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
byte[] bytes = rs.getBytes(1);
rs.close();
return bytes;
} else {
rs.close();
return null;
}
} catch (SQLException e) {
logger.severe(String.format("Exception during cover art query mbid=%s ex=%s",
mbid, e.getMessage()));
throw e;
}
}
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 {
Properties config = new Properties();
config.setProperty("open_mode", "1");
try {
if (connection == null || connection.isClosed()) {
logger.fine("Connecting to SQLite cover art DB");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile, config);
}
} catch (SQLException e) {
logger.fine("Connecting to SQLite cover art DB");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile, config);
}
}
}

View File

@ -6,7 +6,7 @@ import java.util.logging.Logger;
public class LogManager {
private static Logger logger;
private static final Logger logger;
static {
logger = Logger.getLogger("music-graph");

View File

@ -14,7 +14,7 @@ import java.util.logging.Logger;
@Path("/artist")
public class ArtistController {
private static Logger logger = LogManager.getLogger();
private static final Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;

View File

@ -1,5 +1,7 @@
package net.simon987.musicgraph.webapi;
import net.simon987.musicgraph.entities.AutocompleteLine;
import java.util.ArrayList;
import java.util.List;

View File

@ -14,7 +14,7 @@ import java.util.logging.Logger;
@Path("/")
public class AutocompleteController {
private static Logger logger = LogManager.getLogger();
private static final Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;

View File

@ -1,48 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -10,7 +10,7 @@ import java.util.logging.Logger;
@Path("/cover/{mbid}")
public class CoverController {
private static Logger logger = LogManager.getLogger();
private static final Logger logger = LogManager.getLogger();
@Inject
private ICoverArtDatabase db;

View File

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

View File

@ -12,7 +12,7 @@ public class Index {
private static final ApiInfo INFO = new ApiInfo(
"music-graph-api",
"1.1",
"2.0",
"/application.wadl",
"https://github.com/simon987/music-graph-api"
);

View File

@ -16,7 +16,7 @@ import java.util.logging.Logger;
@Path("/label")
public class LabelController {
private static Logger logger = LogManager.getLogger();
private static final Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;

View File

@ -1,306 +0,0 @@
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

@ -16,7 +16,7 @@ import java.util.logging.Logger;
@Path("/tag")
public class TagController {
private static Logger logger = LogManager.getLogger();
private static final Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;