mirror of
https://github.com/simon987/music-graph-api.git
synced 2025-04-10 13:56:43 +00:00
Cover & artist related/info endpoints
This commit is contained in:
parent
9bc45958e2
commit
606a95279e
19
pom.xml
19
pom.xml
@ -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>
|
@ -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 {
|
||||
|
29
src/main/java/net/simon987/musicgraph/entities/Artist.java
Normal file
29
src/main/java/net/simon987/musicgraph/entities/Artist.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package net.simon987.musicgraph.entities;
|
||||
|
||||
|
||||
public class Relation {
|
||||
public float weight;
|
||||
public long source;
|
||||
public long target;
|
||||
}
|
@ -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<>();
|
||||
}
|
||||
}
|
12
src/main/java/net/simon987/musicgraph/entities/Tag.java
Normal file
12
src/main/java/net/simon987/musicgraph/entities/Tag.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
158
src/main/java/net/simon987/musicgraph/io/MusicDatabase.java
Normal file
158
src/main/java/net/simon987/musicgraph/io/MusicDatabase.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user