text: /*
   This file is part of Subsonic.
  
   Subsonic 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.
  
   Subsonic 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 Subsonic.  If not, see <http://www.gnu.org/licenses/>.
  
   Copyright 2009 (C) Sindre Mehus
   */
  package net.sourceforge.subsonic.io;
  
  import net.sourceforge.subsonic.Logger;
  import net.sourceforge.subsonic.domain.MediaFile;
  import net.sourceforge.subsonic.domain.Playlist;
  import net.sourceforge.subsonic.service.SettingsService;
  import org.apache.commons.lang.StringUtils;
  
  import java.io.IOException;
  import java.io.OutputStream;
  import java.io.UnsupportedEncodingException;
  
  /**
   * Implements SHOUTcast support by decorating an existing output stream.
   * <p/>
   * Based on protocol description found on
   * <em>http://www.smackfu.com/stuff/programming/shoutcast.html</em>
   *
   * @author Sindre Mehus
   */
  public class ShoutCastOutputStream extends OutputStream {
  
      private static final Logger LOG = Logger.getLogger(ShoutCastOutputStream.class);
  
      /**
       * Number of bytes between each SHOUTcast metadata block.
       */
      public static final int META_DATA_INTERVAL = 20480;
  
      /**
       * The underlying output stream to decorate.
       */
      private OutputStream out;
  
      /**
       * What to write in the SHOUTcast metadata is fetched from the playlist.
       */
      private Playlist playlist;
  
      /**
       * Keeps track of the number of bytes written (excluding meta-data).  Between 0 and {@link #META_DATA_INTERVAL}.
       */
      private int byteCount;
  
      /**
       * The last stream title sent.
       */
      private String previousStreamTitle;
  
      private SettingsService settingsService;
  
      /**
       * Creates a new SHOUTcast-decorated stream for the given output stream.
       *
       * @param out      The output stream to decorate.
       * @param playlist Meta-data is fetched from this playlist.
       */
      public ShoutCastOutputStream(OutputStream out, Playlist playlist, SettingsService settingsService) {
          this.out = out;
          this.playlist = playlist;
          this.settingsService = settingsService;
      }
  
      /**
       * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary.
       */
      public void write(byte[] b, int off, int len) throws IOException {
  
          int bytesWritten = 0;
          while (bytesWritten < len) {
  
              // 'n' is the number of bytes to write before the next potential meta-data block.
              int n = Math.min(len - bytesWritten, ShoutCastOutputStream.META_DATA_INTERVAL - byteCount);
  
              out.write(b, off + bytesWritten, n);
              bytesWritten += n;
              byteCount += n;
  
              // Reached meta-data block?
              if (byteCount % ShoutCastOutputStream.META_DATA_INTERVAL == 0) {
                  writeMetaData();
                  byteCount = 0;
              }
          }
      }
  
      /**
       * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary.
       */
      public void write(byte[] b) throws IOException {
          write(b, 0, b.length);
      }
  
      /**
       * Writes the given byte to the underlying stream, adding SHOUTcast meta-data as necessary.
       */
      public void write(int b) throws IOException {
          byte[] buf = new byte[]{(byte) b};
          write(buf);
      }
  
      /**
       * Flushes the underlying stream.
       */
      public void flush() throws IOException {
          out.flush();
      }
  
      /**
       * Closes the underlying stream.
       */
      public void close() throws IOException {
          out.close();
      }
  
      private void writeMetaData() throws IOException {
          String streamTitle = StringUtils.trimToEmpty(settingsService.getWelcomeTitle());
  
          MediaFile result;
          synchronized (playlist) {
              result = playlist.getCurrentFile();
          }
          MediaFile mediaFile = result;
          if (mediaFile != null) {
              streamTitle = mediaFile.getArtist() + " - " + mediaFile.getTitle();
          }
  
          byte[] bytes;
  
          if (streamTitle.equals(previousStreamTitle)) {
              bytes = new byte[0];
          } else {
              try {
                  previousStreamTitle = streamTitle;
                  bytes = createStreamTitle(streamTitle);
              } catch (UnsupportedEncodingException x) {
                  LOG.warn("Failed to create SHOUTcast meta-data.  Ignoring.", x);
                  bytes = new byte[0];
              }
          }
  
          // Length in groups of 16 bytes.
          int length = bytes.length / 16;
          if (bytes.length % 16 > 0) {
              length++;
          }
  
          // Write the length as a single byte.
          out.write(length);
  
          // Write the message.
          out.write(bytes);
  
          // Write padding zero bytes.
          int padding = length * 16 - bytes.length;
          for (int i = 0; i < padding; i++) {
              out.write(0);
          }
      }
  
      private byte[] createStreamTitle(String title) throws UnsupportedEncodingException {
          // Remove any quotes from the title.
          title = title.replaceAll("'", "");
  
          // Convert non-ascii characters to similar ascii characters.
          for (char[] chars : ShoutCastOutputStream.CHAR_MAP) {
              title = title.replace(chars[0], chars[1]);
          }
  
          title = "StreamTitle='" + title + "';";
          return title.getBytes("US-ASCII");
      }
  
      /**
       * Maps from miscellaneous accented characters to similar-looking ASCII characters.
       */
      private static final char[][] CHAR_MAP = {
              {'\u00C0', 'A'}, {'\u00C1', 'A'}, {'\u00C2', 'A'}, {'\u00C3', 'A'}, {'\u00C4', 'A'}, {'\u00C5', 'A'}, {'\u00C6', 'A'},
              {'\u00C8', 'E'}, {'\u00C9', 'E'}, {'\u00CA', 'E'}, {'\u00CB', 'E'}, {'\u00CC', 'I'}, {'\u00CD', 'I'}, {'\u00CE', 'I'},
              {'\u00CF', 'I'}, {'\u00D2', 'O'}, {'\u00D3', 'O'}, {'\u00D4', 'O'}, {'\u00D5', 'O'}, {'\u00D6', 'O'}, {'\u00D9', 'U'},
              {'\u00DA', 'U'}, {'\u00DB', 'U'}, {'\u00DC', 'U'}, {'\u00DF', 'B'}, {'\u00E0', 'a'}, {'\u00E1', 'a'}, {'\u00E2', 'a'},
              {'\u00E3', 'a'}, {'\u00E4', 'a'}, {'\u00E5', 'a'}, {'\u00E6', 'a'}, {'\u00E7', 'c'}, {'\u00E8', 'e'}, {'\u00E9', 'e'},
              {'\u00EA', 'e'}, {'\u00EB', 'e'}, {'\u00EC', 'i'}, {'\u00ED', 'i'}, {'\u00EE', 'i'}, {'\u00EF', 'i'}, {'\u00F1', 'n'},
              {'\u00F2', 'o'}, {'\u00F3', 'o'}, {'\u00F4', 'o'}, {'\u00F5', 'o'}, {'\u00F6', 'o'}, {'\u00F8', 'o'}, {'\u00F9', 'u'},
              {'\u00FA', 'u'}, {'\u00FB', 'u'}, {'\u00FC', 'u'}, {'\u2013', '-'}
      };
  }
  