Optimize epg

release
FongMi 1 month ago
parent 3d9467a5f3
commit 9a41c44a67
  1. 198
      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<String, Channel> 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<String, Channel> prepareLiveChannels(Live live) {
return live.getGroups().stream()
Map<String, Channel> 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<String, Channel> liveChannelMap, String today) {
Map<String, Epg> epgMap = new HashMap<>();
private static ProgrammeResult processProgramme(XmlData data, Map<String, Channel> liveChannelMap, ZoneId zoneId) {
Map<String, Map<String, Epg>> epgMap = new HashMap<>();
Map<String, String> srcMap = new HashMap<>();
Map<String, Channel> channelCache = new HashMap<>();
Set<String> 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<Tv.Channel> 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<Tv.Channel> 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<String, Epg> 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<String, Epg> epgMap;
Map<String, Map<String, Epg>> epgMap;
Map<String, String> srcMap;
public ProgrammeResult(Map<String, Epg> epgMap, Map<String, String> srcMap) {
public ProgrammeResult(Map<String, Map<String, Epg>> epgMap, Map<String, String> srcMap) {
this.epgMap = epgMap;
this.srcMap = srcMap;
}

Loading…
Cancel
Save