Get Stream Metadata with Plain Java for Your Android App

When developing for Android streaming music isn’t much of a problem using MediaPlayer API, getting stream’s metadata is little bit of a challenge. Since the SHOUTcast standard isn’t open-source there is no clear documentation on how to deal with metadata. One resource was able to explain the protocol. However, many online implementation are using a 3rd party library which isn’t suitable to drop-in into your Android project.

Below is a preliminary Java class written specifically to obtain metadata from a stream URL using only Java API thus suitable for any Android project. The class is based on another great example to retrieve metadata.

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class IcyStreamMeta {
 
	protected URL streamUrl;
	private Map<String, String> metadata;
	private boolean isError;
 
	public IcyStreamMeta(URL streamUrl) {
		setStreamUrl(streamUrl);
 
		isError = false;
	}
 
	/**
	 * Get artist using stream's title
	 *
	 * @return String
	 * @throws IOException
	 */
	public String getArtist() throws IOException {
		Map<String, String> data = getMetadata();
 
		if (!data.containsKey("StreamTitle"))
			return "";
 
		String streamTitle = data.get("StreamTitle");
		String title = streamTitle.substring(0, streamTitle.indexOf("-"));
		return title.trim();
	}
 
	/**
	 * Get title using stream's title
	 *
	 * @return String
	 * @throws IOException
	 */
	public String getTitle() throws IOException {
		Map<String, String> data = getMetadata();
 
		if (!data.containsKey("StreamTitle"))
			return "";
 
		String streamTitle = data.get("StreamTitle");
		String artist = streamTitle.substring(streamTitle.indexOf("-")+1);
		return artist.trim();
	}
 
	public Map<String, String> getMetadata() throws IOException {
		if (metadata == null) {
			refreshMeta();
		}
 
		return metadata;
	}
 
	public void refreshMeta() throws IOException {
		retreiveMetadata();
	}
 
	private void retreiveMetadata() throws IOException {
		URLConnection con = streamUrl.openConnection();
		con.setRequestProperty("Icy-MetaData", "1");
		con.setRequestProperty("Connection", "close");
		con.setRequestProperty("Accept", null);
		con.connect();
 
		int metaDataOffset = 0;
		Map<String, List<String>> headers = con.getHeaderFields();
		InputStream stream = con.getInputStream();
 
		if (headers.containsKey("icy-metaint")) {
			// Headers are sent via HTTP
			metaDataOffset = Integer.parseInt(headers.get("icy-metaint").get(0));
		} else {
			// Headers are sent within a stream
			StringBuilder strHeaders = new StringBuilder();
			char c;
			while ((c = (char)stream.read()) != -1) {
				strHeaders.append(c);
				if (strHeaders.length() > 5 && (strHeaders.substring((strHeaders.length() - 4), strHeaders.length()).equals("\r\n\r\n"))) {
					// end of headers
					break;
				}
			}
 
			// Match headers to get metadata offset within a stream
			Pattern p = Pattern.compile("\\r\\n(icy-metaint):\\s*(.*)\\r\\n");
			Matcher m = p.matcher(strHeaders.toString());
			if (m.find()) {
				metaDataOffset = Integer.parseInt(m.group(2));
			}
		}
 
		// In case no data was sent
		if (metaDataOffset == 0) {
			isError = true;
			return;
		}
 
		// Read metadata
		int b;
		int count = 0;
		int metaDataLength = 4080; // 4080 is the max length
		boolean inData = false;
		StringBuilder metaData = new StringBuilder();
		// Stream position should be either at the beginning or right after headers
		while ((b = stream.read()) != -1) {
			count++;
 
			// Length of the metadata
			if (count == metaDataOffset + 1) {
				metaDataLength = b * 16;
			}
 
			if (count > metaDataOffset + 1 && count < (metaDataOffset + metaDataLength)) { 				
				inData = true;
			} else { 				
				inData = false; 			
			} 	 			
			if (inData) { 				
				if (b != 0) { 					
					metaData.append((char)b); 				
				} 			
			} 	 			
			if (count > (metaDataOffset + metaDataLength)) {
				break;
			}
 
		}
 
		// Set the data
		metadata = IcyStreamMeta.parseMetadata(metaData.toString());
 
		// Close
		stream.close();
	}
 
	public boolean isError() {
		return isError;
	}
 
	public URL getStreamUrl() {
		return streamUrl;
	}
 
	public void setStreamUrl(URL streamUrl) {
		this.metadata = null;
		this.streamUrl = streamUrl;
		this.isError = false;
	}
 
	public static Map<String, String> parseMetadata(String metaString) {
		Map<String, String> metadata = new HashMap();
		String[] metaParts = metaString.split(";");
		Pattern p = Pattern.compile("^([a-zA-Z]+)=\\'([^\\']*)\\'$");
		Matcher m;
		for (int i = 0; i < metaParts.length; i++) {
			m = p.matcher(metaParts[i]);
			if (m.find()) {
				metadata.put((String)m.group(1), (String)m.group(2));
			}
		}
 
		return metadata;
	}
}

The class is suitable for most cases. You’ll be able to retrieve some common information like artist and song title. Other metadata is available in the Map returned by getMetadata().

To effectively use the class in your Android application I would suggest using AsyncTask. This will allow your app to asynchronously get the metadata and properly interact with UI thread. Below is the code I used:

protected class MetadataTask extends AsyncTask {
	protected IcyStreamMeta streamMeta;
 
	@Override
	protected IcyStreamMeta doInBackground(URL... urls) {
		streamMeta = new IcyStreamMeta(urls[0]);
		try {
			streamMeta.refreshMeta();
		} catch (IOException e) {
			// TODO: Handle
			Log.e(MetadataTask.class.toString(), e.getMessage());
		}
		return streamMeta;
	}
 
	@Override
	protected void onPostExecute(IcyStreamMeta result) {
		try {
			txtArtist.setText(streamMeta.getArtist());
			txtTitle.setText(streamMeta.getTitle());
		} catch (IOException e) {
			// TODO: Handle
			Log.e(MetadataTask.class.toString(), e.getMessage());
		}
	}
}

The code isn’t final, questions and comments are welcome.

How to serve Excel files on Google App Engine

Google App Engine is a hosting service for your Python and, lately, Java applications. In this case I’m going to talk about Python. Since an application cannot save files it might seem hard to serve an Excel file. Thankfully a great library pyExcelerator is very flexibile and instead of saving a file you can offer a user to download the dynamically generated Excel file. Here is what you do:

from pyExcelerator import *
 
class MainPage(webapp.RequestHandler):
    def get(self):
        wb = Workbook()
        ws0 = wb.add_sheet('Sheet 1')
        # Rows and columns count starts from 0
        for x in range(10):
            for y in range(10):
                # writing to a specific x,y
                ws0.write(x, y, "this is cell %s, %s" % (x,y))
 
        # HTTP headers to force file download
        self.response.headers['Content-Type'] = 'application/ms-excel'
        self.response.headers['Content-Transfer-Encoding'] = 'Binary'
        self.response.headers['Content-disposition'] = 'attachment; filename="workbook.xls"'
 
        # output to user
        wb.save(self.response.out)

Happy coding!