diff --git a/Build.bat b/Build.bat new file mode 100644 index 0000000..cf8a0fa --- /dev/null +++ b/Build.bat @@ -0,0 +1,24 @@ +@echo off +echo === Building RadioDJ-TunaBridge === + +REM Clean previous build +mvn clean + +REM Compile and package the JAR +mvn package + +IF %ERRORLEVEL% NEQ 0 ( + echo ❌ Build failed. + pause + exit /b %ERRORLEVEL% +) + +REM Copy final JAR to top-level folder (optional) +IF EXIST target\radiodj-tuna-bridge-1.0-SNAPSHOT.jar ( + copy target\radiodj-tuna-bridge-1.0-SNAPSHOT.jar radiodj-tuna-bridge.jar >nul + echo ✅ Build complete. Output: radiodj-tuna-bridge.jar +) ELSE ( + echo ❌ JAR not found! +) + +pause \ No newline at end of file diff --git a/README.md b/README.md index a1ce872..132adc0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# RadioDJ-Tuna +# 🎧 RadioDJ Tuna Bridge +A lightweight Java utility that pulls "Now Playing" info from RadioDJ's REST XML API, and outputs it as `.txt` files and album art for tools like Tuna (OBS plugin). + +--- + +## 🔧 Features + +- ✅ Pulls title / artist / album / album art via REST +- 🛑 Only updates files if the song actually changed +- đŸ–ŧī¸ Copies matching album art, or uses fallback image +- 💾 Creates & reads from `config.yml` and `last-played.xml` +- đŸĒ„ Fully customizable update interval + +--- + +## 🚀 Usage + +1. **First Run:** + - Launch the app with `java -jar radiodj-tuna-bridge.jar` + - You'll be prompted to enter your API URL, art folder, fallback filename, and polling interval + +2. **Outputs:** + - Stored in `tuna-output/` folder: + - `track-title.txt` + - `track-artist.txt` + - `track-album.txt` + - `album-art.jpg` (copied from your RadioDJ art folder) + +3. **Reconfigure:** + - Edit the `config.yml` file to change settings at any time + +--- + +## đŸ› ī¸ Build Instructions + +```bash +git clone https://github.com/you/radiodj-tuna-bridge.git +cd radiodj-tuna-bridge +build.bat \ No newline at end of file diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..3c6e40d --- /dev/null +++ b/run.bat @@ -0,0 +1,7 @@ +@echo off +echo === Running RadioDJ Tuna Bridge === + +REM Run the JAR (assumes you built it already) +java -jar radiodj-tuna-bridge.jar + +pause \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/Config.java b/src/main/java/com/minster586/radiodjtuna/Config.java new file mode 100644 index 0000000..aa27636 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/Config.java @@ -0,0 +1,40 @@ +package com.minster586.radiodjtuna; + +public class Config { + private String radioDjApiUrl; + private String albumArtDirectory; + private int updateIntervalSeconds; + private String fallbackArtFilename; + + public String getRadioDjApiUrl() { + return radioDjApiUrl; + } + + public void setRadioDjApiUrl(String radioDjApiUrl) { + this.radioDjApiUrl = radioDjApiUrl; + } + + public String getAlbumArtDirectory() { + return albumArtDirectory; + } + + public void setAlbumArtDirectory(String albumArtDirectory) { + this.albumArtDirectory = albumArtDirectory; + } + + public int getUpdateIntervalSeconds() { + return updateIntervalSeconds; + } + + public void setUpdateIntervalSeconds(int updateIntervalSeconds) { + this.updateIntervalSeconds = updateIntervalSeconds; + } + + public String getFallbackArtFilename() { + return fallbackArtFilename; + } + + public void setFallbackArtFilename(String fallbackArtFilename) { + this.fallbackArtFilename = fallbackArtFilename; + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/ConfigManager.java b/src/main/java/com/minster586/radiodjtuna/ConfigManager.java new file mode 100644 index 0000000..de21ef6 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/ConfigManager.java @@ -0,0 +1,74 @@ +package com.minster586.radiodjtuna; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileWriter; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Scanner; + +public class ConfigManager { + + private final Path configPath; + + public ConfigManager(Path configPath) { + this.configPath = configPath; + } + + public Config loadOrCreateConfig() { + File configFile = configPath.toFile(); + if (!configFile.exists()) { + System.out.println("âš™ī¸ Creating new config.yml..."); + Config newConfig = promptForConfig(); + saveConfig(newConfig); + return newConfig; + } + + try (InputStream input = java.nio.file.Files.newInputStream(configPath)) { + Yaml yaml = new Yaml(new Constructor(Config.class)); + Config loaded = yaml.load(input); + System.out.println("✅ Config loaded successfully."); + return loaded; + } catch (Exception e) { + System.err.println("❌ Failed to load config: " + e.getMessage()); + System.exit(1); + return null; // unreachable, but satisfies compiler + } + } + + private Config promptForConfig() { + Scanner scanner = new Scanner(System.in); + Config config = new Config(); + + System.out.print("🔗 Enter RadioDJ API URL (e.g. http://localhost:1234/playing): "); + config.setRadioDjApiUrl(scanner.nextLine().trim()); + + System.out.print("📁 Enter full path to RadioDJ album art folder: "); + config.setAlbumArtDirectory(scanner.nextLine().trim()); + + System.out.print("âąī¸ Enter update interval (seconds): "); + config.setUpdateIntervalSeconds(Integer.parseInt(scanner.nextLine().trim())); + + System.out.print("đŸ–ŧī¸ Enter fallback album art filename (e.g. fallback-art.jpg): "); + config.setFallbackArtFilename(scanner.nextLine().trim()); + + return config; + } + + private void saveConfig(Config config) { + try (FileWriter writer = new FileWriter(configPath.toFile())) { + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setPrettyFlow(true); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + Yaml yaml = new Yaml(options); + yaml.dump(config, writer); + System.out.println("💾 Config saved to " + configPath); + } catch (Exception e) { + System.err.println("❌ Failed to save config: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/MainApp.java b/src/main/java/com/minster586/radiodjtuna/MainApp.java index e69de29..da53824 100644 --- a/src/main/java/com/minster586/radiodjtuna/MainApp.java +++ b/src/main/java/com/minster586/radiodjtuna/MainApp.java @@ -0,0 +1,68 @@ +package com.minster586.radiodjtuna; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class MainApp { + + // Base paths determined at runtime + private static Path jarDir; + private static Path outputDir; + private static Path configFile; + private static Path cacheFile; + + // Core components + private static Config config; + private static MetadataCacheManager cacheManager; + private static TrackInfo lastPlayed; + + public static void main(String[] args) { + System.out.println("🔊 RadioDJ Tuna Bridge starting up..."); + + initializePaths(); + ensureOutputFolder(); + + // Load or create config + ConfigManager configManager = new ConfigManager(configFile); + config = configManager.loadOrCreateConfig(); + + // Set up metadata cache manager + cacheManager = new MetadataCacheManager(cacheFile); + lastPlayed = cacheManager.loadCache(); + + if (lastPlayed != null) { + System.out.println("đŸ—ƒī¸ Loaded cached track: " + lastPlayed.getTitle()); + } else { + System.out.println("📁 No metadata cache found yet."); + } + + // TODO: Poll RadioDJ API using config.getRadioDjApiUrl() + // TODO: Compare result to lastPlayed using lastPlayed.equals() + // TODO: If different, write new metadata to output folder + // TODO: Save new metadata to last-played.xml + // TODO: Handle album art copy or fallback + // TODO: Show toast if API is unreachable + + System.out.println("✅ System initialized. Ready for polling phase."); + } + + private static void initializePaths() { + jarDir = Paths.get("").toAbsolutePath(); + outputDir = jarDir.resolve("tuna-output"); + configFile = jarDir.resolve("config.yml"); + cacheFile = jarDir.resolve("last-played.xml"); + } + + private static void ensureOutputFolder() { + File outFolder = outputDir.toFile(); + if (!outFolder.exists()) { + boolean created = outFolder.mkdirs(); + if (created) { + System.out.println("📁 Created output folder: " + outputDir); + } else { + System.err.println("❌ Failed to create output folder. Check permissions."); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java b/src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java new file mode 100644 index 0000000..3379003 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java @@ -0,0 +1,82 @@ +package com.minster586.radiodjtuna; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.*; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.*; + +import java.io.File; +import java.nio.file.Path; + +public class MetadataCacheManager { + private final Path cachePath; + + public MetadataCacheManager(Path cachePath) { + this.cachePath = cachePath; + } + + public TrackInfo loadCache() { + File file = cachePath.toFile(); + if (!file.exists()) { + return null; + } + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(file); + + Element root = doc.getDocumentElement(); + + String title = getText(root, "Title"); + String artist = getText(root, "Artist"); + String album = getText(root, "Album"); + String albumArt = getText(root, "AlbumArt"); + + return new TrackInfo(title, artist, album, albumArt); + + } catch (Exception e) { + System.err.println("âš ī¸ Failed to load last-played.xml: " + e.getMessage()); + return null; + } + } + + public void saveCache(TrackInfo track) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.newDocument(); + + Element root = doc.createElement("LastPlayed"); + doc.appendChild(root); + + addText(doc, root, "Title", track.getTitle()); + addText(doc, root, "Artist", track.getArtist()); + addText(doc, root, "Album", track.getAlbum()); + addText(doc, root, "AlbumArt", track.getAlbumArt()); + + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + Result output = new StreamResult(cachePath.toFile()); + Source input = new DOMSource(doc); + transformer.transform(input, output); + + } catch (Exception e) { + System.err.println("❌ Failed to save last-played.xml: " + e.getMessage()); + } + } + + private String getText(Element root, String tag) { + NodeList list = root.getElementsByTagName(tag); + return (list.getLength() > 0) ? list.item(0).getTextContent() : ""; + } + + private void addText(Document doc, Element parent, String tag, String value) { + Element node = doc.createElement(tag); + node.appendChild(doc.createTextNode(value != null ? value : "")); + parent.appendChild(node); + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/OutputManager.java b/src/main/java/com/minster586/radiodjtuna/OutputManager.java new file mode 100644 index 0000000..c8652d6 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/OutputManager.java @@ -0,0 +1,75 @@ +package com.minster586.radiodjtuna; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; + +public class OutputManager { + + private final Path outputDir; + private final Path albumArtSourceFolder; + private final String fallbackArtFilename; + + public OutputManager(Path outputDir, String albumArtSourceDir, String fallbackArtFilename) { + this.outputDir = outputDir; + this.albumArtSourceFolder = Paths.get(albumArtSourceDir); + this.fallbackArtFilename = fallbackArtFilename; + } + + public void writeOutputFiles(TrackInfo track) { + try { + writeTextFile("track-title.txt", track.getTitle()); + writeTextFile("track-artist.txt", track.getArtist()); + writeTextFile("track-album.txt", track.getAlbum()); + copyAlbumArt(track.getAlbumArt()); + } catch (IOException e) { + System.err.println("❌ Failed writing output files: " + e.getMessage()); + } + } + + private void writeTextFile(String fileName, String content) throws IOException { + Path file = outputDir.resolve(fileName); + Files.writeString(file, content != null ? content : "", StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + private void copyAlbumArt(String albumArtFileName) { + Path destination = outputDir.resolve("album-art.jpg"); + + if (albumArtFileName == null || albumArtFileName.isBlank()) { + System.out.println("â„šī¸ No album art in tag, using fallback."); + copyFallback(destination); + return; + } + + Path sourceFile = albumArtSourceFolder.resolve(albumArtFileName); + + if (!Files.exists(sourceFile)) { + System.out.println("âš ī¸ Album art not found: " + sourceFile.getFileName() + ", using fallback."); + copyFallback(destination); + return; + } + + try { + Files.copy(sourceFile, destination, StandardCopyOption.REPLACE_EXISTING); + System.out.println("🎨 Album art updated: " + albumArtFileName); + } catch (IOException e) { + System.err.println("❌ Failed copying album art: " + e.getMessage()); + copyFallback(destination); + } + } + + private void copyFallback(Path destination) { + Path fallbackSource = outputDir.resolve(fallbackArtFilename); + if (Files.exists(fallbackSource)) { + try { + Files.copy(fallbackSource, destination, StandardCopyOption.REPLACE_EXISTING); + System.out.println("đŸ–ŧī¸ Fallback album art used."); + } catch (IOException e) { + System.err.println("❌ Failed to copy fallback art: " + e.getMessage()); + } + } else { + System.err.println("❌ Fallback art file is missing: " + fallbackSource); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java b/src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java new file mode 100644 index 0000000..3ea88dd --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java @@ -0,0 +1,46 @@ +package com.minster586.radiodjtuna; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class RadioDjApiClient { + private final String apiUrl; + + public RadioDjApiClient(String apiUrl) { + this.apiUrl = apiUrl; + } + + public TrackInfo fetchNowPlaying() throws Exception { + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(3000); // 3s timeout + connection.setReadTimeout(3000); + + try (InputStream in = connection.getInputStream()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(in); + Element root = doc.getDocumentElement(); + + String title = getText(root, "Title"); + String artist = getText(root, "Artist"); + String album = getText(root, "Album"); + String albumArt = getText(root, "AlbumArt"); + + return new TrackInfo(title, artist, album, albumArt); + } + } + + private String getText(Element root, String tag) { + return root.getElementsByTagName(tag).getLength() > 0 + ? root.getElementsByTagName(tag).item(0).getTextContent() + : ""; + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/TrackInfo.java b/src/main/java/com/minster586/radiodjtuna/TrackInfo.java new file mode 100644 index 0000000..32b493e --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/TrackInfo.java @@ -0,0 +1,63 @@ +package com.minster586.radiodjtuna; + +public class TrackInfo { + private String title; + private String artist; + private String album; + private String albumArt; + + public TrackInfo() { + // Default constructor for XML deserialization + } + + public TrackInfo(String title, String artist, String album, String albumArt) { + this.title = title; + this.artist = artist; + this.album = album; + this.albumArt = albumArt; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getAlbum() { + return album; + } + + public void setAlbum(String album) { + this.album = album; + } + + public String getAlbumArt() { + return albumArt; + } + + public void setAlbumArt(String albumArt) { + this.albumArt = albumArt; + } + + public boolean equals(TrackInfo other) { + if (other == null) return false; + return safeEquals(this.title, other.title) + && safeEquals(this.artist, other.artist) + && safeEquals(this.album, other.album) + && safeEquals(this.albumArt, other.albumArt); + } + + private boolean safeEquals(String a, String b) { + return (a == null && b == null) || (a != null && a.equals(b)); + } +} \ No newline at end of file diff --git a/target/classes/com/minster586/radiodjtuna/Config.class b/target/classes/com/minster586/radiodjtuna/Config.class new file mode 100644 index 0000000..3d96209 Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/Config.class differ diff --git a/target/classes/com/minster586/radiodjtuna/ConfigManager.class b/target/classes/com/minster586/radiodjtuna/ConfigManager.class new file mode 100644 index 0000000..fae0cff Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/ConfigManager.class differ diff --git a/target/classes/com/minster586/radiodjtuna/MainApp.class b/target/classes/com/minster586/radiodjtuna/MainApp.class new file mode 100644 index 0000000..f4dcc1f Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/MainApp.class differ diff --git a/target/classes/com/minster586/radiodjtuna/MetadataCacheManager.class b/target/classes/com/minster586/radiodjtuna/MetadataCacheManager.class new file mode 100644 index 0000000..45bce32 Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/MetadataCacheManager.class differ diff --git a/target/classes/com/minster586/radiodjtuna/OutputManager.class b/target/classes/com/minster586/radiodjtuna/OutputManager.class new file mode 100644 index 0000000..17f7663 Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/OutputManager.class differ diff --git a/target/classes/com/minster586/radiodjtuna/RadioDjApiClient.class b/target/classes/com/minster586/radiodjtuna/RadioDjApiClient.class new file mode 100644 index 0000000..eff1f5a Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/RadioDjApiClient.class differ diff --git a/target/classes/com/minster586/radiodjtuna/TrackInfo.class b/target/classes/com/minster586/radiodjtuna/TrackInfo.class new file mode 100644 index 0000000..f538a5a Binary files /dev/null and b/target/classes/com/minster586/radiodjtuna/TrackInfo.class differ