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 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 mvn clean
REM Compile and package the JAR
mvn package
IF %ERRORLEVEL% NEQ 0 ( IF %ERRORLEVEL% NEQ 0 (
echoBuild failed. echoMaven clean failed.
pause pause
exit /b %ERRORLEVEL% exit /b %ERRORLEVEL%
) )
REM Copy final JAR to top-level folder (optional) REM Step 2: Compile and package
IF EXIST target\radiodj-tuna-bridge-1.0-SNAPSHOT.jar ( echo 🔨 Compiling...
copy target\radiodj-tuna-bridge-1.0-SNAPSHOT.jar radiodj-tuna-bridge.jar >nul mvn package
echo ✅ Build complete. Output: radiodj-tuna-bridge.jar IF %ERRORLEVEL% NEQ 0 (
) ELSE ( echo ❌ Maven build failed.
echo ❌ JAR not found! 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 pause

86
pom.xml
View File

@@ -1,42 +1,64 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 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> <modelVersion>4.0.0</modelVersion>
<groupId>com.minster586</groupId>
<artifactId>radiodj-tuna-bridge</artifactId>
<version>1.0-SNAPSHOT</version>
<name>RadioDJ Tuna Bridge</name>
<groupId>com.example</groupId> <properties>
<artifactId>radiodj-tuna-bridge</artifactId> <maven.compiler.source>17</maven.compiler.source>
<version>1.0-SNAPSHOT</version> <maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<properties> <dependencies>
<maven.compiler.source>17</maven.compiler.source> <!-- ✅ YAML parsing -->
<maven.compiler.target>17</maven.compiler.target> <dependency>
</properties> <groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
<dependencies> <!-- ✅ XML support if needed -->
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.2</version>
</dependency>
</dependencies>
<!-- Jackson for XML & JSON processing --> <build>
<dependency> <plugins>
<groupId>com.fasterxml.jackson.dataformat</groupId> <!-- ✅ Assembly Plugin: builds JAR with dependencies -->
<artifactId>jackson-dataformat-xml</artifactId> <plugin>
<version>2.17.0</version> <groupId>org.apache.maven.plugins</groupId>
</dependency> <artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<!-- SnakeYAML for config.yml parsing --> <configuration>
<dependency> <descriptorRefs>
<groupId>org.yaml</groupId> <descriptorRef>jar-with-dependencies</descriptorRef>
<artifactId>snakeyaml</artifactId> </descriptorRefs>
<version>2.2</version> <archive>
</dependency> <manifest>
<mainClass>com.minster586.radiodjtuna.MainApp</mainClass>
<!-- Dorkbox SystemTray for Windows notifications --> </manifest>
<dependency> </archive>
<groupId>com.dorkbox</groupId> </configuration>
<artifactId>SystemTray</artifactId> <executions>
<version>3.17</version> <execution>
</dependency> <id>assemble-all</id>
<phase>package</phase>
</dependencies> <goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -3,9 +3,21 @@ package com.minster586.radiodjtuna;
public class Config { public class Config {
private String radioDjApiUrl; private String radioDjApiUrl;
private String albumArtDirectory; private String albumArtDirectory;
private int updateIntervalSeconds;
private String fallbackArtFilename; 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() { public String getRadioDjApiUrl() {
return radioDjApiUrl; return radioDjApiUrl;
} }
@@ -22,14 +34,6 @@ public class Config {
this.albumArtDirectory = albumArtDirectory; this.albumArtDirectory = albumArtDirectory;
} }
public int getUpdateIntervalSeconds() {
return updateIntervalSeconds;
}
public void setUpdateIntervalSeconds(int updateIntervalSeconds) {
this.updateIntervalSeconds = updateIntervalSeconds;
}
public String getFallbackArtFilename() { public String getFallbackArtFilename() {
return fallbackArtFilename; return fallbackArtFilename;
} }
@@ -37,4 +41,12 @@ public class Config {
public void setFallbackArtFilename(String fallbackArtFilename) { public void setFallbackArtFilename(String fallbackArtFilename) {
this.fallbackArtFilename = 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; 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.File;
import java.io.FileInputStream;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.InputStream; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Scanner; 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 { public class ConfigManager {
@@ -19,56 +26,90 @@ public class ConfigManager {
} }
public Config loadOrCreateConfig() { public Config loadOrCreateConfig() {
File configFile = configPath.toFile(); File file = configPath.toFile();
if (!configFile.exists()) { if (file.exists()) {
System.out.println("⚙️ Creating new config.yml..."); try (FileInputStream in = new FileInputStream(file)) {
Config newConfig = promptForConfig(); Yaml yaml = new Yaml(new Constructor(Config.class, new LoaderOptions()));
saveConfig(newConfig); return yaml.load(in);
return newConfig; } catch (Exception e) {
System.err.println("❌ Failed to load config.yml: " + e.getMessage());
}
} }
try (InputStream input = java.nio.file.Files.newInputStream(configPath)) { System.out.println("📝 Creating new config.yml...");
Yaml yaml = new Yaml(new Constructor(Config.class)); Config config = promptUserConfig();
Config loaded = yaml.load(input); saveConfig(config);
System.out.println("✅ Config loaded successfully."); convertFallbackToPngIfNecessary(config);
return loaded; return config;
} catch (Exception e) {
System.err.println("❌ Failed to load config: " + e.getMessage());
System.exit(1);
return null; // unreachable, but satisfies compiler
}
} }
private Config promptForConfig() { private Config promptUserConfig() {
Scanner scanner = new Scanner(System.in); Scanner scanner = new Scanner(System.in);
Config config = new Config();
System.out.print("🔗 Enter RadioDJ API URL (e.g. http://localhost:1234/playing): "); System.out.print("🔗 Enter RadioDJ API URL (e.g., http://localhost:1234/playing): ");
config.setRadioDjApiUrl(scanner.nextLine().trim()); String apiUrl = scanner.nextLine().trim();
System.out.print("📁 Enter full path to RadioDJ album art folder: "); System.out.print("🖼️ Enter path to album art folder: ");
config.setAlbumArtDirectory(scanner.nextLine().trim()); String artDir = scanner.nextLine().trim();
System.out.print("⏱️ Enter update interval (seconds): "); System.out.print("🧩 Enter filename of fallback album art (e.g., fallback.jpg): ");
config.setUpdateIntervalSeconds(Integer.parseInt(scanner.nextLine().trim())); String fallback = scanner.nextLine().trim();
System.out.print("🖼️ Enter fallback album art filename (e.g. fallback-art.jpg): "); int interval = promptForInterval(scanner);
config.setFallbackArtFilename(scanner.nextLine().trim());
return config; 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) { private void saveConfig(Config config) {
try (FileWriter writer = new FileWriter(configPath.toFile())) { try (FileWriter writer = new FileWriter(configPath.toFile())) {
DumperOptions options = new DumperOptions(); DumperOptions options = new DumperOptions();
options.setIndent(2);
options.setPrettyFlow(true);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 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); yaml.dump(config, writer);
System.out.println("💾 Config saved to " + configPath); System.out.println(" Config saved: " + configPath.getFileName());
} catch (Exception e) { } catch (IOException e) {
System.err.println("❌ Failed to save config: " + e.getMessage()); 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.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Scanner;
import java.util.concurrent.*;
public class MainApp { public class MainApp {
// Base paths determined at runtime
private static Path jarDir; private static Path jarDir;
private static Path outputDir; private static Path outputDir;
private static Path configFile; private static Path configFile;
private static Path cacheFile; private static Path cacheFile;
// Core components
private static Config config;
private static MetadataCacheManager cacheManager;
private static TrackInfo lastPlayed;
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("🔊 RadioDJ Tuna Bridge starting up..."); System.out.println("🔊 RadioDJ Tuna Bridge starting up...");
initializePaths(); initializePaths();
ensureOutputFolder(); ensureOutputFolder();
// Load or create config
ConfigManager configManager = new ConfigManager(configFile); ConfigManager configManager = new ConfigManager(configFile);
config = configManager.loadOrCreateConfig(); Config config = configManager.loadOrCreateConfig();
// Set up metadata cache manager MetadataCacheManager cacheManager = new MetadataCacheManager(cacheFile);
cacheManager = new MetadataCacheManager(cacheFile); final TrackInfo[] lastPlayed = { cacheManager.loadCache() };
lastPlayed = cacheManager.loadCache();
if (lastPlayed != null) { RadioDjApiClient apiClient = new RadioDjApiClient(config.getRadioDjApiUrl());
System.out.println("🗃️ Loaded cached track: " + lastPlayed.getTitle()); OutputManager outputManager = new OutputManager(
} else { outputDir,
System.out.println("📁 No metadata cache found yet."); 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());
} }
// TODO: Poll RadioDJ API using config.getRadioDjApiUrl() System.exit(0);
// 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() { private static void initializePaths() {

View File

@@ -3,6 +3,9 @@ package com.minster586.radiodjtuna;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.*; import java.nio.file.*;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
public class OutputManager { public class OutputManager {
@@ -21,9 +24,14 @@ public class OutputManager {
writeTextFile("track-title.txt", track.getTitle()); writeTextFile("track-title.txt", track.getTitle());
writeTextFile("track-artist.txt", track.getArtist()); writeTextFile("track-artist.txt", track.getArtist());
writeTextFile("track-album.txt", track.getAlbum()); writeTextFile("track-album.txt", track.getAlbum());
copyAlbumArt(track.getAlbumArt());
} catch (IOException e) { } 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); Path file = outputDir.resolve(fileName);
Files.writeString(file, content != null ? content : "", StandardCharsets.UTF_8, Files.writeString(file, content != null ? content : "", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println("📄 Wrote file: " + file.getFileName());
} }
private void copyAlbumArt(String albumArtFileName) { private void convertAlbumArtToPng(String albumArtFileName) {
Path destination = outputDir.resolve("album-art.jpg"); Path destination = outputDir.resolve("album-art.png");
if (albumArtFileName == null || albumArtFileName.isBlank()) { if (albumArtFileName == null || albumArtFileName.isBlank()) {
System.out.println(" No album art in tag, using fallback."); System.out.println(" No album art in tag, using fallback.");
copyFallback(destination); convertFallbackToPng(destination);
return; return;
} }
Path sourceFile = albumArtSourceFolder.resolve(albumArtFileName); Path sourceFile = albumArtSourceFolder.resolve(albumArtFileName);
if (!Files.exists(sourceFile)) { if (!Files.exists(sourceFile)) {
System.out.println("⚠️ Album art not found: " + sourceFile.getFileName() + ", using fallback."); System.out.println("⚠️ Album art not found: " + sourceFile.getFileName() + ", using fallback.");
copyFallback(destination); convertFallbackToPng(destination);
return; return;
} }
try { try {
Files.copy(sourceFile, destination, StandardCopyOption.REPLACE_EXISTING); BufferedImage image = ImageIO.read(sourceFile.toFile());
System.out.println("🎨 Album art updated: " + albumArtFileName); 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) { } catch (IOException e) {
System.err.println("❌ Failed copying album art: " + e.getMessage()); System.err.println("❌ Failed converting album art: " + e.getMessage());
copyFallback(destination); convertFallbackToPng(destination);
} }
} }
private void copyFallback(Path destination) { private void convertFallbackToPng(Path destination) {
Path fallbackSource = outputDir.resolve(fallbackArtFilename); Path fallbackSource = outputDir.resolve(fallbackArtFilename);
if (Files.exists(fallbackSource)) { 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); 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());
} }
} }
} }