diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java
index b1e6753c8..cfcc90410 100644
--- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java
@@ -24,6 +24,7 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
+import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
@@ -46,6 +47,7 @@
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
+ @JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java
index 28bd61995..255ef9698 100644
--- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java
@@ -22,6 +22,9 @@
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
+import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
+import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
+import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
@@ -61,6 +64,9 @@ public static AbstractAudioBroadcaster getBroadcaster(BroadcastConfiguration con
case BROADCASTIFY_CALL:
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
+ case RDIOSCANNER_CALL:
+ return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
+ inputAudioFormat, mp3Setting, aliasModel);
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
inputAudioFormat, mp3Setting, aliasModel);
@@ -99,6 +105,8 @@ public static BroadcastConfiguration getConfiguration(BroadcastServerType server
{
case BROADCASTIFY_CALL:
return new BroadcastifyCallConfiguration(format);
+ case RDIOSCANNER_CALL:
+ return new RdioScannerConfiguration(format);
case BROADCASTIFY:
return new BroadcastifyFeedConfiguration(format);
case ICECAST_HTTP:
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastServerType.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastServerType.java
index a76138314..aab98a001 100644
--- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastServerType.java
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastServerType.java
@@ -31,6 +31,7 @@ public enum BroadcastServerType
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
+ RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/FormField.java b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/FormField.java
new file mode 100644
index 000000000..42a11a3a4
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/FormField.java
@@ -0,0 +1,56 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.audio.broadcast.rdioscanner;
+
+/**
+ * HTTP headers used for posting to Rdio Scanner API
+ */
+public enum FormField
+{
+ AUDIO("audio"),
+ AUDIO_NAME("audioName"),
+ AUDIO_TYPE("audioType"),
+ DATE_TIME("dateTime"),
+ FREQUENCIES("frequencies"),
+ FREQUENCY("frequency"),
+ KEY("key"),
+ PATCHES("patches"),
+ SOURCE("source"),
+ SOURCES("sources"),
+ SYSTEM("system"),
+ SYSTEM_LABEL("systemLabel"),
+ TALKGROUP_ID("talkgroup"),
+ TALKGROUP_GROUP("talkgroupGroup"),
+ TALKGROUP_LABEL("talkgroupLabel"),
+ TALKGROUP_TAG("talkgroupTag"),
+ TEST("test");
+
+ private String mHeader;
+
+ FormField(String header)
+ {
+ mHeader = header;
+ }
+
+ public String getHeader()
+ {
+ return mHeader;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java
new file mode 100644
index 000000000..6f8f510ac
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java
@@ -0,0 +1,622 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2022 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.audio.broadcast.rdioscanner;
+
+import com.google.common.net.HttpHeaders;
+import com.google.common.base.Joiner;
+import io.github.dsheirer.alias.Alias;
+import io.github.dsheirer.alias.AliasList;
+import io.github.dsheirer.alias.AliasModel;
+import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
+import io.github.dsheirer.audio.broadcast.AudioRecording;
+import io.github.dsheirer.audio.broadcast.BroadcastEvent;
+import io.github.dsheirer.audio.broadcast.BroadcastState;
+import io.github.dsheirer.audio.convert.InputAudioFormat;
+import io.github.dsheirer.audio.convert.MP3Setting;
+import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
+import io.github.dsheirer.identifier.Form;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierClass;
+import io.github.dsheirer.identifier.MutableIdentifierCollection;
+import io.github.dsheirer.identifier.Role;
+import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
+import io.github.dsheirer.identifier.patch.PatchGroup;
+import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
+import io.github.dsheirer.identifier.radio.RadioIdentifier;
+import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
+import io.github.dsheirer.util.ThreadPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Audio broadcaster to push completed audio recordings to the Rdio Scanner call upload API.
+ *
+ */
+public class RdioScannerBroadcaster extends AbstractAudioBroadcaster
+{
+ private final static Logger mLog = LoggerFactory.getLogger(RdioScannerBroadcaster.class);
+
+ private static final String ENCODING_TYPE_MP3 = "mp3";
+ private static final String MULTIPART_TYPE = "multipart";
+ private static final String DEFAULT_SUBTYPE = "form-data";
+ private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
+ private Queue mAudioRecordingQueue = new LinkedTransferQueue<>();
+ private ScheduledFuture> mAudioRecordingProcessorFuture;
+ private HttpClient mHttpClient = HttpClient.newBuilder()
+ .version(HttpClient.Version.HTTP_2)
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .connectTimeout(Duration.ofSeconds(20))
+ .build();
+ private long mLastConnectionAttempt;
+ private long mConnectionAttemptInterval = 5000; //Every 5 seconds
+ private AliasModel mAliasModel;
+
+ /**
+ * Constructs an instance of the broadcaster
+ * @param config to use
+ * @param aliasModel for access to aliases
+ */
+ public RdioScannerBroadcaster(RdioScannerConfiguration config, InputAudioFormat inputAudioFormat,
+ MP3Setting mp3Setting, AliasModel aliasModel)
+ {
+ super(config);
+ mAliasModel = aliasModel;
+ }
+
+ /**
+ * Starts the audio recording processor thread
+ */
+ @Override
+ public void start()
+ {
+ setBroadcastState(BroadcastState.CONNECTING);
+ String response = testConnection(getBroadcastConfiguration());
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ /**
+ * Rdio Scanner API does not currently expose a test method.
+ */
+ if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
+ {
+ setBroadcastState(BroadcastState.CONNECTED);
+ }
+ else
+ {
+ mLog.error("Error connecting to Rdio Scanner server on startup [" + response + "]");
+ setBroadcastState(BroadcastState.ERROR);
+ }
+
+ if(mAudioRecordingProcessorFuture == null)
+ {
+ mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
+ 0, 500, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ /**
+ * Stops the audio recording processor thread
+ */
+ @Override
+ public void stop()
+ {
+ if(mAudioRecordingProcessorFuture != null)
+ {
+ mAudioRecordingProcessorFuture.cancel(true);
+ mAudioRecordingProcessorFuture = null;
+ dispose();
+ setBroadcastState(BroadcastState.DISCONNECTED);
+ }
+ }
+
+ /**
+ * Prepares for disposal
+ */
+ @Override
+ public void dispose()
+ {
+ AudioRecording audioRecording = mAudioRecordingQueue.poll();
+
+ while(audioRecording != null)
+ {
+ audioRecording.removePendingReplay();
+ audioRecording = mAudioRecordingQueue.poll();
+ }
+ }
+
+ /**
+ * Indicates if this broadcaster continues to have successful connections to and transactions with the remote
+ * server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
+ * the audio processor thread will persistently invoke this method to attempt a reconnect.
+ *
+ * Rdio Scanner does not have a test API endpoint, so we look for the incomplete call response.
+ */
+ private boolean connected()
+ {
+ if(getBroadcastState() != BroadcastState.CONNECTED &&
+ (System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
+ {
+ setBroadcastState(BroadcastState.CONNECTING);
+
+ String response = testConnection(getBroadcastConfiguration());
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
+ {
+ setBroadcastState(BroadcastState.CONNECTED);
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.ERROR);
+ }
+ }
+
+ return getBroadcastState() == BroadcastState.CONNECTED;
+ }
+
+ @Override
+ public int getAudioQueueSize()
+ {
+ return mAudioRecordingQueue.size();
+ }
+
+ @Override
+ public void receive(AudioRecording audioRecording)
+ {
+ mAudioRecordingQueue.offer(audioRecording);
+ broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
+ }
+
+ /**
+ * Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
+ * exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
+ * deleted to ensure that the in-memory queue size doesn't blow up.
+ * @param audioRecording to test
+ * @return true if the recording is valid
+ */
+ private boolean isValid(AudioRecording audioRecording)
+ {
+ return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
+ getBroadcastConfiguration().getMaximumRecordingAge();
+ }
+
+ /**
+ * Processes any enqueued audio recordings. This method employs asynchronous
+ * interaction with the server, so multiple audio recording uploads can occur simultaneously.
+ */
+ private void processRecordingQueue()
+ {
+
+
+ while(connected() && !mAudioRecordingQueue.isEmpty())
+ {
+ final AudioRecording audioRecording = mAudioRecordingQueue.poll();
+ broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
+
+ if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
+ {
+ float durationSeconds = (float)(audioRecording.getRecordingLength() / 1E3f);
+ long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
+ String talkgroup = getTo(audioRecording);
+ String radioId = getFrom(audioRecording);
+ Long frequency = getFrequency(audioRecording);
+ String patches = getPatches(audioRecording);
+ String talkgroupLabel = getTalkgroupLabel(audioRecording);
+ String talkgroupGroup = getTalkgroupGroup(audioRecording);
+ String systemLabel = getSystemLabel(audioRecording);
+
+ try
+ {
+ byte[] audioBytes = null;
+
+ try
+ {
+ audioBytes = Files.readAllBytes(audioRecording.getPath());
+ }
+ catch(IOException e)
+ {
+ mLog.error("Rdio Scanner API - audio recording file not found - ignoring upload");
+ }
+
+ if(audioBytes != null)
+ {
+
+ RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
+ bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
+ .addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
+ .addFile(audioBytes)
+ .addPart(FormField.DATE_TIME, timestampSeconds)
+ .addPart(FormField.TALKGROUP_ID, talkgroup)
+ .addPart(FormField.SOURCE, radioId)
+ .addPart(FormField.FREQUENCY, frequency)
+ .addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
+ .addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
+ .addPart(FormField.SYSTEM_LABEL, systemLabel)
+ .addPart(FormField.PATCHES, patches);
+
+ HttpRequest fileRequest = HttpRequest.newBuilder()
+ .uri(URI.create(getBroadcastConfiguration().getHost()))
+ .header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
+ .header(HttpHeaders.USER_AGENT, "sdrtrunk")
+ .header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
+ .POST(bodyBuilder.build())
+ .build();
+
+ mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
+ .whenComplete((fileResponse, throwable1) -> {
+ if(throwable1 != null || fileResponse.statusCode() != 200)
+ {
+ if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
+ {
+ //We get socket reset exceptions occasionally when the remote server doesn't
+ //fully read our request and immediately responds.
+ setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
+ mLog.error("Rdio Scanner API file upload fail [" +
+ fileResponse.statusCode() + "] response [" +
+ fileResponse.body() + "]");
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
+ mLog.error("Rdio Scanner API file upload fail [" +
+ fileResponse.statusCode() + "] response [" +
+ fileResponse.body() + "]");
+ }
+
+ incrementErrorAudioCount();
+ broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
+ BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
+ }
+ else
+ {
+ String fileResponseString = fileResponse.body();
+
+ if(fileResponseString.contains("Call imported successfully."))
+ {
+ incrementStreamedAudioCount();
+ broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
+ BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
+ audioRecording.removePendingReplay();
+ }
+ else if(fileResponseString.contains("duplicate call rejected"))
+ {
+ //Rdio Scanner is telling us to skip audio upload - someone already uploaded it
+ audioRecording.removePendingReplay();
+ }
+ else
+ {
+ setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
+ mLog.error("Rdio Scanner API file upload fail [" +
+ fileResponse.statusCode() + "] response [" +
+ fileResponse.body() + "]");
+ }
+
+
+ }
+
+ });
+ }
+ else
+ {
+ //Register an error for the file not found exception
+ mLog.error("Rdio Scanner API - upload file not found [" +
+ audioRecording.getPath().toString() + "]");
+ incrementErrorAudioCount();
+ broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
+ BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
+ audioRecording.removePendingReplay();
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.error("Unknown Error", e);
+ setBroadcastState(BroadcastState.ERROR);
+ incrementErrorAudioCount();
+ broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
+ audioRecording.removePendingReplay();
+ }
+ }
+ }
+
+ //If we're not connected and there are recordings in the queue, check the recording at the head of the queue
+ // and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
+ AudioRecording audioRecording = mAudioRecordingQueue.peek();
+
+ while(audioRecording != null)
+ {
+ if(isValid(audioRecording))
+ {
+ return;
+ }
+ else
+ {
+ //Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
+ mAudioRecordingQueue.poll();
+ audioRecording.removePendingReplay();
+ incrementAgedOffAudioCount();
+ broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
+ audioRecording = mAudioRecordingQueue.peek();
+ }
+ }
+ }
+
+ /**
+ * Creates a frequency value from the audio recording identifier collection.
+ */
+ private static Long getFrequency(AudioRecording audioRecording)
+ {
+ Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
+ Form.CHANNEL_FREQUENCY, Role.ANY);
+
+ if(identifier instanceof ConfigurationLongIdentifier)
+ {
+ Long value = ((ConfigurationLongIdentifier)identifier).getValue();
+
+ if(value != null)
+ {
+ return value;
+ }
+ }
+
+ return Long.valueOf(0);
+ }
+
+ /**
+ * Creates a formatted string with the FROM identifier or uses a default of zero(0)
+ */
+ private static String getFrom(AudioRecording audioRecording)
+ {
+ for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
+ {
+ if(identifier instanceof RadioIdentifier)
+ {
+ return ((RadioIdentifier)identifier).getValue().toString();
+ }
+ }
+
+ return "0";
+ }
+
+ /**
+ * Creates a formatted string with the TO identifiers or uses a default of zero (0)
+ *
+ */
+ private static String getTo(AudioRecording audioRecording)
+ {
+ Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
+
+ if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
+ {
+ return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
+ }
+ else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
+ {
+ return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
+ talkgroupIdentifier.getProtocol()));
+ }
+ else if(identifier instanceof RadioIdentifier radioIdentifier)
+ {
+ return radioIdentifier.getValue().toString();
+ }
+
+ return "0";
+ }
+
+ /**
+ * Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
+ * If this is a PatchGroup we return only the first label as the primary talkgroup label.
+ *
+ */
+ private String getTalkgroupLabel(AudioRecording audioRecording)
+ {
+
+ AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
+ Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
+
+ StringBuilder sb = new StringBuilder();
+ if(identifier != null)
+ {
+ List aliases = aliasList.getAliases(identifier);
+ if(!aliases.isEmpty())
+ {
+ sb.append(aliases.get(0));
+ }
+
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
+ * If this is a PatchGroup we return only the first group as the primary talkgroup group.
+ *
+ */
+ private String getTalkgroupGroup(AudioRecording audioRecording)
+ {
+
+ AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
+ Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
+
+ StringBuilder sb = new StringBuilder();
+ if(identifier != null)
+ {
+ List aliases = aliasList.getAliases(identifier);
+ if(!aliases.isEmpty())
+ {
+ sb.append(aliases.get(0).getGroup());
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Creates a formatted string with the System Label from the Audio Recording Alias
+ * If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
+ *
+ */
+ private String getSystemLabel(AudioRecording audioRecording)
+ {
+ List systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
+
+ StringBuilder sb = new StringBuilder();
+ if(!systems.isEmpty())
+ {
+ sb.append(systems.get(0));
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Creates a formatted string with the patched talkgroups
+ */
+ public static String getPatches(AudioRecording audioRecording)
+ {
+ Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
+
+ if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
+ {
+ PatchGroup patchGroup = patchGroupIdentifier.getValue();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+
+ sb.append(patchGroup.getPatchGroup().getValue().toString());
+
+ for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
+ {
+ sb.append(",").append(patched.getValue());
+ }
+
+ sb.append("]");
+ return sb.toString();
+ }
+
+ return "[]";
+ }
+
+ /**
+ * Tests both the connection and configuration against the RdioScanner Call API service
+ * @param configuration containing API key and system id
+ * @return error string or null if test is successful
+ */
+ public static String testConnection(RdioScannerConfiguration configuration)
+ {
+ HttpClient httpClient = HttpClient.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .connectTimeout(Duration.ofSeconds(20))
+ .build();
+
+ RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
+ bodyBuilder.addPart(FormField.KEY, configuration.getApiKey())
+ .addPart(FormField.SYSTEM, configuration.getSystemID())
+ .addPart(FormField.TEST, 1);
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(configuration.getHost()))
+ .header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
+ .header(HttpHeaders.USER_AGENT, "sdrtrunk")
+ .header(HttpHeaders.ACCEPT, "*/*")
+ .POST(bodyBuilder.build())
+ .build();
+
+ HttpResponse.BodyHandler responseHandler = HttpResponse.BodyHandlers.ofString();
+
+ try
+ {
+ HttpResponse response = httpClient.send(request, responseHandler);
+ String responseBody = response.body();
+ return (responseBody != null ? responseBody : "(no response)") + " Status Code:" + response.statusCode();
+ }
+ catch(Exception e)
+ {
+ return e.getLocalizedMessage();
+ }
+ }
+
+ public class AudioRecordingProcessor implements Runnable
+ {
+ @Override
+ public void run()
+ {
+ processRecordingQueue();
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ mLog.debug("Starting ...");
+
+ RdioScannerConfiguration config = new RdioScannerConfiguration();
+ config.setHost("https://api.RdioScanner.com/call-upload-dev");
+ config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
+ config.setSystemID(11);
+
+ String response = testConnection(config);
+
+ if(response == null)
+ {
+ mLog.debug("Test Successful!");
+ }
+ else
+ {
+ if(response.contains("1 Invalid-API-Key"))
+ {
+ mLog.error("Invalid API Key");
+ }
+ else if(response.contains("1 API-Key-Access-Denied"))
+ {
+ mLog.error("System ID not valid for API Key");
+ }
+ else
+ {
+ mLog.debug("Response: " + response);
+ }
+ }
+
+ mLog.debug("Finished!");
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBuilder.java b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBuilder.java
new file mode 100644
index 000000000..2e19ea530
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBuilder.java
@@ -0,0 +1,216 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.audio.broadcast.rdioscanner;
+
+import java.net.http.HttpRequest;
+import java.util.ArrayList;
+import java.util.List;
+import java.lang.*;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Builder for an HTTP body publisher to produce a RdioScanner call event
+ */
+public class RdioScannerBuilder
+{
+ private static final String DASH_DASH = "--";
+ private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
+ private List mParts = new ArrayList<>();
+ private byte[] audioBytes = null;
+
+ /**
+ * Constructs an instance
+ */
+ public RdioScannerBuilder()
+ {
+ }
+
+ /**
+ * Access the static multi-part boundary string
+ */
+ public String getBoundary()
+ {
+ return BOUNDARY;
+ }
+
+ /**
+ * Adds a Audio bytes part to the call
+ */
+ public RdioScannerBuilder addFile(byte[] value)
+ {
+ audioBytes = value;
+ return this;
+ }
+
+ /**
+ * Adds a string part to the call
+ */
+ public RdioScannerBuilder addPart(FormField key, String value)
+ {
+ if(key != null && value != null)
+ {
+ mParts.add(new Part(key.getHeader(), value));
+ }
+
+ return this;
+ }
+
+ /**
+ * Adds a number part to the call
+ */
+ public RdioScannerBuilder addPart(FormField key, Number value)
+ {
+ if(key != null && value != null)
+ {
+ mParts.add(new Part(key.getHeader(), value.toString()));
+ }
+
+ return this;
+ }
+
+ /**
+ * Creates a form-data item
+ */
+ private static String formatPart(Part part, String boundary)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(DASH_DASH).append(boundary).append("\r\n");
+ sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
+ sb.append(part.getValue()).append("\r\n");
+ return sb.toString();
+ }
+
+ /**
+ * Creates the audio file item
+ */
+ private String formatFilePart(String boundary)
+ {
+
+ if(audioBytes == null)
+ {
+ return "";
+ }
+ StringBuilder sb= new StringBuilder();
+ sb.append(DASH_DASH).append(boundary).append("\r\n");
+ sb.append("Content-Disposition: form-data; name=\"").append("audio").append("\"\r\n\r\n");
+ return sb.toString();
+
+ }
+
+ /**
+ * Creates the boundary closing item
+ */
+ private static String getClosingBoundary(String boundary)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
+ return sb.toString();
+ }
+
+ /**
+ * Creates a BodyPublisher for accessing the call form data
+ */
+ public HttpRequest.BodyPublisher build()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ for(Part part: mParts)
+ {
+ sb.append(formatPart(part, BOUNDARY));
+ }
+
+ sb.append(formatFilePart(BOUNDARY));
+
+
+ /**
+ * We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
+ */
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ try
+ {
+
+ outputStream.write(sb.toString().getBytes());
+
+ if(audioBytes != null)
+ {
+ outputStream.write(audioBytes);
+ }
+
+ sb = new StringBuilder();
+ sb.append("\r\n");
+ sb.append(getClosingBoundary(BOUNDARY));
+
+ outputStream.write(sb.toString().getBytes());
+ }
+ catch(IOException e)
+ {
+ //mLog.error("Rdio Scanner API - unable to create POST reqeust.");
+ }
+
+ return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ for(Part part: mParts)
+ {
+ sb.append(formatPart(part, BOUNDARY));
+ }
+
+ sb.append(getClosingBoundary(BOUNDARY));
+
+ return sb.toString();
+ }
+
+ /**
+ * Key:Value pair holder
+ */
+ public class Part
+ {
+ private String mKey;
+ private String mValue;
+
+ /**
+ * Constructs a new part
+ * @param key value
+ * @param value item
+ */
+ public Part(String key, String value)
+ {
+ mKey = key;
+ mValue = value;
+ }
+
+ public String getKey()
+ {
+ return mKey;
+ }
+
+ public String getValue()
+ {
+ return mValue;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerConfiguration.java
new file mode 100644
index 000000000..59702855c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerConfiguration.java
@@ -0,0 +1,132 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.audio.broadcast.rdioscanner;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
+import io.github.dsheirer.audio.broadcast.BroadcastFormat;
+import io.github.dsheirer.audio.broadcast.BroadcastServerType;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
+/**
+ * Streaming configuration for Rdio Scanner API.
+ *
+ * Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
+ * with the structure of the audio streaming subsystem in sdrtrunk
+ */
+public class RdioScannerConfiguration extends BroadcastConfiguration
+{
+ private IntegerProperty mSystemID = new SimpleIntegerProperty();
+ private StringProperty mApiKey = new SimpleStringProperty();
+
+ /**
+ * Constructor for faster jackson
+ */
+ public RdioScannerConfiguration()
+ {
+ this(BroadcastFormat.MP3);
+ }
+
+ /**
+ * Public constructor.
+ * @param format to use for audio recording (MP3)
+ */
+ public RdioScannerConfiguration(BroadcastFormat format)
+ {
+ super(format);
+
+ //The parent class binds this property, so we unbind it and rebind it here
+ mValid.unbind();
+ mValid.bind(Bindings.and(Bindings.and(Bindings.greaterThan(mSystemID, 0), Bindings.isNotNull(mApiKey)),
+ Bindings.isNotNull(mHost)));
+ }
+
+ /**
+ * System ID as a property
+ */
+ public IntegerProperty systemIDProperty()
+ {
+ return mSystemID;
+ }
+
+ /**
+ * API key as a property
+ */
+ public StringProperty apiKeyProperty()
+ {
+ return mApiKey;
+ }
+
+ /**
+ * API Key
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "api_key")
+ public String getApiKey()
+ {
+ return mApiKey.get();
+ }
+
+ /**
+ * Sets the api key
+ * @param apiKey
+ */
+ public void setApiKey(String apiKey)
+ {
+ mApiKey.setValue(apiKey);
+ }
+
+ /**
+ * System ID as provided by RdioScanner.com
+ */
+ @JacksonXmlProperty(isAttribute = true, localName = "system_id")
+ public int getSystemID()
+ {
+ return mSystemID.get();
+ }
+
+ /**
+ * Sets the system ID provided by RdioScanner.com
+ */
+ public void setSystemID(int systemID)
+ {
+ mSystemID.set(systemID);
+ }
+
+ @JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.RDIOSCANNER_CALL;
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ RdioScannerConfiguration copy = new RdioScannerConfiguration();
+ copy.setSystemID(getSystemID());
+ return copy;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerFeedConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerFeedConfiguration.java
new file mode 100644
index 000000000..302ce13c4
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerFeedConfiguration.java
@@ -0,0 +1,126 @@
+/*
+ *
+ * * ******************************************************************************
+ * * Copyright (C) 2014-2020 Dennis Sheirer
+ * *
+ * * This program is free software: you can redistribute it and/or modify
+ * * it under the terms of the GNU General Public License as published by
+ * * the Free Software Foundation, either version 3 of the License, or
+ * * (at your option) any later version.
+ * *
+ * * This program is distributed in the hope that it will be useful,
+ * * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * * GNU General Public License for more details.
+ * *
+ * * You should have received a copy of the GNU General Public License
+ * * along with this program. If not, see
+ * * *****************************************************************************
+ *
+ *
+ */
+package io.github.dsheirer.audio.broadcast.rdioscanner;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
+import io.github.dsheirer.audio.broadcast.BroadcastFormat;
+import io.github.dsheirer.audio.broadcast.BroadcastServerType;
+import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
+import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RdioScannerFeedConfiguration extends IcecastTCPConfiguration
+{
+ private final static Logger mLog = LoggerFactory.getLogger(RdioScannerFeedConfiguration.class);
+
+ private int mFeedID;
+
+ public RdioScannerFeedConfiguration()
+ {
+ //No-arg constructor for JAXB
+ this(BroadcastFormat.MP3);
+ }
+
+ /**
+ * RdioScanner configuration for Icecast 2.3.2 compatible servers
+ *
+ * @param format of output audio (MP3)
+ */
+ public RdioScannerFeedConfiguration(BroadcastFormat format)
+ {
+ super(format);
+
+ setBitRate(16);
+ setChannels(1);
+ setSampleRate(8000);
+ setInline(true);
+ }
+
+ public static RdioScannerFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
+ {
+ RdioScannerFeedConfiguration config = new RdioScannerFeedConfiguration(BroadcastFormat.MP3);
+ config.setName(userFeedBroadcast.getDescription());
+ config.setHost(userFeedBroadcast.getHostname());
+ config.setMountPoint(userFeedBroadcast.getMount());
+ config.setFeedID(userFeedBroadcast.getFeedId());
+ config.setPassword(userFeedBroadcast.getPassword());
+
+ try
+ {
+ config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
+ }
+ catch(Exception e)
+ {
+ mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
+ }
+
+ return config;
+ }
+
+ @Override
+ public BroadcastConfiguration copyOf()
+ {
+ RdioScannerFeedConfiguration copy = new RdioScannerFeedConfiguration(getBroadcastFormat());
+
+ //Broadcast Configuration Parameters
+ copy.setName(getName());
+ copy.setHost(getHost());
+ copy.setPort(getPort());
+ copy.setInline(getInline());
+ copy.setPassword(getPassword());
+ copy.setDelay(getDelay());
+ copy.setEnabled(false);
+
+ //Icecast Configuration Parameters
+ copy.setUserName(getUserName());
+ copy.setMountPoint(getMountPoint());
+ copy.setDescription(getDescription());
+ copy.setGenre(getGenre());
+ copy.setPublic(isPublic());
+ copy.setURL(getURL());
+
+ //RdioScanner Configuration Parameters
+ copy.setFeedID(getFeedID());
+
+ return copy;
+ }
+
+ @JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.RDIOSCANNER_CALL;
+ }
+
+ @JacksonXmlProperty(isAttribute = true, localName = "feed_id")
+ public int getFeedID()
+ {
+ return mFeedID;
+ }
+
+ public void setFeedID(int feedID)
+ {
+ mFeedID = feedID;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/RdioScannerEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/RdioScannerEditor.java
new file mode 100644
index 000000000..b56e44f92
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/RdioScannerEditor.java
@@ -0,0 +1,234 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.gui.playlist.streaming;
+
+import io.github.dsheirer.audio.broadcast.BroadcastServerType;
+import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
+import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
+import io.github.dsheirer.gui.control.IntegerTextField;
+import io.github.dsheirer.playlist.PlaylistManager;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.GridPane;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.controlsfx.control.ToggleSwitch;
+
+/**
+ * RdioScanner calls API configuration editor
+ */
+public class RdioScannerEditor extends AbstractBroadcastEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(RdioScannerEditor.class);
+ private IntegerTextField mSystemIdTextField;
+ private IntegerTextField mMaxAgeTextField;
+ private TextField mApiKeyTextField;
+ private TextField mHostTextField;
+ private GridPane mEditorPane;
+
+ /**
+ * Constructs an instance
+ * @param playlistManager for accessing the broadcast model
+ */
+ public RdioScannerEditor(PlaylistManager playlistManager)
+ {
+ super(playlistManager);
+ }
+
+ @Override
+ public void setItem(RdioScannerConfiguration item)
+ {
+ super.setItem(item);
+
+ getSystemIdTextField().setDisable(item == null);
+ getApiKeyTextField().setDisable(item == null);
+ getHostTextField().setDisable(item == null);
+ getMaxAgeTextField().setDisable(item == null);
+
+ if(item != null)
+ {
+ getSystemIdTextField().set(item.getSystemID());
+ getApiKeyTextField().setText(item.getApiKey());
+ getHostTextField().setText(item.getHost());
+ getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
+ }
+ else
+ {
+ getSystemIdTextField().set(0);
+ getApiKeyTextField().setText(null);
+ getHostTextField().setText(null);
+ getMaxAgeTextField().set(0);
+ }
+
+ modifiedProperty().set(false);
+ }
+
+ @Override
+ public void dispose()
+ {
+ }
+
+ @Override
+ public void save()
+ {
+ if(getItem() != null)
+ {
+ int systemID = getSystemIdTextField().get() != null ? getSystemIdTextField().get() : 0;
+ getItem().setSystemID(systemID);
+ getItem().setHost(getHostTextField().getText());
+ getItem().setApiKey(getApiKeyTextField().getText());
+ getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
+ }
+
+ super.save();
+ }
+
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.RDIOSCANNER_CALL;
+ }
+
+ protected GridPane getEditorPane()
+ {
+ if(mEditorPane == null)
+ {
+ mEditorPane = new GridPane();
+ mEditorPane.setPadding(new Insets(10, 5, 10,10));
+ mEditorPane.setVgap(10);
+ mEditorPane.setHgap(5);
+
+ int row = 0;
+
+ Label formatLabel = new Label("Format");
+ GridPane.setHalignment(formatLabel, HPos.RIGHT);
+ GridPane.setConstraints(formatLabel, 0, row);
+ mEditorPane.getChildren().add(formatLabel);
+
+ GridPane.setConstraints(getFormatField(), 1, row);
+ mEditorPane.getChildren().add(getFormatField());
+
+ Label enabledLabel = new Label("Enabled");
+ GridPane.setHalignment(enabledLabel, HPos.RIGHT);
+ GridPane.setConstraints(enabledLabel, 2, row);
+ mEditorPane.getChildren().add(enabledLabel);
+
+ GridPane.setConstraints(getEnabledSwitch(), 3, row);
+ mEditorPane.getChildren().add(getEnabledSwitch());
+
+ Label systemLabel = new Label("Name");
+ GridPane.setHalignment(systemLabel, HPos.RIGHT);
+ GridPane.setConstraints(systemLabel, 0, ++row);
+ mEditorPane.getChildren().add(systemLabel);
+
+ GridPane.setConstraints(getNameTextField(), 1, row);
+ mEditorPane.getChildren().add(getNameTextField());
+
+ Label apiKeyLabel = new Label("API Key");
+ GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
+ GridPane.setConstraints(apiKeyLabel, 0, ++row);
+ mEditorPane.getChildren().add(apiKeyLabel);
+
+ GridPane.setConstraints(getApiKeyTextField(), 1, row);
+ mEditorPane.getChildren().add(getApiKeyTextField());
+
+ Label systemIdLabel = new Label("System ID");
+ GridPane.setHalignment(systemIdLabel, HPos.RIGHT);
+ GridPane.setConstraints(systemIdLabel, 0, ++row);
+ mEditorPane.getChildren().add(systemIdLabel);
+
+ GridPane.setConstraints(getSystemIdTextField(), 1, row);
+ mEditorPane.getChildren().add(getSystemIdTextField());
+
+ Label hostLabel = new Label("RdioScanner URL");
+ GridPane.setHalignment(hostLabel, HPos.RIGHT);
+ GridPane.setConstraints(hostLabel, 0, ++row);
+ mEditorPane.getChildren().add(hostLabel);
+
+ GridPane.setConstraints(getHostTextField(), 1, row);
+ mEditorPane.getChildren().add(getHostTextField());
+
+ Label maxAgeLabel = new Label("Max Recording Age (seconds)");
+ GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
+ GridPane.setConstraints(maxAgeLabel, 0, ++row);
+ mEditorPane.getChildren().add(maxAgeLabel);
+
+ GridPane.setConstraints(getMaxAgeTextField(), 1, row);
+ mEditorPane.getChildren().add(getMaxAgeTextField());
+
+ }
+
+ return mEditorPane;
+ }
+
+ private IntegerTextField getMaxAgeTextField()
+ {
+ if(mMaxAgeTextField == null)
+ {
+ mMaxAgeTextField = new IntegerTextField();
+ mMaxAgeTextField.setDisable(true);
+ mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
+ }
+
+ return mMaxAgeTextField;
+ }
+
+ private TextField getHostTextField()
+ {
+ if(mHostTextField == null)
+ {
+ mHostTextField = new TextField();
+ mHostTextField.setDisable(true);
+ mHostTextField.textProperty().addListener(mEditorModificationListener);
+ }
+
+ return mHostTextField;
+ }
+
+ private TextField getApiKeyTextField()
+ {
+ if(mApiKeyTextField == null)
+ {
+ mApiKeyTextField = new TextField();
+ mApiKeyTextField.setDisable(true);
+ mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
+ }
+
+ return mApiKeyTextField;
+ }
+
+ private IntegerTextField getSystemIdTextField()
+ {
+ if(mSystemIdTextField == null)
+ {
+ mSystemIdTextField = new IntegerTextField();
+ mSystemIdTextField.setDisable(true);
+ mSystemIdTextField.textProperty().addListener(mEditorModificationListener);
+ }
+
+ return mSystemIdTextField;
+ }
+
+
+}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamEditorFactory.java b/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamEditorFactory.java
index 4d49d8f5c..2faf57c41 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamEditorFactory.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/streaming/StreamEditorFactory.java
@@ -38,6 +38,8 @@ public static AbstractBroadcastEditor getEditor(BroadcastServerType broadcastSer
{
case BROADCASTIFY:
return new BroadcastifyStreamEditor(playlistManager);
+ case RDIOSCANNER_CALL:
+ return new RdioScannerEditor(playlistManager);
case BROADCASTIFY_CALL:
return new BroadcastifyCallEditor(playlistManager);
case ICECAST_HTTP:
diff --git a/src/main/resources/images/rdioscanner.png b/src/main/resources/images/rdioscanner.png
new file mode 100644
index 000000000..141f4a10b
Binary files /dev/null and b/src/main/resources/images/rdioscanner.png differ