|
|
|
|
@ -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; |
|
|
|
|
} |
|
|
|
|
|