Add name to all controlled components.
[sonitus.git] / src / main / java / net / pterodactylus / sonitus / data / sink / Icecast2Sink.java
index f3c6039..36413f9 100644 (file)
 
 package net.pterodactylus.sonitus.data.sink;
 
-import static com.google.common.base.Preconditions.*;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.Socket;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import net.pterodactylus.sonitus.data.ConnectException;
-import net.pterodactylus.sonitus.data.Connection;
-import net.pterodactylus.sonitus.data.Format;
+import net.pterodactylus.sonitus.data.Controller;
+import net.pterodactylus.sonitus.data.Metadata;
 import net.pterodactylus.sonitus.data.Sink;
-import net.pterodactylus.sonitus.data.Source;
 import net.pterodactylus.sonitus.io.InputStreamDrainer;
 
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.Closeables;
 
 /**
- * {@link Sink} implementation that delivers all incoming data to an Icecast2
- * server.
+ * {@link net.pterodactylus.sonitus.data.Sink} implementation that delivers all
+ * incoming data to an Icecast2 server.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -71,6 +76,8 @@ public class Icecast2Sink implements Sink {
        /** Whether to publish the server. */
        private final boolean publishServer;
 
+       private OutputStream socketOutputStream;
+
        /**
         * Creates a new Icecast2 sink.
         *
@@ -104,57 +111,103 @@ public class Icecast2Sink implements Sink {
        }
 
        //
+       // CONTROLLED METHODS
+       //
+
+       @Override
+       public String name() {
+               return String.format("icecast://%s:%d/%s", server, port, mountPoint);
+       }
+
+       @Override
+       public List<Controller<?>> controllers() {
+               return Collections.emptyList();
+       }
+
+       //
        // SINK METHODS
        //
 
        @Override
-       public void connect(Source source) throws ConnectException {
-               checkNotNull(source, "source must not be null");
+       public void open(Metadata metadata) throws IOException {
+               logger.info(String.format("Connecting to %s:%d...", server, port));
+               Socket socket = new Socket(server, port);
+               logger.info("Connected.");
+               socketOutputStream = socket.getOutputStream();
+               InputStream socketInputStream = socket.getInputStream();
+
+               sendLine(socketOutputStream, String.format("SOURCE /%s ICE/1.0", mountPoint));
+               sendLine(socketOutputStream, String.format("Authorization: Basic %s", generatePassword(password)));
+               sendLine(socketOutputStream, String.format("Content-Type: %s", getContentType(metadata)));
+               sendLine(socketOutputStream, String.format("ICE-Name: %s", serverName));
+               sendLine(socketOutputStream, String.format("ICE-Description: %s", serverDescription));
+               sendLine(socketOutputStream, String.format("ICE-Genre: %s", genre));
+               sendLine(socketOutputStream, String.format("ICE-Public: %d", publishServer ? 1 : 0));
+               sendLine(socketOutputStream, "");
+               socketOutputStream.flush();
+
+               new Thread(new InputStreamDrainer(socketInputStream)).start();
 
+               metadataUpdated(metadata);
+       }
+
+       @Override
+       public void close() {
                try {
-                       logger.info(String.format("Icecast2Sink: Connecting to %s:%d...", server, port));
-                       final Socket socket = new Socket(server, port);
-                       logger.info("Icecast2Sink: Connected.");
-                       final OutputStream socketOutputStream = socket.getOutputStream();
-                       final InputStream socketInputStream = socket.getInputStream();
-
-                       sendLine(socketOutputStream, String.format("SOURCE /%s ICE/1.0", mountPoint));
-                       sendLine(socketOutputStream, String.format("Authorization: Basic %s", generatePassword(password)));
-                       sendLine(socketOutputStream, String.format("Content-Type: %s", getContentType(source.format())));
-                       sendLine(socketOutputStream, String.format("ICE-Name: %s", serverName));
-                       sendLine(socketOutputStream, String.format("ICE-Description: %s", serverDescription));
-                       sendLine(socketOutputStream, String.format("ICE-Genre: %s", genre));
-                       sendLine(socketOutputStream, String.format("ICE-Public: %d", publishServer ? 1 : 0));
-                       sendLine(socketOutputStream, "");
-                       socketOutputStream.flush();
-
-                       new Thread(new InputStreamDrainer(socketInputStream)).start();
-                       new Thread(new Connection(source) {
-
-                               private long counter;
-
-                               @Override
-                               protected int bufferSize() {
-                                       return 4096;
-                               }
+                       Closeables.close(socketOutputStream, true);
+               } catch (IOException e) {
+                       /* will never throw. */
+               }
+       }
+
+       @Override
+       public void metadataUpdated(final Metadata metadata) {
+               new Thread(new Runnable() {
+
+                       @Override
+                       public void run() {
+                               String metadataString = String.format("%s (%s)", Joiner.on(" - ").skipNulls().join(FluentIterable.from(Arrays.asList(metadata.artist(), metadata.name())).transform(new Function<Optional<String>, Object>() {
 
-                               @Override
-                               protected void feed(byte[] buffer) throws IOException {
-                                       socketOutputStream.write(buffer);
+                                       @Override
+                                       public Object apply(Optional<String> input) {
+                                               return input.orNull();
+                                       }
+                               })), "Sonitus");
+                               logger.info(String.format("Updating metadata to %s", metadataString));
+
+                               Socket socket = null;
+                               OutputStream socketOutputStream = null;
+                               try {
+                                       socket = new Socket(server, port);
+                                       socketOutputStream = socket.getOutputStream();
+
+                                       sendLine(socketOutputStream, String.format("GET /admin/metadata?pass=%s&mode=updinfo&mount=/%s&song=%s HTTP/1.0", password, mountPoint, URLEncoder.encode(metadataString, "UTF-8")));
+                                       sendLine(socketOutputStream, String.format("Authorization: Basic %s", generatePassword(password)));
+                                       sendLine(socketOutputStream, String.format("User-Agent: Mozilla/Sonitus"));
+                                       sendLine(socketOutputStream, "");
                                        socketOutputStream.flush();
-                                       counter += buffer.length;
-                                       logger.finest(String.format("Wrote %d Bytes.", counter));
-                               }
 
-                               @Override
-                               protected void finish() throws IOException {
-                                       Closeables.close(socketOutputStream, true);
-                                       Closeables.close(socket, true);
+                                       new InputStreamDrainer(socket.getInputStream()).run();
+                               } catch (IOException ioe1) {
+                                       logger.log(Level.WARNING, "Could not update metadata!", ioe1);
+                               } finally {
+                                       try {
+                                               Closeables.close(socketOutputStream, true);
+                                               if (socket != null) {
+                                                       socket.close();
+                                               }
+                                       } catch (IOException ioe1) {
+                                               /* ignore. */
+                                       }
                                }
-                       }).start();
-               } catch (IOException ioe1) {
-                       throw new ConnectException(ioe1);
-               }
+                       }
+               }).start();
+       }
+
+       @Override
+       public void process(byte[] buffer) throws IOException {
+               socketOutputStream.write(buffer);
+               socketOutputStream.flush();
        }
 
        //
