Cover & artist related/info endpoints

This commit is contained in:
simon987 2019-04-22 17:38:55 -04:00
parent 9bc45958e2
commit 606a95279e
13 changed files with 423 additions and 4 deletions

19
pom.xml
View File

@ -109,5 +109,24 @@
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<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

@ -1,6 +1,9 @@
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 org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
@ -12,12 +15,10 @@ import java.util.logging.Logger;
public class Main {
private final static Logger LOGGER = LogManager.getLogger();
private final static Logger logger = LogManager.getLogger();
public static void main(String[] args) {
LOGGER.info("test");
startHttpServer();
}
@ -25,8 +26,12 @@ public class Main {
ResourceConfig rc = new ResourceConfig();
rc.registerClasses(Index.class);
rc.registerInstances(new MusicDatabase());
rc.registerInstances(new SQLiteCoverArtDatabase("covers.db"));
rc.registerClasses(Index.class);
rc.registerClasses(ArtistController.class);
rc.registerClasses(CoverController.class);
rc.registerClasses(JacksonFeature.class);
try {

View File

@ -0,0 +1,29 @@
package net.simon987.musicgraph.entities;
import java.util.List;
public class Artist {
public Long id;
public String mbid;
public String name;
public List<String> labels;
public int listeners;
public int playCount;
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Artist) {
return ((Artist) obj).id.equals(id);
}
return false;
}
}

View File

@ -0,0 +1,16 @@
package net.simon987.musicgraph.entities;
import java.util.ArrayList;
import java.util.List;
public class ArtistDetails {
public ArtistDetails() {
releases = new ArrayList<>();
tags = new ArrayList<>();
}
public String name;
public List<String> releases;
public List<Tag> tags;
}

View File

@ -0,0 +1,8 @@
package net.simon987.musicgraph.entities;
public class Relation {
public float weight;
public long source;
public long target;
}

View File

@ -0,0 +1,18 @@
package net.simon987.musicgraph.entities;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SearchResult {
public Set<Artist> artists;
public List<Relation> relations;
public SearchResult() {
artists = new HashSet<>();
relations = new ArrayList<>();
}
}

View File

@ -0,0 +1,12 @@
package net.simon987.musicgraph.entities;
public class Tag {
public String name;
public long weight;
public Tag(String name, long weight) {
this.name = name;
this.weight = weight;
}
}

View File

@ -0,0 +1,11 @@
package net.simon987.musicgraph.io;
public interface ICoverArtDatabase {
/**
* @param mbid MusicBrainz id
* @return null if not found
* @throws Exception if unexpected error
*/
byte[] getCover(String mbid) throws Exception;
}

View File

@ -0,0 +1,158 @@
package net.simon987.musicgraph.io;
import com.google.common.collect.ImmutableList;
import net.simon987.musicgraph.entities.*;
import net.simon987.musicgraph.logging.LogManager;
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 javax.inject.Singleton;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@Singleton
public class MusicDatabase extends AbstractBinder {
private Driver driver;
private static Logger logger = LogManager.getLogger();
@Override
protected void configure() {
bind(this).to(MusicDatabase.class);
}
public MusicDatabase() {
driver = GraphDatabase.driver("bolt://localhost:7687",
AuthTokens.basic("neo4j", "neo4j"));
}
private StatementResult query(Session session, String query, Map<String, Object> args) {
long start = System.nanoTime();
StatementResult result = session.run(query, args);
long end = System.nanoTime();
long took = (end - start) / 1000000;
logger.info(String.format("Query %s (Took %dms)", query, took));
return result;
}
public ArtistDetails getArtistDetails(String mbid) {
Map<String, Object> params = new HashMap<>();
params.put("mbid", mbid);
try (Session session = driver.session()) {
StatementResult result = query(session,
"MATCH (a:Artist {id: $mbid})-[:CREDITED_FOR]->(r:Release)\n" +
"WITH collect(r.id) as releases, a\n" +
"OPTIONAL MATCH (a)-[r:IS_TAGGED]->(t:Tag)\n" +
"RETURN a {name:a.name, releases:releases, tags:collect({weight: r.weight, name: t.name})}\n" +
"LIMIT 1",
params);
ArtistDetails details = new ArtistDetails();
try {
if (result.hasNext()) {
Map<String, Object> map = result.next().get("a").asMap();
details.name = (String) map.get("name");
details.releases.addAll((Collection<String>) map.get("releases"));
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;
}
}
public SearchResult getRelated(String mbid) {
try (Session session = driver.session()) {
Map<String, Object> params = new HashMap<>();
params.put("mbid", mbid);
StatementResult result = query(session,
"MATCH (a:Artist)-[r:IS_RELATED_TO]-(b)\n" +
"WHERE a.id = $mbid\n" +
// Only match artists with > 0 releases
"MATCH (b)-[:CREDITED_FOR]->(:Release)\n" +
"WHERE r.weight > 0.25\n" +
"RETURN a as artist, a {rels: collect(DISTINCT r), nodes: collect(DISTINCT b)} as rank1\n" +
"LIMIT 1",
params);
SearchResult out = new SearchResult();
long start = System.nanoTime();
while (result.hasNext()) {
Record row = result.next();
out.artists.add(makeArtist(row.get(0).asNode()));
var rank1 = row.get(1).asMap();
out.relations.addAll(((List<Relationship>) rank1.get("rels"))
.stream()
.map(MusicDatabase::makeRelation)
.collect(Collectors.toList()
));
out.artists.addAll(((List<Node>) rank1.get("nodes"))
.stream()
.map(MusicDatabase::makeArtist)
.collect(Collectors.toList()
));
}
long end = System.nanoTime();
long took = (end - start) / 1000000;
logger.info(String.format("Fetched search result (Took %dms)", took));
return out;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static Artist makeArtist(Node node) {
Artist artist = new Artist();
artist.id = node.id();
artist.mbid = node.get("id").asString();
artist.name = node.get("name").asString();
artist.labels = ImmutableList.copyOf(node.labels());
artist.listeners = node.get("listeners").asInt();
artist.playCount = node.get("playcount").asInt();
return artist;
}
private static Relation makeRelation(Relationship rel) {
Relation relation = new Relation();
relation.source = rel.startNodeId();
relation.target = rel.endNodeId();
relation.weight = rel.get("weight").asFloat();
return relation;
}
}

View File

@ -0,0 +1,61 @@
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 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;
}
}
private void setupConn() throws SQLException {
try {
if (connection == null || connection.isClosed()) {
logger.fine("Connecting to SQLite cover art DB");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile);
}
} catch (SQLException e) {
logger.fine("Connecting to SQLite cover art DB");
connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile);
}
}
}

