Added Genius API wrapper

for fetching song lyrics from Genius Database

# Disclaimer. This commit contains parts that are for educational purposes only!

FindSong:
- Uses Genius API to find the song queried

GetLyrics:
# Educational purposes only
- Scrapes Genius lyrics page for the Lyrics
This commit is contained in:
2025-06-08 21:54:56 +02:00
parent 796271443c
commit 840ad468e3
7 changed files with 374 additions and 0 deletions

24
GeniusAPI/build.gradle Normal file
View File

@@ -0,0 +1,24 @@
plugins {
id 'java'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
implementation "org.jsoup:jsoup:1.20.1"
implementation 'io.github.classgraph:classgraph:4.8.158'
implementation project(":Core")
}
test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,11 @@
package me.zacharias.neuro.dock.genius;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface GeniusEndpoint {
}

View File

@@ -0,0 +1,12 @@
package me.zacharias.neuro.dock.genius;
import me.zacharias.chat.ollama.OllamaFunctionTool;
public abstract class GeniusEndpointTool extends OllamaFunctionTool {
protected GeniusTools geniusToolsInstance;
public GeniusEndpointTool(GeniusTools geniusTools) {
super();
this.geniusToolsInstance = geniusTools;
}
}

View File

@@ -0,0 +1,127 @@
package me.zacharias.neuro.dock.genius;
import com.google.common.reflect.ClassPath;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaFunctionTools;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
public class GeniusTools {
public final String Client_ID;
public final String Client_Secret;
public final String Access_Token;
public final String BaseURL = "https://api.genius.com";
public final OllamaFunctionTools GeniusTools;
public final File CacheFile = new File(Core.DATA_DIR + "/genius_cache.json");
public JSONObject CacheData;
public GeniusTools() {
super();
try {
JSONObject obj = new JSONObject(Files.readString(Path.of(Core.DATA_DIR + "/geniusapi.json")));
this.Client_ID = obj.getString("client_id");
this.Client_Secret = obj.getString("client_secret");
this.Access_Token = obj.getString("access_token");
} catch (IOException e) {
throw new RuntimeException(e);
}
if(CacheFile.exists()) {
try{
CacheData = new JSONObject(Files.readString(CacheFile.toPath()));
}catch (IOException ex)
{
ex.printStackTrace();
if(CacheData == null)
CacheData = new JSONObject();
CacheFile.delete();
}
}
else {
CacheData = new JSONObject();
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
Files.writeString(CacheFile.toPath(), CacheData.toString());
} catch (IOException e) {
e.printStackTrace();
}
}));
OllamaFunctionTools.OllamaFunctionToolsBuilder builder = new OllamaFunctionTools.OllamaFunctionToolsBuilder();
try(ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("me.zacharias.neuro.dock.genius.endpoints").scan()) {
ClassInfoList endpoints = scanResult.getClassesWithAnnotation(GeniusEndpoint.class.getName());
for (ClassInfo classInfo : endpoints) {
Class<?> clazz = classInfo.loadClass();
if (OllamaFunctionTool.class.isAssignableFrom(clazz)) {
//System.out.println("Found endpoint: " + clazz.getName() + " With constructor: " + clazz.getDeclaredConstructors().f.getName() + " and arguments: " + Arrays.toString(clazz.getDeclaredConstructor().getParameterTypes()));
GeniusEndpointTool tool = (GeniusEndpointTool) clazz.getDeclaredConstructor(GeniusTools.class).newInstance(this);
builder.addTool(tool, Core.Source.INTERNAL);
}
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
this.GeniusTools = builder.build();
}
public OllamaFunctionTools getGeniusTools() {
return this.GeniusTools;
}
public JSONObject getGeniusEndpoint(String endpoint, Map<String, String> params) {
try {
URL url = new URL(this.BaseURL + endpoint + "?" + ParameterStringBuilder.getParamsString(params));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "Bearer " + this.Access_Token);
conn.setRequestProperty("Accept", "application/json");
if (conn.getResponseCode() != 200) {
throw new IOException("Failed to connect to Genius API: " + conn.getResponseCode());
}
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return new JSONObject(response.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String hasCache(int song_id) {
if(CacheData.has(Integer.toString(song_id)))
return CacheData.getString(Integer.toString(song_id));
return null;
}
public void cacheLyrics(int song_id, String lyrics) {
CacheData.put(Integer.toString(song_id), lyrics);
}
}

View File

@@ -0,0 +1,27 @@
package me.zacharias.neuro.dock.genius;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class ParameterStringBuilder {
public static String getParamsString(Map<String, String> params)
throws UnsupportedEncodingException {
if(params == null)
return "";
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
result.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
result.append("&");
}
String resultString = result.toString();
return !resultString.isEmpty()
? resultString.substring(0, resultString.length() - 1)
: resultString;
}
}

View File

@@ -0,0 +1,68 @@
package me.zacharias.neuro.dock.genius.endpoints;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.zacharias.neuro.dock.genius.GeniusEndpoint;
import me.zacharias.neuro.dock.genius.GeniusEndpointTool;
import me.zacharias.neuro.dock.genius.GeniusTools;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.Map;
import static me.zacharias.chat.ollama.OllamaPerameter.OllamaPerameterBuilder.Type.STRING;
@GeniusEndpoint
public class FindSong extends GeniusEndpointTool {
public FindSong(GeniusTools geniusTools) {
super(geniusTools);
}
@Override
public String name() {
return "findsong";
}
@Override
public String description() {
return "Finds a song by title from the Genius API.";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("title", STRING, "The title, artitst, and song_id of the song to find.", true)
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
String title = (String) args[0].value();
if (title == null || title.isEmpty()) {
throw new OllamaToolErrorException(this.name(), "Title cannot be null or empty.");
}
JSONObject response = this.geniusToolsInstance.getGeniusEndpoint("/search", Map.of("q", title));
JSONArray responseData = new JSONArray();
for(Object obj : response.getJSONObject("response").getJSONArray("hits")) {
if(obj instanceof JSONObject songResult) {
JSONObject song = songResult.getJSONObject("result");
JSONObject songData = new JSONObject();
songData.put("title", song.getString("title"));
songData.put("artist", song.getString("artist_names"));
songData.put("id", song.getInt("id"));
responseData.put(songData);
}
}
if (responseData.isEmpty()) {
throw new OllamaToolErrorException(this.name(), "No songs found for the given title.");
}
return new OllamaToolRespnce(this.name(), responseData.toString());
}
}

View File

@@ -0,0 +1,105 @@
package me.zacharias.neuro.dock.genius.endpoints;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.zacharias.neuro.dock.genius.GeniusEndpoint;
import me.zacharias.neuro.dock.genius.GeniusEndpointTool;
import me.zacharias.neuro.dock.genius.GeniusTools;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import java.util.stream.Collectors;
import static me.zacharias.chat.core.Core.writeLog;
@GeniusEndpoint
public class GetLyrics extends GeniusEndpointTool {
public GetLyrics(GeniusTools geniusTools) {
super(geniusTools);
}
@Override
public String name() {
return "get_lyrics";
}
@Override
public String description() {
return "Gets the lyrics of a song by its ID from the Genius API.";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("song_id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The ID of the song to get lyrics for.", true)
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
String lyricsStr = geniusToolsInstance.hasCache((int) args[0].value());
if(lyricsStr != null)
{
// If we have a cached response, return it
}
JSONObject obj = geniusToolsInstance.getGeniusEndpoint("/songs/" + args[0].value(), null);
String lyrics_path = obj.getJSONObject("response").getJSONObject("song").getString("url");
try {
Document doc = Jsoup.connect(lyrics_path)
.userAgent("Mozilla/5.0")
.get();
writeLog("Fetching lyrics from: " + lyrics_path);
Elements containers = doc.select("div[data-lyrics-container=true]");
StringBuilder lyrics = new StringBuilder();
for (Element container : containers) {
for(Node n : container.childNodes())
{
if(n instanceof Element e) {
if (e.attribute("data-exclude-from-selection") != null && e.attr("data-exclude-from-selection").equals("true")) {
continue;
}
else if(e.tagName().equalsIgnoreCase("br"))
{
lyrics.append("\n");
}
else {
//System.out.println(container.tagName());
String s = e.text();
lyrics.append(s.trim());
}
}
else if(n instanceof TextNode tn)
{
String s = tn.text();
if (!s.isBlank()) {
lyrics.append(s.trim());
}
}
}
}
geniusToolsInstance.cacheLyrics((int) args[0].value(), lyrics.toString());
return new OllamaToolRespnce(this.name(), lyrics.toString().trim());
}catch (Exception ex)
{
ex.printStackTrace();
throw new OllamaToolErrorException(this.name(), "Failed to fetch lyrics.");
}
}
}