Add setting to restrict recording by bit rate

This commit is contained in:
0xb00bface 2023-12-30 18:48:14 +01:00
parent 959b41e3b9
commit 257bdda8f7
32 changed files with 567 additions and 796 deletions

View File

@ -12,8 +12,8 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.event.PlayerStartedEvent; import ctbrec.ui.event.PlayerStartedEvent;
import ctbrec.variableexpansion.ModelVariableExpander; import ctbrec.variableexpansion.ModelVariableExpander;
import javafx.scene.Scene; import javafx.scene.Scene;
import org.slf4j.Logger; import lombok.Getter;
import org.slf4j.LoggerFactory; import lombok.extern.slf4j.Slf4j;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
import java.io.File; import java.io.File;
@ -29,8 +29,8 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@Slf4j
public class Player { public class Player {
private static final Logger LOG = LoggerFactory.getLogger(Player.class);
private static PlayerThread playerThread; private static PlayerThread playerThread;
private static Scene scene; private static Scene scene;
@ -47,7 +47,7 @@ public class Player {
playerThread = new PlayerThread(rec); playerThread = new PlayerThread(rec);
return true; return true;
} catch (Exception e1) { } catch (Exception e1) {
LOG.error("Couldn't start player", e1); log.error("Couldn't start player", e1);
return false; return false;
} }
} }
@ -81,11 +81,11 @@ public class Player {
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.error("Couldn't get stream information for model {}", model, e); log.error("Couldn't get stream information for model {}", model, e);
Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e); Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e);
return false; return false;
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't get stream information for model {}", model, e); log.error("Couldn't get stream information for model {}", model, e);
Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e); Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e);
return false; return false;
} }
@ -98,6 +98,7 @@ public class Player {
} }
private static class PlayerThread extends Thread { private static class PlayerThread extends Thread {
@Getter
private boolean running = false; private boolean running = false;
private Process playerProcess; private Process playerProcess;
private Recording rec; private Recording rec;
@ -134,9 +135,9 @@ public class Player {
} else if (model != null) { } else if (model != null) {
url = getPlaylistUrl(model); url = getPlaylistUrl(model);
} }
LOG.debug("Playing {}", url); log.debug("Playing {}", url);
String[] cmdline = createCmdline(url, model); String[] cmdline = createCmdline(url, model);
LOG.debug("Player command line: {}", Arrays.toString(cmdline)); log.debug("Player command line: {}", Arrays.toString(cmdline));
playerProcess = rt.exec(cmdline); playerProcess = rt.exec(cmdline);
} }
@ -152,13 +153,13 @@ public class Player {
err.start(); err.start();
playerProcess.waitFor(); playerProcess.waitFor();
LOG.debug("Media player finished."); log.debug("Media player finished.");
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.error("Error in player thread", e); log.error("Error in player thread", e);
Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e); Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error in player thread", e); log.error("Error in player thread", e);
Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e); Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e);
} }
running = false; running = false;
@ -172,8 +173,8 @@ public class Player {
if (maxRes > 0 && !sources.isEmpty()) { if (maxRes > 0 && !sources.isEmpty()) {
for (Iterator<StreamSource> iterator = sources.iterator(); iterator.hasNext(); ) { for (Iterator<StreamSource> iterator = sources.iterator(); iterator.hasNext(); ) {
StreamSource streamSource = iterator.next(); StreamSource streamSource = iterator.next();
if (streamSource.height > 0 && maxRes < streamSource.height) { if (streamSource.getHeight() > 0 && maxRes < streamSource.getHeight()) {
LOG.trace("Res too high {} > {}", streamSource.height, maxRes); log.trace("Res too high {} > {}", streamSource.getHeight(), maxRes);
iterator.remove(); iterator.remove();
} }
} }
@ -181,7 +182,7 @@ public class Player {
if (sources.isEmpty()) { if (sources.isEmpty()) {
throw new NoStreamFoundException("No stream left in playlist, because player resolution is set to " + maxRes); throw new NoStreamFoundException("No stream left in playlist, because player resolution is set to " + maxRes);
} else { } else {
LOG.debug("{} selected {}", model.getName(), sources.get(sources.size() - 1)); log.debug("{} selected {}", model.getName(), sources.get(sources.size() - 1));
best = sources.get(sources.size() - 1); best = sources.get(sources.size() - 1);
} }
return best.getMediaPlaylistUrl(); return best.getMediaPlaylistUrl();
@ -226,10 +227,6 @@ public class Player {
return recUrl; return recUrl;
} }
public boolean isRunning() {
return running;
}
public void stopThread() { public void stopThread() {
if (playerProcess != null) { if (playerProcess != null) {
playerProcess.destroy(); playerProcess.destroy();

View File

@ -33,6 +33,7 @@ import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.util.Duration; import javafx.util.Duration;
import lombok.Getter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -128,10 +129,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleStringProperty dateTimeFormat; private SimpleStringProperty dateTimeFormat;
private final VariablePlayGroundDialogFactory variablePlayGroundDialogFactory = new VariablePlayGroundDialogFactory(); private final VariablePlayGroundDialogFactory variablePlayGroundDialogFactory = new VariablePlayGroundDialogFactory();
private SimpleBooleanProperty checkForUpdates; private SimpleBooleanProperty checkForUpdates;
private PostProcessingStepPanel postProcessingStepPanel;
private SimpleStringProperty filterBlacklist; private SimpleStringProperty filterBlacklist;
private SimpleStringProperty filterWhitelist; private SimpleStringProperty filterWhitelist;
private SimpleBooleanProperty deleteOrphanedRecordingMetadata; private SimpleBooleanProperty deleteOrphanedRecordingMetadata;
private SimpleIntegerProperty restrictBitrate;
public SettingsTab(List<Site> sites, Recorder recorder) { public SettingsTab(List<Site> sites, Recorder recorder) {
this.sites = sites; this.sites = sites;
@ -213,6 +214,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
filterBlacklist = new SimpleStringProperty(null, "filterBlacklist", settings.filterBlacklist); filterBlacklist = new SimpleStringProperty(null, "filterBlacklist", settings.filterBlacklist);
filterWhitelist = new SimpleStringProperty(null, "filterWhitelist", settings.filterWhitelist); filterWhitelist = new SimpleStringProperty(null, "filterWhitelist", settings.filterWhitelist);
deleteOrphanedRecordingMetadata = new SimpleBooleanProperty(null, "deleteOrphanedRecordingMetadata", settings.deleteOrphanedRecordingMetadata); deleteOrphanedRecordingMetadata = new SimpleBooleanProperty(null, "deleteOrphanedRecordingMetadata", settings.deleteOrphanedRecordingMetadata);
restrictBitrate = new SimpleIntegerProperty(null, "restrictBitrate", settings.restrictBitrate);
} }
private void createGui() { private void createGui() {
@ -275,6 +277,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Split recordings after", splitAfter).converter(SplitAfterOption.converter()).onChange(this::splitValuesChanged), Setting.of("Split recordings after", splitAfter).converter(SplitAfterOption.converter()).onChange(this::splitValuesChanged),
Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter()).onChange(this::splitValuesChanged), Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter()).onChange(this::splitValuesChanged),
Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"), Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"),
Setting.of("Restrict Video Bitrate (kbps, 0 = unlimited)", restrictBitrate, "Only record streams with a video bitrate below this limit (kbps)"),
Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings), Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings),
Setting.of("Default Priority", defaultPriority, "lowest 0 - 10000 highest"), Setting.of("Default Priority", defaultPriority, "lowest 0 - 10000 highest"),
Setting.of("Default duration for \"Record until\" (minutes)", recordUntilDefaultDurationInMinutes), Setting.of("Default duration for \"Record until\" (minutes)", recordUntilDefaultDurationInMinutes),
@ -544,11 +547,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
return transition; return transition;
} }
public record SplitAfterOption(String label, int value) { public record SplitAfterOption(String label, @Getter int value) {
public int getValue() {
return value;
}
@Override @Override
public String toString() { public String toString() {
@ -590,11 +589,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
} }
} }
public record SplitBiggerThanOption(String label, long value) { public record SplitBiggerThanOption(String label, @Getter long value) {
public long getValue() {
return value;
}
@Override @Override
public String toString() { public String toString() {

View File

@ -1,37 +1,7 @@
package ctbrec.ui.sites.myfreecams; package ctbrec.ui.sites.myfreecams;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import javax.xml.bind.JAXBException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool; import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
@ -46,15 +16,7 @@ import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.menu.ModelMenuContributor; import ctbrec.ui.menu.ModelMenuContributor;
import ctbrec.ui.tabs.TabSelectionListener; import ctbrec.ui.tabs.TabSelectionListener;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.*;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -65,19 +27,8 @@ import javafx.geometry.Insets;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
@ -85,22 +36,41 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration; import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.WRITE;
@Slf4j
public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class); private final ScrollPane scrollPane = new ScrollPane();
private ScrollPane scrollPane = new ScrollPane(); private final TableView<ModelTableRow> table = new TableView<>();
private TableView<ModelTableRow> table = new TableView<>(); private final ObservableList<ModelTableRow> filteredModels = FXCollections.observableArrayList();
private ObservableList<ModelTableRow> filteredModels = FXCollections.observableArrayList(); private final ObservableList<ModelTableRow> observableModels = FXCollections.observableArrayList();
private ObservableList<ModelTableRow> observableModels = FXCollections.observableArrayList(); private final Recorder recorder;
private final MyFreeCams mfc;
private final ReentrantLock lock = new ReentrantLock();
private final Label count = new Label("models");
private final List<TableColumn<ModelTableRow, ?>> columns = new ArrayList<>();
private TableUpdateService updateService; private TableUpdateService updateService;
private MyFreeCams mfc;
private ReentrantLock lock = new ReentrantLock();
private SearchBox filterInput; private SearchBox filterInput;
private Label count = new Label("models");
private List<TableColumn<ModelTableRow, ?>> columns = new ArrayList<>();
private ContextMenu popup; private ContextMenu popup;
private long lastJsonWrite = 0; private long lastJsonWrite = 0;
private Recorder recorder;
public MyFreeCamsTableTab(MyFreeCams mfc, Recorder recorder) { public MyFreeCamsTableTab(MyFreeCams mfc, Recorder recorder) {
this.mfc = mfc; this.mfc = mfc;
@ -118,7 +88,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
updateService = new TableUpdateService(mfc); updateService = new TableUpdateService(mfc);
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1))); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1)));
updateService.setOnSucceeded(this::onSuccess); updateService.setOnSucceeded(this::onSuccess);
updateService.setOnFailed(event -> LOG.info("Couldn't update MyFreeCams model table", event.getSource().getException())); updateService.setOnFailed(event -> log.info("Couldn't update MyFreeCams model table", event.getSource().getException()));
} }
private void onSuccess(WorkerStateEvent evt) { private void onSuccess(WorkerStateEvent evt) {
@ -139,16 +109,16 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
} }
} }
for (Iterator<ModelTableRow> iterator = observableModels.iterator(); iterator.hasNext();) { for (Iterator<ModelTableRow> iterator = observableModels.iterator(); iterator.hasNext(); ) {
ModelTableRow model = iterator.next(); ModelTableRow model = iterator.next();
var found = false; var found = false;
for (SessionState sessionState : sessionStates) { for (SessionState sessionState : sessionStates) {
if(Objects.equals(sessionState.getUid(), model.uid)) { if (Objects.equals(sessionState.getUid(), model.uid)) {
found = true; found = true;
break; break;
} }
} }
if(!found) { if (!found) {
iterator.remove(); iterator.remove();
} }
} }
@ -161,7 +131,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
table.sort(); table.sort();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if( (now - lastJsonWrite) > TimeUnit.SECONDS.toMillis(30)) { if ((now - lastJsonWrite) > TimeUnit.SECONDS.toMillis(30)) {
lastJsonWrite = now; lastJsonWrite = now;
saveData(); saveData();
} }
@ -174,7 +144,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
filterInput = new SearchBox(false); filterInput = new SearchBox(false);
filterInput.setPromptText("Filter"); filterInput.setPromptText("Filter");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { filterInput.textProperty().addListener((observableValue, oldValue, newValue) -> {
String filter = filterInput.getText(); String filter = filterInput.getText();
Config.getInstance().getSettings().mfcModelsTableFilter = filter; Config.getInstance().getSettings().mfcModelsTableFilter = filter;
lock.lock(); lock.lock();
@ -218,7 +188,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
popup.hide(); popup.hide();
} }
}); });
table.getColumns().addListener((ListChangeListener<TableColumn<?, ?>>)(e -> saveState())); table.getColumns().addListener((ListChangeListener<TableColumn<?, ?>>) (e -> saveState()));
var idx = 0; var idx = 0;
TableColumn<ModelTableRow, Number> uid = createTableColumn("UID", 65, idx++); TableColumn<ModelTableRow, Number> uid = createTableColumn("UID", 65, idx++);
@ -313,9 +283,9 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
ContextMenu menu = new CustomMouseBehaviorContextMenu(); ContextMenu menu = new CustomMouseBehaviorContextMenu();
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) // ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
.afterwards(table::refresh) .afterwards(table::refresh)
.contributeToMenu(selectedModels, menu); .contributeToMenu(selectedModels, menu);
addDebuggingInDevMode(menu, selectedModels); addDebuggingInDevMode(menu, selectedModels);
@ -331,11 +301,11 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
try { try {
List<StreamSource> sources = m.getStreamSources(); List<StreamSource> sources = m.getStreamSources();
for (StreamSource src : sources) { for (StreamSource src : sources) {
LOG.info("m:{} s:{} bandwidth:{} height:{}", m.getName(), src.mediaPlaylistUrl, src.bandwidth, src.height); log.info("m:{} s:{} bandwidth:{} height:{}", m.getName(), src.getMediaPlaylistUrl(), src.getBandwidth(), src.getHeight());
} }
LOG.info("==============="); log.info("===============");
} catch (IOException | ExecutionException | ParseException | PlaylistException | JAXBException e1) { } catch (IOException | ExecutionException | ParseException | PlaylistException | JAXBException e1) {
LOG.error("Couldn't get stream sources", e1); log.error("Couldn't get stream sources", e1);
} }
} }
})); }));
@ -357,7 +327,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
} }
private void addTableColumnIfEnabled(TableColumn<ModelTableRow, ?> tc) { private void addTableColumnIfEnabled(TableColumn<ModelTableRow, ?> tc) {
if(isColumnEnabled(tc)) { if (isColumnEnabled(tc)) {
table.getColumns().add(tc); table.getColumns().add(tc);
} }
} }
@ -444,7 +414,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
ps.println(); ps.println();
} }
} catch (Exception e) { } catch (Exception e) {
LOG.debug("Couldn't write mfc models table data", e); log.debug("Couldn't write mfc models table data", e);
} }
} }
} }
@ -466,7 +436,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
boolean added = false; boolean added = false;
for (int i = table.getColumns().size() - 1; i >= 0; i--) { for (int i = table.getColumns().size() - 1; i >= 0; i--) {
TableColumn<ModelTableRow, ?> other = table.getColumns().get(i); TableColumn<ModelTableRow, ?> other = table.getColumns().get(i);
LOG.debug("Adding column {}", tc.getText()); log.debug("Adding column {}", tc.getText());
int idx = (int) tc.getUserData(); int idx = (int) tc.getUserData();
int otherIdx = (int) other.getUserData(); int otherIdx = (int) other.getUserData();
if (otherIdx < idx) { if (otherIdx < idx) {
@ -516,7 +486,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
@Override @Override
public void deselected() { public void deselected() {
if(updateService != null) { if (updateService != null) {
updateService.cancel(); updateService.cancel();
} }
saveData(); saveData();
@ -546,26 +516,26 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
data.put(model); data.put(model);
} }
var file = new File(Config.getInstance().getConfigDir(), "mfc-models.json"); var file = new File(Config.getInstance().getConfigDir(), "mfc-models.json");
Files.write(file.toPath(), data.toString(2).getBytes(UTF_8), CREATE, WRITE); Files.writeString(file.toPath(), data.toString(2), CREATE, WRITE);
saveState(); saveState();
} catch (Exception e) { } catch (Exception e) {
LOG.debug("Couldn't write mfc models table data: {}", e.getMessage()); log.debug("Couldn't write mfc models table data: {}", e.getMessage());
} }
} }
private void loadData() { private void loadData() {
try { try {
var file = new File(Config.getInstance().getConfigDir(), "mfc-models.json"); var file = new File(Config.getInstance().getConfigDir(), "mfc-models.json");
if(!file.exists()) { if (!file.exists()) {
return; return;
} }
var json = new String(Files.readAllBytes(file.toPath()), UTF_8); var json = Files.readString(file.toPath());
var data = new JSONArray(json); var data = new JSONArray(json);
for (var i = 0; i < data.length(); i++) { for (var i = 0; i < data.length(); i++) {
createRow(data, i).ifPresent(observableModels::add); createRow(data, i).ifPresent(observableModels::add);
} }
} catch (Exception e) { } catch (Exception e) {
LOG.debug("Couldn't read mfc models table data: {}", e.getMessage()); log.debug("Couldn't read mfc models table data: {}", e.getMessage());
} }
} }
@ -634,10 +604,10 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
private void restoreColumnOrder() { private void restoreColumnOrder() {
String[] columnIds = Config.getInstance().getSettings().mfcModelsTableColumnIds; String[] columnIds = Config.getInstance().getSettings().mfcModelsTableColumnIds;
ObservableList<TableColumn<ModelTableRow,?>> tableColumns = table.getColumns(); ObservableList<TableColumn<ModelTableRow, ?>> tableColumns = table.getColumns();
for (var i = 0; i < columnIds.length; i++) { for (var i = 0; i < columnIds.length; i++) {
for (var j = 0; j < table.getColumns().size(); j++) { for (var j = 0; j < table.getColumns().size(); j++) {
if(Objects.equals(columnIds[i], tableColumns.get(j).getId())) { if (Objects.equals(columnIds[i], tableColumns.get(j).getId())) {
TableColumn<ModelTableRow, ?> col = tableColumns.get(j); TableColumn<ModelTableRow, ?> col = tableColumns.get(j);
tableColumns.remove(j); // NOSONAR tableColumns.remove(j); // NOSONAR
tableColumns.add(i, col); tableColumns.add(i, col);
@ -661,21 +631,21 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
private static class ModelTableRow { private static class ModelTableRow {
private Integer uid; private Integer uid;
private StringProperty name = new SimpleStringProperty(); private final StringProperty name = new SimpleStringProperty();
private StringProperty state = new SimpleStringProperty(); private final StringProperty state = new SimpleStringProperty();
private DoubleProperty camScore = new SimpleDoubleProperty(); private final DoubleProperty camScore = new SimpleDoubleProperty();
private StringProperty newModel = new SimpleStringProperty(); private final StringProperty newModel = new SimpleStringProperty();
private StringProperty ethnic = new SimpleStringProperty(); private final StringProperty ethnic = new SimpleStringProperty();
private StringProperty country = new SimpleStringProperty(); private final StringProperty country = new SimpleStringProperty();
private StringProperty continent = new SimpleStringProperty(); private final StringProperty continent = new SimpleStringProperty();
private StringProperty occupation = new SimpleStringProperty(); private final StringProperty occupation = new SimpleStringProperty();
private StringProperty tags = new SimpleStringProperty(); private final StringProperty tags = new SimpleStringProperty();
private StringProperty blurp = new SimpleStringProperty(); private final StringProperty blurp = new SimpleStringProperty();
private StringProperty topic = new SimpleStringProperty(); private final StringProperty topic = new SimpleStringProperty();
private BooleanProperty isHd = new SimpleBooleanProperty(); private final BooleanProperty isHd = new SimpleBooleanProperty();
private BooleanProperty isWebrtc = new SimpleBooleanProperty(); private final BooleanProperty isWebrtc = new SimpleBooleanProperty();
private SimpleIntegerProperty uidProperty = new SimpleIntegerProperty(); private final SimpleIntegerProperty uidProperty = new SimpleIntegerProperty();
private SimpleIntegerProperty flagsProperty = new SimpleIntegerProperty(); private final SimpleIntegerProperty flagsProperty = new SimpleIntegerProperty();
public ModelTableRow(SessionState st) { public ModelTableRow(SessionState st) {
update(st); update(st);
@ -691,9 +661,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
setProperty(state, Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString())); setProperty(state, Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()));
setProperty(camScore, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getCamscore)); setProperty(camScore, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getCamscore));
Optional<Integer> isNew = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getNewModel); Optional<Integer> isNew = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getNewModel);
if (isNew.isPresent()) { isNew.ifPresent(integer -> newModel.set(integer == 1 ? "new" : ""));
newModel.set(isNew.get() == 1 ? "new" : "");
}
setProperty(ethnic, Optional.ofNullable(st.getU()).map(User::getEthnic)); setProperty(ethnic, Optional.ofNullable(st.getU()).map(User::getEthnic));
setProperty(country, Optional.ofNullable(st.getU()).map(User::getCountry)); setProperty(country, Optional.ofNullable(st.getU()).map(User::getCountry));
setProperty(continent, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getContinent)); setProperty(continent, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getContinent));
@ -712,16 +680,12 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
} }
setProperty(blurp, Optional.ofNullable(st.getU()).map(User::getBlurb)); setProperty(blurp, Optional.ofNullable(st.getU()).map(User::getBlurb));
String tpc = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getTopic).orElse("n/a"); String tpc = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getTopic).orElse("n/a");
try { tpc = URLDecoder.decode(tpc, UTF_8);
tpc = URLDecoder.decode(tpc, "utf-8");
} catch (UnsupportedEncodingException e) {
LOG.warn("Couldn't url decode topic", e);
}
topic.set(tpc); topic.set(tpc);
} }
private <T> void setProperty(Property<T> prop, Optional<T> value) { private <T> void setProperty(Property<T> prop, Optional<T> value) {
if(value.isPresent() && !Objects.equals(value.get(), prop.getValue())) { if (value.isPresent() && !Objects.equals(value.get(), prop.getValue())) {
prop.setValue(value.get()); prop.setValue(value.get());
} }
} }
@ -804,11 +768,8 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
return false; return false;
ModelTableRow other = (ModelTableRow) obj; ModelTableRow other = (ModelTableRow) obj;
if (uid == null) { if (uid == null) {
if (other.uid != null) return other.uid == null;
return false; } else return uid.equals(other.uid);
} else if (!uid.equals(other.uid))
return false;
return true;
} }
} }
} }

