mirror of
https://github.com/simon987/music-graph-api.git
synced 2025-04-19 09: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>
|
<scope>test</scope>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
</project>
|
</project>
|
@ -1,6 +1,9 @@
|
|||||||
package net.simon987.musicgraph;
|
package net.simon987.musicgraph;
|
||||||
|
|
||||||
|
import net.simon987.musicgraph.io.*;
|
||||||
import net.simon987.musicgraph.logging.LogManager;
|
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.Index;
|
||||||
import org.glassfish.grizzly.http.server.HttpServer;
|
import org.glassfish.grizzly.http.server.HttpServer;
|
||||||
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
|
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
|
||||||
@ -12,12 +15,10 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
|
|
||||||
private final static Logger LOGGER = LogManager.getLogger();
|
private final static Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
LOGGER.info("test");
|
|
||||||
|
|
||||||
startHttpServer();
|
startHttpServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,8 +26,12 @@ public class Main {
|
|||||||
|
|
||||||
ResourceConfig rc = new ResourceConfig();
|
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);
|
rc.registerClasses(JacksonFeature.class);
|
||||||
|
|
||||||
try {
|
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;
|
package net.simon987.musicgraph.logging;
|
||||||
|
|
||||||
import java.util.logging.Handler;
|
import java.util.logging.Handler;
|
||||||
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class LogManager {
|
public class LogManager {
|
||||||
@ -14,6 +15,7 @@ public class LogManager {
|
|||||||
|
|
||||||
logger.setUseParentHandlers(false);
|
logger.setUseParentHandlers(false);
|
||||||
logger.addHandler(handler);
|
logger.addHandler(handler);
|
||||||
|
logger.setLevel(Level.ALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Logger getLogger() {
|
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