diff --git a/GeniusAPI/build.gradle b/GeniusAPI/build.gradle new file mode 100644 index 0000000..3250df5 --- /dev/null +++ b/GeniusAPI/build.gradle @@ -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() +} \ No newline at end of file diff --git a/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusEndpoint.java b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusEndpoint.java new file mode 100644 index 0000000..9f935b9 --- /dev/null +++ b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusEndpoint.java @@ -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 { +} diff --git a/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusEndpointTool.java b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusEndpointTool.java new file mode 100644 index 0000000..8f2d09e --- /dev/null +++ b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusEndpointTool.java @@ -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; + } +} diff --git a/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusTools.java b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusTools.java new file mode 100644 index 0000000..5847708 --- /dev/null +++ b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/GeniusTools.java @@ -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 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); + } +} diff --git a/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/ParameterStringBuilder.java b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/ParameterStringBuilder.java new file mode 100644 index 0000000..3088a0a --- /dev/null +++ b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/ParameterStringBuilder.java @@ -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 params) + throws UnsupportedEncodingException { + if(params == null) + return ""; + StringBuilder result = new StringBuilder(); + + for (Map.Entry 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; + } +} \ No newline at end of file diff --git a/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/endpoints/FindSong.java b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/endpoints/FindSong.java new file mode 100644 index 0000000..443ca20 --- /dev/null +++ b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/endpoints/FindSong.java @@ -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()); + } +} diff --git a/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/endpoints/GetLyrics.java b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/endpoints/GetLyrics.java new file mode 100644 index 0000000..0de4647 --- /dev/null +++ b/GeniusAPI/src/main/java/me/zacharias/neuro/dock/genius/endpoints/GetLyrics.java @@ -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."); + } + } +}