// Copyright 2009 Google Inc.
// Copyright 2011 NPR
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.npr.api;
import android.content.Context;
import android.util.Log;
import org.apache.http.client.ClientProtocolException;
import org.npr.android.util.NodeUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Station {
private static final String LOG_TAG = Station.class.getName();
private final String id;
private final String name;
private final String marketCity;
private final String frequency;
private final String band;
private final List<AudioStream> audioStreams;
private final List<Podcast> podcasts;
private final String tagline;
private final String image;
private final HashMap<String, String> identiferAudioUrlByType;
private static final Pattern callLettersPattern = Pattern.compile("[KW][A-Z][A-Z][A-Z]");
public static final String[] stationListBySize = new String[]{"WNYC","KPCC","KUSD","KPCV","KQED","WAMU","WBUR","WBEZ","WBEQ","WBEW","KNOW","KSJN","KCMP","KMSE","KBPN","KCCD","KLNI","KNBJ","KNCM","KNGA","KNSE","KNSR","KNSW","KNTN","KNWF","KZSE","WGGL","WIRN","WLSN","WSCN","KCCM","KCMF","KLSE","KRSD","KRSW","KSJR","WIRR","WMLS","WSCD","WHYY","KCRW","KCRI","KCRU","KUOW","KOAC","KOAB","KOAP","KOBK","KOPB","KRBM","KTMK","KTVR","KMHD","WABE","WEVN","WSUF","WUOM","WFUM","WVGR","KJZZ","KBAQ","WKWM","WLRN","KPLU","KPLI","KVIX","KERA","KUHF","KXOT","WUSF","KPBS","KQVO","WUNC","WBUX","WRQM","WUND","WURI","WAMC","WAMK","WAMQ","WCAN","WOSR","WRUN","KCFR","KCFC","KCFP","KKPC","KPRE","KPRH","KPRN","KPRU","KPYR","KVOV","WGBH","WCAI","WNAN","WNCK","WZAI","WMEH","WMED","WMEF","WMEM","WMEP","WMEW","WSUI","KDWI","KOWI","KSUI","KTPR","KUNZ","KWOI","WOI","KUT","KUTX","KWMU","WVPS","WBTN","WNCH","WOXR","WRVT","WVPA","WVPR","WVTI","WVTQ","WWPV","KXJZ","KXPR","KKTO","KQNC","KUOP","KXJS","KXSR","WSHU","WFAE","WFHE","WLTR","WEPR","WHMC","WJWJ","WLJK","WNSC","WRJA","WSCI","WFCR","WNPR","WEDW","WPKT","WRLI","KQEI","WCPN","WYPR","WYPF","KCUR","WERN","WHND","WHRM","WHSA","WLSU","WPNE","WSSW","WUEC","WVSS","KUWS","WHA","WHBM","WHDI","WHHI","WHID","WHLA","WHWC","WLBL","WRFW","WRST","WMFE","KUNC","KRNC","KDMR","KDUB","KRNI","KUNY","WHRS","WPLN","WTML","WDUQ","WEVC","WEVH","WEVJ","WEVO","WEVS","WYPM","WITF","WHRV","WHRO","WFFC","WISE","WVTF","WVTR","WVTW","WVTU","WWVT","WCVE","WCNV","WMVE","WVXU","WMUB","WKRJ","WKSU","WKRW","WKSV","WNRK","WDET","WFYI","KHPR","KANO","KIPO","KKUA","KWSU","KRFA","KFAE","KLWS","KMWS","KNWO","KNWP","KNWR","KNWV","KNWY","KQWS","KWWS","KZAZ","KSTX","KTXI","KNPR","KLNR","KSGU","KTPH","KWPR","KUER","WRVO","WRCU","WRVD","WRVJ","WRVN","WSUC","WGCU","WMKO","WJCT","WFSU","WFSL","WFSQ","WFSW","WKAR","WJSP","WSVH","WUGA","WABR","WACG","WATY","WGPB","WJWV","WMUM","WNGH","WNGU","WPPR","WUNV","WUWG","WWET","WWIO","WXVS","WVPN","WAUA","WVEP","WVNP","WVPB","WVPG","WVPM","WVPW","WVWV","KUAZ","WPBI","WMPN","WMAB","WMAE","WMAH","WMAO","WMAU","WMAV","WMAW","WUWM","WUOT","WXXI","WRUR","WXXY","WSKG","WSQA","WSQC","WSQE","WSQG","WSQX","WBFO","WOLN","WUBJ","WFDD","WBHM","WFPL","KPBX","KIBX","KSFC","WOSU","WOSB","WOSE","WOSP","WOSV","KLCC","KLBR","KLCO","KLFO","KLFR","KMPQ","WCQS","WFQS","WYQS","WCBE","KNAU","KGHR","KNAA","KNAD","KNAG","KNAQ","KPUB","KUNM","KANU","KANH","KANV","KUWR","KBUW","KDUW","KSUW","KUWA","KUWC","KUWD","KUWG","KUWJ","KUWL","KUWN","KUWP","KUWT","KUWX","KUWY","KUWZ","WOUB","WOUC","WOUH","WOUL","WOUZ","WQCS","WKNO","WKNP","KUAR","KLRE","WCMU","WCMB","WCML","WCMW","WCMZ","WUCX","WWCM","KWGS","KWTU","KBSW","KBSJ","KBSQ","KBSX","KBSY","KBSK","KBSS","KBSU","WVIA","WVYA","KJZA","KXLC","WHAD","WMEA","KUCV","KCNE","KHNE","KLNE","KMNE","KPNE","KRNE","KTNE","KXNE","KVPR","KPRX","WFIU","WUFT","WJUF","KSOR","KAGI","KLDD","KLMF","KNCA","KNHT","KNSQ","KNYR","KSBA","KSKF","KSMF","KSRG","KSRS","KRCC","KECC","WVPE","WUWF","KGOU","KROU","WWNO","KTLN","WNIJ","WNIE","WNIQ","WNIW","WNIU","KUAF","KMUW","WBLQ","WRNI","WRKF","WSLU","WSLJ","WSLL","WSLO","WXLB","WXLG","WXLH","WXLQ","WXLU","KVCR","WLRH","KCLU","KALW","WUTC","KIOS","WMRA","WMRL","WMRY","WILL","WSCL","WSDL","WGTE","WGBE","WGDE","WGLE","KCHO","KFPR","KUNR","KNCC","KDAQ","KBSA","KLDN","KLSA","WPSU","WPSX","WHQR","WUKY","WIAA","WIAB","WICA","WICV","WYSO","WUAL","WAPR","WQPR","WYSU","KEMC","KYPR","KBMC","KPRQ","WBAA","WBNI","WBOI","WCKZ","WEKU","WEKF","WEKH","KSMU","KSMS","KSMW","KBHE","KCSD","KDSD","KESD","KPSD","KQSD","KTSD","KZSD","KCBX","KSBX","KUFM","KAPC","KUFN","KUHM","KUKL","KRWG","WETS","WTEB","WBJD","WKNS","WZNB","WKYU","WDCL","WKPB","WKUE","KBIA","WQLN","KCPW","KHCC","KHCD","KHCT","WAER","WGVU","WGVS","WSIU","WUSI","WVSI","WUIS","WIPA","WTSU","WRWA","WTJB","WMUK","KUSP","KBDH","KHSU","WGLT","WANC","WCEL","WXPN","WCBU","KCND","KDPR","KMPR","KPPR","KPRJ","WBST","WBSB","WBSH","WBSJ","WBSW","WKMS","WNIN","KRPS","KEDT","KVRT","WHIL","WEMU","WIUM","WIUW","KAZU","WNJT","WNJB","WNJM","WNJN","WNJP","WNJS","WNJZ","KCCU","KLCU","KMCU","KOCU","KYCU","KZCU","WVIK","KAMU","WDIY","WXLV","WNED","KAWC","KANZ","KJJP","KTOT","KTXP","KZAN","KZNA","WNMU","KWIT","KOJI","KRVS","WNKU","KOHM","KOSU","KOSN","KXCV","KRNW","KMBH","KHID","WFUV","WETA","WGMS","KEDM","KUSU","KUSR","KWBU","WXPR","WXPW","KASU","WRTI","WJAZ","WRTQ","WRTX","WRTY","KVLU","WKGC","WMKY","WOCS","KENW","KMTH","KSUT","KUTE","WFIT","WBLV","WBLU","KZYX","KZYZ","KAJX","KCJX","KPCW","KCUA","KANW","KNLK","WBGO","KRCU","KSEF","KPRG","KTBG","WQUB","WFSS","WYEP","WUCF","KAXE","WNCW","WQED","WQEJ","WDAV","WWFM","WWNJ","WWCJ","WWPJ","WBJB","KRCB","WCLK","WRVS","WTMD","WMNF","WESM","WMHT","WEXT","WRHV","WEAA","WCNY","WJNY","WUNY","WJAB","KETR","WBPR","WFPB","WNEF","WUMB","KUNV","WHDD","WLPR","WYPO","WPRL","WICN","KBYU","WNCU","WVAS","WURC","KCSM","KWSO","WNYE","WSNC","WMOT","KCIE","KEXP","KGPR","KTEP","KUSC","WCPE","WEMC","WPPB","WVUB","WZPE","KDSC","KMST","KPSC","KQSC","KUAC","WCWP","WJSU","WSIE"};
public static class Listenable {
private final String url;
private final String title;
public Listenable(String url, String title) {
this.url = url;
this.title = title;
}
public String getUrl() {
return url;
}
public String getTitle() {
return title;
}
}
public static class AudioStream extends Listenable {
public AudioStream(String url, String title) {
super(url, title);
}
}
public static class Podcast extends Listenable {
public Podcast(String url, String title) {
super(url, title);
}
}
private Station(String id, String name, String city, String frequency,
String band, List<AudioStream> audioStreams, List<Podcast> podcasts,
String tagline, String image, HashMap<String, String> audioIdentifiersByType) {
this.id = id;
this.name = name;
this.marketCity = city;
this.frequency = frequency;
this.band = band;
this.audioStreams = audioStreams;
this.podcasts = podcasts;
this.tagline = tagline;
this.image = image;
this.identiferAudioUrlByType = audioIdentifiersByType;
}
public String getTagline() {
return tagline;
}
public String getName() {
return name;
}
public String getId() {
return id;
}
public String getMarketCity() {
return marketCity;
}
public String getFrequency() {
return frequency;
}
public String getBand() {
return band;
}
public List<AudioStream> getAudioStreams() {
return audioStreams;
}
public List<Podcast> getPodcasts() {
return podcasts;
}
public String getImage() {
return image;
}
public HashMap<String, String> getIdentiferAudioUrlByType() {
return identiferAudioUrlByType;
}
public String getCallLettersInName() {
// try to find call letters from title
Matcher matcher = callLettersPattern.matcher(getName());
if (matcher.find())
return matcher.group();
return getName();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(name);
sb.append(" ");
sb.append(marketCity);
// TODO: Add market state if the feed has it?
sb.append(" ");
sb.append(frequency);
sb.append(band);
return sb.toString();
}
public static class StationBuilder {
private String id;
private String name;
private String marketCity;
private String frequency;
private String band;
private List<AudioStream> audioStreams = new LinkedList<AudioStream>();
private List<Podcast> podcasts = new LinkedList<Podcast>();
private String tagline;
private String image;
private HashMap<String, String> audioIdentifiersByType =
new HashMap<String, String>();
public StationBuilder(String id) {
this.id = id;
}
public StationBuilder withId(String id) {
this.id = id;
return this;
}
public StationBuilder withName(String name) {
this.name = name;
return this;
}
public StationBuilder withMarketCity(String city) {
this.marketCity = city;
return this;
}
public StationBuilder withFrequency(String frequency) {
this.frequency = frequency;
return this;
}
public StationBuilder withBand(String band) {
this.band = band;
return this;
}
public StationBuilder withAudioStreams(List<AudioStream> audioStreams) {
this.audioStreams = audioStreams;
return this;
}
public StationBuilder withPodcasts(List<Podcast> podcasts) {
this.podcasts = podcasts;
return this;
}
public StationBuilder withTagline(String tagline) {
this.tagline = tagline;
return this;
}
public StationBuilder withImage(String image) {
this.image = image;
return this;
}
public StationBuilder withAudioIdentifierByType(
HashMap<String, String> audioIdentifiers) {
this.audioIdentifiersByType = audioIdentifiers;
return this;
}
public Station build() {
return new Station(id, name, marketCity, frequency, band, audioStreams,
podcasts, tagline, image, audioIdentifiersByType);
}
}
public static class StationFactory {
public static List<Station> parseStations(Node rootNode) {
if (rootNode.getNodeName().equals("rss")) {
return parseRssStationList(rootNode);
}
return parseNprmlStationList(rootNode);
}
private static List<Station> parseNprmlStationList(Node rootNode) {
List<Station> result = new ArrayList<Station>();
NodeList stationList = rootNode.getChildNodes();
for (Node stationNode : new IterableNodeList(stationList)) {
Station station = createNprmlStation(stationNode);
if (station != null &&
(station.getAudioStreams().size() > 0 ||
station.getPodcasts().size() > 0)) {
result.add(station);
}
}
return result;
}
private static List<Station> parseRssStationList(Node rootNode) {
List<Station> result = new ArrayList<Station>();
for (Node channelNode : new IterableNodeList(rootNode.getChildNodes())) {
if (channelNode.getNodeName().equals("channel")) {
NodeList stationList = channelNode.getChildNodes();
for (Node stationNode : new IterableNodeList(stationList)) {
Station station = createRssStation(stationNode);
if (station != null &&
// Only show stations with audio as this is 'find live streams'
station.getAudioStreams().size() > 0) {
result.add(station);
}
}
}
}
return result;
}
private static Station createRssStation(Node node) {
if (!node.getNodeName().equals("item") ||
!node.hasChildNodes()) {
return null;
}
StationBuilder sb = new StationBuilder(
// Create an default ID because some stations don't have one
Long.toHexString(new Date().getTime() * 1000 + (long)(Math.random()
* 1000))
);
List<AudioStream> streams = new LinkedList<AudioStream>();
String callLetters = null;
for (Node n : new IterableNodeList(node.getChildNodes())) {
String nodeName = n.getNodeName();
Node nodeChild = n.getChildNodes().item(0);
if (nodeName.equals("station:nprID")) {
sb.withId(NodeUtils.getTextContent(n));
} else if (nodeName.equals("station:callLetters")) {
callLetters = NodeUtils.getTextContent(n);
} else if (nodeName.equals("station:verboseName") &&
callLetters == null) {
callLetters = NodeUtils.getTextContent(n);
} else if (nodeName.equals("station:band") && nodeChild != null) {
sb.withBand(NodeUtils.getTextContent(n));
} else if (nodeName.equals("station:frequency") && nodeChild != null) {
sb.withFrequency(NodeUtils.getTextContent(n));
} else if (nodeName.equals("station:city") && nodeChild != null) {
sb.withMarketCity(NodeUtils.getTextContent(n));
} else if (nodeName.equals("station:tagline") && nodeChild != null) {
sb.withTagline(NodeUtils.getTextContent(n));
} else if (nodeName.equals("station:stream")) {
Attr urlAttr = (Attr) n.getAttributes().getNamedItem("url");
Attr encodingAttr = (Attr) n.getAttributes().getNamedItem("encoding");
if (encodingAttr != null && urlAttr != null) {
String url = urlAttr.getValue();
String encoding = encodingAttr.getValue();
if (encoding.equals("mp3") && url != null) {
streams.add(new AudioStream(url, null));
}
}
}
}
sb.withAudioStreams(streams);
sb.withName(callLetters);
return sb.build();
}
private static Station createNprmlStation(Node node) {
if (!node.getNodeName().equals("station") ||
!node.hasChildNodes()) {
return null;
}
StationBuilder sb = new StationBuilder(
node.getAttributes().getNamedItem("id").getNodeValue()
);
List<AudioStream> streams = new LinkedList<AudioStream>();
List<Podcast> podcasts = new LinkedList<Podcast>();
HashMap<String, String> identifierAudioUrlByType = new HashMap<String, String>();
for (Node n : new IterableNodeList(node.getChildNodes())) {
String nodeName = n.getNodeName();
Node nodeChild = n.getChildNodes().item(0);
if (nodeName.equals("name")) {
sb.withName(NodeUtils.getTextContent(n));
} else if (nodeName.equals("band") && nodeChild != null) {
sb.withBand(NodeUtils.getTextContent(n));
} else if (nodeName.equals("frequency") && nodeChild != null) {
sb.withFrequency(NodeUtils.getTextContent(n));
} else if (nodeName.equals("marketCity") && nodeChild != null) {
sb.withMarketCity(NodeUtils.getTextContent(n));
} else if (nodeName.equals("tagline") && nodeChild != null) {
sb.withTagline(NodeUtils.getTextContent(n));
} else if (nodeName.equals("image") && nodeChild != null) {
sb.withImage(NodeUtils.getTextContent(n));
} else if (nodeName.equals("url")) {
Attr typeIdAttr = (Attr) n.getAttributes().getNamedItem("typeId");
Attr typeAttr = (Attr) n.getAttributes().getNamedItem("type");
Attr titleAttr = (Attr) n.getAttributes().getNamedItem("title");
if (typeIdAttr != null && typeAttr != null) {
String typeId = typeIdAttr.getValue();
String type = typeAttr.getValue();
String url = NodeUtils.getTextContent(n);
String title = titleAttr.getValue();
if (typeId.equals("10") && type.equals("Audio MP3 Stream")
&& nodeChild != null) {
streams.add(new AudioStream(url, title));
}
if (typeId.equals("9") && type.equals("Podcast")
&& nodeChild != null) {
podcasts.add(new Podcast(url, title));
}
}
} else if (nodeName.equals("identifierAudioUrl")) {
Attr typeAttr = (Attr) n.getAttributes().getNamedItem("type");
if (typeAttr != null) {
identifierAudioUrlByType.put(typeAttr.getValue(), NodeUtils.getTextContent(n));
}
}
}
sb.withAudioStreams(streams);
sb.withPodcasts(podcasts);
sb.withAudioIdentifierByType(identifierAudioUrlByType);
return sb.build();
}
public static Station downloadStation(String stationId, Context context) {
Log.d(LOG_TAG, "downloading station: " + stationId);
Map<String, String> params = new HashMap<String, String>();
params.put(ApiConstants.PARAM_ID, stationId);
String url =
ApiConstants.instance().createUrl(ApiConstants.STATIONS_PATH, params);
Node stations = null;
try {
stations = new Client(url, context).execute();
} catch (ClientProtocolException e) {
Log.e(LOG_TAG, "", e);
} catch (IOException e) {
Log.e(LOG_TAG, "", e);
} catch (SAXException e) {
Log.e(LOG_TAG, "", e);
} catch (ParserConfigurationException e) {
Log.e(LOG_TAG, "", e);
}
if (stations == null) {
return null;
}
Log.d(LOG_TAG, "node " + stations.getNodeName() + " "
+ stations.getChildNodes().getLength());
List<Station> result = parseStations(stations);
return result.size() > 0 ? result.get(0) : null;
}
}
}