View File

@ -1,6 +1,7 @@
package net.simon987.musicgraph.logging;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LogManager {
@ -14,6 +15,7 @@ public class LogManager {
logger.setUseParentHandlers(false);
logger.addHandler(handler);
logger.setLevel(Level.ALL);
}
public static Logger getLogger() {

View File

@ -0,0 +1,41 @@
package net.simon987.musicgraph.webapi;
import net.simon987.musicgraph.entities.ArtistDetails;
import net.simon987.musicgraph.io.MusicDatabase;
import net.simon987.musicgraph.entities.SearchResult;
import net.simon987.musicgraph.logging.LogManager;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.logging.Logger;
@Path("/artist")
public class ArtistController {
private static Logger logger = LogManager.getLogger();
@Inject
private MusicDatabase db;
public ArtistController() {
}
@GET
@Path("related/{mbid}")
@Produces(MediaType.APPLICATION_JSON)
public SearchResult getRelated(@PathParam("mbid") String mbid) {
logger.info(String.format("Searching for %s", mbid));
return db.getRelated(mbid);
}
@GET
@Path("details/{mbid}")
@Produces(MediaType.APPLICATION_JSON)
public ArtistDetails getDetails(@PathParam("mbid") String mbid) {
logger.info(String.format("Details for %s", mbid));
return db.getArtistDetails(mbid);
}
}

View File

@ -0,0 +1,39 @@
package net.simon987.musicgraph.webapi;
import net.simon987.musicgraph.io.ICoverArtDatabase;
import net.simon987.musicgraph.logging.LogManager;
import javax.inject.Inject;
import javax.ws.rs.*;
import java.util.logging.Logger;
@Path("/cover/{mbid}")
public class CoverController {
private static Logger logger = LogManager.getLogger();
@Inject
private ICoverArtDatabase db;
public CoverController() {
}
@GET
@Produces("image/jpg")
public byte[] getByReleaseId(@PathParam("mbid") String mbid) throws Exception {
if (mbid == null) {
throw new BadRequestException();
}
logger.info(String.format("Cover for %s", mbid));
byte[] cover = db.getCover(mbid);
if (cover == null) {
throw new NotFoundException();
}
return cover;
}
}