Compare commits

...

10 Commits

Author SHA1 Message Date
minster586
32b56b56d7 removed Emotes from code
I have removed the emotes from System.out.println
and System.err.println as they did not show in windows CMD
2025-07-16 05:02:09 -04:00
minster586
540eedba94 Update .gitignore 2025-07-16 04:49:45 -04:00
minster586
3f0337af50 Update pom.xml 2025-07-16 04:25:48 -04:00
minster586
c0e7e2fcb5 Update pom.xml 2025-07-16 04:18:32 -04:00
minster586
787e7e22ce Update pom.xml
update version
2025-07-16 04:00:19 -04:00
minster586
77343481d5 Update MainApp.java
removed console spam
2025-07-16 03:57:57 -04:00
minster586
6b6811cb7a Create .gitignore 2025-07-16 03:46:11 -04:00
minster586
e3dbe80669 working project 2025-06-28 21:50:19 -04:00
minster586
4c9c10b013 Done 2025-06-28 16:45:23 -04:00
minster586
91a53ec119 starting 2025-06-28 16:19:40 -04:00
12 changed files with 740 additions and 1 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
dependency-reduced-pom.xml

40
Build.bat Normal file
View File

@@ -0,0 +1,40 @@
@echo off
echo.
echo === 🚀 Building RadioDJ-TunaBridge with Java 17 ===
echo.
REM Manually set Java 17 location
set "JAVA_HOME=C:\java_runtime\17"
set "PATH=%JAVA_HOME%\bin;%PATH%"
REM Step 1: Clean project
echo 🧹 Cleaning...
mvn clean
IF %ERRORLEVEL% NEQ 0 (
echo ❌ Maven clean failed.
pause
exit /b %ERRORLEVEL%
)
REM Step 2: Compile and package
echo 🔨 Compiling...
mvn package
IF %ERRORLEVEL% NEQ 0 (
echo ❌ Maven build failed.
pause
exit /b %ERRORLEVEL%
)
REM Step 3: Copy JAR to top level
set "TARGET_JAR=target\radiodj-tuna-bridge-1.0-SNAPSHOT.jar"
set "OUTPUT_JAR=radiodj-tuna-bridge.jar"
IF EXIST "%TARGET_JAR%" (
copy "%TARGET_JAR%" "%OUTPUT_JAR%" >nul
echo ✅ JAR ready: %OUTPUT_JAR%
) ELSE (
echo ❌ No JAR found in target folder.
)
echo.
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

102
pom.xml Normal file
View File

@@ -0,0 +1,102 @@
<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.minster586</groupId>
<artifactId>radiodj-tuna-bridge</artifactId>
<version>1.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>RadioDJ Tuna Bridge</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<javafx.version>20</javafx.version>
</properties>
<dependencies>
<!-- JavaFX modules -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- YAML parsing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
<!-- XML support -->
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<!-- Jar plugin for manifest -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.minster586.radiodjtuna.MainApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Shade plugin to bundle everything -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.minster586.radiodjtuna.MainApp</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</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,52 @@
package com.minster586.radiodjtuna;
public class Config {
private String radioDjApiUrl;
private String albumArtDirectory;
private String fallbackArtFilename;
private int updateIntervalSeconds;
// ✅ Required no-arg constructor for SnakeYAML
public Config() {}
// ✅ Full constructor for manual creation
public Config(String radioDjApiUrl, String albumArtDirectory, String fallbackArtFilename, int updateIntervalSeconds) {
this.radioDjApiUrl = radioDjApiUrl;
this.albumArtDirectory = albumArtDirectory;
this.fallbackArtFilename = fallbackArtFilename;
this.updateIntervalSeconds = updateIntervalSeconds;
}
// ✅ Getters and Setters
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 String getFallbackArtFilename() {
return fallbackArtFilename;
}
public void setFallbackArtFilename(String fallbackArtFilename) {
this.fallbackArtFilename = fallbackArtFilename;
}
public int getUpdateIntervalSeconds() {
return updateIntervalSeconds;
}
public void setUpdateIntervalSeconds(int updateIntervalSeconds) {
this.updateIntervalSeconds = updateIntervalSeconds;
}
}

View File