@@ -169,7 +222,7 @@ public class Icecast2Sink implements Sink {
         *              The output stream to send the line to
         * @param line
         *              The line to send
-        * @throws IOException
+        * @throws java.io.IOException
         *              if an I/O error occurs
         */
        private static void sendLine(OutputStream outputStream, String line) throws IOException {
@@ -183,7 +236,7 @@ public class Icecast2Sink implements Sink {
         * @param password
         *              The password to encode
         * @return The encoded password
-        * @throws UnsupportedEncodingException
+        * @throws java.io.UnsupportedEncodingException
         *              if the UTF-8 encoding is not supported (which can never happen)
         */
        private static String generatePassword(String password) throws UnsupportedEncodingException {
@@ -191,24 +244,34 @@ public class Icecast2Sink implements Sink {
        }
 
        /**
-        * Returns a MIME type for the given format. Currently only Vorbis, MP3, and
-        * PCM formats are recognized.
+        * Returns a MIME type for the given metadata. Currently only Vorbis, MP3, PCM,
+        * Ogg Vorbis, Opus, and FLAC formats are recognized.
         *
-        * @param format
-        *              The format to get a MIME type for
-        * @return The MIME type of the format
+        * @param metadata
+        *              The metadata to get a MIME type for
+        * @return The MIME type of the metadata
         */
-       private static String getContentType(Format format) {
-               switch (format.encoding().toUpperCase()) {
-                       case "VORBIS":
-                               return "audio/ogg";
-                       case "MP3":
-                               return "audio/mpeg";
-                       case "PCM":
-                               return "audio/vnd.wave";
-                       default:
-                               return "application/octet-stream";
+       private static String getContentType(Metadata metadata) {
+               String encoding = metadata.encoding();
+               if ("Vorbis".equalsIgnoreCase(encoding)) {
+                       return "audio/ogg";
+               }
+               if ("MP3".equalsIgnoreCase(encoding)) {
+                       return "audio/mpeg";
+               }
+               if ("PCM".equalsIgnoreCase(encoding)) {
+                       return "audio/vnd.wave";
+               }
+               if ("Vorbis".equalsIgnoreCase(encoding)) {
+                       return "application/ogg";
+               }
+               if ("Opus".equalsIgnoreCase(encoding)) {
+                       return "audio/ogg; codecs=opus";
+               }
+               if ("FLAC".equalsIgnoreCase(encoding)) {
+                       return "audio/flac";
                }
+               return "application/octet-stream";
        }
 
 }