package ctbrec.sites.streamray; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; import ctbrec.recorder.download.Download; import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.hls.FfmpegHlsDownload; import okhttp3.Request; import okhttp3.Response; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.bind.JAXBException; import java.io.IOException; import java.net.URLEncoder; import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; import static ctbrec.Model.State.*; import static ctbrec.io.HttpConstants.*; import static java.nio.charset.StandardCharsets.UTF_8; public class StreamrayModel extends AbstractModel { private static final Logger LOG = LoggerFactory.getLogger(StreamrayModel.class); private String status = null; private String gender = null; private LocalDate regDate = LocalDate.EPOCH; private JSONObject modelInfo; private transient Instant lastInfoRequest = Instant.EPOCH; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if (ignoreCache) { try { JSONObject json = getModelInfo(); if (json.has("online")) { status = json.optString("online"); mapOnlineState(status); } } catch (Exception e) { setOnlineState(UNKNOWN); } } return onlineState == ONLINE; } private void mapOnlineState(String status) { boolean goalShows = Config.getInstance().getSettings().streamrayRecordGoalShows; switch (status) { case "0" -> setOnlineState(OFFLINE); case "1" -> setOnlineState(ONLINE); case "6" -> setOnlineState(goalShows ? ONLINE : PRIVATE); case "2", "3", "4", "7", "10", "11", "12", "13", "14" -> setOnlineState(PRIVATE); default -> setOnlineState(OFFLINE); } } @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 { List sources = new ArrayList<>(); try { String url = getMasterPlaylistUrl(); StreamSource src = new StreamSource(); src.mediaPlaylistUrl = url; src.height = 0; src.width = 0; src.bandwidth = 0; sources.add(src); } catch (IOException e) { LOG.error("Can not get stream sources for {}", getName()); } return sources; } private String getMasterPlaylistUrl() throws IOException { JSONObject json = getModelInfo(); String mpp = json.getString("mpp"); String lname = getName().toLowerCase(); return MessageFormat.format("https://stream14.cams.com/h5live/http/playlist.m3u8?url=rtmp%3A%2F%2F{0}%3A1935%2Fcams%2F{1}%3Fcams%2F{1}_720p&stream={2}", mpp, lname, getName()); } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { return new int[]{0, 0}; } private JSONObject getModelInfo() throws IOException { if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) { modelInfo = Optional.ofNullable(modelInfo).orElse(loadModelInfo()); } else { modelInfo = loadModelInfo(); } return modelInfo; } private JSONObject loadModelInfo() throws IOException { lastInfoRequest = Instant.now(); String url = "https://beta-api.cams.com/models/stream/" + getName() + "/"; Request req = new Request.Builder().url(url) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT_LANGUAGE, "en") .header(REFERER, getSite().getBaseUrl() + '/' + getName()) .build(); try (Response response = site.getHttpClient().execute(req)) { if (response.isSuccessful()) { JSONObject jsonResponse = new JSONObject(response.body().string()); return jsonResponse; } else { throw new HttpException(response.code(), response.message()); } } } public String getPreviewURL() { String lname = getName().toLowerCase(); String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0, 1), lname.substring(lname.length() - 1), lname); try { return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:320::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, UTF_8)); } catch (Exception ex) { return url; } } @Override public Download createDownload() { return new FfmpegHlsDownload(getSite().getHttpClient()); } @Override public boolean follow() throws IOException { return false; } @Override public boolean unfollow() throws IOException { return false; } @Override public void receiveTip(Double tokens) throws IOException { // not implemented } @Override public void invalidateCacheEntries() { status = null; lastInfoRequest = Instant.EPOCH; modelInfo = null; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public void setRegDate(LocalDate reg) { this.regDate = reg; } public boolean isNew() { return ChronoUnit.DAYS.between(this.regDate, LocalDate.now()) < 30; } }