From 4c9c10b013b8642b0f532ce48081746e0b34e1b7 Mon Sep 17 00:00:00 2001 From: minster586 <43217359+minster586@users.noreply.github.com> Date: Sat, 28 Jun 2025 16:45:23 -0400 Subject: [PATCH] Done --- Build.bat | 24 +++++ README.md | 40 ++++++++- run.bat | 7 ++ .../com/minster586/radiodjtuna/Config.java | 40 +++++++++ .../minster586/radiodjtuna/ConfigManager.java | 74 ++++++++++++++++ .../com/minster586/radiodjtuna/MainApp.java | 68 +++++++++++++++ .../radiodjtuna/MetadataCacheManager.java | 82 ++++++++++++++++++ .../minster586/radiodjtuna/OutputManager.java | 75 ++++++++++++++++ .../radiodjtuna/RadioDjApiClient.java | 46 ++++++++++ .../com/minster586/radiodjtuna/TrackInfo.java | 63 ++++++++++++++ .../com/minster586/radiodjtuna/Config.class | Bin 0 -> 1272 bytes .../radiodjtuna/ConfigManager.class | Bin 0 -> 3721 bytes .../com/minster586/radiodjtuna/MainApp.class | Bin 0 -> 3136 bytes .../radiodjtuna/MetadataCacheManager.class | Bin 0 -> 5000 bytes .../radiodjtuna/OutputManager.class | Bin 0 -> 4359 bytes .../radiodjtuna/RadioDjApiClient.class | Bin 0 -> 2809 bytes .../minster586/radiodjtuna/TrackInfo.class | Bin 0 -> 1833 bytes 17 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 Build.bat create mode 100644 run.bat create mode 100644 src/main/java/com/minster586/radiodjtuna/Config.java create mode 100644 src/main/java/com/minster586/radiodjtuna/ConfigManager.java create mode 100644 src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java create mode 100644 src/main/java/com/minster586/radiodjtuna/OutputManager.java create mode 100644 src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java create mode 100644 src/main/java/com/minster586/radiodjtuna/TrackInfo.java create mode 100644 target/classes/com/minster586/radiodjtuna/Config.class create mode 100644 target/classes/com/minster586/radiodjtuna/ConfigManager.class create mode 100644 target/classes/com/minster586/radiodjtuna/MainApp.class create mode 100644 target/classes/com/minster586/radiodjtuna/MetadataCacheManager.class create mode 100644 target/classes/com/minster586/radiodjtuna/OutputManager.class create mode 100644 target/classes/com/minster586/radiodjtuna/RadioDjApiClient.class create mode 100644 target/classes/com/minster586/radiodjtuna/TrackInfo.class diff --git a/Build.bat b/Build.bat new file mode 100644 index 0000000..cf8a0fa --- /dev/null +++ b/Build.bat @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index a1ce872..132adc0 100644 --- a/README.md +++ b/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 \ No newline at end of file diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..3c6e40d --- /dev/null +++ b/run.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 \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/Config.java b/src/main/java/com/minster586/radiodjtuna/Config.java new file mode 100644 index 0000000..aa27636 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/Config.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/ConfigManager.java b/src/main/java/com/minster586/radiodjtuna/ConfigManager.java new file mode 100644 index 0000000..de21ef6 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/ConfigManager.java @@ -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()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/MainApp.java b/src/main/java/com/minster586/radiodjtuna/MainApp.java index e69de29..da53824 100644 --- a/src/main/java/com/minster586/radiodjtuna/MainApp.java +++ b/src/main/java/com/minster586/radiodjtuna/MainApp.java @@ -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."); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java b/src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java new file mode 100644 index 0000000..3379003 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/MetadataCacheManager.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/OutputManager.java b/src/main/java/com/minster586/radiodjtuna/OutputManager.java new file mode 100644 index 0000000..c8652d6 --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/OutputManager.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java b/src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java new file mode 100644 index 0000000..3ea88dd --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/RadioDjApiClient.java @@ -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() + : ""; + } +} \ No newline at end of file diff --git a/src/main/java/com/minster586/radiodjtuna/TrackInfo.java b/src/main/java/com/minster586/radiodjtuna/TrackInfo.java new file mode 100644 index 0000000..32b493e --- /dev/null +++ b/src/main/java/com/minster586/radiodjtuna/TrackInfo.java @@ -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)); + } +} \ No newline at end of file diff --git a/target/classes/com/minster586/radiodjtuna/Config.class b/target/classes/com/minster586/radiodjtuna/Config.class new file mode 100644 index 0000000000000000000000000000000000000000..3d962099aad5380f498f2dc9ed69f30fac989fef GIT binary patch literal 1272 zcmaiy-%b-j6vn@4ODkoerL+rD1quRfP24Mui9pgoNSZX7SmA!UJJhNBhitbbzLOW4 zNKCx&0emRqIZKnq?Ka-b*_l1xIluGG{QmRv7l2c=3&;pG?7*`;?T;fBzI^@43Z

