Compare commits

...

2 Commits

Author SHA1 Message Date
minster586
4c9c10b013 Done 2025-06-28 16:45:23 -04:00
minster586
91a53ec119 starting 2025-06-28 16:19:40 -04:00
18 changed files with 560 additions and 1 deletions

24
Build.bat Normal file
View File

@@ -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

View File

@@ -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

42
pom.xml Normal file
View File

@@ -0,0 +1,42 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>radiodj-tuna-bridge</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- Jackson for XML & JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.17.0</version>
</dependency>
<!-- SnakeYAML for config.yml parsing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
<!-- Dorkbox SystemTray for Windows notifications -->
<dependency>
<groupId>com.dorkbox</groupId>
<artifactId>SystemTray</artifactId>
<version>3.17</version>
</dependency>
</dependencies>
</project>

7
run.bat Normal file
View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()
: "";
}
}

View File

@@ -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));
}
}

Binary file not shown.