diff --git a/app/src/main/java/com/fongmi/android/tv/api/EpgParser.java b/app/src/main/java/com/fongmi/android/tv/api/EpgParser.java index b55e5b9ae..ee066683d 100644 --- a/app/src/main/java/com/fongmi/android/tv/api/EpgParser.java +++ b/app/src/main/java/com/fongmi/android/tv/api/EpgParser.java @@ -1,5 +1,7 @@ package com.fongmi.android.tv.api; +import android.util.Log; + import com.fongmi.android.tv.bean.Channel; import com.fongmi.android.tv.bean.Epg; import com.fongmi.android.tv.bean.EpgData; @@ -7,8 +9,8 @@ import com.fongmi.android.tv.bean.Live; import com.fongmi.android.tv.bean.Tv; import com.fongmi.android.tv.utils.Download; import com.fongmi.android.tv.utils.FileUtil; +import com.fongmi.android.tv.utils.Formatters; import com.fongmi.android.tv.utils.UrlUtil; -import com.fongmi.android.tv.utils.Util; import com.github.catvod.utils.Path; import org.simpleframework.xml.core.Persister; @@ -16,43 +18,78 @@ import org.simpleframework.xml.core.Persister; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.AbstractMap; -import java.util.Calendar; -import java.util.Date; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import java.util.stream.Stream; public class EpgParser { - private static final SimpleDateFormat formatTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); - private static final SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); - private static final SimpleDateFormat formatFull = new SimpleDateFormat("yyyyMMddHHmmss Z", Locale.getDefault()); + private static final String TAG = EpgParser.class.getSimpleName(); + + private static ZoneId zoneIdOf(String tz) { + if (tz.isEmpty()) return ZoneId.systemDefault(); + try { + return ZoneId.of(tz); + } catch (Exception ignored) { + return ZoneId.systemDefault(); + } + } + + private static OffsetDateTime parseFull(String source, ZoneId zoneId) { + String s = source.trim(); + int len = s.length(); + try { + if (len >= 20) return OffsetDateTime.parse(s, s.charAt(len - 3) == ':' ? Formatters.EPG_FULL_COLON : Formatters.EPG_FULL); + return LocalDateTime.parse(len > 14 ? s.substring(0, 14) : s, Formatters.EPG_FULL_NO_TZ).atZone(zoneId).toOffsetDateTime(); + } catch (Exception e) { + Log.w(TAG, "parseFull failed: " + s + " -> " + e.getMessage()); + return OffsetDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC); + } + } - public static boolean start(Live live, String url) throws Exception { + public static void start(Live live, String url) throws Exception { + long t0 = System.currentTimeMillis(); File file = Path.epg(UrlUtil.path(url)); - boolean refresh = shouldRefresh(file); + String reason = refreshReason(file); + boolean refresh = reason != null; + Log.i(TAG, "start url=" + url + " file=" + file.getName() + " refresh=" + refresh + (refresh ? " reason=" + reason : "")); if (refresh) Download.create(url, file).get(); - if (isGzip(file)) readGzip(live, file, refresh); + boolean gzip = isGzip(file); + if (gzip) readGzip(live, file, refresh); else readXml(live, file); - return true; + Log.i(TAG, "start done elapsed=" + (System.currentTimeMillis() - t0) + "ms"); } - public static Epg getEpg(String xml, String key) throws Exception { - Tv tv = new Persister().read(Tv.class, xml, false); - Epg epg = Epg.create(key, formatDate.format(Util.parse(formatFull, tv.getDate()))); - tv.getProgramme().forEach(programme -> epg.getList().add(getEpgData(programme))); - return epg; + public static Epg getEpg(String xml, String key, ZoneId zoneId) { + try { + Tv tv = new Persister().read(Tv.class, xml, false); + String rawDate = tv.getDate(); + String date = rawDate.isEmpty() ? LocalDate.now(zoneId).format(Formatters.DATE) : parseFull(rawDate, zoneId).format(Formatters.DATE); + Epg epg = Epg.create(key, date); + tv.getProgramme().forEach(programme -> epg.getList().add(getEpgData(programme, zoneId))); + return epg; + } catch (Exception e) { + Log.w(TAG, "getEpg parse failed key=" + key + ": " + e.getMessage()); + return new Epg(); + } } - private static boolean shouldRefresh(File file) { - return !Path.exists(file) || !isToday(file.lastModified()) || System.currentTimeMillis() - file.lastModified() > TimeUnit.HOURS.toMillis(6); + private static String refreshReason(File file) { + if (!Path.exists(file)) return "file-missing"; + if (!isToday(file.lastModified())) return "not-today"; + if (System.currentTimeMillis() - file.lastModified() > TimeUnit.HOURS.toMillis(6)) return "older-than-6h"; + return null; } private static boolean isGzip(File file) { @@ -64,9 +101,7 @@ public class EpgParser { } private static boolean isToday(long millis) { - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(millis); - return calendar.get(Calendar.DAY_OF_MONTH) == Calendar.getInstance().get(Calendar.DAY_OF_MONTH); + return LocalDate.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()).equals(LocalDate.now()); } private static void readGzip(Live live, File file, boolean refresh) throws Exception { @@ -76,24 +111,23 @@ public class EpgParser { } private static void readXml(Live live, File file) throws Exception { + ZoneId zoneId = zoneIdOf(live.getTimeZone()); Map liveChannelMap = prepareLiveChannels(live); XmlData xmlData = parseXmlData(file); - String today = formatDate.format(new Date()); - bindResultsToLive(live, processProgramme(xmlData, liveChannelMap, today)); + ProgrammeResult result = processProgramme(xmlData, liveChannelMap, zoneId); + bindResultsToLive(live, result); } private static Map prepareLiveChannels(Live live) { - return live.getGroups().stream() + Map map = new HashMap<>(); + live.getGroups().stream() .flatMap(group -> group.getChannel().stream()) - .flatMap(channel -> Stream.of(channel.getTvgId(), channel.getTvgName(), channel.getName()) - .filter(key -> !key.isEmpty()) - .map(key -> new AbstractMap.SimpleEntry<>(key, channel))) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (oldValue, newValue) -> oldValue, - HashMap::new - )); + .forEach(channel -> { + if (!channel.getTvgId().isEmpty()) map.putIfAbsent(channel.getTvgId(), channel); + if (!channel.getTvgName().isEmpty()) map.putIfAbsent(channel.getTvgName(), channel); + if (!channel.getName().isEmpty()) map.putIfAbsent(channel.getName(), channel); + }); + return map; } private static XmlData parseXmlData(File file) throws Exception { @@ -102,20 +136,48 @@ public class EpgParser { return new XmlData(tv, map); } - private static ProgrammeResult processProgramme(XmlData data, Map liveChannelMap, String today) { - Map epgMap = new HashMap<>(); + private static ProgrammeResult processProgramme(XmlData data, Map liveChannelMap, ZoneId zoneId) { + Map> epgMap = new HashMap<>(); Map srcMap = new HashMap<>(); + Map channelCache = new HashMap<>(); + Set channelMiss = new HashSet<>(); + int skipped = 0; for (Tv.Programme programme : data.tv.getProgramme()) { String xmlChannelId = programme.getChannel(); - Channel targetChannel = findTargetChannel(xmlChannelId, liveChannelMap, data.map); - if (targetChannel == null) continue; - Date startDate = Util.parse(formatFull, programme.getStart()); - if (!isToday(startDate.getTime())) continue; + Channel targetChannel; + if (channelCache.containsKey(xmlChannelId)) { + targetChannel = channelCache.get(xmlChannelId); + } else if (channelMiss.contains(xmlChannelId)) { + targetChannel = null; + } else { + targetChannel = findTargetChannel(xmlChannelId, liveChannelMap, data.map); + if (targetChannel != null) channelCache.put(xmlChannelId, targetChannel); + else channelMiss.add(xmlChannelId); + } + if (targetChannel == null) { + skipped++; + continue; + } + OffsetDateTime startDate = parseFull(programme.getStart(), zoneId); + OffsetDateTime endDate = parseFull(programme.getStop(), zoneId); String liveTvgId = targetChannel.getTvgId(); - Date endDate = Util.parse(formatFull, programme.getStop()); - epgMap.computeIfAbsent(liveTvgId, key -> Epg.create(key, today)).getList().add(getEpgData(startDate, endDate, programme)); - Optional.ofNullable(data.map.get(xmlChannelId)).flatMap(list -> list.stream().filter(Tv.Channel::hasSrc).findFirst()).ifPresent(ch -> srcMap.putIfAbsent(liveTvgId, ch.getSrc())); + String programmeDate = startDate.format(Formatters.DATE); + epgMap.computeIfAbsent(liveTvgId, k -> new HashMap<>()) + .computeIfAbsent(programmeDate, d -> Epg.create(liveTvgId, d)) + .getList().add(getEpgData(startDate, endDate, programme)); + if (!srcMap.containsKey(liveTvgId)) { + List xmlChannels = data.map.get(xmlChannelId); + if (xmlChannels != null) { + for (Tv.Channel ch : xmlChannels) { + if (ch.hasSrc()) { + srcMap.put(liveTvgId, ch.getSrc()); + break; + } + } + } + } } + Log.i(TAG, "processProgramme skipped(no match)=" + skipped + " matched channels=" + epgMap.size()); return new ProgrammeResult(epgMap, srcMap); } @@ -124,40 +186,44 @@ public class EpgParser { if (targetChannel != null) return targetChannel; List channels = xmlChannelIdMap.get(xmlChannelId); if (channels == null) return null; - return channels.stream() - .flatMap(xmlChannel -> xmlChannel.getDisplayName().stream()) - .map(Tv.DisplayName::getText) - .filter(name -> !name.isEmpty()) - .filter(liveChannelMap::containsKey) - .findFirst() - .map(liveChannelMap::get) - .orElse(null); + return channels.stream().flatMap(xmlChannel -> xmlChannel.getDisplayName().stream()).map(Tv.DisplayName::getText).filter(name -> !name.isEmpty()).filter(liveChannelMap::containsKey).findFirst().map(liveChannelMap::get).orElse(null); } private static void bindResultsToLive(Live live, ProgrammeResult result) { + int[] counts = {0, 0}; live.getGroups().stream() .flatMap(group -> group.getChannel().stream()) .forEach(channel -> { String tvgId = channel.getTvgId(); - Optional.ofNullable(result.epgMap.get(tvgId)).ifPresent(channel::setData); - Optional.ofNullable(result.srcMap.get(tvgId)).ifPresent(channel::setLogo); + Map dateMap = result.epgMap.get(tvgId); + if (dateMap != null) { + channel.setDataList(new ArrayList<>(dateMap.values())); + counts[0]++; + } else { + counts[1]++; + } + if (channel.getLogo().isEmpty()) { + String src = result.srcMap.get(tvgId); + if (src != null) channel.setLogo(src); + } }); + Log.i(TAG, "bindResultsToLive with-epg=" + counts[0] + " without-epg=" + counts[1]); } - private static EpgData getEpgData(Tv.Programme programme) { - Date startDate = Util.parse(formatFull, programme.getStart()); - Date endDate = Util.parse(formatFull, programme.getStop()); + private static EpgData getEpgData(Tv.Programme programme, ZoneId zoneId) { + OffsetDateTime startDate = parseFull(programme.getStart(), zoneId); + OffsetDateTime endDate = parseFull(programme.getStop(), zoneId); return getEpgData(startDate, endDate, programme); } - private static EpgData getEpgData(Date startDate, Date endDate, Tv.Programme programme) { + private static EpgData getEpgData(OffsetDateTime startDate, OffsetDateTime endDate, Tv.Programme programme) { try { EpgData epgData = new EpgData(); epgData.setTitle(programme.getTitle()); - epgData.setStart(formatTime.format(startDate)); - epgData.setEnd(formatTime.format(endDate)); - epgData.setStartTime(startDate.getTime()); - epgData.setEndTime(endDate.getTime()); + epgData.setStart(startDate.format(Formatters.TIME)); + epgData.setEnd(endDate.format(Formatters.TIME)); + epgData.setStartTime(startDate.toInstant().toEpochMilli()); + epgData.setEndTime(endDate.toInstant().toEpochMilli()); epgData.trans(); return epgData; } catch (Exception e) { @@ -178,10 +244,10 @@ public class EpgParser { private static class ProgrammeResult { - Map epgMap; + Map> epgMap; Map srcMap; - public ProgrammeResult(Map epgMap, Map srcMap) { + public ProgrammeResult(Map> epgMap, Map srcMap) { this.epgMap = epgMap; this.srcMap = srcMap; }