Compare commits
10 Commits
500db0bbe3
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
32b56b56d7 | ||
|
540eedba94 | ||
|
3f0337af50 | ||
|
c0e7e2fcb5 | ||
|
787e7e22ce | ||
|
77343481d5 | ||
|
6b6811cb7a | ||
|
e3dbe80669 | ||
|
4c9c10b013 | ||
|
91a53ec119 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
dependency-reduced-pom.xml
|
40
Build.bat
Normal file
40
Build.bat
Normal 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
|
40
README.md
40
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
|
102
pom.xml
Normal file
102
pom.xml
Normal 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
7
run.bat
Normal 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
|
52
src/main/java/com/minster586/radiodjtuna/Config.java
Normal file
52
src/main/java/com/minster586/radiodjtuna/Config.java
Normal 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;
|
||||
}
|
||||
}
|
115
src/main/java/com/minster586/radiodjtuna/ConfigManager.java
Normal file
115
src/main/java/com/minster586/radiodjtuna/ConfigManager.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
src/main/java/com/minster586/radiodjtuna/MainApp.java
Normal file
97
src/main/java/com/minster586/radiodjtuna/MainApp.java
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
95
src/main/java/com/minster586/radiodjtuna/OutputManager.java
Normal file
95
src/main/java/com/minster586/radiodjtuna/OutputManager.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
: "";
|
||||
}
|
||||
}
|
63
src/main/java/com/minster586/radiodjtuna/TrackInfo.java
Normal file
63
src/main/java/com/minster586/radiodjtuna/TrackInfo.java
Normal 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));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user