@@ -0,0 +1,115 @@
package com.minster586.radiodjtuna;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.nodes.Tag;
public class ConfigManager {
private final Path configPath;
public ConfigManager(Path configPath) {
this.configPath = configPath;
}
public Config loadOrCreateConfig() {
File file = configPath.toFile();
if (file.exists()) {
try (FileInputStream in = new FileInputStream(file)) {
Yaml yaml = new Yaml(new Constructor(Config.class, new LoaderOptions()));
return yaml.load(in);
} catch (Exception e) {
System.err.println(" Failed to load config.yml: " + e.getMessage());
}
}
System.out.println(" Creating new config.yml...");
Config config = promptUserConfig();
saveConfig(config);
convertFallbackToPngIfNecessary(config);
return config;
}
private Config promptUserConfig() {
Scanner scanner = new Scanner(System.in);
System.out.print(" Enter RadioDJ API URL (e.g., http://localhost:1234/playing): ");
String apiUrl = scanner.nextLine().trim();
System.out.print(" Enter path to album art folder: ");
String artDir = scanner.nextLine().trim();
System.out.print(" Enter filename of fallback album art (e.g., fallback.jpg): ");
String fallback = scanner.nextLine().trim();
int interval = promptForInterval(scanner);
return new Config(apiUrl, artDir, fallback, interval);
}
private int promptForInterval(Scanner scanner) {
while (true) {
System.out.print(" Enter metadata update interval (seconds): ");
String input = scanner.nextLine().trim();
if (!input.isEmpty()) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
System.out.println(" Please enter a valid number.");
}
} else {
System.out.println(" This field is required.");
}
}
}
private void saveConfig(Config config) {
try (FileWriter writer = new FileWriter(configPath.toFile())) {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setPrettyFlow(true);
Representer representer = new Representer(options);
representer.addClassTag(Config.class, Tag.MAP); // 👈 Prevents !!class tag
Yaml yaml = new Yaml(representer, options);
yaml.dump(config, writer);
System.out.println(" Config saved: " + configPath.getFileName());
} catch (IOException e) {
System.err.println(" Failed to save config.yml: " + e.getMessage());
}
}
private void convertFallbackToPngIfNecessary(Config config) {
String fallback = config.getFallbackArtFilename();
if (fallback.toLowerCase().endsWith(".jpg")) {
File jpgFile = new File(config.getAlbumArtDirectory(), fallback);
File pngFile = new File(config.getAlbumArtDirectory(), fallback.replaceAll("(?i)\\.jpg$", ".png"));
try {
BufferedImage image = ImageIO.read(jpgFile);
if (image != null) {
ImageIO.write(image, "png", pngFile);
System.out.println(" Converted fallback image to PNG: " + pngFile.getName());
config.setFallbackArtFilename(pngFile.getName());
saveConfig(config); // Resave with updated filename
}
} catch (IOException e) {
System.err.println(" Failed to convert fallback image: " + e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,97 @@
package com.minster586.radiodjtuna;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
import java.util.concurrent.*;
public class MainApp {
private static Path jarDir;
private static Path outputDir;
private static Path configFile;
private static Path cacheFile;
public static void main(String[] args) {
System.out.println(" RadioDJ Tuna Bridge starting up...");
initializePaths();
ensureOutputFolder();
ConfigManager configManager = new ConfigManager(configFile);
Config config = configManager.loadOrCreateConfig();
MetadataCacheManager cacheManager = new MetadataCacheManager(cacheFile);
final TrackInfo[] lastPlayed = { cacheManager.loadCache() };
RadioDjApiClient apiClient = new RadioDjApiClient(config.getRadioDjApiUrl());
OutputManager outputManager = new OutputManager(
outputDir,
config.getAlbumArtDirectory(),
config.getFallbackArtFilename()
);
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
Runnable pollingTask = () -> {
try {
TrackInfo current = apiClient.fetchNowPlaying();
if (current == null || current.getTitle().isBlank()) {
System.out.println(" No track data received.");
return;
}
if (lastPlayed[0] == null || !current.equals(lastPlayed[0])) {
System.out.println(" New track detected: " + current.getTitle());
outputManager.writeOutputFiles(current);
cacheManager.saveCache(current);
lastPlayed[0] = current;
} else {
// System.out.println(" No change — current track: " + current.getTitle());
}
} catch (Exception e) {
System.err.println(" Failed to fetch or process now playing data: " + e.getMessage());
}
};
int interval = Math.max(config.getUpdateIntervalSeconds(), 3);
scheduler.scheduleAtFixedRate(pollingTask, 0, interval, TimeUnit.SECONDS);
System.out.println(" Press 0 + Enter to stop the app...");
try (Scanner scanner = new Scanner(System.in)) {
while (true) {
String input = scanner.nextLine().trim();
if ("0".equals(input)) {
System.out.println(" Shutdown requested. Closing...");
scheduler.shutdownNow();
break;
}
}
} catch (Exception e) {
System.err.println(" Error reading input: " + e.getMessage());
}
System.exit(0);
}
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,95 @@
package com.minster586.radiodjtuna;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
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());
} catch (IOException e) {
System.err.println(" Failed writing metadata text files: " + e.getMessage());
}
try {
convertAlbumArtToPng(track.getAlbumArt());
} catch (Exception e) {
System.err.println(" Failed converting album art: " + 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);
System.out.println(" Wrote file: " + file.getFileName());
}
private void convertAlbumArtToPng(String albumArtFileName) {
Path destination = outputDir.resolve("album-art.png");
if (albumArtFileName == null || albumArtFileName.isBlank()) {
System.out.println(" No album art in tag, using fallback.");
convertFallbackToPng(destination);
return;
}
Path sourceFile = albumArtSourceFolder.resolve(albumArtFileName);
if (!Files.exists(sourceFile)) {
System.out.println(" Album art not found: " + sourceFile.getFileName() + ", using fallback.");
convertFallbackToPng(destination);
return;
}
try {
BufferedImage image = ImageIO.read(sourceFile.toFile());
if (image != null) {
ImageIO.write(image, "png", destination.toFile());
System.out.println(" Album art converted to PNG: " + destination.getFileName());
} else {
System.out.println(" Could not read album art image, using fallback.");
convertFallbackToPng(destination);
}
} catch (IOException e) {
System.err.println(" Failed converting album art: " + e.getMessage());
convertFallbackToPng(destination);
}
}
private void convertFallbackToPng(Path destination) {
Path fallbackSource = outputDir.resolve(fallbackArtFilename);
if (!Files.exists(fallbackSource)) {
System.err.println(" Fallback art file is missing: " + fallbackSource);
return;
}
try {
BufferedImage image = ImageIO.read(fallbackSource.toFile());
if (image != null) {
ImageIO.write(image, "png", destination.toFile());
System.out.println(" Fallback album art converted to PNG.");
} else {
System.err.println(" Could not read fallback image.");
}
} catch (IOException e) {
System.err.println(" Failed to convert fallback art: " + e.getMessage());
}
}
}

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