View File

@ -219,4 +219,5 @@ public class Settings {
public boolean dreamcamVR = false; public boolean dreamcamVR = false;
public String filterBlacklist = ""; public String filterBlacklist = "";
public String filterWhitelist = ""; public String filterWhitelist = "";
public int restrictBitrate = 0;
} }

View File

@ -65,6 +65,8 @@ public class OnlineMonitor extends Thread {
} }
private void updateModels(List<Model> models) { private void updateModels(List<Model> models) {
// sort models by adding date (new go first)
models.sort((a, b) -> b.getAddedTimestamp().compareTo(a.getAddedTimestamp()));
// sort models by priority // sort models by priority
models.sort((a, b) -> b.getPriority() - a.getPriority()); models.sort((a, b) -> b.getPriority() - a.getPriority());
// submit online check jobs to the executor for the model's site // submit online check jobs to the executor for the model's site

View File

@ -93,23 +93,27 @@ public abstract class AbstractDownload implements RecordingProcess {
streamSources.forEach(ss -> log.debug(ss.toString())); streamSources.forEach(ss -> log.debug(ss.toString()));
StreamSource source = streamSources.get(model.getStreamUrlIndex()); StreamSource source = streamSources.get(model.getStreamUrlIndex());
log.debug("{} selected {}", model.getName(), source); log.debug("{} selected {}", model.getName(), source);
selectedResolution = source.height; selectedResolution = source.getHeight();
return source; return source;
} else { } else {
// filter out stream resolutions, which are out of range of the configured min and max // filter out stream resolutions, which are out of range of the configured min and max
int minRes = Config.getInstance().getSettings().minimumResolution; int minRes = Config.getInstance().getSettings().minimumResolution;
int maxRes = Config.getInstance().getSettings().maximumResolution; int maxRes = Config.getInstance().getSettings().maximumResolution;
int bitrateLimit = Config.getInstance().getSettings().restrictBitrate * 1024;
List<StreamSource> filteredStreamSources = streamSources.stream() List<StreamSource> filteredStreamSources = streamSources.stream()
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height) .filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || minRes <= src.getHeight())
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height) .filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || maxRes >= src.getHeight())
.filter(src -> bitrateLimit == 0 || src.getBandwidth() == UNKNOWN || src.getBandwidth() <= bitrateLimit)
.toList(); .toList();
if (filteredStreamSources.isEmpty()) { if (filteredStreamSources.isEmpty()) {
// TODO save, why a stream has been filtered out and convey this information to the UI, so that the user understands why a recording
// doesn't start
throw new ExecutionException(new NoStreamFoundException("No stream left in playlist")); throw new ExecutionException(new NoStreamFoundException("No stream left in playlist"));
} else { } else {
StreamSource source = filteredStreamSources.get(filteredStreamSources.size() - 1); StreamSource source = filteredStreamSources.get(filteredStreamSources.size() - 1);
log.debug("{} selected {}", model.getName(), source); log.debug("{} selected {}", model.getName(), source);
selectedResolution = source.height; selectedResolution = source.getHeight();
return source; return source;
} }
} }

View File

