mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
271 lines
9.9 KiB
Java
271 lines
9.9 KiB
Java
/*******************************************************************************
|
|
* sdrtrunk
|
|
* Copyright (C) 2014-2017 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 audio.broadcast.icecast;
|
|
|
|
import audio.broadcast.IBroadcastMetadataUpdater;
|
|
import channel.metadata.Metadata;
|
|
import org.apache.mina.core.future.ConnectFuture;
|
|
import org.apache.mina.core.service.IoHandlerAdapter;
|
|
import org.apache.mina.core.session.IoSession;
|
|
import org.apache.mina.http.HttpClientCodec;
|
|
import org.apache.mina.http.HttpRequestImpl;
|
|
import org.apache.mina.http.api.HttpMethod;
|
|
import org.apache.mina.http.api.HttpRequest;
|
|
import org.apache.mina.http.api.HttpVersion;
|
|
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import properties.SystemProperties;
|
|
import util.ThreadPool;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.net.ConnectException;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.URLEncoder;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Queue;
|
|
import java.util.concurrent.LinkedTransferQueue;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
|
{
|
|
private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class);
|
|
private final static String UTF8 = "UTF-8";
|
|
|
|
private IcecastConfiguration mIcecastConfiguration;
|
|
|
|
private NioSocketConnector mSocketConnector;
|
|
private AtomicBoolean mUpdating = new AtomicBoolean();
|
|
private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
|
|
private Map<String,String> mHTTPHeaders;
|
|
private boolean mStackTraceLoggingSuppressed;
|
|
|
|
/**
|
|
* Icecast song metadata updater. Each metadata update is processed in the order received and the dispatch of
|
|
* the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
|
|
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
|
|
* sequence, those updates will be queued and processed in the order received.
|
|
*/
|
|
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration)
|
|
{
|
|
mIcecastConfiguration = icecastConfiguration;
|
|
}
|
|
|
|
/**
|
|
* Socket connector - lazy constructor.
|
|
*/
|
|
private NioSocketConnector getSocketConnector()
|
|
{
|
|
if(mSocketConnector == null)
|
|
{
|
|
mSocketConnector = new NioSocketConnector();
|
|
// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastBroadcastMetadataUpdater.class));
|
|
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
|
|
|
|
//Each metadata update session is single-use, so we'll shut it down upon success or failure
|
|
mSocketConnector.setHandler(new IoHandlerAdapter()
|
|
{
|
|
@Override
|
|
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
|
{
|
|
//Single-use session - close it after we receive an error
|
|
session.closeNow();
|
|
|
|
mLog.error("Metadata update failed");
|
|
}
|
|
|
|
@Override
|
|
public void messageReceived(IoSession session, Object message) throws Exception
|
|
{
|
|
//Single-use session - close it after we receive a response
|
|
session.closeNow();
|
|
}
|
|
});
|
|
}
|
|
|
|
return mSocketConnector;
|
|
}
|
|
|
|
/**
|
|
* Sends a song metadata update to the remote server. Additional metadata updates received while the updater
|
|
* is in the process of sending an existing metadata update will be queued and processed in received order.
|
|
*
|
|
* Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
|
|
* and will remain in the update queue until the next metadata update is requested. However, this is a design
|
|
* trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
|
|
*/
|
|
public void update(Metadata metadata)
|
|
{
|
|
mMetadataQueue.offer(getSong(metadata));
|
|
|
|
if(mUpdating.compareAndSet(false, true))
|
|
{
|
|
String song = mMetadataQueue.poll();
|
|
|
|
while(song != null)
|
|
{
|
|
HttpRequest updateRequest = createUpdateRequest(song);
|
|
|
|
if(updateRequest != null)
|
|
{
|
|
ThreadPool.SCHEDULED.schedule(new Runnable()
|
|
{
|
|
@Override
|
|
public void run()
|
|
{
|
|
try
|
|
{
|
|
ConnectFuture connectFuture = getSocketConnector()
|
|
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
|
mIcecastConfiguration.getPort()));
|
|
connectFuture.awaitUninterruptibly();
|
|
IoSession session = connectFuture.getSession();
|
|
|
|
if(session != null)
|
|
{
|
|
session.write(updateRequest);
|
|
}
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
Throwable throwableCause = e.getCause();
|
|
|
|
if(throwableCause instanceof ConnectException)
|
|
{
|
|
//Do nothing, the server is unavailable
|
|
}
|
|
else
|
|
{
|
|
if(!mStackTraceLoggingSuppressed)
|
|
{
|
|
mLog.error("Error sending metadata update. Future errors will " +
|
|
"be suppressed", e);
|
|
|
|
mStackTraceLoggingSuppressed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, 0l, TimeUnit.SECONDS);
|
|
}
|
|
|
|
//Fetch next metadata update to send
|
|
song = mMetadataQueue.poll();
|
|
}
|
|
|
|
mUpdating.set(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the song information for a metadata update
|
|
*/
|
|
private static String getSong(Metadata metadata)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
if(metadata != null)
|
|
{
|
|
String to = metadata.getPrimaryAddressTo().getIdentifier();
|
|
|
|
sb.append("TO:");
|
|
|
|
if(metadata.getPrimaryAddressTo().hasAlias())
|
|
{
|
|
sb.append(metadata.getPrimaryAddressTo().getAlias().getName());
|
|
}
|
|
else if(metadata.getPrimaryAddressTo().hasIdentifier())
|
|
{
|
|
sb.append(metadata.getPrimaryAddressTo().getIdentifier());
|
|
}
|
|
else
|
|
{
|
|
sb.append("UNKNOWN");
|
|
}
|
|
|
|
|
|
sb.append(" FROM:");
|
|
|
|
if(metadata.getPrimaryAddressFrom().hasAlias())
|
|
{
|
|
sb.append(metadata.getPrimaryAddressFrom().getAlias().getName());
|
|
}
|
|
else if(metadata.getPrimaryAddressFrom().hasIdentifier())
|
|
{
|
|
sb.append(metadata.getPrimaryAddressFrom().getIdentifier());
|
|
}
|
|
else
|
|
{
|
|
sb.append("UNKNOWN");
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
sb.append("Scanning ....");
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Creates an HTTP GET request to update the metadata on the remote server
|
|
*/
|
|
private HttpRequest createUpdateRequest(String song)
|
|
{
|
|
try
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("mode=updinfo");
|
|
sb.append("&mount=").append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8));
|
|
sb.append("&charset=UTF%2d8");
|
|
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
|
|
|
|
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
|
|
sb.toString(), getHTTPHeaders());
|
|
|
|
return request;
|
|
}
|
|
catch(UnsupportedEncodingException e)
|
|
{
|
|
//This should never happen
|
|
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* HTTP headers to use for a metadata update request.
|
|
*/
|
|
private Map<String,String> getHTTPHeaders()
|
|
{
|
|
if(mHTTPHeaders == null)
|
|
{
|
|
mHTTPHeaders = new HashMap<>();
|
|
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
|
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials());
|
|
}
|
|
|
|
return mHTTPHeaders;
|
|
}
|
|
}
|