/*
* IcyInputStream.
*
* jicyshout : http://sourceforge.net/projects/jicyshout/
*
* JavaZOOM : mp3spi@javazoom.net
* http://www.javazoom.net
*
*-----------------------------------------------------------------------
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as published
* by the Free Software Foundation; either version 2 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*----------------------------------------------------------------------
*/
package javazoom.spi.mpeg.sampled.file.tag;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.StringTokenizer;
/** An BufferedInputStream that parses Shoutcast's "icy" metadata
from the stream. Gets headers at the beginning and if the
"icy-metaint" tag is found, it parses and strips in-stream
metadata.
<p>
<b>The deal with metaint</b>: Icy streams don't try to put
tags between MP3 frames the way that ID3 does. Instead, it
requires the client to strip metadata from the stream before
it hits the decoder. You get an
<code>icy-metaint</code> name/val in the beginning of the
stream iff you sent "Icy-Metadata" with value "1" in the
request headers (SimpleMP3DataSource does this if the
"parseStreamMetadata" boolean is true). If this is the case
then the value of icy-metaint is the amount of real data
between metadata blocks. Each block begins with an int
indicating how much metadata there is -- the block is this
value times 16 (it can be, and often is, 0).
<p>
Originally thought that "icy" implied Icecast, but this is
completely wrong -- real Icecast servers, found through
www.icecast.net and typified by URLs with a trailing directory
(like CalArts School of Music - http://65.165.174.100:8000/som)
do not have the "ICY 200 OK" magic string or any of the
CRLF-separated headers. Apparently, "icy" means "Shoutcast".
Yep, that's weird.
@author Chris Adamson, invalidname@mac.com
*/
public class IcyInputStream
extends BufferedInputStream
implements MP3MetadataParser {
public static boolean DEBUG = false;
MP3TagParseSupport tagParseSupport;
/** inline tags are delimited by ';', also filter out
null bytes
*/
protected static final String INLINE_TAG_SEPARATORS = ";\u0000";
/* looks like icy streams start start with
ICY 200 OK\r\n
then the tags are like
icy-notice1:<BR>This stream requires <a href="http://www.winamp.com/">Winamp</a><BR>\r\n
icy-notice2:SHOUTcast Distributed Network Audio Server/win32 v1.8.2<BR>\r\n
icy-name:Core-upt Radio\r\n
icy-genre:Punk Ska Emo\r\n
icy-url:http://www.core-uptrecords.com\r\n
icy-pub:1\r\n
icy-metaint:8192\r\n
icy-br:56\r\n
\r\n (signifies end of headers)
we only get icy-metaint if the http request that created
this stream sent the header "icy-metadata:1"
//
in in-line metadata, we read a byte that tells us how
many 16-byte blocks there are (presumably, we still use
\r\n for the separator... the block is padded out with
0x00's that we can ignore)
// when server is full/down/etc, we get the following for
// one of the notice lines:
icy-notice2:This server has reached its user limit<BR>
or
icy-notice2:The resource requested is currently unavailable<BR>
*/
/** Tags that have been discovered in the stream.
*/
HashMap tags;
/** Buffer for readCRLF line... note this limits lines to
1024 chars (I've read that WinAmp barfs at 128, so
this is generous)
*/
protected byte[] crlfBuffer = new byte[1024];
/** value of the "metaint" tag, which tells us how many bytes
of real data are between the metadata tags. if -1, this stream
does not have metadata after the header.
*/
protected int metaint = -1;
/** how many bytes of real data remain before the next
block of metadata. Only meaningful if metaint != -1.
*/
protected int bytesUntilNextMetadata = -1;
// TODO: comment for constructor
/** Reads the initial headers of the stream and adds
tags appropriatly. Gets set up to find, read,
and strip blocks of in-line metadata if the
<code>icy-metaint</code> header is found.
*/
public IcyInputStream(InputStream in) throws IOException {
super(in);
tags = new HashMap();
tagParseSupport = new MP3TagParseSupport();
// read the initial tags here, including the metaint
// and set the counter for how far we read until
// the next metadata block (if any).
readInitialHeaders();
IcyTag metaIntTag = (IcyTag) getTag("icy-metaint");
if (DEBUG) System.out.println("METATAG:"+metaIntTag);
if (metaIntTag != null) {
String metaIntString = metaIntTag.getValueAsString();
try {
metaint = Integer.parseInt(metaIntString.trim());
if (DEBUG) System.out.println("METAINT:"+metaint);
bytesUntilNextMetadata = metaint;
}
catch (NumberFormatException nfe) {
}
}
}
/**
* IcyInputStream constructor for know meta-interval (Icecast 2)
* @param in
* @param metaint
* @throws IOException
*/
public IcyInputStream(InputStream in, String metaIntString) throws IOException {
super(in);
tags = new HashMap();
tagParseSupport = new MP3TagParseSupport();
try
{
metaint = Integer.parseInt(metaIntString.trim());
if (DEBUG) System.out.println("METAINT:"+metaint);
bytesUntilNextMetadata = metaint;
}
catch (NumberFormatException nfe) {
}
}
/** Assuming we're at the top of the stream, read lines one
by one until we hit a completely blank \r\n. Parse the
data as IcyTags.
*/
protected void readInitialHeaders() throws IOException {
String line = null;
while (!((line = readCRLFLine()).equals(""))) {
int colonIndex = line.indexOf(':');
// does it have a ':' separator
if (colonIndex == -1)
continue;
IcyTag tag =
new IcyTag(
line.substring(0, colonIndex),
line.substring(colonIndex + 1));
//System.out.println(tag);
addTag(tag);
}
}
/** Read everything up to the next CRLF, return it as
a String.
*/
protected String readCRLFLine() throws IOException {
int i = 0;
for (; i < crlfBuffer.length; i++) {
byte aByte = (byte) read();
if (aByte == '\r') {
// possible end of line
byte anotherByte = (byte) read();
i++; // since we read again
if (anotherByte == '\n') {
break; // break out of while
}
else {
// oops, not end of line - put these in array
crlfBuffer[i - 1] = aByte;
crlfBuffer[i] = anotherByte;
}
}
else {
// if not \r
crlfBuffer[i] = aByte;
}
} // for
// get the string from the byte[]. i is 1 too high because of
// read-ahead in crlf block
return new String(crlfBuffer, 0, i - 1);
}
/** Reads and returns a single byte.
If the next byte is a metadata block, then that
block is read, stripped, and parsed before reading
and returning the first byte after the metadata block.
*/
public int read() throws IOException {
if (bytesUntilNextMetadata > 0) {
bytesUntilNextMetadata--;
return super.read();
}
else if (bytesUntilNextMetadata == 0) {
// we need to read next metadata block
readMetadata();
bytesUntilNextMetadata = metaint - 1;
// -1 because we read byte on next line
return super.read();
}
else {
// no metadata in this stream
return super.read();
}
}
/** Reads a block of bytes. If the next byte is known
to be a block of metadata, then that is read, parsed,
and stripped, and then a block of bytes is read and
returned.
Otherwise, it may read up to but
not into the next metadata block if
<code>bytesUntilNextMetadata < length</code>
*/
public int read(byte[] buf, int offset, int length) throws IOException {
// if not on metadata, do the usual read so long as we
// don't read past metadata
if (bytesUntilNextMetadata > 0) {
int adjLength = Math.min(length, bytesUntilNextMetadata);
int got = super.read(buf, offset, adjLength);
bytesUntilNextMetadata -= got;
return got;
}
else if (bytesUntilNextMetadata == 0) {
// read/parse the metadata
readMetadata();
// now as above, except that we reset
// bytesUntilNextMetadata differently
//int adjLength = Math.min(length, bytesUntilNextMetadata);
//int got = super.read(buf, offset, adjLength);
//bytesUntilNextMetadata = metaint - got;
// Chop Fix - JavaZOOM (3 lines above seem buggy)
bytesUntilNextMetadata = metaint;
int adjLength = Math.min(length, bytesUntilNextMetadata);
int got = super.read(buf, offset, adjLength);
bytesUntilNextMetadata -= got;
// End fix - JavaZOOM
return got;
}
else {
// not even reading metadata
return super.read(buf, offset, length);
}
}
/** trivial <code>return read (buf, 0, buf.length)</code>
*/
public int read(byte[] buf) throws IOException {
return read(buf, 0, buf.length);
}
/** Read the next segment of metadata. The stream <b>must</b>
be right on the segment, ie, the next byte to read is
the metadata block count. The metadata is parsed and
new tags are added with addTag(), which fires events
*/
protected void readMetadata() throws IOException {
int blockCount = super.read();
if (DEBUG) System.out.println("BLOCKCOUNT:"+blockCount);
// System.out.println ("blocks to read: " + blockCount);
int byteCount = (blockCount * 16); // 16 bytes per block
if (byteCount < 0)
return; // WTF?!
byte[] metadataBlock = new byte[byteCount];
int index = 0;
// build an array of this metadata
while (byteCount > 0) {
int bytesRead = super.read(metadataBlock, index, byteCount);
index += bytesRead;
byteCount -= bytesRead;
}
// now parse it
if (blockCount > 0)
parseInlineIcyTags(metadataBlock);
} // readMetadata
/** Parse metadata from an in-stream "block" of bytes, add
a tag for each one.
<p>
Hilariously, the inline data format is totally different
than the top-of-stream header. For example, here's a
block I saw on "Final Fantasy Radio":
<pre>
StreamTitle='Final Fantasy 8 - Nobuo Uematsu - Blue Fields';StreamUrl='';
</pre>
In other words:
<ol>
<li>Tags are delimited by semicolons
<li>Keys/values are delimited by equals-signs
<li>Values are wrapped in single-quotes
<li>Key names are in SentenceCase, not lowercase-dashed
</ol>
*/
protected void parseInlineIcyTags(byte[] tagBlock)
{
String blockString = null;
try
{
// Parse string as ISO-8859-1 even if meta-data are in US-ASCII.
blockString = new String(tagBlock,"ISO-8859-1");
}
catch(UnsupportedEncodingException e)
{
blockString = new String(tagBlock);
}
if (DEBUG) System.out.println("BLOCKSTR:"+blockString);
StringTokenizer izer =
new StringTokenizer(blockString, INLINE_TAG_SEPARATORS);
int i = 0;
while (izer.hasMoreTokens()) {
String tagString = izer.nextToken();
int separatorIdx = tagString.indexOf('=');
if (separatorIdx == -1)
continue; // bogus tagString if no '='
// try to strip single-quotes around value, if present
int valueStartIdx =
(tagString.charAt(separatorIdx + 1) == '\'')
? separatorIdx + 2
: separatorIdx + 1;
int valueEndIdx =
(tagString.charAt(tagString.length() - 1)) == '\''
? tagString.length() - 1
: tagString.length();
String name = tagString.substring(0, separatorIdx);
String value = tagString.substring(valueStartIdx, valueEndIdx);
// System.out.println (i++ + " " + name + ":" + value);
IcyTag tag = new IcyTag(name, value);
addTag(tag);
}
}
/** adds the tag to the HashMap of tags we have encountered
either in-stream or as headers, replacing any previous
tag with this name.
*/
protected void addTag(IcyTag tag) {
tags.put(tag.getName(), tag);
// fire this as an event too
tagParseSupport.fireTagParsed(this, tag);
}
/** Get the named tag from the HashMap of headers and
in-line tags. Null if no such tag has been encountered.
*/
public MP3Tag getTag(String tagName) {
return (MP3Tag) tags.get(tagName);
}
/** Get all tags (headers or in-stream) encountered thus far.
*/
public MP3Tag[] getTags() {
return (MP3Tag[]) tags.values().toArray(new MP3Tag[0]);
}
/** Returns a HashMap of all headers and in-stream tags
parsed so far.
*/
public HashMap getTagHash() {
return tags;
}
/** Adds a TagParseListener to be notified when this stream
parses MP3Tags.
*/
public void addTagParseListener(TagParseListener tpl) {
tagParseSupport.addTagParseListener(tpl);
}
/** Removes a TagParseListener, so it won't be notified when
this stream parses MP3Tags.
*/
public void removeTagParseListener(TagParseListener tpl) {
tagParseSupport.removeTagParseListener(tpl);
}
/** Quickie unit-test.
*/
public static void main(String args[]) {
byte[] chow = new byte[200];
if (args.length != 1) {
//System.out.println("Usage: IcyInputStream <url>");
return;
}
try {
URL url = new URL(args[0]);
URLConnection conn = url.openConnection();
conn.setRequestProperty("Icy-Metadata", "1");
IcyInputStream icy =
new IcyInputStream(
new BufferedInputStream(conn.getInputStream()));
while (icy.available() > -1) {
// icy.read();
icy.read(chow, 0, chow.length);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
}