@ -1,46 +1,20 @@
package ctbrec.recorder.download; package ctbrec.recorder.download;
import lombok.Getter;
import lombok.Setter;
import java.text.DecimalFormat; import java.text.DecimalFormat;
@Getter
@Setter
public class StreamSource implements Comparable<StreamSource> { public class StreamSource implements Comparable<StreamSource> {
public static final int ORIGIN = Integer.MAX_VALUE - 1; public static final int ORIGIN = Integer.MAX_VALUE - 1;
public static final int UNKNOWN = Integer.MAX_VALUE; public static final int UNKNOWN = Integer.MAX_VALUE;
public int bandwidth;
public int width;
public int height;
public String mediaPlaylistUrl;
public int getBandwidth() { private int bandwidth;
return bandwidth; private int width;
} private int height;
private String mediaPlaylistUrl;
public void setBandwidth(int bandwidth) {
this.bandwidth = bandwidth;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public String getMediaPlaylistUrl() {
return mediaPlaylistUrl;
}
public void setMediaPlaylistUrl(String mediaPlaylistUrl) {
this.mediaPlaylistUrl = mediaPlaylistUrl;
}
@Override @Override
public String toString() { public String toString() {
@ -51,6 +25,9 @@ public class StreamSource implements Comparable<StreamSource> {
} else if (height == ORIGIN) { } else if (height == ORIGIN) {
return "Origin"; return "Origin";
} else { } else {
if (height * width > 0) {
return height + "p (" + df.format(mbit) + " Mbit/s) [" + df.format((float) bandwidth / height / width) + " bit/pix]";
}
return height + "p (" + df.format(mbit) + " Mbit/s)"; return height + "p (" + df.format(mbit) + " Mbit/s)";
} }
} }
@ -62,7 +39,7 @@ public class StreamSource implements Comparable<StreamSource> {
@Override @Override
public int compareTo(StreamSource o) { public int compareTo(StreamSource o) {
int heightDiff = height - o.height; int heightDiff = height - o.height;
if(heightDiff != 0 && height != UNKNOWN && o.height != UNKNOWN) { if (heightDiff != 0 && height != UNKNOWN && o.height != UNKNOWN) {
return heightDiff; return heightDiff;
} else { } else {
return bandwidth - o.bandwidth; return bandwidth - o.bandwidth;
@ -98,10 +75,6 @@ public class StreamSource implements Comparable<StreamSource> {
return false; return false;
} else if (!mediaPlaylistUrl.equals(other.mediaPlaylistUrl)) } else if (!mediaPlaylistUrl.equals(other.mediaPlaylistUrl))
return false; return false;
if (width != other.width) return width == other.width;
return false;
return true;
} }
} }

View File

@ -29,6 +29,7 @@ import javax.xml.bind.JAXBException;
import java.io.*; import java.io.*;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.file.Files; import java.nio.file.Files;
import java.text.DecimalFormat; import java.text.DecimalFormat;
@ -248,7 +249,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
} }
try { try {
LOG.debug("Waiting {}ms before trying to update the playlist URL", A_FEW_SECONDS); LOG.debug("Waiting {}ms before trying to update the playlist URL", A_FEW_SECONDS);
waitSomeTime(A_FEW_SECONDS); waitSomeTime();
segmentPlaylistUrl = getSegmentPlaylistUrl(model); segmentPlaylistUrl = getSegmentPlaylistUrl(model);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Playlist URL couldn't be updated after waiting for {}ms", A_FEW_SECONDS, e); LOG.error("Playlist URL couldn't be updated after waiting for {}ms", A_FEW_SECONDS, e);
@ -264,7 +265,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
} }
StreamSource selectedStreamSource = selectStreamSource(streamSources); StreamSource selectedStreamSource = selectStreamSource(streamSources);
String url = selectedStreamSource.getMediaPlaylistUrl(); String url = selectedStreamSource.getMediaPlaylistUrl();
selectedResolution = selectedStreamSource.height; selectedResolution = selectedStreamSource.getHeight();
LOG.debug("Segment playlist url {}", url); LOG.debug("Segment playlist url {}", url);
return url; return url;
} }
@ -272,7 +273,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
protected SegmentPlaylist getNextSegments(String segmentPlaylistUrl) throws IOException, ParseException, PlaylistException { protected SegmentPlaylist getNextSegments(String segmentPlaylistUrl) throws IOException, ParseException, PlaylistException {
Instant start = Instant.now(); Instant start = Instant.now();
recordingEvents.add(RecordingEvent.of("Playlist request")); recordingEvents.add(RecordingEvent.of("Playlist request"));
URL segmentsUrl = new URL(segmentPlaylistUrl); URL segmentsUrl = URI.create(segmentPlaylistUrl).toURL();
Builder builder = new Request.Builder().url(segmentsUrl); Builder builder = new Request.Builder().url(segmentsUrl);
addHeaders(builder, Optional.ofNullable(model).map(Model::getHttpHeaderFactory).map(HttpHeaderFactory::createSegmentPlaylistHeaders).orElse(new HashMap<>()), model); addHeaders(builder, Optional.ofNullable(model).map(Model::getHttpHeaderFactory).map(HttpHeaderFactory::createSegmentPlaylistHeaders).orElse(new HashMap<>()), model);
Request request = builder.build(); Request request = builder.build();
@ -321,27 +322,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
List<TrackData> tracks = mediaPlaylist.getTracks(); List<TrackData> tracks = mediaPlaylist.getTracks();
for (TrackData trackData : tracks) { for (TrackData trackData : tracks) {
if (trackData.hasMapInfo()) { parseSegmentData(lsp, trackData);
var mapInfoUri = trackData.getMapInfo().getUri();
if (!mapInfoUri.startsWith("http")) {
URL context = new URL(segmentPlaylistUrl);
mapInfoUri = new URL(context, mapInfoUri).toExternalForm();
}
lsp.segments.add(new Segment(mapInfoUri, Math.max(0, trackData.getTrackInfo().duration)));
}
String uri = trackData.getUri();
if (!uri.startsWith("http")) {
URL context = new URL(segmentPlaylistUrl);
uri = new URL(context, uri).toExternalForm();
}
lsp.totalDuration += trackData.getTrackInfo().duration;
lsp.segments.add(new Segment(uri, Math.max(0, trackData.getTrackInfo().duration)));
if (trackData.hasEncryptionData()) {
lsp.encrypted = true;
EncryptionData data = trackData.getEncryptionData();
lsp.encryptionKeyUrl = data.getUri();
lsp.encryptionMethod = data.getMethod().getValue();
}
} }
lsp.avgSegDuration = lsp.totalDuration / tracks.size(); lsp.avgSegDuration = lsp.totalDuration / tracks.size();
return lsp; return lsp;
@ -349,6 +330,30 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
throw new InvalidPlaylistException("Playlist has no media playlist"); throw new InvalidPlaylistException("Playlist has no media playlist");
} }
private void parseSegmentData(SegmentPlaylist lsp, TrackData trackData) {
if (trackData.hasMapInfo()) {
var mapInfoUri = trackData.getMapInfo().getUri();
if (!mapInfoUri.startsWith("http")) {
URI context = URI.create(segmentPlaylistUrl);
mapInfoUri = context.resolve(mapInfoUri).toString();
}
lsp.segments.add(new Segment(mapInfoUri, Math.max(0, trackData.getTrackInfo().duration)));
}
String uri = trackData.getUri();
if (!uri.startsWith("http")) {
URI context = URI.create(segmentPlaylistUrl);
uri = context.resolve(uri).toString();
}
lsp.totalDuration += trackData.getTrackInfo().duration;
lsp.segments.add(new Segment(uri, Math.max(0, trackData.getTrackInfo().duration)));
if (trackData.hasEncryptionData()) {
lsp.encrypted = true;
EncryptionData data = trackData.getEncryptionData();
lsp.encryptionKeyUrl = data.getUri();
lsp.encryptionMethod = data.getMethod().getValue();
}
}
protected void emptyPlaylistCheck(SegmentPlaylist playlist) { protected void emptyPlaylistCheck(SegmentPlaylist playlist) {
if (playlist.segments.isEmpty()) { if (playlist.segments.isEmpty()) {
playlistEmptyCount++; playlistEmptyCount++;
@ -423,9 +428,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
* This is used to slow down retries, if something is wrong with the playlist. * This is used to slow down retries, if something is wrong with the playlist.
* E.g. HTTP 403 or 404 * E.g. HTTP 403 or 404
*/ */
protected void waitSomeTime(long waitForMillis) { protected void waitSomeTime() {
try { try {
Thread.sleep(waitForMillis); Thread.sleep(A_FEW_SECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
if (running) { if (running) {
@ -452,11 +457,6 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
return running; return running;
} }
@Override
public int getSelectedResolution() {
return selectedResolution;
}
private static class RecordingEvent { private static class RecordingEvent {
Instant timestamp; Instant timestamp;
String message; String message;

View File

@ -28,7 +28,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@ -61,7 +61,6 @@ public class FfmpegHlsDownload extends AbstractDownload {
private volatile boolean running; private volatile boolean running;
private volatile boolean started; private volatile boolean started;
private int selectedResolution = 0;
public FfmpegHlsDownload(HttpClient httpClient) { public FfmpegHlsDownload(HttpClient httpClient) {
this.httpClient = httpClient; this.httpClient = httpClient;
@ -82,11 +81,6 @@ public class FfmpegHlsDownload extends AbstractDownload {
} }
} }
@Override
public int getSelectedResolution() {
return selectedResolution;
}
@Override @Override
public void stop() { public void stop() {
if (running) { if (running) {
@ -232,7 +226,7 @@ public class FfmpegHlsDownload extends AbstractDownload {
} }
StreamSource selectedStreamSource = selectStreamSource(streamSources); StreamSource selectedStreamSource = selectStreamSource(streamSources);
String playlistUrl = selectedStreamSource.getMediaPlaylistUrl(); String playlistUrl = selectedStreamSource.getMediaPlaylistUrl();
selectedResolution = selectedStreamSource.height; selectedResolution = selectedStreamSource.getHeight();
Request req = new Request.Builder() Request req = new Request.Builder()
.url(playlistUrl) .url(playlistUrl)
@ -255,8 +249,8 @@ public class FfmpegHlsDownload extends AbstractDownload {
} }
String uri = firstTrack.getUri(); String uri = firstTrack.getUri();
if (!uri.startsWith("http")) { if (!uri.startsWith("http")) {
URL context = new URL(playlistUrl); URI context = URI.create(playlistUrl);
uri = new URL(context, uri).toExternalForm(); uri = context.resolve(uri).toURL().toExternalForm();
} }
LOG.debug("Media url {}", uri); LOG.debug("Media url {}", uri);
return uri; return uri;
@ -301,16 +295,16 @@ public class FfmpegHlsDownload extends AbstractDownload {
} }
} }
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
LOG.debug("Socket timeout while downloading MP4 for {}. Stop recording.", model.getName()); LOG.debug("Socket timeout while downloading MP4 for {}. Stop recording.", model.getName());
model.delay(); model.delay();
stop(); stop();
} catch (IOException e) { } catch (IOException e) {
LOG.debug("IO error while downloading MP4 for {}. Stop recording.", model.getName()); LOG.debug("IO error while downloading MP4 for {}. Stop recording.", model.getName());
model.delay(); model.delay();
stop(); stop();
} catch (Exception e) { } catch (Exception e) {
LOG.error("Error while downloading MP4", e); LOG.error("Error while downloading MP4", e);
stop(); stop();
} finally { } finally {
ffmpegStreamLock.unlock(); ffmpegStreamLock.unlock();
} }

View File

@ -25,7 +25,6 @@ import java.util.Map.Entry;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static ctbrec.recorder.download.StreamSource.UNKNOWN; import static ctbrec.recorder.download.StreamSource.UNKNOWN;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
@ -141,9 +140,9 @@ public class HlsdlDownload extends AbstractDownload {
int minRes = Config.getInstance().getSettings().minimumResolution; int minRes = Config.getInstance().getSettings().minimumResolution;
int maxRes = Config.getInstance().getSettings().maximumResolution; int maxRes = Config.getInstance().getSettings().maximumResolution;
List<StreamSource> filteredStreamSources = streamSources.stream() List<StreamSource> filteredStreamSources = streamSources.stream()
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height) .filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || minRes <= src.getHeight())
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height) .filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || maxRes >= src.getHeight())
.collect(Collectors.toList()); .toList();
if (filteredStreamSources.isEmpty()) { if (filteredStreamSources.isEmpty()) {
throw new ExecutionException(new NoStreamFoundException("No stream left in playlist")); throw new ExecutionException(new NoStreamFoundException("No stream left in playlist"));

View File

@ -134,9 +134,9 @@ public class AmateurTvDownload extends AbstractDownload {
running = true; running = true;
try { try {
StreamSource src = model.getStreamSources().get(0); StreamSource src = model.getStreamSources().get(0);
LOG.debug("Loading video from {}", src.mediaPlaylistUrl); LOG.debug("Loading video from {}", src.getMediaPlaylistUrl());
Request request = new Request.Builder() Request request = new Request.Builder()
.url(src.mediaPlaylistUrl) .url(src.getMediaPlaylistUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en") .header(ACCEPT_LANGUAGE, "en")

View File

@ -76,10 +76,10 @@ public class AmateurTvModel extends AbstractModel {
String value = (String) item; String value = (String) item;
String[] res = value.split("x"); String[] res = value.split("x");
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.mediaPlaylistUrl = MessageFormat.format("{0}&variant={1}", mediaPlaylistUrl, res[1]); src.setMediaPlaylistUrl(MessageFormat.format("{0}&variant={1}", mediaPlaylistUrl, res[1]));
src.width = Integer.parseInt(res[0]); src.setWidth(Integer.parseInt(res[0]));
src.height = Integer.parseInt(res[1]); src.setHeight(Integer.parseInt(res[1]));
src.bandwidth = 0; src.setBandwidth(0);
streamSources.add(src); streamSources.add(src);
}); });
return streamSources; return streamSources;

View File

@ -10,14 +10,14 @@ import ctbrec.Config;
import ctbrec.io.HtmlParser; import ctbrec.io.HtmlParser;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONObject; import org.json.JSONObject;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -30,15 +30,16 @@ import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
@Slf4j
public class BongaCamsModel extends AbstractModel { public class BongaCamsModel extends AbstractModel {
private static final String ARGS = "args[]"; private static final String ARGS = "args[]";
private static final Logger LOG = LoggerFactory.getLogger(BongaCamsModel.class);
private static final String SUCCESS = "success"; private static final String SUCCESS = "success";
private static final String STATUS = "status"; private static final String STATUS = "status";
private static final Pattern ONLINE_BADGE_REGEX = Pattern.compile("class=\"badge_online\s*\""); private static final Pattern ONLINE_BADGE_REGEX = Pattern.compile("class=\"badge_online\s*\"");
@Setter
private boolean online = false; private boolean online = false;
private final transient List<StreamSource> streamSources = new ArrayList<>(); private final transient List<StreamSource> streamSources = new ArrayList<>();
private int[] resolution; private int[] resolution;
@ -102,23 +103,21 @@ public class BongaCamsModel extends AbstractModel {
} }
} }
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Couldn't check if model is connected: {}", e.getLocalizedMessage()); log.warn("Couldn't check if model is connected: {}", e.getLocalizedMessage());
return false; return false;
} }
} }
public State mapState(String roomState) { public State mapState(String roomState) {
switch (roomState) { return switch (roomState) {
case "private", "fullprivate": case "private", "fullprivate" -> PRIVATE;
return PRIVATE; case "group" -> GROUP;
case "group": case "public" -> ONLINE;
return GROUP; default -> {
case "public": log.debug(roomState);
return ONLINE; yield OFFLINE;
default: }
LOG.debug(roomState); };
return OFFLINE;
}
} }
private boolean isStreamAvailable() { private boolean isStreamAvailable() {
@ -134,7 +133,7 @@ public class BongaCamsModel extends AbstractModel {
} }
} }
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Couldn't check if stream is available: {}", e.getLocalizedMessage()); log.warn("Couldn't check if stream is available: {}", e.getLocalizedMessage());
return false; return false;
} }
} }
@ -162,10 +161,6 @@ public class BongaCamsModel extends AbstractModel {
} }
} }
public void setOnline(boolean online) {
this.online = online;
}
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (!failFast) { if (!failFast) {
@ -181,11 +176,6 @@ public class BongaCamsModel extends AbstractModel {
return onlineState; return onlineState;
} }
@Override
public void setOnlineState(State onlineState) {
this.onlineState = onlineState;
}
@Override @Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
String streamUrl = getStreamUrl(); String streamUrl = getStreamUrl();
@ -208,16 +198,16 @@ public class BongaCamsModel extends AbstractModel {
streamSources.clear(); streamSources.clear();
for (PlaylistData playlistData : master.getPlaylists()) { for (PlaylistData playlistData : master.getPlaylists()) {
StreamSource streamsource = new StreamSource(); StreamSource streamsource = new StreamSource();
streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); streamsource.setMediaPlaylistUrl(streamUrl.replace("playlist.m3u8", playlistData.getUri()));
if (playlistData.hasStreamInfo()) { if (playlistData.hasStreamInfo()) {
StreamInfo info = playlistData.getStreamInfo(); StreamInfo info = playlistData.getStreamInfo();
streamsource.bandwidth = info.getBandwidth(); streamsource.setBandwidth(info.getBandwidth());
streamsource.width = info.hasResolution() ? info.getResolution().width : 0; streamsource.setWidth(info.hasResolution() ? info.getResolution().width : 0);
streamsource.height = info.hasResolution() ? info.getResolution().height : 0; streamsource.setHeight(info.hasResolution() ? info.getResolution().height : 0);
} else { } else {
streamsource.bandwidth = 0; streamsource.setBandwidth(0);
streamsource.width = 0; streamsource.setWidth(0);
streamsource.height = 0; streamsource.setHeight(0);
} }
streamSources.add(streamsource); streamSources.add(streamsource);
} }
@ -261,7 +251,7 @@ public class BongaCamsModel extends AbstractModel {
if (response.isSuccessful()) { if (response.isSuccessful()) {
JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string()); JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string());
if (!json.optString(STATUS).equals(SUCCESS)) { if (!json.optString(STATUS).equals(SUCCESS)) {
LOG.error("Sending tip failed {}", json.toString(2)); log.error("Sending tip failed {}", json.toString(2));
throw new IOException("Sending tip failed"); throw new IOException("Sending tip failed");
} }
} else { } else {
@ -283,13 +273,13 @@ public class BongaCamsModel extends AbstractModel {
List<StreamSource> sources = getStreamSources(); List<StreamSource> sources = getStreamSources();
Collections.sort(sources); Collections.sort(sources);
StreamSource best = sources.get(sources.size() - 1); StreamSource best = sources.get(sources.size() - 1);
resolution = new int[]{best.width, best.height}; resolution = new int[]{best.getWidth(), best.getHeight()};
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2]; resolution = new int[2];
} catch (ExecutionException | IOException | ParseException | PlaylistException e) { } catch (ExecutionException | IOException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2]; resolution = new int[2];
} }
} }
@ -303,7 +293,7 @@ public class BongaCamsModel extends AbstractModel {
} }
String url = getSite().getBaseUrl() + "/follow/" + getName(); String url = getSite().getBaseUrl() + "/follow/" + getName();
LOG.debug("Calling {}", url); log.debug("Calling {}", url);
RequestBody body = new FormBody.Builder() RequestBody body = new FormBody.Builder()
.add("src", "public-chat") .add("src", "public-chat")
.add("_csrf_token", getCsrfToken()) .add("_csrf_token", getCsrfToken())
@ -319,10 +309,10 @@ public class BongaCamsModel extends AbstractModel {
String msg = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); String msg = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
JSONObject json = new JSONObject(msg); JSONObject json = new JSONObject(msg);
if (json.optBoolean(SUCCESS)) { if (json.optBoolean(SUCCESS)) {
LOG.debug("Follow/Unfollow -> {}", msg); log.debug("Follow/Unfollow -> {}", msg);
return true; return true;
} else { } else {
LOG.debug(msg); log.debug(msg);
throw new IOException("Response was " + msg); throw new IOException("Response was " + msg);
} }
} else { } else {
@ -338,7 +328,7 @@ public class BongaCamsModel extends AbstractModel {
String content = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); String content = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
Element html = HtmlParser.getTag(content, "html"); Element html = HtmlParser.getTag(content, "html");
String csrfToken = html.attr("data-csrf_value"); String csrfToken = html.attr("data-csrf_value");
LOG.debug("CSRF-Token {}", csrfToken); log.debug("CSRF-Token {}", csrfToken);
return csrfToken; return csrfToken;
} else { } else {
throw new HttpException(resp.code(), resp.message()); throw new HttpException(resp.code(), resp.message());
@ -353,7 +343,7 @@ public class BongaCamsModel extends AbstractModel {
} }
String url = getSite().getBaseUrl() + "/unfollow/" + getName(); String url = getSite().getBaseUrl() + "/unfollow/" + getName();
LOG.debug("Calling {}", url); log.debug("Calling {}", url);
RequestBody body = new FormBody.Builder() RequestBody body = new FormBody.Builder()
.add("_csrf_token", getCsrfToken()) .add("_csrf_token", getCsrfToken())
.build(); .build();
@ -368,10 +358,10 @@ public class BongaCamsModel extends AbstractModel {
String msg = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); String msg = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
JSONObject json = new JSONObject(msg); JSONObject json = new JSONObject(msg);
if (json.optBoolean(SUCCESS)) { if (json.optBoolean(SUCCESS)) {
LOG.debug("Follow/Unfollow -> {}", msg); log.debug("Follow/Unfollow -> {}", msg);
return true; return true;
} else { } else {
LOG.debug(msg); log.debug(msg);
throw new IOException("Response was " + msg); throw new IOException("Response was " + msg);
} }
} else { } else {
@ -388,7 +378,7 @@ public class BongaCamsModel extends AbstractModel {
setOnline(true); setOnline(true);
} }
default -> { default -> {
LOG.debug(roomState); log.debug(roomState);
setOnlineState(OFFLINE); setOnlineState(OFFLINE);
} }
} }

View File

