working project

This commit is contained in:
minster586
2025-06-28 21:50:19 -04:00
parent 4c9c10b013
commit e3dbe80669
13 changed files with 278 additions and 138 deletions

View File

@@ -1,24 +1,40 @@
@echo off
echo === Building RadioDJ-TunaBridge ===
echo.
echo === 🚀 Building RadioDJ-TunaBridge with Java 17 ===
echo.
REM Clean previous build
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
REM Compile and package the JAR
mvn package
IF %ERRORLEVEL% NEQ 0 (
echoBuild failed.
echoMaven clean 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!
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

56
pom.xml
View File

@@ -1,42 +1,64 @@
<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">
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<groupId>com.minster586</groupId>
<artifactId>radiodj-tuna-bridge</artifactId>
<version>1.0-SNAPSHOT</version>
<name>RadioDJ Tuna Bridge</name>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</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 -->
<!-- ✅ YAML parsing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
<!-- Dorkbox SystemTray for Windows notifications -->
<!-- ✅ XML support if needed -->
<dependency>
<groupId>com.dorkbox</groupId>
<artifactId>SystemTray</artifactId>
<version>3.17</version>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- ✅ Assembly Plugin: builds JAR with dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.minster586.radiodjtuna.MainApp</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -3,9 +3,21 @@ package com.minster586.radiodjtuna;
public class Config {
private String radioDjApiUrl;
private String albumArtDirectory;
private int updateIntervalSeconds;
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;
}
@@ -22,14 +34,6 @@ public class Config {
this.albumArtDirectory = albumArtDirectory;
}
public int getUpdateIntervalSeconds() {
return updateIntervalSeconds;
}
public void setUpdateIntervalSeconds(int updateIntervalSeconds) {
this.updateIntervalSeconds = updateIntervalSeconds;
}
public String getFallbackArtFilename() {
return fallbackArtFilename;
}
@@ -37,4 +41,12 @@ public class Config {
public void setFallbackArtFilename(String fallbackArtFilename) {
this.fallbackArtFilename = fallbackArtFilename;
}
public int getUpdateIntervalSeconds() {
return updateIntervalSeconds;
}
public void setUpdateIntervalSeconds(int updateIntervalSeconds) {
this.updateIntervalSeconds = updateIntervalSeconds;
}
}

View File

@@ -1,14 +1,21 @@
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.FileInputStream;
import java.io.FileWriter;
import java.io.InputStream;
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 {
@@ -19,56 +26,90 @@ public class ConfigManager {
}
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;
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: " + e.getMessage());
System.exit(1);
return null; // unreachable, but satisfies compiler
System.err.println("❌ Failed to load config.yml: " + e.getMessage());
}
}
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());
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.setIndent(2);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
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 to " + configPath);
} catch (Exception e) {
System.err.println("❌ Failed to save config: " + e.getMessage());
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

@@ -3,48 +3,77 @@ 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 {
// 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();
Config config = configManager.loadOrCreateConfig();
// Set up metadata cache manager
cacheManager = new MetadataCacheManager(cacheFile);
lastPlayed = cacheManager.loadCache();
MetadataCacheManager cacheManager = new MetadataCacheManager(cacheFile);
final TrackInfo[] lastPlayed = { cacheManager.loadCache() };
if (lastPlayed != null) {
System.out.println("🗃️ Loaded cached track: " + lastPlayed.getTitle());
} else {
System.out.println("📁 No metadata cache found yet.");
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;
}
// 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
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());
}
System.out.println("✅ System initialized. Ready for polling phase.");
} 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() {

View File

@@ -3,6 +3,9 @@ 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 {
@@ -21,9 +24,14 @@ public class OutputManager {
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());
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());
}
}
@@ -31,45 +39,57 @@ public class OutputManager {
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 copyAlbumArt(String albumArtFileName) {
Path destination = outputDir.resolve("album-art.jpg");
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.");
copyFallback(destination);
convertFallbackToPng(destination);
return;
}
Path sourceFile = albumArtSourceFolder.resolve(albumArtFileName);
if (!Files.exists(sourceFile)) {
System.out.println("⚠️ Album art not found: " + sourceFile.getFileName() + ", using fallback.");
copyFallback(destination);
convertFallbackToPng(destination);
return;
}
try {
Files.copy(sourceFile, destination, StandardCopyOption.REPLACE_EXISTING);
System.out.println("🎨 Album art updated: " + albumArtFileName);
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 copying album art: " + e.getMessage());
copyFallback(destination);
System.err.println("❌ Failed converting album art: " + e.getMessage());
convertFallbackToPng(destination);
}
}
private void copyFallback(Path destination) {
private void convertFallbackToPng(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 {
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());
}
}
}