package ctbrec.sites.xlovecam; import static ctbrec.Model.State.*; import static ctbrec.io.HttpConstants.*; import static java.nio.charset.StandardCharsets.*; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.bind.JAXBException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; import okhttp3.Request; import okhttp3.Response; public class XloveCamModel extends AbstractModel { private static final Logger LOG = LoggerFactory.getLogger(XloveCamModel.class); private static final Pattern HLS_PLAYLIST_PATTERN = Pattern.compile("\"hlsPlaylist\":\"(.*?)\","); private boolean online = false; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if (ignoreCache) { String body = getModelPage(); Matcher m = HLS_PLAYLIST_PATTERN.matcher(body); online = m.find(); onlineState = online ? ONLINE : OFFLINE; } return online; } @Override public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if (failFast && onlineState != UNKNOWN) { return onlineState; } else { try { onlineState = isOnline(true) ? ONLINE : OFFLINE; } catch (InterruptedException e) { Thread.currentThread().interrupt(); onlineState = OFFLINE; } catch (IOException | ExecutionException e) { onlineState = OFFLINE; } return onlineState; } } @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { MasterPlaylist masterPlaylist = getMasterPlaylist(); List sources = new ArrayList<>(); for (PlaylistData playlist : masterPlaylist.getPlaylists()) { if (playlist.hasStreamInfo()) { StreamSource src = new StreamSource(); src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.height = Optional.ofNullable(playlist.getStreamInfo().getResolution()).map(r -> r.height).orElse(0); src.mediaPlaylistUrl = playlist.getUri(); LOG.trace("Media playlist {}", src.mediaPlaylistUrl); sources.add(src); } } return sources; } private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { String modelPage = getModelPage(); Matcher m = HLS_PLAYLIST_PATTERN.matcher(modelPage); if (m.find() && m.groupCount() > 0) { String hlsPlaylist = m.group(1); Request req = new Request.Builder() .url(hlsPlaylist) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .build(); try (Response response = getSite().getHttpClient().execute(req)) { if (response.isSuccessful()) { String body = response.body().string(); LOG.trace(body); InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); MasterPlaylist master = playlist.getMasterPlaylist(); return master; } else { throw new HttpException(response.code(), response.message()); } } } else { throw new HttpException(404, "HLS playlist not found"); } } private String getModelPage() throws IOException { String url = XloveCam.mobileUrl + "/en/model/" + getName() + '/'; Request req = new Request.Builder() .url(url) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .build(); try (Response response = getSite().getHttpClient().execute(req)) { if (response.isSuccessful()) { return response.body().string(); } else { throw new HttpException(response.code(), response.message()); } } } @Override public void invalidateCacheEntries() { // nothing to do } @Override public void receiveTip(Double tokens) throws IOException { // not implemented yet } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { return new int[] {0, 0}; } @Override public boolean follow() throws IOException { return false; } @Override public boolean unfollow() throws IOException { return false; } }