@ -13,11 +13,11 @@ import ctbrec.io.HttpException;
import ctbrec.recorder.download.HttpHeaderFactory; import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl; import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -33,12 +33,13 @@ import static ctbrec.io.HttpConstants.*;
import static java.util.regex.Pattern.DOTALL; import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.MULTILINE; import static java.util.regex.Pattern.MULTILINE;
@Slf4j
public class Cam4Model extends AbstractModel { public class Cam4Model extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(Cam4Model.class); @Setter
private String playlistUrl; private String playlistUrl;
private int[] resolution = null; private int[] resolution = null;
private JSONObject modelInfo; private transient JSONObject modelInfo;
@Override @Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
@ -57,7 +58,7 @@ public class Cam4Model extends AbstractModel {
private JSONObject loadModelInfo() throws IOException { private JSONObject loadModelInfo() throws IOException {
JSONObject roomState = new Cam4WsClient(Config.getInstance(), (Cam4) getSite(), this).getRoomState(); JSONObject roomState = new Cam4WsClient(Config.getInstance(), (Cam4) getSite(), this).getRoomState();
LOG.trace(roomState.toString(2)); log.trace(roomState.toString(2));
String state = roomState.optString("newShowsState"); String state = roomState.optString("newShowsState");
setOnlineStateByShowType(state); setOnlineStateByShowType(state);
setDescription(roomState.optString("status")); setDescription(roomState.optString("status"));
@ -73,7 +74,7 @@ public class Cam4Model extends AbstractModel {
case "PAUSED" -> onlineState = AWAY; case "PAUSED" -> onlineState = AWAY;
case "OFFLINE" -> onlineState = OFFLINE; case "OFFLINE" -> onlineState = OFFLINE;
default -> { default -> {
LOG.debug("############################## Unknown show type [{} {}]", this, showType); log.debug("############################## Unknown show type [{} {}]", this, showType);
onlineState = UNKNOWN; onlineState = UNKNOWN;
} }
} }
@ -86,7 +87,7 @@ public class Cam4Model extends AbstractModel {
try { try {
modelInfo = loadModelInfo(); modelInfo = loadModelInfo();
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Couldn't load model details {}", e.getMessage()); log.warn("Couldn't load model details {}", e.getMessage());
} }
} }
return onlineState; return onlineState;
@ -99,11 +100,11 @@ public class Cam4Model extends AbstractModel {
return playlistUrl; return playlistUrl;
} }
} catch (IOException e) { } catch (IOException e) {
LOG.debug("Couldn't get playlist url from stream info: {}", e.getMessage()); log.debug("Couldn't get playlist url from stream info: {}", e.getMessage());
} }
if (modelInfo != null && modelInfo.has("hls")) { if (modelInfo != null && modelInfo.has("hls")) {
String hls = modelInfo.optString("hls"); String hls = modelInfo.optString("hls");
LOG.debug("Stream hls: {}", hls); log.debug("Stream hls: {}", hls);
if (StringUtil.isNotBlank(hls) && hls.startsWith("http")) { if (StringUtil.isNotBlank(hls) && hls.startsWith("http")) {
playlistUrl = hls; playlistUrl = hls;
return playlistUrl; return playlistUrl;
@ -111,7 +112,7 @@ public class Cam4Model extends AbstractModel {
} }
if (modelInfo != null && modelInfo.has("streamUUID")) { if (modelInfo != null && modelInfo.has("streamUUID")) {
String uuid = modelInfo.optString("streamUUID"); String uuid = modelInfo.optString("streamUUID");
LOG.debug("Stream UUID: {}", uuid); log.debug("Stream UUID: {}", uuid);
String[] parts = uuid.split("-"); String[] parts = uuid.split("-");
if (parts.length > 3) { if (parts.length > 3) {
String urlTemplate = "https://cam4-hls.xcdnpro.com/{0}/cam4-origin-live/{1}_aac/playlist.m3u8"; String urlTemplate = "https://cam4-hls.xcdnpro.com/{0}/cam4-origin-live/{1}_aac/playlist.m3u8";
@ -133,7 +134,7 @@ public class Cam4Model extends AbstractModel {
private void getPlaylistUrlFromStreamUrl() throws IOException { private void getPlaylistUrlFromStreamUrl() throws IOException {
String url = getSite().getBaseUrl() + "/rest/v1.0/profile/" + getName() + "/streamInfo"; String url = getSite().getBaseUrl() + "/rest/v1.0/profile/" + getName() + "/streamInfo";
LOG.trace("Getting playlist url from {}", url); log.trace("Getting playlist url from {}", url);
Request req = new Request.Builder() // @formatter:off Request req = new Request.Builder() // @formatter:off
.url(url) .url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
@ -146,7 +147,7 @@ public class Cam4Model extends AbstractModel {
try (Response response = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
JSONObject json = new JSONObject(bodyToJsonObject(response)); JSONObject json = new JSONObject(bodyToJsonObject(response));
if (LOG.isTraceEnabled()) LOG.trace(json.toString(2)); if (log.isTraceEnabled()) log.trace(json.toString(2));
if (json.has("canUseCDN")) { if (json.has("canUseCDN")) {
if (json.getBoolean("canUseCDN")) { if (json.getBoolean("canUseCDN")) {
playlistUrl = json.optString("cdnURL"); playlistUrl = json.optString("cdnURL");
@ -186,15 +187,15 @@ public class Cam4Model extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0); src.setHeight(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0));
if (playlist.getUri().startsWith("http")) { if (playlist.getUri().startsWith("http")) {
src.mediaPlaylistUrl = playlist.getUri(); src.setMediaPlaylistUrl(playlist.getUri());
} else { } else {
String baseUrl = playlistUrl.substring(0, playlistUrl.lastIndexOf('/') + 1); String baseUrl = playlistUrl.substring(0, playlistUrl.lastIndexOf('/') + 1);
src.mediaPlaylistUrl = baseUrl + playlist.getUri(); src.setMediaPlaylistUrl(baseUrl + playlist.getUri());
} }
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -204,7 +205,7 @@ public class Cam4Model extends AbstractModel {
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
String masterPlaylistUrl = getPlaylistUrl(); String masterPlaylistUrl = getPlaylistUrl();
masterPlaylistUrl = masterPlaylistUrl.replace("_sfm4s", ""); masterPlaylistUrl = masterPlaylistUrl.replace("_sfm4s", "");
LOG.debug("Loading master playlist [{}]", masterPlaylistUrl); log.debug("Loading master playlist [{}]", masterPlaylistUrl);
Request.Builder builder = new Request.Builder().url(masterPlaylistUrl); Request.Builder builder = new Request.Builder().url(masterPlaylistUrl);
getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header); getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header);
Request req = builder.build(); Request req = builder.build();
@ -245,13 +246,13 @@ public class Cam4Model extends AbstractModel {
List<StreamSource> sources = getStreamSources(); List<StreamSource> sources = getStreamSources();
Collections.sort(sources); Collections.sort(sources);
StreamSource best = sources.get(sources.size() - 1); StreamSource best = sources.get(sources.size() - 1);
resolution = new int[]{best.width, best.height}; resolution = new int[]{best.getWidth(), best.getHeight()};
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2]; resolution = new int[2];
} catch (ExecutionException | IOException | ParseException | PlaylistException e) { } catch (ExecutionException | IOException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2]; resolution = new int[2];
} }
} }
@ -285,10 +286,6 @@ public class Cam4Model extends AbstractModel {
} }
} }
public void setPlaylistUrl(String playlistUrl) {
this.playlistUrl = playlistUrl;
}
@Override @Override
public void setUrl(String url) { public void setUrl(String url) {
String normalizedUrl = url.toLowerCase(); String normalizedUrl = url.toLowerCase();

View File

@ -9,14 +9,15 @@ import ctbrec.AbstractModel;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -26,16 +27,19 @@ import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
@Slf4j
public class CamsodaModel extends AbstractModel { public class CamsodaModel extends AbstractModel {
private static final String STREAM_NAME = "stream_name"; private static final String STREAM_NAME = "stream_name";
private static final String EDGE_SERVERS = "edge_servers"; private static final String EDGE_SERVERS = "edge_servers";
private static final String STATUS = "status"; private static final String STATUS = "status";
private static final Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
private transient List<StreamSource> streamSources = null; private transient List<StreamSource> streamSources = null;
private transient boolean isNew; private transient boolean isNew;
@Getter
@Setter
private transient String gender; private transient String gender;
@Getter
@Setter
private float sortOrder = 0; private float sortOrder = 0;
private final Random random = new Random(); private final Random random = new Random();
int[] resolution = new int[2]; int[] resolution = new int[2];
@ -68,7 +72,7 @@ public class CamsodaModel extends AbstractModel {
if (!isPublic(streamName)) { if (!isPublic(streamName)) {
url.append("?token=").append(token); url.append("?token=").append(token);
} }
LOG.trace("Stream URL: {}", url); log.trace("Stream URL: {}", url);
return url.toString(); return url.toString();
} }
@ -104,7 +108,7 @@ public class CamsodaModel extends AbstractModel {
if (playlistUrl == null) { if (playlistUrl == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
LOG.trace("Loading playlist {}", playlistUrl); log.trace("Loading playlist {}", playlistUrl);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(playlistUrl) .url(playlistUrl)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
@ -121,21 +125,21 @@ public class CamsodaModel extends AbstractModel {
StreamSource streamsource = new StreamSource(); StreamSource streamsource = new StreamSource();
int cutOffAt = Math.max(playlistUrl.indexOf("index.m3u8"), playlistUrl.indexOf("playlist.m3u8")); int cutOffAt = Math.max(playlistUrl.indexOf("index.m3u8"), playlistUrl.indexOf("playlist.m3u8"));
String segmentPlaylistUrl = playlistUrl.substring(0, cutOffAt) + playlistData.getUri(); String segmentPlaylistUrl = playlistUrl.substring(0, cutOffAt) + playlistData.getUri();
streamsource.mediaPlaylistUrl = segmentPlaylistUrl; streamsource.setMediaPlaylistUrl(segmentPlaylistUrl);
if (playlistData.hasStreamInfo()) { if (playlistData.hasStreamInfo()) {
StreamInfo info = playlistData.getStreamInfo(); StreamInfo info = playlistData.getStreamInfo();
streamsource.bandwidth = info.getBandwidth(); streamsource.setBandwidth(info.getBandwidth());
streamsource.width = info.hasResolution() ? info.getResolution().width : 0; streamsource.setWidth(info.hasResolution() ? info.getResolution().width : 0);
streamsource.height = info.hasResolution() ? info.getResolution().height : 0; streamsource.setHeight(info.hasResolution() ? info.getResolution().height : 0);
} else { } else {
streamsource.bandwidth = 0; streamsource.setBandwidth(0);
streamsource.width = 0; streamsource.setWidth(0);
streamsource.height = 0; streamsource.setHeight(0);
} }
streamSources.add(streamsource); streamSources.add(streamsource);
} }
} else { } else {
LOG.trace("Response: {}", response.body().string()); log.trace("Response: {}", response.body().string());
throw new HttpException(playlistUrl, response.code(), response.message()); throw new HttpException(playlistUrl, response.code(), response.message());
} }
} }
@ -177,7 +181,7 @@ public class CamsodaModel extends AbstractModel {
case "private" -> onlineState = PRIVATE; case "private" -> onlineState = PRIVATE;
case "limited" -> onlineState = GROUP; case "limited" -> onlineState = GROUP;
default -> { default -> {
LOG.debug("Unknown show type {}", status); log.debug("Unknown show type {}", status);
onlineState = UNKNOWN; onlineState = UNKNOWN;
} }
} }
@ -211,12 +215,12 @@ public class CamsodaModel extends AbstractModel {
} else { } else {
try { try {
List<StreamSource> sources = getStreamSources(); List<StreamSource> sources = getStreamSources();
LOG.debug("{}:{} stream sources {}", getSite().getName(), getName(), sources); log.debug("{}:{} stream sources {}", getSite().getName(), getName(), sources);
if (sources.isEmpty()) { if (sources.isEmpty()) {
return new int[]{0, 0}; return new int[]{0, 0};
} else { } else {
StreamSource src = sources.get(sources.size() - 1); StreamSource src = sources.get(sources.size() - 1);
resolution = new int[]{src.width, src.height}; resolution = new int[]{src.getWidth(), src.getHeight()};
return resolution; return resolution;
} }
} catch (IOException | ParseException | PlaylistException e) { } catch (IOException | ParseException | PlaylistException e) {
@ -230,7 +234,7 @@ public class CamsodaModel extends AbstractModel {
String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken(); String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken();
String url = site.getBaseUrl() + "/api/v1/tip/" + getName(); String url = site.getBaseUrl() + "/api/v1/tip/" + getName();
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
LOG.debug("Sending tip {}", url); log.debug("Sending tip {}", url);
RequestBody body = new FormBody.Builder() RequestBody body = new FormBody.Builder()
.add("amount", Integer.toString(tokens.intValue())) .add("amount", Integer.toString(tokens.intValue()))
.add("comment", "") .add("comment", "")
@ -255,7 +259,7 @@ public class CamsodaModel extends AbstractModel {
@Override @Override
public boolean follow() throws IOException { public boolean follow() throws IOException {
String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName(); String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName();
LOG.debug("Sending follow request {}", url); log.debug("Sending follow request {}", url);
String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken(); String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken();
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
@ -278,7 +282,7 @@ public class CamsodaModel extends AbstractModel {
@Override @Override
public boolean unfollow() throws IOException { public boolean unfollow() throws IOException {
String url = Camsoda.BASE_URI + "/api/v1/unfollow/" + getName(); String url = Camsoda.BASE_URI + "/api/v1/unfollow/" + getName();
LOG.debug("Sending unfollow request {}", url); log.debug("Sending unfollow request {}", url);
String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken(); String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken();
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
@ -298,14 +302,6 @@ public class CamsodaModel extends AbstractModel {
} }
} }
public float getSortOrder() {
return sortOrder;
}
public void setSortOrder(float sortOrder) {
this.sortOrder = sortOrder;
}
public boolean isNew() { public boolean isNew() {
return isNew; return isNew;
} }
@ -313,12 +309,4 @@ public class CamsodaModel extends AbstractModel {
public void setNew(boolean isNew) { public void setNew(boolean isNew) {
this.isNew = isNew; this.isNew = isNew;
} }
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
} }

View File

@ -11,13 +11,12 @@ import ctbrec.StringUtil;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.io.json.ObjectMapperFactory; import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.EOFException; import java.io.EOFException;
@ -32,10 +31,10 @@ import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class ChaturbateModel extends AbstractModel { public class ChaturbateModel extends AbstractModel {
private static final String PUBLIC = "public"; private static final String PUBLIC = "public";
private static final Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class);
private int[] resolution = new int[2]; private int[] resolution = new int[2];
private transient StreamInfo streamInfo; private transient StreamInfo streamInfo;
private transient Instant lastStreamInfoRequest = Instant.EPOCH; private transient Instant lastStreamInfoRequest = Instant.EPOCH;
@ -60,11 +59,11 @@ public class ChaturbateModel extends AbstractModel {
if (isOffline()) { if (isOffline()) {
roomStatus = "offline"; roomStatus = "offline";
onlineState = State.OFFLINE; onlineState = State.OFFLINE;
LOG.trace("Model {} offline", getName()); log.trace("Model {} offline", getName());
} else { } else {
StreamInfo info = getStreamInfo(); StreamInfo info = getStreamInfo();
roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse(""); roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse("");
LOG.trace("Model {} room status: {}", getName(), Optional.ofNullable(info).map(i -> i.room_status).orElse("unknown")); log.trace("Model {} room status: {}", getName(), Optional.ofNullable(info).map(i -> i.room_status).orElse("unknown"));
} }
} else { } else {
StreamInfo info = getStreamInfo(true); StreamInfo info = getStreamInfo(true);
@ -165,7 +164,7 @@ public class ChaturbateModel extends AbstractModel {
case "away" -> onlineState = AWAY; case "away" -> onlineState = AWAY;
case "group" -> onlineState = State.GROUP; case "group" -> onlineState = State.GROUP;
default -> { default -> {
LOG.debug("Unknown show type {}", roomStatus); log.debug("Unknown show type {}", roomStatus);
onlineState = State.UNKNOWN; onlineState = State.UNKNOWN;
} }
} }
@ -203,16 +202,16 @@ public class ChaturbateModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = playlist.getStreamInfo().getResolution().height; src.setHeight(playlist.getStreamInfo().getResolution().height);
String masterUrl = streamInfo.url; String masterUrl = streamInfo.url;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri(); String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri; src.setMediaPlaylistUrl(segmentUri);
if (src.mediaPlaylistUrl.contains("?")) { if (src.getMediaPlaylistUrl().contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
} }
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -261,10 +260,10 @@ public class ChaturbateModel extends AbstractModel {
String responseBody = resp2.body().string(); String responseBody = resp2.body().string();
JSONObject json = new JSONObject(responseBody); JSONObject json = new JSONObject(responseBody);
if (!json.has("following")) { if (!json.has("following")) {
LOG.debug(responseBody); log.debug(responseBody);
throw new IOException("Response was " + responseBody.substring(0, Math.min(responseBody.length(), 500))); throw new IOException("Response was " + responseBody.substring(0, Math.min(responseBody.length(), 500)));
} else { } else {
LOG.debug("Follow/Unfollow -> {}", responseBody); log.debug("Follow/Unfollow -> {}", responseBody);
return json.getBoolean("following") == follow; return json.getBoolean("following") == follow;
} }
} else { } else {
@ -303,7 +302,7 @@ public class ChaturbateModel extends AbstractModel {
lastStreamInfoRequest = Instant.now(); lastStreamInfoRequest = Instant.now();
if (response.isSuccessful()) { if (response.isSuccessful()) {
String content = response.body().string(); String content = response.body().string();
LOG.trace("Raw stream info for model {}: {}", getName(), content); log.trace("Raw stream info for model {}: {}", getName(), content);
streamInfo = mapper.readValue(content, StreamInfo.class); streamInfo = mapper.readValue(content, StreamInfo.class);
return streamInfo; return streamInfo;
} else { } else {
@ -355,7 +354,7 @@ public class ChaturbateModel extends AbstractModel {
} }
private MasterPlaylist getMasterPlaylist(StreamInfo streamInfo) throws IOException, ParseException, PlaylistException { private MasterPlaylist getMasterPlaylist(StreamInfo streamInfo) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", streamInfo.url); log.trace("Loading master playlist {}", streamInfo.url);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(streamInfo.url) .url(streamInfo.url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
@ -363,7 +362,7 @@ public class ChaturbateModel extends AbstractModel {
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
LOG.trace(body); log.trace(body);
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();

View File

@ -7,14 +7,15 @@ import com.iheartradio.m3u8.data.PlaylistData;
import ctbrec.*; import ctbrec.*;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -30,14 +31,17 @@ import static ctbrec.Model.State.ONLINE;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class CherryTvModel extends AbstractModel { public class CherryTvModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(CherryTvModel.class);
private static final Pattern NEXT_DATA = Pattern.compile("<script id=\"__NEXT_DATA__\" type=\"application/json\">(.*?)</script>"); private static final Pattern NEXT_DATA = Pattern.compile("<script id=\"__NEXT_DATA__\" type=\"application/json\">(.*?)</script>");
@Setter
private boolean online = false; private boolean online = false;
private int[] resolution; private int[] resolution;
private String masterPlaylistUrl; private String masterPlaylistUrl;
@Getter
@Setter
private String id; private String id;
@Override @Override
@ -57,25 +61,25 @@ public class CherryTvModel extends AbstractModel {
JSONObject json = new JSONObject(m.group(1)); JSONObject json = new JSONObject(m.group(1));
updateModelProperties(json); updateModelProperties(json);
} else { } else {
LOG.error("NEXT_DATA not found in model page {}", getUrl()); log.error("NEXT_DATA not found in model page {}", getUrl());
return false; return false;
} }
} catch (JSONException e) { } catch (JSONException e) {
LOG.error("Unable to determine online state for {}. Probably the JSON structure in NEXT_DATA changed", getName()); log.error("Unable to determine online state for {}. Probably the JSON structure in NEXT_DATA changed", getName());
} }
} }
return online; return online;
} }
private void updateModelProperties(JSONObject json) { private void updateModelProperties(JSONObject json) {
LOG.trace(json.toString(2)); log.trace(json.toString(2));
JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState"); JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState");
online = false; online = false;
onlineState = OFFLINE; onlineState = OFFLINE;
for (Iterator<String> iter = apolloState.keys(); iter.hasNext(); ) { for (Iterator<String> iter = apolloState.keys(); iter.hasNext(); ) {
String key = iter.next(); String key = iter.next();
if (key.startsWith("Broadcast:")) { if (key.startsWith("Broadcast:")) {
LOG.trace("Model properties:\n{}", apolloState.toString(2)); log.trace("Model properties:\n{}", apolloState.toString(2));
JSONObject broadcast = apolloState.getJSONObject(key); JSONObject broadcast = apolloState.getJSONObject(key);
setDisplayName(broadcast.optString("title")); setDisplayName(broadcast.optString("title"));
online = broadcast.optString("showStatus").equalsIgnoreCase("Public") online = broadcast.optString("showStatus").equalsIgnoreCase("Public")
@ -89,10 +93,6 @@ public class CherryTvModel extends AbstractModel {
} }
} }
public void setOnline(boolean online) {
this.online = online;
}
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (!failFast) { if (!failFast) {
@ -117,16 +117,16 @@ public class CherryTvModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = playlist.getStreamInfo().getResolution().height; src.setHeight(playlist.getStreamInfo().getResolution().height);
String masterUrl = masterPlaylistUrl; String masterUrl = masterPlaylistUrl;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri(); String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri; src.setMediaPlaylistUrl(segmentUri);
if (src.mediaPlaylistUrl.contains("?")) { if (src.getMediaPlaylistUrl().contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
} }
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -139,10 +139,10 @@ public class CherryTvModel extends AbstractModel {
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
if (masterPlaylistUrl == null) { if (masterPlaylistUrl == null) {
LOG.info("Master playlist not found for {}:{}. This probably is webrtc stream", getSite().getName(), getName()); log.info("Master playlist not found for {}:{}. This probably is webrtc stream", getSite().getName(), getName());
throw new StreamNotFoundException("Webrtc streams are not supported for " + getSite().getName()); throw new StreamNotFoundException("Webrtc streams are not supported for " + getSite().getName());
} }
LOG.trace("Loading master playlist {}", masterPlaylistUrl); log.trace("Loading master playlist {}", masterPlaylistUrl);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(masterPlaylistUrl) .url(masterPlaylistUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
@ -150,7 +150,7 @@ public class CherryTvModel extends AbstractModel {
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body()).string(); String body = Objects.requireNonNull(response.body()).string();
LOG.trace(body); log.trace(body);
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
@ -185,13 +185,13 @@ public class CherryTvModel extends AbstractModel {
List<StreamSource> sources = getStreamSources(); List<StreamSource> sources = getStreamSources();
Collections.sort(sources); Collections.sort(sources);
StreamSource best = sources.get(sources.size() - 1); StreamSource best = sources.get(sources.size() - 1);
resolution = new int[]{best.width, best.height}; resolution = new int[]{best.getWidth(), best.getHeight()};
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2]; resolution = new int[2];
} catch (ExecutionException | IOException | ParseException | PlaylistException e) { } catch (ExecutionException | IOException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2]; resolution = new int[2];
} }
} }
@ -210,11 +210,11 @@ public class CherryTvModel extends AbstractModel {
private boolean followUnfollow(String action, String persistedQueryHash) throws IOException { private boolean followUnfollow(String action, String persistedQueryHash) throws IOException {
Request request = createFollowUnfollowRequest(action, persistedQueryHash); Request request = createFollowUnfollowRequest(action, persistedQueryHash);
LOG.debug("Sending follow request for model {} with ID {}", getName(), getId()); log.debug("Sending follow request for model {} with ID {}", getName(), getId());
try (Response response = getSite().getHttpClient().execute(request)) { try (Response response = getSite().getHttpClient().execute(request)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String responseBody = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); String responseBody = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
LOG.debug(responseBody); log.debug(responseBody);
JSONObject resp = new JSONObject(responseBody); JSONObject resp = new JSONObject(responseBody);
if (resp.has("data") && !resp.isNull("data")) { if (resp.has("data") && !resp.isNull("data")) {
JSONObject data = resp.getJSONObject("data"); JSONObject data = resp.getJSONObject("data");
@ -227,7 +227,7 @@ public class CherryTvModel extends AbstractModel {
return true; return true;
} }
} }
LOG.debug(resp.toString(2)); log.debug(resp.toString(2));
return false; return false;
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -271,14 +271,6 @@ public class CherryTvModel extends AbstractModel {
.build(); .build();
} }
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
@Override @Override
public void readSiteSpecificData(Map<String, String> data) { public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id"); id = data.get("id");

View File

@ -80,7 +80,7 @@ public class DreamcamModel extends AbstractModel {
List<StreamSource> sources = new ArrayList<>(); List<StreamSource> sources = new ArrayList<>();
try { try {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.mediaPlaylistUrl = getPlaylistUrl(); src.setMediaPlaylistUrl(getPlaylistUrl());
sources.add(src); sources.add(src);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Can not get stream sources for {}: {}", getName(), e.getMessage()); LOG.error("Can not get stream sources for {}: {}", getName(), e.getMessage());

View File

@ -10,12 +10,13 @@ import ctbrec.Config;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import okio.ByteString; import okio.ByteString;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -27,15 +28,19 @@ import java.util.function.BiConsumer;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
@Slf4j
public class Fc2Model extends AbstractModel { public class Fc2Model extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(Fc2Model.class); @Getter
@Setter
private String id; private String id;
@Getter
@Setter
private int viewerCount; private int viewerCount;
private boolean online; private boolean online;
private String version; private String version;
private WebSocket ws;
private String playlistUrl; private String playlistUrl;
private AtomicInteger websocketUsage = new AtomicInteger(0); private transient WebSocket ws;
private final transient AtomicInteger websocketUsage = new AtomicInteger(0);
private long lastHeartBeat = System.currentTimeMillis(); private long lastHeartBeat = System.currentTimeMillis();
private int messageId = 1; private int messageId = 1;
@ -67,7 +72,7 @@ public class Fc2Model extends AbstractModel {
if (resp.isSuccessful()) { if (resp.isSuccessful()) {
String msg = resp.body().string(); String msg = resp.body().string();
JSONObject json = new JSONObject(msg); JSONObject json = new JSONObject(msg);
// LOG.debug(json.toString(2)); // log.debug(json.toString(2));
JSONObject data = json.getJSONObject("data"); JSONObject data = json.getJSONObject("data");
JSONObject channelData = data.getJSONObject("channel_data"); JSONObject channelData = data.getJSONObject("channel_data");
online = channelData.optInt("is_publish") == 1; online = channelData.optInt("is_publish") == 1;
@ -101,10 +106,8 @@ public class Fc2Model extends AbstractModel {
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
try { try {
openWebsocket(); openWebsocket();
List<StreamSource> sources = new ArrayList<>(); log.debug("Paylist url {}", playlistUrl);
LOG.debug("Paylist url {}", playlistUrl); return parseMasterPlaylist(playlistUrl);
sources.addAll(parseMasterPlaylist(playlistUrl));
return sources;
} catch (InterruptedException e1) { } catch (InterruptedException e1) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new ExecutionException(e1); throw new ExecutionException(e1);
@ -129,23 +132,22 @@ public class Fc2Model extends AbstractModel {
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist(); MasterPlaylist master = playlist.getMasterPlaylist();
sources.clear();
for (PlaylistData playlistData : master.getPlaylists()) { for (PlaylistData playlistData : master.getPlaylists()) {
StreamSource streamsource = new StreamSource(); StreamSource streamsource = new StreamSource();
streamsource.mediaPlaylistUrl = playlistData.getUri(); streamsource.setMediaPlaylistUrl(playlistData.getUri());
if (playlistData.hasStreamInfo()) { if (playlistData.hasStreamInfo()) {
StreamInfo info = playlistData.getStreamInfo(); StreamInfo info = playlistData.getStreamInfo();
streamsource.bandwidth = info.getBandwidth(); streamsource.setBandwidth(info.getBandwidth());
streamsource.width = info.hasResolution() ? info.getResolution().width : 0; streamsource.setWidth(info.hasResolution() ? info.getResolution().width : 0);
streamsource.height = info.hasResolution() ? info.getResolution().height : 0; streamsource.setHeight(info.hasResolution() ? info.getResolution().height : 0);
} else { } else {
streamsource.bandwidth = 0; streamsource.setBandwidth(0);
streamsource.width = 0; streamsource.setWidth(0);
streamsource.height = 0; streamsource.setHeight(0);
} }
sources.add(streamsource); sources.add(streamsource);
} }
LOG.debug(sources.toString()); log.debug(sources.toString());
return sources; return sources;
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -172,7 +174,7 @@ public class Fc2Model extends AbstractModel {
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build(); .build();
LOG.debug("Fetching page {}", url); log.debug("Fetching page {}", url);
try (Response resp = getSite().getHttpClient().execute(req)) { try (Response resp = getSite().getHttpClient().execute(req)) {
if (resp.isSuccessful()) { if (resp.isSuccessful()) {
String msg = resp.body().string(); String msg = resp.body().string();
@ -236,48 +238,28 @@ public class Fc2Model extends AbstractModel {
JSONObject json = new JSONObject(content); JSONObject json = new JSONObject(content);
return json.optInt("status") == 1; return json.optInt("status") == 1;
} else { } else {
LOG.error("Login failed {} {}", resp.code(), resp.message()); log.error("Login failed {} {}", resp.code(), resp.message());
return false; return false;
} }
} }
} }
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getViewerCount() {
return viewerCount;
}
public void setViewerCount(int viewerCount) {
this.viewerCount = viewerCount;
}
/** /**
* Opens a chat websocket connection. This connection is used to retrieve the HLS playlist url. It also has to be kept open as long as the HLS stream is * Opens a chat websocket connection. This connection is used to retrieve the HLS playlist url. It also has to be kept open as long as the HLS stream is
* "played". Fc2Model keeps track of the number of objects, which tried to open or close the websocket. As long as at least one object is using the * "played". Fc2Model keeps track of the number of objects, which tried to open or close the websocket. As long as at least one object is using the
* websocket, it is kept open. If the last object, which is using it, calls closeWebsocket, the websocket is closed. * websocket, it is kept open. If the last object, which is using it, calls closeWebsocket, the websocket is closed.
*
* @throws IOException
*/ */
public void openWebsocket() throws InterruptedException, IOException { public void openWebsocket() throws InterruptedException, IOException {
messageId = 1; messageId = 1;
int usage = websocketUsage.incrementAndGet(); int usage = websocketUsage.incrementAndGet();
LOG.debug("{} objects using the websocket for {}", usage, this); log.debug("{} objects using the websocket for {}", usage, this);
if (ws != null) { if (ws == null) {
return;
} else {
Object monitor = new Object(); Object monitor = new Object();
loadModelInfo(); loadModelInfo();
getControlToken((token, url) -> { getControlToken((token, url) -> {
url = url + "?control_token=" + token; url = url + "?control_token=" + token;
LOG.debug("Session token: {}", token); log.debug("Session token: {}", token);
LOG.debug("Getting playlist token over websocket {}", url); log.debug("Getting playlist token over websocket {}", url);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
@ -302,18 +284,19 @@ public class Fc2Model extends AbstractModel {
JSONArray playlists = args.getJSONArray("playlists_high_latency"); JSONArray playlists = args.getJSONArray("playlists_high_latency");
JSONObject playlist = playlists.getJSONObject(0); JSONObject playlist = playlists.getJSONObject(0);
playlistUrl = playlist.getString("url"); playlistUrl = playlist.getString("url");
LOG.debug("Master Playlist: {}", playlistUrl); log.debug("Master Playlist: {}", playlistUrl);
synchronized (monitor) { synchronized (monitor) {
monitor.notifyAll(); monitor.notifyAll();
} }
} else { } else {
LOG.trace(json.toString()); log.trace(json.toString());
} }
} }
} else if (json.optString("name").equals("user_count") || json.optString("name").equals("comment")) { } else if (json.optString("name").equals("user_count") || json.optString("name").equals("comment")) {
// ignore // ignore
log.trace("WS <-- {}: {}", getName(), text);
} else { } else {
LOG.trace("WS <-- {}: {}", getName(), text); log.trace("WS <-- {}: {}", getName(), text);
} }
// send heartbeat every now and again // send heartbeat every now and again
@ -321,24 +304,24 @@ public class Fc2Model extends AbstractModel {
if ((now - lastHeartBeat) > TimeUnit.SECONDS.toMillis(30)) { if ((now - lastHeartBeat) > TimeUnit.SECONDS.toMillis(30)) {
webSocket.send("{\"name\":\"heartbeat\",\"arguments\":{},\"id\":" + messageId + "}"); webSocket.send("{\"name\":\"heartbeat\",\"arguments\":{},\"id\":" + messageId + "}");
lastHeartBeat = now; lastHeartBeat = now;
LOG.trace("Sending heartbeat for {} (messageId: {})", getName(), messageId); log.trace("Sending heartbeat for {} (messageId: {})", getName(), messageId);
messageId++; messageId++;
} }
} }
@Override @Override
public void onMessage(WebSocket webSocket, ByteString bytes) { public void onMessage(WebSocket webSocket, ByteString bytes) {
LOG.debug("ws btxt {}", bytes); log.debug("ws btxt {}", bytes);
} }
@Override @Override
public void onClosed(WebSocket webSocket, int code, String reason) { public void onClosed(WebSocket webSocket, int code, String reason) {
LOG.debug("ws closed {} - {}", code, reason); log.debug("ws closed {} - {}", code, reason);
} }
@Override @Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) { public void onFailure(WebSocket webSocket, Throwable t, Response response) {
LOG.debug("ws failure", t); log.debug("ws failure", t);
response.close(); response.close();
} }
}); });
@ -355,9 +338,9 @@ public class Fc2Model extends AbstractModel {
public void closeWebsocket() { public void closeWebsocket() {
int websocketUsers = websocketUsage.decrementAndGet(); int websocketUsers = websocketUsage.decrementAndGet();
LOG.debug("{} objects using the websocket for {}", websocketUsers, this); log.debug("{} objects using the websocket for {}", websocketUsers, this);
if (websocketUsers == 0) { if (websocketUsers == 0) {
LOG.debug("Closing the websocket for {}", this); log.debug("Closing the websocket for {}", this);
ws.close(1000, ""); ws.close(1000, "");
ws = null; ws = null;
} }

View File

@ -10,11 +10,12 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -32,18 +33,22 @@ import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Locale.ENGLISH; import static java.util.Locale.ENGLISH;
@Slf4j
public class Flirt4FreeModel extends AbstractModel { public class Flirt4FreeModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class); @Setter
private String id; private String id;
private String chatHost; private String chatHost;
private String chatPort; private String chatPort;
private String chatToken; private String chatToken;
private String streamHost; private String streamHost;
@Setter
private String streamUrl; private String streamUrl;
int[] resolution = new int[2]; int[] resolution = new int[2];
private final transient Object monitor = new Object(); private final transient Object monitor = new Object();
@Getter
private final transient List<String> categories = new LinkedList<>(); private final transient List<String> categories = new LinkedList<>();
@Setter
private boolean online = false; private boolean online = false;
private boolean isInteractiveShow = false; private boolean isInteractiveShow = false;
private boolean isNew = false; private boolean isNew = false;
@ -93,7 +98,7 @@ public class Flirt4FreeModel extends AbstractModel {
JSONObject json = new JSONObject(body); JSONObject json = new JSONObject(body);
if (Objects.equals(json.optString("status"), "failed")) { if (Objects.equals(json.optString("status"), "failed")) {
if (Objects.equals(json.optString("message"), "Model is inactive")) { if (Objects.equals(json.optString("message"), "Model is inactive")) {
LOG.debug("Model inactive or deleted: {}", getName()); log.debug("Model inactive or deleted: {}", getName());
setMarkedForLaterRecording(true); setMarkedForLaterRecording(true);
} }
online = false; online = false;
@ -115,17 +120,15 @@ public class Flirt4FreeModel extends AbstractModel {
private void updateModelId(JSONObject json) { private void updateModelId(JSONObject json) {
if (json.has(MODEL_ID)) { if (json.has(MODEL_ID)) {
Object modelId = json.get(MODEL_ID); Object modelId = json.get(MODEL_ID);
if (modelId instanceof Number n) { if (modelId instanceof Number n && n.intValue() > 0) {
if (n.intValue() > 0) { id = String.valueOf(json.get(MODEL_ID));
id = String.valueOf(json.get(MODEL_ID));
}
} }
} }
} }
private void loadModelInfo() throws IOException { private void loadModelInfo() throws IOException {
String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id; String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id;
LOG.trace("Loading url {}", url); log.trace("Loading url {}", url);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -152,7 +155,7 @@ public class Flirt4FreeModel extends AbstractModel {
JSONObject user = config.getJSONObject("user"); JSONObject user = config.getJSONObject("user");
userIp = user.getString("ip"); userIp = user.getString("ip");
} else { } else {
LOG.trace("Loading model info failed. Assuming model {} is offline", getName()); log.trace("Loading model info failed. Assuming model {} is offline", getName());
online = false; online = false;
onlineState = Model.State.OFFLINE; onlineState = Model.State.OFFLINE;
} }
@ -171,21 +174,14 @@ public class Flirt4FreeModel extends AbstractModel {
}; };
} }
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
return getStreamSources(true);
}
private List<StreamSource> getStreamSources(boolean withWebsocket) throws IOException, ExecutionException, ParseException, PlaylistException {
MasterPlaylist masterPlaylist; MasterPlaylist masterPlaylist;
try { try {
if (withWebsocket) { acquireSlot();
acquireSlot(); try {
try { loadStreamUrl();
loadStreamUrl(); } finally {
} finally { releaseSlot();
releaseSlot();
}
} }
masterPlaylist = getMasterPlaylist(); masterPlaylist = getMasterPlaylist();
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -197,12 +193,12 @@ public class Flirt4FreeModel extends AbstractModel {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
StreamInfo info = playlist.getStreamInfo(); StreamInfo info = playlist.getStreamInfo();
src.bandwidth = info.getBandwidth(); src.setBandwidth(info.getBandwidth());
src.height = (info.hasResolution()) ? info.getResolution().height : 0; src.setHeight((info.hasResolution()) ? info.getResolution().height : 0);
src.width = (info.hasResolution()) ? info.getResolution().width : 0; src.setWidth((info.hasResolution()) ? info.getResolution().width : 0);
HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl); HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl);
src.mediaPlaylistUrl = "https://" + masterPlaylistUrl.host() + '/' + playlist.getUri(); src.setMediaPlaylistUrl("https://" + masterPlaylistUrl.host() + '/' + playlist.getUri());
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -210,7 +206,7 @@ public class Flirt4FreeModel extends AbstractModel {
} }
public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException { public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException {
LOG.trace("Loading master playlist {}", streamUrl); log.trace("Loading master playlist {}", streamUrl);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(streamUrl) .url(streamUrl)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -240,7 +236,7 @@ public class Flirt4FreeModel extends AbstractModel {
Objects.requireNonNull(chatHost, "chatHost is null"); Objects.requireNonNull(chatHost, "chatHost is null");
String h = chatHost.replace("chat", "chat-vip"); String h = chatHost.replace("chat", "chat-vip");
String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, UTF_8) + "&port_to_be=" + chatPort; String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, UTF_8) + "&port_to_be=" + chatPort;
LOG.trace("Opening chat websocket {}", url); log.trace("Opening chat websocket {}", url);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -253,16 +249,16 @@ public class Flirt4FreeModel extends AbstractModel {
getSite().getHttpClient().newWebSocket(req, new WebSocketListener() { getSite().getHttpClient().newWebSocket(req, new WebSocketListener() {
@Override @Override
public void onOpen(WebSocket webSocket, Response response) { public void onOpen(WebSocket webSocket, Response response) {
LOG.trace("Chat websocket for {} opened", getName()); log.trace("Chat websocket for {} opened", getName());
} }
@Override @Override
public void onMessage(WebSocket webSocket, String text) { public void onMessage(WebSocket webSocket, String text) {
LOG.trace("Chat wbesocket for {}: {}", getName(), text); log.trace("Chat wbesocket for {}: {}", getName(), text);
JSONObject json = new JSONObject(text); JSONObject json = new JSONObject(text);
if (json.optString("command").equals("8011")) { if (json.optString("command").equals("8011")) {
JSONObject data = json.getJSONObject("data"); JSONObject data = json.getJSONObject("data");
LOG.trace("stream info:\n{}", data.toString(2)); log.trace("stream info:\n{}", data.toString(2));
streamHost = data.getString("stream_host"); streamHost = data.getString("stream_host");
online = true; online = true;
isInteractiveShow = data.optString("devices").equals("1"); isInteractiveShow = data.optString("devices").equals("1");
@ -277,7 +273,7 @@ public class Flirt4FreeModel extends AbstractModel {
resolution[0] = Integer.parseInt(data.getString("stream_width")); resolution[0] = Integer.parseInt(data.getString("stream_width"));
resolution[1] = Integer.parseInt(data.getString("stream_height")); resolution[1] = Integer.parseInt(data.getString("stream_height"));
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Couldn't determine stream resolution", e); log.warn("Couldn't determine stream resolution", e);
} }
webSocket.close(1000, ""); webSocket.close(1000, "");
} }
@ -285,7 +281,7 @@ public class Flirt4FreeModel extends AbstractModel {
@Override @Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) { public void onFailure(WebSocket webSocket, Throwable t, Response response) {
LOG.error("Chat websocket for {} failed", getName(), t); log.error("Chat websocket for {} failed", getName(), t);
synchronized (monitor) { synchronized (monitor) {
monitor.notifyAll(); monitor.notifyAll();
} }
@ -296,7 +292,7 @@ public class Flirt4FreeModel extends AbstractModel {
@Override @Override
public void onClosed(WebSocket webSocket, int code, String reason) { public void onClosed(WebSocket webSocket, int code, String reason) {
LOG.trace("Chat websocket for {} closed {} {}", getName(), code, reason); log.trace("Chat websocket for {} closed {} {}", getName(), code, reason);
synchronized (monitor) { synchronized (monitor) {
monitor.notifyAll(); monitor.notifyAll();
} }
@ -312,7 +308,7 @@ public class Flirt4FreeModel extends AbstractModel {
+ "model_id=" + id + "model_id=" + id
+ "&video_host=" + streamHost + "&video_host=" + streamHost
+ "&t=" + System.currentTimeMillis(); + "&t=" + System.currentTimeMillis();
LOG.debug("Loading master playlist information: {}", url); log.debug("Loading master playlist information: {}", url);
req = new Request.Builder() req = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -325,7 +321,7 @@ public class Flirt4FreeModel extends AbstractModel {
JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), "HTTP response body is null").string()); JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), "HTTP response body is null").string());
JSONArray hls = json.getJSONObject("data").getJSONArray("hls"); JSONArray hls = json.getJSONObject("data").getJSONArray("hls");
streamUrl = "https:" + hls.getJSONObject(0).getString("url"); streamUrl = "https:" + hls.getJSONObject(0).getString("url");
LOG.debug("Stream URL is {}", streamUrl); log.debug("Stream URL is {}", streamUrl);
} }
} }
} }
@ -346,7 +342,7 @@ public class Flirt4FreeModel extends AbstractModel {
// send the tip // send the tip
int giftId = isInteractiveShow ? 775 : 171; int giftId = isInteractiveShow ? 775 : 171;
int amount = tokens.intValue(); int amount = tokens.intValue();
LOG.debug("Sending tip of {} to {}", amount, getName()); log.debug("Sending tip of {} to {}", amount, getName());
String url = "https://ws.vs3.com/rooms/send-tip.php?" + String url = "https://ws.vs3.com/rooms/send-tip.php?" +
"gift_id=" + giftId + "gift_id=" + giftId +
"&num_credits=" + amount + "&num_credits=" + amount +
@ -355,7 +351,7 @@ public class Flirt4FreeModel extends AbstractModel {
"&userIP=" + userIp + "&userIP=" + userIp +
"&anonymous=N&response_type=json" + "&anonymous=N&response_type=json" +
"&t=" + System.currentTimeMillis(); "&t=" + System.currentTimeMillis();
LOG.debug("Trying to send tip: {}", url); log.debug("Trying to send tip: {}", url);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -373,8 +369,8 @@ public class Flirt4FreeModel extends AbstractModel {
if (json.has("error_message")) { if (json.has("error_message")) {
msg = json.getString("error_message"); msg = json.getString("error_message");
} }
LOG.error("Sending tip failed: {}", msg); log.error("Sending tip failed: {}", msg);
LOG.debug("Response: {}", json.toString(2)); log.debug("Response: {}", json.toString(2));
throw new IOException(msg); throw new IOException(msg);
} }
} else { } else {
@ -434,10 +430,10 @@ public class Flirt4FreeModel extends AbstractModel {
public int[] getStreamResolution(boolean failFast) throws ExecutionException { public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if (!failFast && streamUrl != null && resolution[0] == 0) { if (!failFast && streamUrl != null && resolution[0] == 0) {
try { try {
List<StreamSource> streamSources = getStreamSources(true); List<StreamSource> streamSources = getStreamSources();
Collections.sort(streamSources); Collections.sort(streamSources);
StreamSource best = streamSources.get(streamSources.size() - 1); StreamSource best = streamSources.get(streamSources.size() - 1);
resolution = new int[]{best.width, best.height}; resolution = new int[]{best.getHeight(), best.getHeight()};
} catch (IOException | ParseException | PlaylistException e) { } catch (IOException | ParseException | PlaylistException e) {
throw new ExecutionException("Couldn't determine stream resolution", e); throw new ExecutionException("Couldn't determine stream resolution", e);
} }
@ -485,7 +481,7 @@ public class Flirt4FreeModel extends AbstractModel {
"&id=" + id + "&id=" + id +
"&name=" + getName() + "&name=" + getName() +
"&t=" + System.currentTimeMillis(); "&t=" + System.currentTimeMillis();
LOG.debug("Sending follow/unfollow request: {}", url); log.debug("Sending follow/unfollow request: {}", url);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -496,7 +492,7 @@ public class Flirt4FreeModel extends AbstractModel {
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
LOG.debug("Follow/Unfollow response: {}", body); log.debug("Follow/Unfollow response: {}", body);
return Objects.equals(body, "1"); return Objects.equals(body, "1");
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -504,10 +500,6 @@ public class Flirt4FreeModel extends AbstractModel {
} }
} }
public void setId(String id) {
this.id = id;
}
@Override @Override
public void readSiteSpecificData(Map<String, String> data) { public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id"); id = data.get("id");
@ -518,14 +510,6 @@ public class Flirt4FreeModel extends AbstractModel {
data.put("id", id); data.put("id", id);
} }
public void setStreamUrl(String streamUrl) {
this.streamUrl = streamUrl;
}
public void setOnline(boolean b) {
online = b;
}
public boolean isNew() { public boolean isNew() {
return isNew; return isNew;
} }
@ -557,8 +541,4 @@ public class Flirt4FreeModel extends AbstractModel {
lastRequest = System.currentTimeMillis(); lastRequest = System.currentTimeMillis();
requestThrottle.release(); requestThrottle.release();
} }
public List<String> getCategories() {
return categories;
}
} }

View File

@ -167,7 +167,7 @@ public class LiveJasminStreamRegistration {
JSONObject data = message.getJSONArray("data").getJSONArray(0).getJSONObject(0); JSONObject data = message.getJSONArray("data").getJSONArray(0).getJSONObject(0);
String streamId = data.getString("streamId"); String streamId = data.getString("streamId");
String wssUrl = data.getJSONObject("protocol").getJSONObject("h5live").getString("wssUrl"); String wssUrl = data.getJSONObject("protocol").getJSONObject("h5live").getString("wssUrl");
streamSources.stream().filter(src -> Objects.equals(src.getStreamId(), streamId)).findAny().ifPresent(src -> src.mediaPlaylistUrl = wssUrl); streamSources.stream().filter(src -> Objects.equals(src.getStreamId(), streamId)).findAny().ifPresent(src -> src.setMediaPlaylistUrl(wssUrl));
if (--streamCount == 0) { if (--streamCount == 0) {
awaitBarrier(); awaitBarrier();
} }
@ -230,10 +230,10 @@ public class LiveJasminStreamRegistration {
.replace("{streamName}", URLEncoder.encode(streamName, UTF_8)); .replace("{streamName}", URLEncoder.encode(streamName, UTF_8));
LiveJasminStreamSource streamSource = new LiveJasminStreamSource(); LiveJasminStreamSource streamSource = new LiveJasminStreamSource();
streamSource.mediaPlaylistUrl = hlsUrl; streamSource.setMediaPlaylistUrl(hlsUrl);
streamSource.width = w; streamSource.setWidth(w);
streamSource.height = h; streamSource.setHeight(h);
streamSource.bandwidth = bitrate; streamSource.setBandwidth(bitrate);
streamSource.setRtmpUrl(rtmpUrl); streamSource.setRtmpUrl(rtmpUrl);
streamSource.setStreamName(streamName); streamSource.setStreamName(streamName);
streamSource.setStreamId(streamId); streamSource.setStreamId(streamId);

View File

@ -9,6 +9,8 @@ import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.ModelOfflineException; import ctbrec.sites.ModelOfflineException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -36,6 +38,8 @@ public class MVLiveModel extends AbstractModel {
private transient JSONObject roomLocation; private transient JSONObject roomLocation;
private transient Instant lastRoomLocationUpdate = Instant.EPOCH; private transient Instant lastRoomLocationUpdate = Instant.EPOCH;
private String roomNumber; private String roomNumber;
@Getter
@Setter
private String id; private String id;
@Override @Override
@ -74,16 +78,16 @@ public class MVLiveModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = playlist.getStreamInfo().getResolution().height; src.setHeight(playlist.getStreamInfo().getResolution().height);
String masterUrl = streamLocation.masterPlaylist; String masterUrl = streamLocation.masterPlaylist;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri(); String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri; src.setMediaPlaylistUrl(segmentUri);
if (src.mediaPlaylistUrl.contains("?")) { if (src.getMediaPlaylistUrl().contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); src.setMediaPlaylistUrl((src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?'))));
} }
log.debug("Media playlist {}", src.mediaPlaylistUrl); log.debug("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -247,14 +251,6 @@ public class MVLiveModel extends AbstractModel {
id = data.get("id"); id = data.get("id");
} }
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void updateStateFromJson(JSONObject creator) { public void updateStateFromJson(JSONObject creator) {
setId(creator.getString("id")); setId(creator.getString("id"));
setDisplayName(creator.optString("display_name", null)); setDisplayName(creator.optString("display_name", null));

View File

@ -1,22 +1,5 @@
package ctbrec.sites.mfc; package ctbrec.sites.mfc;
import static ctbrec.io.HttpConstants.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.dash.AdaptationSetType; import ctbrec.recorder.download.dash.AdaptationSetType;
@ -26,14 +9,28 @@ import ctbrec.recorder.download.dash.RepresentationType;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static ctbrec.io.HttpConstants.*;
public class DashStreamSourceProvider implements StreamSourceProvider { public class DashStreamSourceProvider implements StreamSourceProvider {
private static final Logger LOG = LoggerFactory.getLogger(DashStreamSourceProvider.class); private static final Logger LOG = LoggerFactory.getLogger(DashStreamSourceProvider.class);
private Config config; private final Config config;
private Site site; private final Site site;
public DashStreamSourceProvider(Config config, Site site) { public DashStreamSourceProvider(Config config, Site site) {
this.config = config; this.config = config;
@ -65,12 +62,12 @@ public class DashStreamSourceProvider implements StreamSourceProvider {
return videoStreams.stream().map(ast -> { return videoStreams.stream().map(ast -> {
RepresentationType representation = ast.getRepresentation().get(0); RepresentationType representation = ast.getRepresentation().get(0);
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.width = ast.getWidth().intValue(); src.setWidth(ast.getWidth().intValue());
src.height = ast.getHeight().intValue(); src.setHeight(ast.getHeight().intValue());
src.bandwidth = (int)representation.getBandwidth(); src.setBandwidth((int) representation.getBandwidth());
src.mediaPlaylistUrl = streamUrl; src.setMediaPlaylistUrl(streamUrl);
return src; return src;
}).collect(Collectors.toList()); }).toList();
} }
} }

View File

@ -1,6 +1,17 @@
package ctbrec.sites.mfc; package ctbrec.sites.mfc;
import static ctbrec.io.HttpConstants.*; import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -10,25 +21,7 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.slf4j.Logger; import static ctbrec.io.HttpConstants.*;
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.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
public record HlsStreamSourceProvider(HttpClient httpClient) implements StreamSourceProvider { public record HlsStreamSourceProvider(HttpClient httpClient) implements StreamSourceProvider {
@ -41,19 +34,19 @@ public record HlsStreamSourceProvider(HttpClient httpClient) implements StreamSo
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
if (playlist.getStreamInfo().getResolution() != null) { if (playlist.getStreamInfo().getResolution() != null) {
src.width = playlist.getStreamInfo().getResolution().width; src.setWidth(playlist.getStreamInfo().getResolution().width);
src.height = playlist.getStreamInfo().getResolution().height; src.setHeight(playlist.getStreamInfo().getResolution().height);
} else { } else {
src.width = StreamSource.UNKNOWN; src.setWidth(StreamSource.UNKNOWN);
src.height = StreamSource.UNKNOWN; src.setHeight(StreamSource.UNKNOWN);
} }
String masterUrl = streamUrl; String masterUrl = streamUrl;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri(); String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri; src.setMediaPlaylistUrl(segmentUri);
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); LOG.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }

View File

@ -10,6 +10,8 @@ import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl; import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
@ -32,9 +34,17 @@ public class MyFreeCamsModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class); private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
@Getter
@Setter
private int uid = -1; // undefined private int uid = -1; // undefined
@Getter
@Setter
private String streamUrl; private String streamUrl;
@Getter
@Setter
private double camScore; private double camScore;
@Getter
@Setter
private int viewerCount; private int viewerCount;
private ctbrec.sites.mfc.State state; private ctbrec.sites.mfc.State state;
private int[] resolution = new int[2]; private int[] resolution = new int[2];
@ -84,21 +94,17 @@ public class MyFreeCamsModel extends AbstractModel {
} }
} }
switch (state) { return switch (state) {
case ONLINE, RECORDING: case ONLINE, RECORDING -> State.ONLINE;
return ctbrec.Model.State.ONLINE; case AWAY -> State.AWAY;
case AWAY: case PRIVATE -> State.PRIVATE;
return ctbrec.Model.State.AWAY; case GROUP_SHOW -> State.GROUP;
case PRIVATE: case OFFLINE, CAMOFF, UNKNOWN -> State.OFFLINE;
return ctbrec.Model.State.PRIVATE; default -> {
case GROUP_SHOW:
return ctbrec.Model.State.GROUP;
case OFFLINE, CAMOFF, UNKNOWN:
return ctbrec.Model.State.OFFLINE;
default:
LOG.debug("State {} is not mapped", this.state); LOG.debug("State {} is not mapped", this.state);
return ctbrec.Model.State.UNKNOWN; yield State.UNKNOWN;
} }
};
} }
@Override @Override
@ -182,7 +188,7 @@ public class MyFreeCamsModel extends AbstractModel {
List<StreamSource> streamSources = getStreamSources(); List<StreamSource> streamSources = getStreamSources();
Collections.sort(streamSources); Collections.sort(streamSources);
StreamSource best = streamSources.get(streamSources.size() - 1); StreamSource best = streamSources.get(streamSources.size() - 1);
resolution = new int[]{best.width, best.height}; resolution = new int[]{best.getWidth(), best.getHeight()};
} catch (JAXBException | ParseException | PlaylistException e) { } catch (JAXBException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution - {}", e.getMessage()); LOG.warn("Couldn't determine stream resolution - {}", e.getMessage());
} catch (ExecutionException | IOException e) { } catch (ExecutionException | IOException e) {
@ -192,22 +198,6 @@ public class MyFreeCamsModel extends AbstractModel {
return resolution; return resolution;
} }
public void setStreamUrl(String streamUrl) {
this.streamUrl = streamUrl;
}
public String getStreamUrl() {
return streamUrl;
}
public double getCamScore() {
return camScore;
}
public void setCamScore(double camScore) {
this.camScore = camScore;
}
public boolean isNew() { public boolean isNew() {
MyFreeCams mfc = (MyFreeCams) getSite(); MyFreeCams mfc = (MyFreeCams) getSite();
SessionState sessionState = mfc.getClient().getSessionState(this); SessionState sessionState = mfc.getClient().getSessionState(this);
@ -292,7 +282,7 @@ public class MyFreeCamsModel extends AbstractModel {
} }
private int toCamServ(String server) { private int toCamServ(String server) {
return Integer.parseInt(server.replaceAll("[^0-9]+", "")); return Integer.parseInt(server.replaceAll("\\D+", ""));
} }
@Override @Override
@ -305,22 +295,6 @@ public class MyFreeCamsModel extends AbstractModel {
return ((MyFreeCams) site).getClient().unfollow(getUid()); return ((MyFreeCams) site).getClient().unfollow(getUid());
} }
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public int getViewerCount() {
return viewerCount;
}
public void setViewerCount(int viewerCount) {
this.viewerCount = viewerCount;
}
@Override @Override
public void readSiteSpecificData(Map<String, String> data) { public void readSiteSpecificData(Map<String, String> data) {
uid = Integer.parseInt(data.get("uid")); uid = Integer.parseInt(data.get("uid"));
@ -337,11 +311,6 @@ public class MyFreeCamsModel extends AbstractModel {
updateStreamUrl(); updateStreamUrl();
} }
return super.createDownload(); return super.createDownload();
// if(isHlsStream()) {
// return super.createDownload();
// } else {
// return new MyFreeCamsWebrtcDownload(uid, streamUrl, ((MyFreeCams)site).getClient());
// }
} }
@Override @Override

View File

@ -90,9 +90,9 @@ public class SecretFriendsModel extends AbstractModel {
.build(); .build();
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.width = 1280; src.setWidth(1280);
src.height = 720; src.setHeight(720);
src.mediaPlaylistUrl = wsUrl.toString(); src.setMediaPlaylistUrl(wsUrl.toString());
return Collections.singletonList(src); return Collections.singletonList(src);
} }

View File

@ -10,6 +10,8 @@ import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl; import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
import java.io.IOException; import java.io.IOException;
@ -21,11 +23,18 @@ import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
public class ShowupModel extends AbstractModel { public class ShowupModel extends AbstractModel {
private static final Random RNG = new Random();
@Getter
@Setter
private String uid; private String uid;
@Getter
@Setter
private String streamId; private String streamId;
@Getter
@Setter
private String streamTranscoderAddr; private String streamTranscoderAddr;
private int[] resolution = new int[2]; private final int[] resolution = new int[2];
@Override @Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
@ -54,8 +63,8 @@ public class ShowupModel extends AbstractModel {
@Override @Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.width = 480; src.setWidth(480);
src.height = 360; src.setHeight(360);
if (streamId == null || streamTranscoderAddr == null) { if (streamId == null || streamTranscoderAddr == null) {
List<Model> modelList = getShowupSite().getModelList(); List<Model> modelList = getShowupSite().getModelList();
@ -68,11 +77,11 @@ public class ShowupModel extends AbstractModel {
} }
} }
int cdnHost = 1 + new Random().nextInt(5); int cdnHost = 1 + RNG.nextInt(5);
int cid = 100_000 + new Random().nextInt(900_000); int cid = 100_000 + RNG.nextInt(900_000);
long pid = 10_000_000_000L + new Random().nextInt(); long pid = 10_000_000_000L + RNG.nextInt();
String urlTemplate = "https://cdn-e0{0}.showup.tv/h5live/http/playlist.m3u8?url=rtmp%3A%2F%2F{1}%3A1935%2Fwebrtc&stream={2}_aac&cid={3}&pid={4}"; String urlTemplate = "https://cdn-e0{0}.showup.tv/h5live/http/playlist.m3u8?url=rtmp%3A%2F%2F{1}%3A1935%2Fwebrtc&stream={2}_aac&cid={3}&pid={4}";
src.mediaPlaylistUrl = MessageFormat.format(urlTemplate, cdnHost, streamTranscoderAddr, streamId, cid, pid); src.setMediaPlaylistUrl(MessageFormat.format(urlTemplate, cdnHost, streamTranscoderAddr, streamId, cid, pid));
List<StreamSource> sources = new ArrayList<>(); List<StreamSource> sources = new ArrayList<>();
sources.add(src); sources.add(src);
return sources; return sources;
@ -107,30 +116,6 @@ public class ShowupModel extends AbstractModel {
return (Showup) getSite(); return (Showup) getSite();
} }
public String getStreamId() {
return streamId;
}
public void setStreamId(String streamId) {
this.streamId = streamId;
}
public String getStreamTranscoderAddr() {
return streamTranscoderAddr;
}
public void setStreamTranscoderAddr(String streamTranscoderAddr) {
this.streamTranscoderAddr = streamTranscoderAddr;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
@Override @Override
public RecordingProcess createDownload() { public RecordingProcess createDownload() {
return new ShowupWebrtcDownload(getSite().getHttpClient()); return new ShowupWebrtcDownload(getSite().getHttpClient());
@ -165,9 +150,9 @@ public class ShowupModel extends AbstractModel {
} }
} }
int cdnHost = 1 + new Random().nextInt(5); int cdnHost = 1 + RNG.nextInt(5);
int cid = 100_000 + new Random().nextInt(900_000); int cid = 100_000 + RNG.nextInt(900_000);
long pid = 10_000_000_000L + new Random().nextInt(); long pid = 10_000_000_000L + RNG.nextInt();
String urlTemplate = "https://cdn-e0{0}.showup.tv/h5live/stream/?url=rtmp%3A%2F%2F{1}%3A1935%2Fwebrtc&stream={2}_aac&cid={3,number,#}&pid={4,number,#}"; String urlTemplate = "https://cdn-e0{0}.showup.tv/h5live/stream/?url=rtmp%3A%2F%2F{1}%3A1935%2Fwebrtc&stream={2}_aac&cid={3,number,#}&pid={4,number,#}";
return MessageFormat.format(urlTemplate, cdnHost, streamTranscoderAddr, streamId, cid, pid); return MessageFormat.format(urlTemplate, cdnHost, streamTranscoderAddr, streamId, cid, pid);
} }

View File

@ -7,13 +7,14 @@ import ctbrec.Config;
import ctbrec.NotImplementedExcetion; import ctbrec.NotImplementedExcetion;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -26,17 +27,18 @@ import static ctbrec.io.HttpConstants.*;
import static ctbrec.sites.streamate.StreamateHttpClient.JSON; import static ctbrec.sites.streamate.StreamateHttpClient.JSON;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class StreamateModel extends AbstractModel { public class StreamateModel extends AbstractModel {
private static final String ORIGIN = "origin"; private static final String ORIGIN = "origin";
private static final Logger LOG = LoggerFactory.getLogger(StreamateModel.class);
private static final Long MODEL_ID_UNDEFINED = -1L; private static final Long MODEL_ID_UNDEFINED = -1L;
@Setter
private boolean online = false; private boolean online = false;
private final transient List<StreamSource> streamSources = new ArrayList<>(); private final transient List<StreamSource> streamSources = new ArrayList<>();
private int[] resolution; private int[] resolution;
@Getter
@Setter
private Long id = MODEL_ID_UNDEFINED; private Long id = MODEL_ID_UNDEFINED;
@Override @Override
@ -57,10 +59,6 @@ public class StreamateModel extends AbstractModel {
return online; return online;
} }
public void setOnline(boolean online) {
this.online = online;
}
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (!failFast && onlineState == UNKNOWN) { if (!failFast && onlineState == UNKNOWN) {
@ -92,10 +90,10 @@ public class StreamateModel extends AbstractModel {
for (int i = 0; i < encodings.length(); i++) { for (int i = 0; i < encodings.length(); i++) {
JSONObject encoding = encodings.getJSONObject(i); JSONObject encoding = encodings.getJSONObject(i);
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.mediaPlaylistUrl = encoding.getString("location"); src.setMediaPlaylistUrl(encoding.getString("location"));
src.width = encoding.optInt("videoWidth"); src.setWidth(encoding.optInt("videoWidth"));
src.height = encoding.optInt("videoHeight"); src.setHeight(encoding.optInt("videoHeight"));
src.bandwidth = (encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024; src.setBandwidth((encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024);
streamSources.add(src); streamSources.add(src);
} }
@ -103,11 +101,10 @@ public class StreamateModel extends AbstractModel {
if (hls.has(ORIGIN) && !hls.isNull(ORIGIN)) { if (hls.has(ORIGIN) && !hls.isNull(ORIGIN)) {
JSONObject origin = hls.getJSONObject(ORIGIN); JSONObject origin = hls.getJSONObject(ORIGIN);
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.mediaPlaylistUrl = origin.getString("location"); src.setMediaPlaylistUrl(origin.getString("location"));
src.width = origin.optInt("videoWidth"); src.setWidth(origin.optInt("videoWidth"));
src.height = origin.optInt("videoHeight"); src.setBandwidth((origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024);
src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024; src.setHeight(StreamSource.ORIGIN);
src.height = StreamSource.ORIGIN;
streamSources.add(src); streamSources.add(src);
} }
@ -155,15 +152,15 @@ public class StreamateModel extends AbstractModel {
List<StreamSource> sources = getStreamSources(); List<StreamSource> sources = getStreamSources();
Collections.sort(sources); Collections.sort(sources);
StreamSource best = sources.get(sources.size() - 1); StreamSource best = sources.get(sources.size() - 1);
if (best.height == StreamSource.ORIGIN) { if (best.getHeight() == StreamSource.ORIGIN) {
best = sources.get(sources.size() - 2); best = sources.get(sources.size() - 2);
} }
resolution = new int[]{best.width, best.height}; resolution = new int[]{best.getWidth(), best.getHeight()};
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} catch (ExecutionException | IOException | ParseException | PlaylistException e) { } catch (ExecutionException | IOException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); log.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
} }
} }
return resolution; return resolution;
@ -213,14 +210,6 @@ public class StreamateModel extends AbstractModel {
} }
} }
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override @Override
public void readSiteSpecificData(Map<String, String> data) { public void readSiteSpecificData(Map<String, String> data) {
id = Long.parseLong(data.get("id")); id = Long.parseLong(data.get("id"));
@ -232,7 +221,7 @@ public class StreamateModel extends AbstractModel {
try { try {
loadModelId(); loadModelId();
} catch (IOException e) { } catch (IOException e) {
LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName(), e); log.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName(), e);
} }
} }
data.put("id", Long.toString(id)); data.put("id", Long.toString(id));

View File

@ -89,10 +89,10 @@ public class StreamrayModel extends AbstractModel {
try { try {
String url = getMasterPlaylistUrl(); String url = getMasterPlaylistUrl();
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.mediaPlaylistUrl = url; src.setMediaPlaylistUrl(url);
src.height = 0; src.setHeight(0);
src.width = 0; src.setWidth(0);
src.bandwidth = 0; src.setBandwidth(0);
sources.add(src); sources.add(src);
} catch (IOException e) { } catch (IOException e) {
log.error("Can not get stream sources for {}", getName()); log.error("Can not get stream sources for {}", getName());

View File

@ -11,13 +11,12 @@ import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.hls.HlsdlDownload; import ctbrec.recorder.download.hls.HlsdlDownload;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload; import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -33,9 +32,9 @@ import static ctbrec.io.HttpConstants.*;
import static ctbrec.sites.stripchat.StripchatHttpClient.JSON; import static ctbrec.sites.stripchat.StripchatHttpClient.JSON;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class StripchatModel extends AbstractModel { public class StripchatModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(StripchatModel.class); private static final Random RNG = new Random();
private int[] resolution = new int[]{0, 0}; private int[] resolution = new int[]{0, 0};
private int modelId = 0; private int modelId = 0;
private boolean isVr = false; private boolean isVr = false;
@ -52,7 +51,7 @@ public class StripchatModel extends AbstractModel {
String status = user.optString("status"); String status = user.optString("status");
mapOnlineState(status); mapOnlineState(status);
if (isBanned(user)) { if (isBanned(user)) {
LOG.debug("Model inactive or deleted: {}", getName()); log.debug("Model inactive or deleted: {}", getName());
setMarkedForLaterRecording(true); setMarkedForLaterRecording(true);
} }
modelId = user.optInt("id"); modelId = user.optInt("id");
@ -75,7 +74,7 @@ public class StripchatModel extends AbstractModel {
case "private", "p2p", "groupShow", "virtualPrivate" -> setOnlineState(PRIVATE); case "private", "p2p", "groupShow", "virtualPrivate" -> setOnlineState(PRIVATE);
case "off" -> setOnlineState(OFFLINE); case "off" -> setOnlineState(OFFLINE);
default -> { default -> {
LOG.debug("Unknown online state {} for model {}", status, getName()); log.debug("Unknown online state {} for model {}", status, getName());
setOnlineState(OFFLINE); setOnlineState(OFFLINE);
} }
} }
@ -122,14 +121,15 @@ public class StripchatModel extends AbstractModel {
for (StreamSource original : extractStreamSources(masterPlaylist)) { for (StreamSource original : extractStreamSources(masterPlaylist)) {
boolean found = false; boolean found = false;
for (StreamSource source : streamSources) { for (StreamSource source : streamSources) {
if (source.height == original.height) { if (source.getHeight() == original.getHeight()) {
found = true; found = true;
break;
} }
} }
if (!found) streamSources.add(original); if (!found) streamSources.add(original);
} }
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Original stream quality not available", e); log.warn("Original stream quality not available", e);
} }
return streamSources; return streamSources;
} }
@ -139,13 +139,13 @@ public class StripchatModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = playlist.getStreamInfo().getResolution().height; src.setHeight(playlist.getStreamInfo().getResolution().height);
src.mediaPlaylistUrl = playlist.getUri(); src.setMediaPlaylistUrl(playlist.getUri());
if (src.mediaPlaylistUrl.contains("?")) { if (src.getMediaPlaylistUrl().contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
} }
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -153,7 +153,7 @@ public class StripchatModel extends AbstractModel {
} }
private MasterPlaylist getMasterPlaylist(String url) throws IOException, ParseException, PlaylistException { private MasterPlaylist getMasterPlaylist(String url) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", url); log.trace("Loading master playlist {}", url);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
@ -161,7 +161,7 @@ public class StripchatModel extends AbstractModel {
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
LOG.trace(body); log.trace(body);
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
@ -174,9 +174,9 @@ public class StripchatModel extends AbstractModel {
} }
private String getMasterPlaylistUrl() throws IOException { private String getMasterPlaylistUrl() throws IOException {
boolean VR = Config.getInstance().getSettings().stripchatVR; boolean isVirtualRealityStream = Config.getInstance().getSettings().stripchatVR;
String hlsUrlTemplate = "https://edge-hls.doppiocdn.com/hls/{0}{1}/master/{0}{1}_auto.m3u8?playlistType=Standart"; String hlsUrlTemplate = "https://edge-hls.doppiocdn.com/hls/{0}{1}/master/{0}{1}_auto.m3u8?playlistType=Standart";
String vrSuffix = (VR && isVr) ? "_vr" : ""; String vrSuffix = (isVirtualRealityStream && isVr) ? "_vr" : "";
if (modelId > 0) { if (modelId > 0) {
return MessageFormat.format(hlsUrlTemplate, String.valueOf(modelId), vrSuffix); return MessageFormat.format(hlsUrlTemplate, String.valueOf(modelId), vrSuffix);
} }
@ -193,12 +193,12 @@ public class StripchatModel extends AbstractModel {
try (Response response = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
LOG.trace(body); log.trace(body);
JSONObject jsonResponse = new JSONObject(body); JSONObject jsonResponse = new JSONObject(body);
String streamName = jsonResponse.optString("streamName"); String streamName = jsonResponse.optString("streamName");
JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings"); JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings");
String vrBroadcastServer = broadcastSettings.optString("vrBroadcastServer"); String vrBroadcastServer = broadcastSettings.optString("vrBroadcastServer");
vrSuffix = (!VR || vrBroadcastServer.isEmpty()) ? "" : "_vr"; vrSuffix = (!isVirtualRealityStream || vrBroadcastServer.isEmpty()) ? "" : "_vr";
return MessageFormat.format(hlsUrlTemplate, streamName, vrSuffix); return MessageFormat.format(hlsUrlTemplate, streamName, vrSuffix);
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -238,12 +238,12 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean follow() throws IOException { public boolean follow() throws IOException {
getSite().getHttpClient().login(); getSite().getHttpClient().login();
JSONObject modelInfo = getModelInfo(); JSONObject info = getModelInfo();
JSONObject user = modelInfo.getJSONObject("user"); JSONObject user = info.getJSONObject("user");
long modelId = user.optLong("id"); long id = user.optLong("id");
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + modelId; String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + id;
JSONObject requestParams = new JSONObject(); JSONObject requestParams = new JSONObject();
requestParams.put("csrfToken", client.getCsrfToken()); requestParams.put("csrfToken", client.getCsrfToken());
requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); requestParams.put("csrfTimestamp", client.getCsrfTimestamp());
@ -272,11 +272,11 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean unfollow() throws IOException { public boolean unfollow() throws IOException {
getSite().getHttpClient().login(); getSite().getHttpClient().login();
JSONObject modelInfo = getModelInfo(); JSONObject info = getModelInfo();
JSONObject user = modelInfo.getJSONObject("user"); JSONObject user = info.getJSONObject("user");
long modelId = user.optLong("id"); long id = user.optLong("id");
JSONArray favoriteIds = new JSONArray(); JSONArray favoriteIds = new JSONArray();
favoriteIds.put(modelId); favoriteIds.put(id);
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites"; String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites";
@ -320,7 +320,7 @@ public class StripchatModel extends AbstractModel {
if (jsonResponse.has("user")) { if (jsonResponse.has("user")) {
JSONObject user = jsonResponse.getJSONObject("user"); JSONObject user = jsonResponse.getJSONObject("user");
if (isBanned(user)) { if (isBanned(user)) {
LOG.debug("Model inactive or deleted: {}", getName()); log.debug("Model inactive or deleted: {}", getName());
return false; return false;
} }
} }
@ -337,11 +337,10 @@ public class StripchatModel extends AbstractModel {
} }
protected String getUniq() { protected String getUniq() {
Random r = new Random();
String dict = "0123456789abcdefghijklmnopqarstvwxyz"; String dict = "0123456789abcdefghijklmnopqarstvwxyz";
char[] text = new char[16]; char[] text = new char[16];
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
text[i] = dict.charAt(r.nextInt(dict.length())); text[i] = dict.charAt(RNG.nextInt(dict.length()));
} }
return new String(text); return new String(text);
} }

View File

@ -35,6 +35,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j @Slf4j
public class WinkTvModel extends AbstractModel { public class WinkTvModel extends AbstractModel {
private static final String KEY_MEDIA = "media";
private int[] resolution = new int[]{0, 0}; private int[] resolution = new int[]{0, 0};
@Getter @Getter
@ -49,8 +50,8 @@ public class WinkTvModel extends AbstractModel {
if (ignoreCache) { if (ignoreCache) {
try { try {
JSONObject json = getModelInfo(); JSONObject json = getModelInfo();
if (json.has("media")) { if (json.has(KEY_MEDIA)) {
JSONObject media = json.getJSONObject("media"); JSONObject media = json.getJSONObject(KEY_MEDIA);
boolean isLive = media.optBoolean("isLive"); boolean isLive = media.optBoolean("isLive");
String meType = media.optString("type"); String meType = media.optString("type");
if (isLive && meType.equals("free")) { if (isLive && meType.equals("free")) {
@ -70,9 +71,7 @@ public class WinkTvModel extends AbstractModel {
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (failFast && onlineState != UNKNOWN) { if (!failFast || onlineState == UNKNOWN) {
return onlineState;
} else {
try { try {
onlineState = isOnline(true) ? ONLINE : OFFLINE; onlineState = isOnline(true) ? ONLINE : OFFLINE;
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -81,8 +80,8 @@ public class WinkTvModel extends AbstractModel {
} catch (IOException | ExecutionException e) { } catch (IOException | ExecutionException e) {
onlineState = OFFLINE; onlineState = OFFLINE;
} }
return onlineState;
} }
return onlineState;
} }
@Override @Override
@ -98,10 +97,10 @@ public class WinkTvModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = playlist.getStreamInfo().getResolution().height; src.setHeight(playlist.getStreamInfo().getResolution().height);
src.mediaPlaylistUrl = playlist.getUri(); src.setMediaPlaylistUrl(playlist.getUri());
log.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -190,7 +189,7 @@ public class WinkTvModel extends AbstractModel {
String url = "https://api.winktv.co.kr/v1/member/bj"; String url = "https://api.winktv.co.kr/v1/member/bj";
FormBody body = new FormBody.Builder() FormBody body = new FormBody.Builder()
.add("userId", getName()) .add("userId", getName())
.add("info", "media") .add("info", KEY_MEDIA)
.build(); .build();
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)

View File

@ -1,9 +1,18 @@
package ctbrec.sites.xlovecam; package ctbrec.sites.xlovecam;
import static ctbrec.Model.State.*; import com.iheartradio.m3u8.*;
import static ctbrec.io.HttpConstants.*; import com.iheartradio.m3u8.data.MasterPlaylist;
import static java.nio.charset.StandardCharsets.*; 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 lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import okhttp3.Response;
import javax.xml.bind.JAXBException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -14,31 +23,13 @@ import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.xml.bind.JAXBException; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.USER_AGENT;
import org.slf4j.Logger; import static java.nio.charset.StandardCharsets.UTF_8;
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;
@Slf4j
public class XloveCamModel extends AbstractModel { 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 static final Pattern HLS_PLAYLIST_PATTERN = Pattern.compile("\"hlsPlaylist\":\"(.*?)\",");
private boolean online = false; private boolean online = false;
@ -56,9 +47,7 @@ public class XloveCamModel extends AbstractModel {
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (failFast && onlineState != UNKNOWN) { if (!failFast || onlineState == UNKNOWN) {
return onlineState;
} else {
try { try {
onlineState = isOnline(true) ? ONLINE : OFFLINE; onlineState = isOnline(true) ? ONLINE : OFFLINE;
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -67,8 +56,8 @@ public class XloveCamModel extends AbstractModel {
} catch (IOException | ExecutionException e) { } catch (IOException | ExecutionException e) {
onlineState = OFFLINE; onlineState = OFFLINE;
} }
return onlineState;
} }
return onlineState;
} }
@Override @Override
@ -78,10 +67,10 @@ public class XloveCamModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.height = Optional.ofNullable(playlist.getStreamInfo().getResolution()).map(r -> r.height).orElse(0); src.setHeight(Optional.ofNullable(playlist.getStreamInfo().getResolution()).map(r -> r.height).orElse(0));
src.mediaPlaylistUrl = playlist.getUri(); src.setMediaPlaylistUrl(playlist.getUri());
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
} }
} }
@ -100,7 +89,7 @@ public class XloveCamModel extends AbstractModel {
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
LOG.trace(body); log.trace(body);
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
@ -142,7 +131,7 @@ public class XloveCamModel extends AbstractModel {
@Override @Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException { public int[] getStreamResolution(boolean failFast) throws ExecutionException {
return new int[] {0, 0}; return new int[]{0, 0};
} }
@Override @Override