package com.felkertech.cumulustv.fileio; import android.util.Log; import com.felkertech.cumulustv.model.JsonChannel; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_AUDIO_ONLY; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_EPG_URL; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_GENRES; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_GENRES_ALT1; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_LOGO; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_NUMBER; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_PLUGIN; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_SPLASH; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.CH_SPLASH_ALT1; import static com.felkertech.cumulustv.fileio.M3uParser.Constants.KEY_COUNTRY; /** * This class is responsible to converting between M3u playlists and the application model. */ public class M3uParser { private static final String TAG = M3uParser.class.getSimpleName(); private static int indexOf(String haystack, String... needles) { int index = haystack.length(); for (String n : needles) { int needleIndex = haystack.indexOf(n); index = (index > needleIndex && needleIndex > -1) ? needleIndex : index; } return index; } private static int indexOf(String haystack, int startAt, String... needles) { int index = haystack.length(); for (String n : needles) { int needleIndex = haystack.indexOf(n, startAt); index = (index > needleIndex && needleIndex > -1) ? needleIndex : index; } return index; } private static String getKey(HashMap<String, String> map, String... keys) { for (String k : keys) { if (map.containsKey(k) && !map.get(k).isEmpty()) { return map.get(k); } } return null; } private static int getLastComma(String haystack) { int comma = -1; for (int i = 0; i < haystack.length(); i++) { // Log.d(TAG, "gLC " + i + " " + comma); int c2 = haystack.indexOf(",", comma + 1); /*Log.d(TAG, c2 + " " + haystack.substring(c2 - 1) + Pattern.matches("[\\\" (-1)],(?!\\\")", haystack.substring(c2 - 1))); Log.d(TAG, Pattern.compile("[\\\" (-1)],(?!\\\")").toString()); Log.d(TAG, "" + Pattern.matches("[,]", "\","));*/ String commaAfter = ""; try { commaAfter = haystack.substring(c2 - 1); } catch (StringIndexOutOfBoundsException e) { throw new StringIndexOutOfBoundsException(e.getMessage() + "; Error occured for '" + haystack + "', " + c2 + " was best guess"); } Log.d(TAG, "cA " + commaAfter); if ((commaAfter.substring(0,1).equals("\"") || commaAfter.substring(0,1).equals(" ") || commaAfter.substring(0,1).equals("1") ) && commaAfter.substring(1,2).equals(",") && commaAfter.indexOf("\"", 2) == -1) { Log.d(TAG, "Update comma to " + c2 + " from " + comma); comma = (c2 > comma) ? c2 : comma; return comma; } else { comma = (c2 > comma) ? c2 : comma; } } return comma; } public static TvListing parse(InputStream inputStream) throws IOException { if (inputStream == null) { return null; } BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); String line; List<M3uTvChannel> channels = new ArrayList<>(); Map<String, String> globalAttributes = new HashMap<>(); // Unused for now boolean isM3u = false; while ((line = in.readLine()) != null) { Log.d(TAG, "Next line: " + line); if (line.startsWith("#EXTINF:")) { // This is a channel isM3u = true; M3uTvChannel channel = new M3uTvChannel(); String channelAttributes = line.substring(0, getLastComma(line)); String channelName = line.substring(getLastComma(line) + 1).trim() .replaceAll("\\[\\/?(COLOR |)[^\\]]*\\]", ""); while (channelAttributes.length() > 0) { // Chip away at data until complete Log.d(TAG, channelAttributes); int valueDivider = indexOf(channelAttributes, ":", "="); String attribute = channelAttributes.substring(0, valueDivider); int valueIndex = valueDivider + 1; int valueEnd = indexOf(channelAttributes, valueIndex, " "); int variableEnd = valueEnd + 1; if (valueEnd == -1) { valueEnd = channelAttributes.length(); // We're at the end } try { if (channelAttributes.charAt(valueDivider + 1) == '"') { valueIndex++; valueEnd = channelAttributes.indexOf("\"", valueIndex); variableEnd = valueEnd + 2; // '" ' } String value = channelAttributes.substring(valueIndex, valueEnd); channel.put(attribute, value); } catch (StringIndexOutOfBoundsException e) { throw new StringIndexOutOfBoundsException("Parsing error: '" + channelAttributes + "' does not fit into range " + valueIndex + " - " + valueEnd + " for line " + line); } if (variableEnd > channelAttributes.length()) { channelAttributes = ""; } else { channelAttributes = channelAttributes.substring(variableEnd).trim(); } } line = in.readLine(); Log.d(TAG, "URL: " + line); if (line.startsWith("http")) { channel.url = line; } else if (line.startsWith("rtmp")) { channel.url = line; } // Set channel properties channel.displayName = channelName; channel.m3uAttributes.putAll(globalAttributes); channel.put("count", String.valueOf(channels.size())); channels.add(channel); } else if (line.startsWith("##")) { // Interpret as a country-group globalAttributes.put(KEY_COUNTRY, line.replaceAll("#", "").trim()); } else if (line.startsWith("#EXTM3U")) { isM3u = true; } } TvListing tvl = new TvListing(channels); Log.d(TAG, "Done parsing"); Log.d(TAG, tvl.toString()); if (!isM3u) { return null; } return new TvListing(channels); } public static class TvListing { public List<M3uTvChannel> channels; public TvListing(List<M3uTvChannel> channels) { this.channels = channels; // Validate channels, making sure they have urls Iterator<M3uTvChannel> xmlTvChannelIterator = channels.iterator(); while(xmlTvChannelIterator.hasNext()) { M3uTvChannel tvChannel = xmlTvChannelIterator.next(); if(tvChannel.url == null) { Log.e(TAG, tvChannel.displayName+" has no url!"); xmlTvChannelIterator.remove(); } } } public void setChannels(List<M3uTvChannel> channels) { this.channels = channels; } @Override public String toString() { String out = ""; for(M3uTvChannel tvChannel: channels) { out += tvChannel.toString(); } return out; } public String getChannelList() { String out = ""; for(M3uTvChannel tvChannel: channels) { out += tvChannel.displayName+"\n"; } return out; } } public static class M3uTvChannel { protected String displayName; public String url; private HashMap<String, String> m3uAttributes = new HashMap<>(); public M3uTvChannel() { } public void put(String key, String value) { m3uAttributes.put(key, value); } public JsonChannel toJsonChannel() { return new JsonChannel.Builder() .setAudioOnly(getKey(m3uAttributes, CH_AUDIO_ONLY) != null) .setEpgUrl(getKey(m3uAttributes, CH_EPG_URL)) .setGenres(getKey(m3uAttributes, CH_GENRES, CH_GENRES_ALT1) + "," + getKey(m3uAttributes, KEY_COUNTRY)) .setLogo(getKey(m3uAttributes, CH_LOGO)) .setName(displayName) .setNumber(getKey(m3uAttributes, "#EXTINF:", CH_NUMBER, "count")) .setPluginSource(getKey(m3uAttributes, CH_PLUGIN)) .setSplashscreen(getKey(m3uAttributes, CH_SPLASH, CH_SPLASH_ALT1)) .setMediaUrl(url) .build(); } @Override public String toString() { return toJsonChannel().toString() + "\n"; } } public static class Constants { public static final String HEADER_TAG = "#EXTM3U"; public static final String CHANNEL_TAG = "#EXTINF:-1"; public static final String CH_NUMBER = "tvg-id"; public static final String CH_LOGO = "tvg-logo"; public static final String CH_AUDIO_ONLY = "audio-only"; public static final String CH_EPG_URL = "epg-url"; public static final String CH_GENRES = "group-title"; public static final String CH_GENRES_ALT1 = "genres"; public static final String CH_PLUGIN = "cumulus-plugin"; public static final String CH_SPLASH = "splashscreen"; public static final String CH_SPLASH_ALT1 = "tvg-splashscreen"; public static final String KEY_COUNTRY = "country"; } }