Archived
Done
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
: "";
|
||||
}
|
||||
}
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user