@iQuVtutKRwJvA2szk=>2zQ zA}6 z_m9l!a)pM0M_8s*s<0q6*X%NcNyRSb4d)_928xVfQjwGtfnqK5?Fb7Dlhm2 zph5C1J+T}zYzuv5c%(QISRAP~MQ%hFNb4%?EQ*5SQu_zizQt)}TJ!N?7VGrQsg!^U zHYk!wsL}eb_&dz6^fQB*xR@&5!sDsg`mJdJ?3P3i31^q@DyZsomxR5H^@WcCbrp(viI{2}ib5LH2Pl70(>0-(xQcvO^%d n3qbgW8RS{2BZo;x_ESNQ(;YcXf*cXZ@d6NjVhqxZM{oTFfs@ZK literal 0 HcmV?d00001 diff --git a/target/classes/com/minster586/radiodjtuna/ConfigManager.class b/target/classes/com/minster586/radiodjtuna/ConfigManager.class new file mode 100644 index 0000000000000000000000000000000000000000..fae0cff560f92b0f29877c99fdc34ce490dd50a7 GIT binary patch literal 3721 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/minster586/radiodjtuna/MetadataCacheManager.class b/target/classes/com/minster586/radiodjtuna/MetadataCacheManager.class new file mode 100644 index 0000000000000000000000000000000000000000..45bce329ffda04565a2ac32abdd9abf02886f3c8 GIT binary patch literal 5000 zcmb7Id3+Pc75+w+y^_3KA{-(lF_;9Os6Zja3J_y(agdFJ9TSrDSW9bLAnmHFm9g8V zBx!olv}sBU`Ozk&M?#xkkR}F0NsqK?o8I@Gw)8&JEE%)B?> zd+&R1-tf$S&pZiWBmS+xClF1TnP?_u^U z@74@$Shp1T1?KM6Mzv^KGlru(2lwg;TOgQ_n|d{SMBuWxvu>o!=uj%HN9Alxpyv9N zk+L@lENZB

