mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-14 01:27:34 -05:00
#1476 Adds rdio-scanner call upload API streaming support --------- Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
|||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
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.icecast.IcecastConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||||
@@ -46,6 +47,7 @@ import java.net.SocketAddress;
|
|||||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||||
|
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import io.github.dsheirer.alias.AliasModel;
|
|||||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
|
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
|
||||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
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.IcecastHTTPAudioBroadcaster;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||||
@@ -61,6 +64,9 @@ public class BroadcastFactory
|
|||||||
case BROADCASTIFY_CALL:
|
case BROADCASTIFY_CALL:
|
||||||
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
|
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
|
||||||
inputAudioFormat, mp3Setting, aliasModel);
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
|
case RDIOSCANNER_CALL:
|
||||||
|
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||||
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||||
inputAudioFormat, mp3Setting, aliasModel);
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
@@ -99,6 +105,8 @@ public class BroadcastFactory
|
|||||||
{
|
{
|
||||||
case BROADCASTIFY_CALL:
|
case BROADCASTIFY_CALL:
|
||||||
return new BroadcastifyCallConfiguration(format);
|
return new BroadcastifyCallConfiguration(format);
|
||||||
|
case RDIOSCANNER_CALL:
|
||||||
|
return new RdioScannerConfiguration(format);
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new BroadcastifyFeedConfiguration(format);
|
return new BroadcastifyFeedConfiguration(format);
|
||||||
case ICECAST_HTTP:
|
case ICECAST_HTTP:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public enum BroadcastServerType
|
|||||||
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
|
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
|
||||||
|
|
||||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.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"),
|
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<RdioScannerConfiguration>
|
||||||
|
{
|
||||||
|
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<AudioRecording> 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<Alias> 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<Alias> 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<Identifier> 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<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponse<String> 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Part> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* * *****************************************************************************
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<RdioScannerConfiguration>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ public class StreamEditorFactory
|
|||||||
{
|
{
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new BroadcastifyStreamEditor(playlistManager);
|
return new BroadcastifyStreamEditor(playlistManager);
|
||||||
|
case RDIOSCANNER_CALL:
|
||||||
|
return new RdioScannerEditor(playlistManager);
|
||||||
case BROADCASTIFY_CALL:
|
case BROADCASTIFY_CALL:
|
||||||
return new BroadcastifyCallEditor(playlistManager);
|
return new BroadcastifyCallEditor(playlistManager);
|
||||||
case ICECAST_HTTP:
|
case ICECAST_HTTP:
|
||||||
|
|||||||
BIN
src/main/resources/images/rdioscanner.png
Normal file
BIN
src/main/resources/images/rdioscanner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Reference in New Issue
Block a user