View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.ambient.stream.shoutcast;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.text.ParseException;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.Comparator;
29  import java.util.Iterator;
30  import java.util.List;
31  
32  import org.xml.sax.Attributes;
33  import org.xml.sax.SAXException;
34  import org.xml.sax.helpers.DefaultHandler;
35  
36  import sk.baka.ambient.collection.TrackMetadataBean;
37  import sk.baka.ambient.commons.IOUtils;
38  import sk.baka.ambient.commons.MiscUtils;
39  import sk.baka.ambient.playlist.Parsers;
40  
41  /***
42   * Utility methods for SHOUTcast support.
43   * 
44   * @author Martin Vysny
45   */
46  public final class ShoutcastUtils {
47  	private ShoutcastUtils() {
48  		throw new Error();
49  	}
50  
51  	/***
52  	 * Parses a playlist and returns list of all radio URLs (the
53  	 * <code>File?=...</code> lines; other lines are ignored).
54  	 * 
55  	 * @param in
56  	 *            the playlist stream. the stream is always closed.
57  	 * @return list of URLs
58  	 * @throws IOException
59  	 *             if i/o error occurs.
60  	 * @throws ParseException
61  	 *             if the playlist is not well formed.
62  	 */
63  	public static List<TrackMetadataBean> parsePlaylist(final InputStream in)
64  			throws IOException, ParseException {
65  		return Parsers.parsePls(in, null);
66  	}
67  
68  	/***
69  	 * The genres list XML location.
70  	 */
71  	public static final URL GENRES_LIST;
72  	static {
73  		try {
74  			GENRES_LIST = new URL("http://www.shoutcast.com/sbin/newxml.phtml");
75  		} catch (MalformedURLException e) {
76  			throw new RuntimeException(e);
77  		}
78  	}
79  
80  	/***
81  	 * Parses the SHOUTcast genre list XML. Reads the XML from the server.
82  	 * 
83  	 * @return list of genres
84  	 * @throws IOException
85  	 * @throws SAXException
86  	 */
87  	public static List<String> parseGenres() throws IOException, SAXException {
88  		return parseGenres(GENRES_LIST.openStream());
89  	}
90  
91  	/***
92  	 * Parses the SHOUTcast genre list XML.
93  	 * 
94  	 * @param in
95  	 *            the XML to read. the stream is always closed.
96  	 * @return list of genres
97  	 * @throws IOException
98  	 * @throws SAXException
99  	 */
100 	public static List<String> parseGenres(final InputStream in)
101 			throws IOException, SAXException {
102 		try {
103 			final List<String> result = new ArrayList<String>();
104 			IOUtils.parseXML(in, new GenreHandler(result));
105 			Collections.sort(result, String.CASE_INSENSITIVE_ORDER);
106 			return result;
107 		} finally {
108 			MiscUtils.closeQuietly(in);
109 		}
110 	}
111 
112 	/***
113 	 * Handles list of genres.
114 	 * 
115 	 * @author Martin Vysny
116 	 */
117 	private static class GenreHandler extends DefaultHandler {
118 		/***
119 		 * Store the genres here.
120 		 */
121 		private final List<String> result;
122 
123 		/***
124 		 * Creates new handler instance.
125 		 * 
126 		 * @param result
127 		 *            store the genres here.
128 		 */
129 		public GenreHandler(List<String> result) {
130 			this.result = result;
131 		}
132 
133 		@Override
134 		public void startElement(String uri, String localName, String name,
135 				Attributes attributes) {
136 			if (Thread.currentThread().isInterrupted()) {
137 				throw new RuntimeException("interrupted");
138 			}
139 			if (!localName.equals("genre")) {
140 				return;
141 			}
142 			final String genre = attributes.getValue("name");
143 			if (genre != null) {
144 				result.add(genre);
145 			}
146 		}
147 	}
148 
149 	/***
150 	 * Parses the server-side XML radio list and returns parsed object
151 	 * instances.
152 	 * 
153 	 * @param genre
154 	 *            the genre
155 	 * @return radio list.
156 	 * @throws SAXException
157 	 * @throws IOException
158 	 *             if i/o error occurs.
159 	 */
160 	public static List<Radio> getRadioList(final String genre)
161 			throws IOException, SAXException {
162 		final URL xml = new URL(GENRES_LIST.toString() + "?genre=" + genre);
163 		return getRadioList(xml.openStream());
164 	}
165 
166 	/***
167 	 * Parses the XML radio list and returns parsed object instances.
168 	 * 
169 	 * @param in
170 	 *            the XML to parse
171 	 * @return radio list. The list is sorted by the radio name and duplicate
172 	 *         items are removed.
173 	 * @throws SAXException
174 	 * @throws IOException
175 	 *             if i/o error occurs.
176 	 */
177 	public static List<Radio> getRadioList(final InputStream in)
178 			throws IOException, SAXException {
179 		try {
180 			final List<Radio> result = new ArrayList<Radio>();
181 			IOUtils.parseXML(in, new RadioListHandler(result));
182 			sortByName(result);
183 			removeDups(result);
184 			return result;
185 		} finally {
186 			MiscUtils.closeQuietly(in);
187 		}
188 	}
189 
190 	/***
191 	 * Sorts given radio list by name.
192 	 * 
193 	 * @param radioList
194 	 *            the list to sort.
195 	 */
196 	public static void sortByName(final List<Radio> radioList) {
197 		Collections.sort(radioList, new Comparator<Radio>() {
198 			public int compare(Radio object1, Radio object2) {
199 				return object1.name.compareToIgnoreCase(object2.name);
200 			}
201 		});
202 	}
203 
204 	private static void removeDups(final List<Radio> list) {
205 		String lastName = null;
206 		for (final Iterator<Radio> i = list.iterator(); i.hasNext();) {
207 			final Radio r = i.next();
208 			if (MiscUtils.nullEquals(lastName, r.name)) {
209 				i.remove();
210 			} else {
211 				lastName = r.name;
212 			}
213 		}
214 	}
215 
216 	/***
217 	 * Handles list of radio stations.
218 	 * 
219 	 * @author Martin Vysny
220 	 */
221 	private static class RadioListHandler extends DefaultHandler {
222 		/***
223 		 * Store the genres here.
224 		 */
225 		private final List<Radio> result;
226 
227 		/***
228 		 * Creates new handler instance.
229 		 * 
230 		 * @param result
231 		 *            store the genres here.
232 		 */
233 		public RadioListHandler(List<Radio> result) {
234 			this.result = result;
235 		}
236 
237 		@Override
238 		public void startElement(String uri, String localName, String name,
239 				Attributes attributes) {
240 			if (Thread.currentThread().isInterrupted()) {
241 				throw new RuntimeException("interrupted");
242 			}
243 			if (!localName.equals("station"))
244 				return;
245 			result.add(Radio.fromXML(attributes));
246 		}
247 	}
248 
249 	/***
250 	 * Removes the ASCII graphics from the string start and end. Graphics in the
251 	 * middle is left as-is. The string is trimmed as well.
252 	 * 
253 	 * @param string
254 	 *            the string
255 	 * @return trimmed string with ASCII graphics removed.
256 	 */
257 	public static String removeAsciiGraphics(final String string) {
258 		if (MiscUtils.isEmpty(string))
259 			return string;
260 		int start, end;
261 		for (start = 0; start < string.length(); start++) {
262 			if (Character.isLetterOrDigit(string.charAt(start)))
263 				break;
264 		}
265 		for (end = string.length() - 1; end >= 0; end--) {
266 			if (Character.isLetterOrDigit(string.charAt(end)))
267 				break;
268 		}
269 		if (start >= end)
270 			return "";
271 		return string.substring(start, end + 1);
272 	}
273 }