aUK-(e>85UNnCLV+q!)zCN)gurYS)u<7e6Hgg>Pd+oKTm9N#S{De% z&4iX7(5#gF_9p%INGd0=I{trJN0h;|sU;nY1sWO}FQ%d2(h~c+j3F~ti-lOMU=iU= zH(f;pOK84rcG4U49<@5V=pQ)~3t=g)RB(lWS~{=dD%4Rvl8p3j{P*6{7 za_d-hER&9AH7loEx#(6ik6m6RBNMO-I~3elmiT?+EEyRUadeZKx@8G0 zaMdwLy|mb--MW@x*mvS41-%0E9d8EO~I`NOy!VO+>Vzs##w1a z+Mr<-oavM+S>9Njigr8cOvxcP^#q!nr5R-z#RlEP6{x?o7)w=E(o6^}EaP8+*j)Bo zrb&V>C|fV^lU%bUsbvA33X4Zol2UU>$z7UcB{)5ZZ=DRL@o?Mi{e_&?oDgsdQz4GmcG~E zwFH$Q5pBJg>jW~*7BhU>luXIHRNO5& z9*{MoP)P1k@kYFfG1rnww~*{AFC<0F%9IwuH{F2gGt!S8c#Dd+%F}~9-K{io_H8Qe zEnspAjhuXkiu>ebt=uZZ&ix_Wk3$OHMT@73wry!fZpgGUQPv)JRCe9Go5U}>5-ujd z0&zbMt2lxOm`c1}N^~+4F0Ch~;HbcrmEi5n+u6LmLm$rt@IJCSWh7bc19*tZGp^@C zIED`@_<+Dwm8466sQ3^*EHE#p+pf-D%gpMQJua}a0-GuYL%}25Tpi5l21A%Y#KQwL5|^s zij#PZ1Sx89F11QPU9~aCI=7}3Wuh+bSmQvL*W}HzyvXi|w3%7blXXR1I`5KY6%Xm3 zq8p1%jp>9}U}?q89wI_X8DznWZS#z(#W{Rd!IQiaN*(1ToQluk^Sqgu8Qx58%Xq&w z+@ob!4Vx-ZV0tl#^Ok3e@)W+L;ENZuD6)RYbrnzJ%Wks9bz@j|(`@^?q|{ecJcF;X ztfzSI%7AuFKZ5GNfp01JCa>yJE2MfA-NE>pDy!_eVN-v z6no_X{6XM>(s+8iU4`2>tnSrHkJ7+e#{h`;;o{ z6Fn6jS~}gwu*c@q;5PyMnce^SqsN{*eP3Ot#ttM|XPb5M*;1G0{nwgxF2GfM8MM`j z0RGDU{QQIWmTZ-Ga)qsYesmdzZgo&gPR}WLj*`>7R>=^fk3=@0+$~}wK#Da%eq@8u(GF>Q_XGuh<_5R z+3Hs}MXJNsPT;z>8adi1M~WP6mZN|iwaZcPEIRf?Y9dM`5DA{bwiDftVLKJ}oIyV@ zfm_502TKA;w!&489h;n=3yEnFaW5v)r5s%$AA+by9s8f3X2bU%%vZ4VdGso@M<9z=dtSbqT4e-} zPq}mgmh@MVw<%}w=$?rG6h7V-U|a$bWdff%j`>q@`Lq+4 zlY|W%!r@|AI%z7fnr>P{L)J1#>*$d6bk7FPTt`Q>p@%QP+xh<|No=B9Hsb(i57NDd z*g+k^Ry;xiOIi*&ggfqqhfcxcc!DST*<`ul2~y4t&m7$2g=Y!Iz3?RY1fIYthUWlj zd>WGs&q1O(gR>0J5n`JPN)W#1ahTzl!zl5A>c#_i@ykyr_027l_>2!dt&{kIz(WYP zR!!n7KI;6sAh7E2cTRAdOU)34z!zVDWRP|=xhjfw)I0d3=j!o262!-ym*e~R0p%q@ zR7sFJ1y8jr_#s!>=6Z0R;UPZ3wHA8UNZS62jC`GX%S$n<4Osz6~7?Ur8%t*R9SUe@hn06sbvLz>EwX~-2vYV zEF?7wgn~5+&Jnxw=Nx>_*P0h-M5W~_cUjt6gwzcA1U-F=jUJWH_plb MLijuWfqxI*2aO#C(VFwaI$I@VrEzbze$e6Ba zo9?vf-awm{E-C3=+~61&)19_y`%m!oSHAuO`AW}wqs>V0g=I9Fx$nMn&pr3PtGEBR zcpJd`@NX403YwGlbadLZ9M{P1KX@RT)l;UOnsjrP9v#ZLnVdVITl%<>RpD2#W>TNg zqiNk5j}DDZ8cA0{oh_}7m{|qu`sI7ew4>+Dv=Ke7yAv@!*3)CT>8`9hZ0E8`qt{NS z3|g-}r>E0ndUA?ydTDOy(}seO=R@u>>}E}CoE}yjHZ9YAN*Wuy)Y8Z^{mg^IPyLiDx|Vm&sh*g(LQ(K#A6VKW_#8|0#Zt{nXH zRiIRhLBQCeLB$Gj_n3wN>J+T*H!Wi@H$7%#6Z%+M@?$6U^oX7{#rOO}zdK<%3L^dQ zHk;(MVR=?$0@qx~ny%sHU8ICQTbdu`G?8Wg`mA$ytYsvCCIWWFlXlm1(?*9o>ju$` zh>8{kJ0F3q=te^;+7tvxXF|B3c$!-(84Qx54h>PfM?t;BoG@mkh!pJ3OQbvrl^z7# zz09@;SI@ep;}-Dl)39Hls3Qt$lWzievKX4AtzddcLuUbSmr%Kg_prwD;`)_heO~$k z@zWZfL6-t8X=g4LUMN^!1+b8196c(IELY@3*6Fl{UL2+7j4Z2Yy%!rx?l_CZrDhF% zIu=A9-ml`>f@t}=Z>VQBX=Gf}wlwq$d_m$HFdT>F9b!uu)G&nO3N}vbQ%2mjlDa!; zx)Z!}T-|c1g{EpqE4dBg1QIHS%Qi2=rs1R@LCpxbX-h$^%mKZ>JE191D2r0qWpPo} z^}{apH=t+ol@Tlk?X1KZoK^86drV~^Yxp2^qRJYMou1(a%@2=td4CHasQ`5_Fs|a9 zf*nQY$q7B{7;co1tdyQj#l7$B851xW_sK->*@FriJqCr>!iv0QOvRLfO+|;YsSaff zYe=TC4%4tzkizhClfs52QwttBu<l;o#1d`1=@kG%jsr=a!j%QyeK@Je{l4of#-x(Sg#vgcOnC|rS@?>Eui|UWtuf0^ z#VXxf)zyQnm7K&{PK(m`hK6tATVB@^N%Z*IP-wr)ITa+ms^K!Okfa$soim0g{bl{Z z8xRU9DeuN1(lDsvwTi&v?283ye;+?karL3nE}3fhA%4Uzd7|fde^w3dBx z2Cr-Q39eBPqAe>pRh7Py9EEJW$>u>y%luXqVhA-D3~~GzBwQ&Lo-QS-SEdZdH7#9q zXf|iZ80LB(46Qh}TO7!YU*UnxP$Hz?t zujctHxAq(!V;kg5v9sT{r*fHmg_jGXtQX|B@fId7W(p0V+&s^}d{jkkc^7fk*f6J> z*yxTa(MAbk`6o0{D}H?PZ$_!HLa_jYAs!WjR9E4hy?t7ZE_D z-5mu_c+R{rS|StygiR+rZ91a&(2%XB&$5hcJgqy9;i&kBf?XB)td=1a|5UK;(WdM> zUOKUZRqf*#;bUvyFwMJ=m}ccCXIXyM(jthJylVXB(9+M(Es?iSA8EaTHIX*o5<&}D zcLR-)_8ZuGov&-Kjn@ra4AgVMD!N+Dk$epr(SYrI8u8p9ObFu77P&poWZpea>l%3{ zAo#fT6WEyt@H)Q>z@3rMuGV?%o=1Bmw1;=^m3IetcTn8%Gd7Q>u3_!K9J<@)acmI- zU~PQhIz0hRXyLTwlQ_3AWDs4Dh}U5wpKap%&BWMjcsntIQF@9nuIF%y zcu&&@f1Q~s`d0HR$6Q{Y=R-fu`XqZxG!X1{MBc=j)|+^Nt);!~4%D`5s11$HAyvDC z$y2^Lq=!%W=a3yH+l#-Og-;32FyJ2&0{Jl?d%90_?ztNuDBh!$%~cD@o%= znOmQPx(7A)8LeN1q9P>!oe{TZ?sL)dr!{;^1BSy%m~SZr|PR~ggz!?8ECzO zAc=qJG8*Raa_F;UJ3{I|f1O5+g?K5KFD>AeD|n1H?Kkmt1q=B0BEBQD8GpO!5nwv#x|>8Fs{ZYmDgMZ~+A*JcvcLM27` zq@C%BBF5_w_8@`1TzP=w;FDZ`kmt)GP7Iwq^bbpN_j$1v$-RW1Npdy3grAd%8v1zw zzmVk4P)R}&eo6B>)V?EriJqx{2T^{!NO^3aJ;w=;N&JVageXP@ll;Hc<5HBB;OR2< zw6#{S5&9*$y@p+bB%)pN@asG|At853=x^i`A@|J;abJ;?CPD|EW>G%FH{GNp&i6<7 z^Qni|qx^e};(oSFRinkgM`gG?sp##x419mS06bM~gzrAL@<~-uG*VK1p;Z*!95=1O pZ}BESo49n5zfH-{|KB&akf&_+MjtF7_k3J#FJ$~^BldD>kQ2%sH@RCJ(IU}r&h z%AAyu)2$+(%-cQEGbM$(RCG(B1`3VqTBa-^Sz#5u=p$=ZcSg*7(czQoT25e3#V^B( zu4BXl=*Ke(qC`-7wisQ^Wutk`F6ee4de%x8b4;SaqLIz$_OOr&VpqUN#2y|}88qJ(QOuX3FB-^B7jr%&1$-_^ zo`Q=49gnHI)tU)l4C6BOCj>^fXb77f*v^T}S=%j|9XE#{V&|rppoAOS91rv*h$$Y?BLn%k~B}6xTBtHRnXcl zG?UlDGcR|$rS%$lV*^j_K0xcG@Lkm3LtCPE8TyRSqcJ6Cmk%pVT-e}J(| zp+G1YQbWAMES8r>R|4GiU9Hj!Z)en=9M#HtR^<<$n+u;1Uedc@7{3BNOfhcQPTndiz#ztp+Q& zF7N|d%UjP1K9q-_3fx9*)7N(>T0*;tc?E=td@p0@BW__Ax%QH|)r{Tlpe z6Vb#AFE#N;8Q(c)DO#4qc+qpt?9R?SGxHw!`RnU90L!?MMM7b!*4(S?`HfcS1&cSA zDuKJ@H@9}fcEhc#2X5`fqsH@Q7Ab|HUH7$Hsk@Es%CqXOR|^%=p&!;gg^|^xcWYtb zH@0spWZWS1TjJ%`tL;679=+cS_$YJ7Z}{O|g=Dd`p^$pe-12frV!%QUeF}rCe#3j( z-m7}Sx?43Vnl-n+;Re3g(Zf`@YQ7?nAb-JOwbU!>x@Ox2z<*_;T9X{3F z?mK*8dhfkzyLGm$RQ$KTl{Ry%uw=nOSs~YQpL-9Z5SL?Uj~7_lqzIbfju$BOuZ0xV z6ZfSaVGb2s&Em>m>M{zlFoOlQ_|FVRcef>;a`~$gXRX-|YTk-3A{po?c3uRDNpdGC z_7oB_D4r5BCcSe=dgq9^TlA#zxLUNXaaY{a&KLB5)NX@ZbNn2fX^M_RJjq9N!Z1d- zOMp{cCBPe<5U?eHQ$E0`Gkbu%Gk1W3GoCtteH7K!0R4Ad^gY@dOYVxpd%SEJ&dnWI7h) zQanhZ8>B=K=L8TLl7LLbg4o?4bFm=P@gRRF%8ceJK^9H`5se7Q3@&pO#lEF_WXvn& zA4n(n9fwjW^Y+-Ibu7MW4^BJ(X_3^!Q% zO