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 }