Add setting to restrict recording by bit rate
This commit is contained in:
parent
959b41e3b9
commit
257bdda8f7
|
@ -12,8 +12,8 @@ import ctbrec.ui.controls.Dialogs;
|
|||
import ctbrec.ui.event.PlayerStartedEvent;
|
||||
import ctbrec.variableexpansion.ModelVariableExpander;
|
||||
import javafx.scene.Scene;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.File;
|
||||
|
@ -29,8 +29,8 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Slf4j
|
||||
public class Player {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Player.class);
|
||||
private static PlayerThread playerThread;
|
||||
private static Scene scene;
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class Player {
|
|||
playerThread = new PlayerThread(rec);
|
||||
return true;
|
||||
} catch (Exception e1) {
|
||||
LOG.error("Couldn't start player", e1);
|
||||
log.error("Couldn't start player", e1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -81,11 +81,11 @@ public class Player {
|
|||
}
|
||||
} catch (InterruptedException e) {
|
||||
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);
|
||||
return false;
|
||||
} 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);
|
||||
return false;
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ public class Player {
|
|||
}
|
||||
|
||||
private static class PlayerThread extends Thread {
|
||||
@Getter
|
||||
private boolean running = false;
|
||||
private Process playerProcess;
|
||||
private Recording rec;
|
||||
|
@ -134,9 +135,9 @@ public class Player {
|
|||
} else if (model != null) {
|
||||
url = getPlaylistUrl(model);
|
||||
}
|
||||
LOG.debug("Playing {}", url);
|
||||
log.debug("Playing {}", url);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -152,13 +153,13 @@ public class Player {
|
|||
err.start();
|
||||
|
||||
playerProcess.waitFor();
|
||||
LOG.debug("Media player finished.");
|
||||
log.debug("Media player finished.");
|
||||
} catch (InterruptedException e) {
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
running = false;
|
||||
|
@ -172,8 +173,8 @@ public class Player {
|
|||
if (maxRes > 0 && !sources.isEmpty()) {
|
||||
for (Iterator<StreamSource> iterator = sources.iterator(); iterator.hasNext(); ) {
|
||||
StreamSource streamSource = iterator.next();
|
||||
if (streamSource.height > 0 && maxRes < streamSource.height) {
|
||||
LOG.trace("Res too high {} > {}", streamSource.height, maxRes);
|
||||
if (streamSource.getHeight() > 0 && maxRes < streamSource.getHeight()) {
|
||||
log.trace("Res too high {} > {}", streamSource.getHeight(), maxRes);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
@ -181,7 +182,7 @@ public class Player {
|
|||
if (sources.isEmpty()) {
|
||||
throw new NoStreamFoundException("No stream left in playlist, because player resolution is set to " + maxRes);
|
||||
} 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);
|
||||
}
|
||||
return best.getMediaPlaylistUrl();
|
||||
|
@ -226,10 +227,6 @@ public class Player {
|
|||
return recUrl;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void stopThread() {
|
||||
if (playerProcess != null) {
|
||||
playerProcess.destroy();
|
||||
|
|
|
@ -33,6 +33,7 @@ import javafx.scene.control.TextInputDialog;
|
|||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -128,10 +129,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleStringProperty dateTimeFormat;
|
||||
private final VariablePlayGroundDialogFactory variablePlayGroundDialogFactory = new VariablePlayGroundDialogFactory();
|
||||
private SimpleBooleanProperty checkForUpdates;
|
||||
private PostProcessingStepPanel postProcessingStepPanel;
|
||||
private SimpleStringProperty filterBlacklist;
|
||||
private SimpleStringProperty filterWhitelist;
|
||||
private SimpleBooleanProperty deleteOrphanedRecordingMetadata;
|
||||
private SimpleIntegerProperty restrictBitrate;
|
||||
|
||||
public SettingsTab(List<Site> sites, Recorder recorder) {
|
||||
this.sites = sites;
|
||||
|
@ -213,6 +214,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
filterBlacklist = new SimpleStringProperty(null, "filterBlacklist", settings.filterBlacklist);
|
||||
filterWhitelist = new SimpleStringProperty(null, "filterWhitelist", settings.filterWhitelist);
|
||||
deleteOrphanedRecordingMetadata = new SimpleBooleanProperty(null, "deleteOrphanedRecordingMetadata", settings.deleteOrphanedRecordingMetadata);
|
||||
restrictBitrate = new SimpleIntegerProperty(null, "restrictBitrate", settings.restrictBitrate);
|
||||
}
|
||||
|
||||
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 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 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("Default Priority", defaultPriority, "lowest 0 - 10000 highest"),
|
||||
Setting.of("Default duration for \"Record until\" (minutes)", recordUntilDefaultDurationInMinutes),
|
||||
|
@ -544,11 +547,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
return transition;
|
||||
}
|
||||
|
||||
public record SplitAfterOption(String label, int value) {
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
public record SplitAfterOption(String label, @Getter int value) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@ -590,11 +589,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
public record SplitBiggerThanOption(String label, long value) {
|
||||
|
||||
public long getValue() {
|
||||
return value;
|
||||
}
|
||||
public record SplitBiggerThanOption(String label, @Getter long value) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
|
|
@ -1,37 +1,7 @@
|
|||
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.PlaylistException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.GlobalThreadPool;
|
||||
import ctbrec.Model;
|
||||
|
@ -46,15 +16,7 @@ import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
|||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.menu.ModelMenuContributor;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
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.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
@ -65,19 +27,8 @@ import javafx.geometry.Insets;
|
|||
import javafx.geometry.Point2D;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Button;
|
||||
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.*;
|
||||
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.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
|
@ -85,22 +36,41 @@ import javafx.scene.layout.HBox;
|
|||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.FileChooser;
|
||||
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 {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class);
|
||||
private ScrollPane scrollPane = new ScrollPane();
|
||||
private TableView<ModelTableRow> table = new TableView<>();
|
||||
private ObservableList<ModelTableRow> filteredModels = FXCollections.observableArrayList();
|
||||
private ObservableList<ModelTableRow> observableModels = FXCollections.observableArrayList();
|
||||
private final ScrollPane scrollPane = new ScrollPane();
|
||||
private final TableView<ModelTableRow> table = new TableView<>();
|
||||
private final ObservableList<ModelTableRow> filteredModels = FXCollections.observableArrayList();
|
||||
private final 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 MyFreeCams mfc;
|
||||
private ReentrantLock lock = new ReentrantLock();
|
||||
private SearchBox filterInput;
|
||||
private Label count = new Label("models");
|
||||
private List<TableColumn<ModelTableRow, ?>> columns = new ArrayList<>();
|
||||
private ContextMenu popup;
|
||||
private long lastJsonWrite = 0;
|
||||
private Recorder recorder;
|
||||
|
||||
public MyFreeCamsTableTab(MyFreeCams mfc, Recorder recorder) {
|
||||
this.mfc = mfc;
|
||||
|
@ -118,7 +88,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
updateService = new TableUpdateService(mfc);
|
||||
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1)));
|
||||
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) {
|
||||
|
@ -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();
|
||||
var found = false;
|
||||
for (SessionState sessionState : sessionStates) {
|
||||
if(Objects.equals(sessionState.getUid(), model.uid)) {
|
||||
if (Objects.equals(sessionState.getUid(), model.uid)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found) {
|
||||
if (!found) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +131,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
table.sort();
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if( (now - lastJsonWrite) > TimeUnit.SECONDS.toMillis(30)) {
|
||||
if ((now - lastJsonWrite) > TimeUnit.SECONDS.toMillis(30)) {
|
||||
lastJsonWrite = now;
|
||||
saveData();
|
||||
}
|
||||
|
@ -174,7 +144,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
|
||||
filterInput = new SearchBox(false);
|
||||
filterInput.setPromptText("Filter");
|
||||
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
|
||||
filterInput.textProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
String filter = filterInput.getText();
|
||||
Config.getInstance().getSettings().mfcModelsTableFilter = filter;
|
||||
lock.lock();
|
||||
|
@ -218,7 +188,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
popup.hide();
|
||||
}
|
||||
});
|
||||
table.getColumns().addListener((ListChangeListener<TableColumn<?, ?>>)(e -> saveState()));
|
||||
table.getColumns().addListener((ListChangeListener<TableColumn<?, ?>>) (e -> saveState()));
|
||||
|
||||
var idx = 0;
|
||||
TableColumn<ModelTableRow, Number> uid = createTableColumn("UID", 65, idx++);
|
||||
|
@ -313,9 +283,9 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
ContextMenu menu = new CustomMouseBehaviorContextMenu();
|
||||
|
||||
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
|
||||
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
|
||||
.afterwards(table::refresh)
|
||||
.contributeToMenu(selectedModels, menu);
|
||||
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
|
||||
.afterwards(table::refresh)
|
||||
.contributeToMenu(selectedModels, menu);
|
||||
|
||||
addDebuggingInDevMode(menu, selectedModels);
|
||||
|
||||
|
@ -331,11 +301,11 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
try {
|
||||
List<StreamSource> sources = m.getStreamSources();
|
||||
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) {
|
||||
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) {
|
||||
if(isColumnEnabled(tc)) {
|
||||
if (isColumnEnabled(tc)) {
|
||||
table.getColumns().add(tc);
|
||||
}
|
||||
}
|
||||
|
@ -444,7 +414,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
ps.println();
|
||||
}
|
||||
} 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;
|
||||
for (int i = table.getColumns().size() - 1; i >= 0; 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 otherIdx = (int) other.getUserData();
|
||||
if (otherIdx < idx) {
|
||||
|
@ -516,7 +486,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
|
||||
@Override
|
||||
public void deselected() {
|
||||
if(updateService != null) {
|
||||
if (updateService != null) {
|
||||
updateService.cancel();
|
||||
}
|
||||
saveData();
|
||||
|
@ -546,26 +516,26 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
data.put(model);
|
||||
}
|
||||
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();
|
||||
} 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() {
|
||||
try {
|
||||
var file = new File(Config.getInstance().getConfigDir(), "mfc-models.json");
|
||||
if(!file.exists()) {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
var json = new String(Files.readAllBytes(file.toPath()), UTF_8);
|
||||
var json = Files.readString(file.toPath());
|
||||
var data = new JSONArray(json);
|
||||
for (var i = 0; i < data.length(); i++) {
|
||||
createRow(data, i).ifPresent(observableModels::add);
|
||||
}
|
||||
} 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() {
|
||||
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 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);
|
||||
tableColumns.remove(j); // NOSONAR
|
||||
tableColumns.add(i, col);
|
||||
|
@ -661,21 +631,21 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private static class ModelTableRow {
|
||||
private Integer uid;
|
||||
private StringProperty name = new SimpleStringProperty();
|
||||
private StringProperty state = new SimpleStringProperty();
|
||||
private DoubleProperty camScore = new SimpleDoubleProperty();
|
||||
private StringProperty newModel = new SimpleStringProperty();
|
||||
private StringProperty ethnic = new SimpleStringProperty();
|
||||
private StringProperty country = new SimpleStringProperty();
|
||||
private StringProperty continent = new SimpleStringProperty();
|
||||
private StringProperty occupation = new SimpleStringProperty();
|
||||
private StringProperty tags = new SimpleStringProperty();
|
||||
private StringProperty blurp = new SimpleStringProperty();
|
||||
private StringProperty topic = new SimpleStringProperty();
|
||||
private BooleanProperty isHd = new SimpleBooleanProperty();
|
||||
private BooleanProperty isWebrtc = new SimpleBooleanProperty();
|
||||
private SimpleIntegerProperty uidProperty = new SimpleIntegerProperty();
|
||||
private SimpleIntegerProperty flagsProperty = new SimpleIntegerProperty();
|
||||
private final StringProperty name = new SimpleStringProperty();
|
||||
private final StringProperty state = new SimpleStringProperty();
|
||||
private final DoubleProperty camScore = new SimpleDoubleProperty();
|
||||
private final StringProperty newModel = new SimpleStringProperty();
|
||||
private final StringProperty ethnic = new SimpleStringProperty();
|
||||
private final StringProperty country = new SimpleStringProperty();
|
||||
private final StringProperty continent = new SimpleStringProperty();
|
||||
private final StringProperty occupation = new SimpleStringProperty();
|
||||
private final StringProperty tags = new SimpleStringProperty();
|
||||
private final StringProperty blurp = new SimpleStringProperty();
|
||||
private final StringProperty topic = new SimpleStringProperty();
|
||||
private final BooleanProperty isHd = new SimpleBooleanProperty();
|
||||
private final BooleanProperty isWebrtc = new SimpleBooleanProperty();
|
||||
private final SimpleIntegerProperty uidProperty = new SimpleIntegerProperty();
|
||||
private final SimpleIntegerProperty flagsProperty = new SimpleIntegerProperty();
|
||||
|
||||
public ModelTableRow(SessionState 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(camScore, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getCamscore));
|
||||
Optional<Integer> isNew = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getNewModel);
|
||||
if (isNew.isPresent()) {
|
||||
newModel.set(isNew.get() == 1 ? "new" : "");
|
||||
}
|
||||
isNew.ifPresent(integer -> newModel.set(integer == 1 ? "new" : ""));
|
||||
setProperty(ethnic, Optional.ofNullable(st.getU()).map(User::getEthnic));
|
||||
setProperty(country, Optional.ofNullable(st.getU()).map(User::getCountry));
|
||||
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));
|
||||
String tpc = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getTopic).orElse("n/a");
|
||||
try {
|
||||
tpc = URLDecoder.decode(tpc, "utf-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
LOG.warn("Couldn't url decode topic", e);
|
||||
}
|
||||
tpc = URLDecoder.decode(tpc, UTF_8);
|
||||
topic.set(tpc);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -804,11 +768,8 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
return false;
|
||||
ModelTableRow other = (ModelTableRow) obj;
|
||||
if (uid == null) {
|
||||
if (other.uid != null)
|
||||
return false;
|
||||
} else if (!uid.equals(other.uid))
|
||||
return false;
|
||||
return true;
|
||||
return other.uid == null;
|
||||
} else return uid.equals(other.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,4 +219,5 @@ public class Settings {
|
|||
public boolean dreamcamVR = false;
|
||||
public String filterBlacklist = "";
|
||||
public String filterWhitelist = "";
|
||||
public int restrictBitrate = 0;
|
||||
}
|
||||
|
|
|
@ -65,6 +65,8 @@ public class OnlineMonitor extends Thread {
|
|||
}
|
||||
|
||||
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
|
||||
models.sort((a, b) -> b.getPriority() - a.getPriority());
|
||||
// submit online check jobs to the executor for the model's site
|
||||
|
|
|
@ -93,23 +93,27 @@ public abstract class AbstractDownload implements RecordingProcess {
|
|||
streamSources.forEach(ss -> log.debug(ss.toString()));
|
||||
StreamSource source = streamSources.get(model.getStreamUrlIndex());
|
||||
log.debug("{} selected {}", model.getName(), source);
|
||||
selectedResolution = source.height;
|
||||
selectedResolution = source.getHeight();
|
||||
return source;
|
||||
} else {
|
||||
// filter out stream resolutions, which are out of range of the configured min and max
|
||||
int minRes = Config.getInstance().getSettings().minimumResolution;
|
||||
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||
int bitrateLimit = Config.getInstance().getSettings().restrictBitrate * 1024;
|
||||
List<StreamSource> filteredStreamSources = streamSources.stream()
|
||||
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height)
|
||||
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height)
|
||||
.filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || minRes <= src.getHeight())
|
||||
.filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || maxRes >= src.getHeight())
|
||||
.filter(src -> bitrateLimit == 0 || src.getBandwidth() == UNKNOWN || src.getBandwidth() <= bitrateLimit)
|
||||
.toList();
|
||||
|
||||
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"));
|
||||
} else {
|
||||
StreamSource source = filteredStreamSources.get(filteredStreamSources.size() - 1);
|
||||
log.debug("{} selected {}", model.getName(), source);
|
||||
selectedResolution = source.height;
|
||||
selectedResolution = source.getHeight();
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,20 @@
|
|||
package ctbrec.recorder.download;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class StreamSource implements Comparable<StreamSource> {
|
||||
public static final int ORIGIN = Integer.MAX_VALUE - 1;
|
||||
public static final int UNKNOWN = Integer.MAX_VALUE;
|
||||
public int bandwidth;
|
||||
public int width;
|
||||
public int height;
|
||||
public String mediaPlaylistUrl;
|
||||
|
||||
public int getBandwidth() {
|
||||
return bandwidth;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
private int bandwidth;
|
||||
private int width;
|
||||
private int height;
|
||||
private String mediaPlaylistUrl;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@ -51,6 +25,9 @@ public class StreamSource implements Comparable<StreamSource> {
|
|||
} else if (height == ORIGIN) {
|
||||
return "Origin";
|
||||
} 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)";
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +39,7 @@ public class StreamSource implements Comparable<StreamSource> {
|
|||
@Override
|
||||
public int compareTo(StreamSource o) {
|
||||
int heightDiff = height - o.height;
|
||||
if(heightDiff != 0 && height != UNKNOWN && o.height != UNKNOWN) {
|
||||
if (heightDiff != 0 && height != UNKNOWN && o.height != UNKNOWN) {
|
||||
return heightDiff;
|
||||
} else {
|
||||
return bandwidth - o.bandwidth;
|
||||
|
@ -98,10 +75,6 @@ public class StreamSource implements Comparable<StreamSource> {
|
|||
return false;
|
||||
} else if (!mediaPlaylistUrl.equals(other.mediaPlaylistUrl))
|
||||
return false;
|
||||
if (width != other.width)
|
||||
return false;
|
||||
return true;
|
||||
return width == other.width;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import javax.xml.bind.JAXBException;
|
|||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.text.DecimalFormat;
|
||||
|
@ -248,7 +249,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
}
|
||||
try {
|
||||
LOG.debug("Waiting {}ms before trying to update the playlist URL", A_FEW_SECONDS);
|
||||
waitSomeTime(A_FEW_SECONDS);
|
||||
waitSomeTime();
|
||||
segmentPlaylistUrl = getSegmentPlaylistUrl(model);
|
||||
} catch (Exception 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);
|
||||
String url = selectedStreamSource.getMediaPlaylistUrl();
|
||||
selectedResolution = selectedStreamSource.height;
|
||||
selectedResolution = selectedStreamSource.getHeight();
|
||||
LOG.debug("Segment playlist url {}", url);
|
||||
return url;
|
||||
}
|
||||
|
@ -272,7 +273,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
protected SegmentPlaylist getNextSegments(String segmentPlaylistUrl) throws IOException, ParseException, PlaylistException {
|
||||
Instant start = Instant.now();
|
||||
recordingEvents.add(RecordingEvent.of("Playlist request"));
|
||||
URL segmentsUrl = new URL(segmentPlaylistUrl);
|
||||
URL segmentsUrl = URI.create(segmentPlaylistUrl).toURL();
|
||||
Builder builder = new Request.Builder().url(segmentsUrl);
|
||||
addHeaders(builder, Optional.ofNullable(model).map(Model::getHttpHeaderFactory).map(HttpHeaderFactory::createSegmentPlaylistHeaders).orElse(new HashMap<>()), model);
|
||||
Request request = builder.build();
|
||||
|
@ -321,27 +322,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
|
||||
List<TrackData> tracks = mediaPlaylist.getTracks();
|
||||
for (TrackData trackData : tracks) {
|
||||
if (trackData.hasMapInfo()) {
|
||||
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();
|
||||
}
|
||||
parseSegmentData(lsp, trackData);
|
||||
}
|
||||
lsp.avgSegDuration = lsp.totalDuration / tracks.size();
|
||||
return lsp;
|
||||
|
@ -349,6 +330,30 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
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) {
|
||||
if (playlist.segments.isEmpty()) {
|
||||
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.
|
||||
* E.g. HTTP 403 or 404
|
||||
*/
|
||||
protected void waitSomeTime(long waitForMillis) {
|
||||
protected void waitSomeTime() {
|
||||
try {
|
||||
Thread.sleep(waitForMillis);
|
||||
Thread.sleep(A_FEW_SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (running) {
|
||||
|
@ -452,11 +457,6 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
return running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedResolution() {
|
||||
return selectedResolution;
|
||||
}
|
||||
|
||||
private static class RecordingEvent {
|
||||
Instant timestamp;
|
||||
String message;
|
||||
|
|
|
@ -28,7 +28,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
@ -61,7 +61,6 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
|
||||
private volatile boolean running;
|
||||
private volatile boolean started;
|
||||
private int selectedResolution = 0;
|
||||
|
||||
public FfmpegHlsDownload(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
|
@ -81,11 +80,6 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSelectedResolution() {
|
||||
return selectedResolution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
|
@ -157,7 +151,7 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void postProcess(Recording recording) {
|
||||
// nothing to do
|
||||
|
@ -206,7 +200,7 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
} catch (Exception e) {
|
||||
LOG.error("Error while downloading MP4", e);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
if (!model.isOnline()) {
|
||||
LOG.debug("Model {} not online. Stop recording.", model);
|
||||
stop();
|
||||
|
@ -232,8 +226,8 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
}
|
||||
StreamSource selectedStreamSource = selectStreamSource(streamSources);
|
||||
String playlistUrl = selectedStreamSource.getMediaPlaylistUrl();
|
||||
selectedResolution = selectedStreamSource.height;
|
||||
|
||||
selectedResolution = selectedStreamSource.getHeight();
|
||||
|
||||
Request req = new Request.Builder()
|
||||
.url(playlistUrl)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -255,8 +249,8 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
}
|
||||
String uri = firstTrack.getUri();
|
||||
if (!uri.startsWith("http")) {
|
||||
URL context = new URL(playlistUrl);
|
||||
uri = new URL(context, uri).toExternalForm();
|
||||
URI context = URI.create(playlistUrl);
|
||||
uri = context.resolve(uri).toURL().toExternalForm();
|
||||
}
|
||||
LOG.debug("Media url {}", uri);
|
||||
return uri;
|
||||
|
@ -301,16 +295,16 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
}
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
LOG.debug("Socket timeout while downloading MP4 for {}. Stop recording.", model.getName());
|
||||
model.delay();
|
||||
stop();
|
||||
LOG.debug("Socket timeout while downloading MP4 for {}. Stop recording.", model.getName());
|
||||
model.delay();
|
||||
stop();
|
||||
} catch (IOException e) {
|
||||
LOG.debug("IO error while downloading MP4 for {}. Stop recording.", model.getName());
|
||||
model.delay();
|
||||
stop();
|
||||
LOG.debug("IO error while downloading MP4 for {}. Stop recording.", model.getName());
|
||||
model.delay();
|
||||
stop();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error while downloading MP4", e);
|
||||
stop();
|
||||
LOG.error("Error while downloading MP4", e);
|
||||
stop();
|
||||
} finally {
|
||||
ffmpegStreamLock.unlock();
|
||||
}
|
||||
|
@ -318,7 +312,7 @@ public class FfmpegHlsDownload extends AbstractDownload {
|
|||
running = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected void createTargetDirectory() throws IOException {
|
||||
Files.createDirectories(targetFile.getParentFile().toPath());
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import java.util.Map.Entry;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ctbrec.recorder.download.StreamSource.UNKNOWN;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
@ -141,9 +140,9 @@ public class HlsdlDownload extends AbstractDownload {
|
|||
int minRes = Config.getInstance().getSettings().minimumResolution;
|
||||
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||
List<StreamSource> filteredStreamSources = streamSources.stream()
|
||||
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height)
|
||||
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height)
|
||||
.collect(Collectors.toList());
|
||||
.filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || minRes <= src.getHeight())
|
||||
.filter(src -> src.getHeight() == 0 || src.getHeight() == UNKNOWN || maxRes >= src.getHeight())
|
||||
.toList();
|
||||
|
||||
if (filteredStreamSources.isEmpty()) {
|
||||
throw new ExecutionException(new NoStreamFoundException("No stream left in playlist"));
|
||||
|
|
|
@ -134,9 +134,9 @@ public class AmateurTvDownload extends AbstractDownload {
|
|||
running = true;
|
||||
try {
|
||||
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()
|
||||
.url(src.mediaPlaylistUrl)
|
||||
.url(src.getMediaPlaylistUrl())
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.header(ACCEPT, "*/*")
|
||||
.header(ACCEPT_LANGUAGE, "en")
|
||||
|
|
|
@ -76,10 +76,10 @@ public class AmateurTvModel extends AbstractModel {
|
|||
String value = (String) item;
|
||||
String[] res = value.split("x");
|
||||
StreamSource src = new StreamSource();
|
||||
src.mediaPlaylistUrl = MessageFormat.format("{0}&variant={1}", mediaPlaylistUrl, res[1]);
|
||||
src.width = Integer.parseInt(res[0]);
|
||||
src.height = Integer.parseInt(res[1]);
|
||||
src.bandwidth = 0;
|
||||
src.setMediaPlaylistUrl(MessageFormat.format("{0}&variant={1}", mediaPlaylistUrl, res[1]));
|
||||
src.setWidth(Integer.parseInt(res[0]));
|
||||
src.setHeight(Integer.parseInt(res[1]));
|
||||
src.setBandwidth(0);
|
||||
streamSources.add(src);
|
||||
});
|
||||
return streamSources;
|
||||
|
|
|
@ -10,14 +10,14 @@ import ctbrec.Config;
|
|||
import ctbrec.io.HtmlParser;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
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.io.HttpConstants.*;
|
||||
|
||||
@Slf4j
|
||||
public class BongaCamsModel extends AbstractModel {
|
||||
|
||||
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 STATUS = "status";
|
||||
|
||||
private static final Pattern ONLINE_BADGE_REGEX = Pattern.compile("class=\"badge_online\s*\"");
|
||||
|
||||
@Setter
|
||||
private boolean online = false;
|
||||
private final transient List<StreamSource> streamSources = new ArrayList<>();
|
||||
private int[] resolution;
|
||||
|
@ -102,23 +103,21 @@ public class BongaCamsModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
public State mapState(String roomState) {
|
||||
switch (roomState) {
|
||||
case "private", "fullprivate":
|
||||
return PRIVATE;
|
||||
case "group":
|
||||
return GROUP;
|
||||
case "public":
|
||||
return ONLINE;
|
||||
default:
|
||||
LOG.debug(roomState);
|
||||
return OFFLINE;
|
||||
}
|
||||
return switch (roomState) {
|
||||
case "private", "fullprivate" -> PRIVATE;
|
||||
case "group" -> GROUP;
|
||||
case "public" -> ONLINE;
|
||||
default -> {
|
||||
log.debug(roomState);
|
||||
yield OFFLINE;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isStreamAvailable() {
|
||||
|
@ -134,7 +133,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -162,10 +161,6 @@ public class BongaCamsModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
public void setOnline(boolean online) {
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
if (!failFast) {
|
||||
|
@ -181,11 +176,6 @@ public class BongaCamsModel extends AbstractModel {
|
|||
return onlineState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnlineState(State onlineState) {
|
||||
this.onlineState = onlineState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
String streamUrl = getStreamUrl();
|
||||
|
@ -208,16 +198,16 @@ public class BongaCamsModel extends AbstractModel {
|
|||
streamSources.clear();
|
||||
for (PlaylistData playlistData : master.getPlaylists()) {
|
||||
StreamSource streamsource = new StreamSource();
|
||||
streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri());
|
||||
streamsource.setMediaPlaylistUrl(streamUrl.replace("playlist.m3u8", playlistData.getUri()));
|
||||
if (playlistData.hasStreamInfo()) {
|
||||
StreamInfo info = playlistData.getStreamInfo();
|
||||
streamsource.bandwidth = info.getBandwidth();
|
||||
streamsource.width = info.hasResolution() ? info.getResolution().width : 0;
|
||||
streamsource.height = info.hasResolution() ? info.getResolution().height : 0;
|
||||
streamsource.setBandwidth(info.getBandwidth());
|
||||
streamsource.setWidth(info.hasResolution() ? info.getResolution().width : 0);
|
||||
streamsource.setHeight(info.hasResolution() ? info.getResolution().height : 0);
|
||||
} else {
|
||||
streamsource.bandwidth = 0;
|
||||
streamsource.width = 0;
|
||||
streamsource.height = 0;
|
||||
streamsource.setBandwidth(0);
|
||||
streamsource.setWidth(0);
|
||||
streamsource.setHeight(0);
|
||||
}
|
||||
streamSources.add(streamsource);
|
||||
}
|
||||
|
@ -261,7 +251,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
if (response.isSuccessful()) {
|
||||
JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string());
|
||||
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");
|
||||
}
|
||||
} else {
|
||||
|
@ -283,13 +273,13 @@ public class BongaCamsModel extends AbstractModel {
|
|||
List<StreamSource> sources = getStreamSources();
|
||||
Collections.sort(sources);
|
||||
StreamSource best = sources.get(sources.size() - 1);
|
||||
resolution = new int[]{best.width, best.height};
|
||||
resolution = new int[]{best.getWidth(), best.getHeight()};
|
||||
} catch (InterruptedException e) {
|
||||
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];
|
||||
} 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];
|
||||
}
|
||||
}
|
||||
|
@ -303,7 +293,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
}
|
||||
|
||||
String url = getSite().getBaseUrl() + "/follow/" + getName();
|
||||
LOG.debug("Calling {}", url);
|
||||
log.debug("Calling {}", url);
|
||||
RequestBody body = new FormBody.Builder()
|
||||
.add("src", "public-chat")
|
||||
.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();
|
||||
JSONObject json = new JSONObject(msg);
|
||||
if (json.optBoolean(SUCCESS)) {
|
||||
LOG.debug("Follow/Unfollow -> {}", msg);
|
||||
log.debug("Follow/Unfollow -> {}", msg);
|
||||
return true;
|
||||
} else {
|
||||
LOG.debug(msg);
|
||||
log.debug(msg);
|
||||
throw new IOException("Response was " + msg);
|
||||
}
|
||||
} else {
|
||||
|
@ -338,7 +328,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
String content = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
|
||||
Element html = HtmlParser.getTag(content, "html");
|
||||
String csrfToken = html.attr("data-csrf_value");
|
||||
LOG.debug("CSRF-Token {}", csrfToken);
|
||||
log.debug("CSRF-Token {}", csrfToken);
|
||||
return csrfToken;
|
||||
} else {
|
||||
throw new HttpException(resp.code(), resp.message());
|
||||
|
@ -353,7 +343,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
}
|
||||
|
||||
String url = getSite().getBaseUrl() + "/unfollow/" + getName();
|
||||
LOG.debug("Calling {}", url);
|
||||
log.debug("Calling {}", url);
|
||||
RequestBody body = new FormBody.Builder()
|
||||
.add("_csrf_token", getCsrfToken())
|
||||
.build();
|
||||
|
@ -368,10 +358,10 @@ public class BongaCamsModel extends AbstractModel {
|
|||
String msg = Objects.requireNonNull(resp.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
|
||||
JSONObject json = new JSONObject(msg);
|
||||
if (json.optBoolean(SUCCESS)) {
|
||||
LOG.debug("Follow/Unfollow -> {}", msg);
|
||||
log.debug("Follow/Unfollow -> {}", msg);
|
||||
return true;
|
||||
} else {
|
||||
LOG.debug(msg);
|
||||
log.debug(msg);
|
||||
throw new IOException("Response was " + msg);
|
||||
}
|
||||
} else {
|
||||
|
@ -388,7 +378,7 @@ public class BongaCamsModel extends AbstractModel {
|
|||
setOnline(true);
|
||||
}
|
||||
default -> {
|
||||
LOG.debug(roomState);
|
||||
log.debug(roomState);
|
||||
setOnlineState(OFFLINE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ import ctbrec.io.HttpException;
|
|||
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
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.MULTILINE;
|
||||
|
||||
@Slf4j
|
||||
public class Cam4Model extends AbstractModel {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Cam4Model.class);
|
||||
@Setter
|
||||
private String playlistUrl;
|
||||
private int[] resolution = null;
|
||||
private JSONObject modelInfo;
|
||||
private transient JSONObject modelInfo;
|
||||
|
||||
@Override
|
||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||
|
@ -57,7 +58,7 @@ public class Cam4Model extends AbstractModel {
|
|||
|
||||
private JSONObject loadModelInfo() throws IOException {
|
||||
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");
|
||||
setOnlineStateByShowType(state);
|
||||
setDescription(roomState.optString("status"));
|
||||
|
@ -73,7 +74,7 @@ public class Cam4Model extends AbstractModel {
|
|||
case "PAUSED" -> onlineState = AWAY;
|
||||
case "OFFLINE" -> onlineState = OFFLINE;
|
||||
default -> {
|
||||
LOG.debug("############################## Unknown show type [{} {}]", this, showType);
|
||||
log.debug("############################## Unknown show type [{} {}]", this, showType);
|
||||
onlineState = UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +87,7 @@ public class Cam4Model extends AbstractModel {
|
|||
try {
|
||||
modelInfo = loadModelInfo();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't load model details {}", e.getMessage());
|
||||
log.warn("Couldn't load model details {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return onlineState;
|
||||
|
@ -99,11 +100,11 @@ public class Cam4Model extends AbstractModel {
|
|||
return playlistUrl;
|
||||
}
|
||||
} 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")) {
|
||||
String hls = modelInfo.optString("hls");
|
||||
LOG.debug("Stream hls: {}", hls);
|
||||
log.debug("Stream hls: {}", hls);
|
||||
if (StringUtil.isNotBlank(hls) && hls.startsWith("http")) {
|
||||
playlistUrl = hls;
|
||||
return playlistUrl;
|
||||
|
@ -111,7 +112,7 @@ public class Cam4Model extends AbstractModel {
|
|||
}
|
||||
if (modelInfo != null && modelInfo.has("streamUUID")) {
|
||||
String uuid = modelInfo.optString("streamUUID");
|
||||
LOG.debug("Stream UUID: {}", uuid);
|
||||
log.debug("Stream UUID: {}", uuid);
|
||||
String[] parts = uuid.split("-");
|
||||
if (parts.length > 3) {
|
||||
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 {
|
||||
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
|
||||
.url(url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -146,7 +147,7 @@ public class Cam4Model extends AbstractModel {
|
|||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
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.getBoolean("canUseCDN")) {
|
||||
playlistUrl = json.optString("cdnURL");
|
||||
|
@ -186,15 +187,15 @@ public class Cam4Model extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0);
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0));
|
||||
if (playlist.getUri().startsWith("http")) {
|
||||
src.mediaPlaylistUrl = playlist.getUri();
|
||||
src.setMediaPlaylistUrl(playlist.getUri());
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +205,7 @@ public class Cam4Model extends AbstractModel {
|
|||
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||
String masterPlaylistUrl = getPlaylistUrl();
|
||||
masterPlaylistUrl = masterPlaylistUrl.replace("_sfm4s", "");
|
||||
LOG.debug("Loading master playlist [{}]", masterPlaylistUrl);
|
||||
log.debug("Loading master playlist [{}]", masterPlaylistUrl);
|
||||
Request.Builder builder = new Request.Builder().url(masterPlaylistUrl);
|
||||
getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header);
|
||||
Request req = builder.build();
|
||||
|
@ -245,13 +246,13 @@ public class Cam4Model extends AbstractModel {
|
|||
List<StreamSource> sources = getStreamSources();
|
||||
Collections.sort(sources);
|
||||
StreamSource best = sources.get(sources.size() - 1);
|
||||
resolution = new int[]{best.width, best.height};
|
||||
resolution = new int[]{best.getWidth(), best.getHeight()};
|
||||
} catch (InterruptedException e) {
|
||||
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];
|
||||
} 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];
|
||||
}
|
||||
}
|
||||
|
@ -285,10 +286,6 @@ public class Cam4Model extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
public void setPlaylistUrl(String playlistUrl) {
|
||||
this.playlistUrl = playlistUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUrl(String url) {
|
||||
String normalizedUrl = url.toLowerCase();
|
||||
|
|
|
@ -9,14 +9,15 @@ import ctbrec.AbstractModel;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -26,16 +27,19 @@ import java.util.concurrent.ExecutionException;
|
|||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
@Slf4j
|
||||
public class CamsodaModel extends AbstractModel {
|
||||
|
||||
private static final String STREAM_NAME = "stream_name";
|
||||
private static final String EDGE_SERVERS = "edge_servers";
|
||||
private static final String STATUS = "status";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
|
||||
private transient List<StreamSource> streamSources = null;
|
||||
private transient boolean isNew;
|
||||
@Getter
|
||||
@Setter
|
||||
private transient String gender;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private float sortOrder = 0;
|
||||
private final Random random = new Random();
|
||||
int[] resolution = new int[2];
|
||||
|
@ -68,7 +72,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
if (!isPublic(streamName)) {
|
||||
url.append("?token=").append(token);
|
||||
}
|
||||
LOG.trace("Stream URL: {}", url);
|
||||
log.trace("Stream URL: {}", url);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
|
@ -104,7 +108,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
if (playlistUrl == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
LOG.trace("Loading playlist {}", playlistUrl);
|
||||
log.trace("Loading playlist {}", playlistUrl);
|
||||
Request req = new Request.Builder()
|
||||
.url(playlistUrl)
|
||||
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||
|
@ -121,21 +125,21 @@ public class CamsodaModel extends AbstractModel {
|
|||
StreamSource streamsource = new StreamSource();
|
||||
int cutOffAt = Math.max(playlistUrl.indexOf("index.m3u8"), playlistUrl.indexOf("playlist.m3u8"));
|
||||
String segmentPlaylistUrl = playlistUrl.substring(0, cutOffAt) + playlistData.getUri();
|
||||
streamsource.mediaPlaylistUrl = segmentPlaylistUrl;
|
||||
streamsource.setMediaPlaylistUrl(segmentPlaylistUrl);
|
||||
if (playlistData.hasStreamInfo()) {
|
||||
StreamInfo info = playlistData.getStreamInfo();
|
||||
streamsource.bandwidth = info.getBandwidth();
|
||||
streamsource.width = info.hasResolution() ? info.getResolution().width : 0;
|
||||
streamsource.height = info.hasResolution() ? info.getResolution().height : 0;
|
||||
streamsource.setBandwidth(info.getBandwidth());
|
||||
streamsource.setWidth(info.hasResolution() ? info.getResolution().width : 0);
|
||||
streamsource.setHeight(info.hasResolution() ? info.getResolution().height : 0);
|
||||
} else {
|
||||
streamsource.bandwidth = 0;
|
||||
streamsource.width = 0;
|
||||
streamsource.height = 0;
|
||||
streamsource.setBandwidth(0);
|
||||
streamsource.setWidth(0);
|
||||
streamsource.setHeight(0);
|
||||
}
|
||||
streamSources.add(streamsource);
|
||||
}
|
||||
} else {
|
||||
LOG.trace("Response: {}", response.body().string());
|
||||
log.trace("Response: {}", response.body().string());
|
||||
throw new HttpException(playlistUrl, response.code(), response.message());
|
||||
}
|
||||
}
|
||||
|
@ -177,7 +181,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
case "private" -> onlineState = PRIVATE;
|
||||
case "limited" -> onlineState = GROUP;
|
||||
default -> {
|
||||
LOG.debug("Unknown show type {}", status);
|
||||
log.debug("Unknown show type {}", status);
|
||||
onlineState = UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
@ -211,12 +215,12 @@ public class CamsodaModel extends AbstractModel {
|
|||
} else {
|
||||
try {
|
||||
List<StreamSource> sources = getStreamSources();
|
||||
LOG.debug("{}:{} stream sources {}", getSite().getName(), getName(), sources);
|
||||
log.debug("{}:{} stream sources {}", getSite().getName(), getName(), sources);
|
||||
if (sources.isEmpty()) {
|
||||
return new int[]{0, 0};
|
||||
} else {
|
||||
StreamSource src = sources.get(sources.size() - 1);
|
||||
resolution = new int[]{src.width, src.height};
|
||||
resolution = new int[]{src.getWidth(), src.getHeight()};
|
||||
return resolution;
|
||||
}
|
||||
} catch (IOException | ParseException | PlaylistException e) {
|
||||
|
@ -230,7 +234,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
String csrfToken = ((CamsodaHttpClient) site.getHttpClient()).getCsrfToken();
|
||||
String url = site.getBaseUrl() + "/api/v1/tip/" + getName();
|
||||
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
LOG.debug("Sending tip {}", url);
|
||||
log.debug("Sending tip {}", url);
|
||||
RequestBody body = new FormBody.Builder()
|
||||
.add("amount", Integer.toString(tokens.intValue()))
|
||||
.add("comment", "")
|
||||
|
@ -255,7 +259,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
@Override
|
||||
public boolean follow() throws IOException {
|
||||
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();
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
|
@ -278,7 +282,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
@Override
|
||||
public boolean unfollow() throws IOException {
|
||||
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();
|
||||
Request request = new Request.Builder()
|
||||
.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() {
|
||||
return isNew;
|
||||
}
|
||||
|
@ -313,12 +309,4 @@ public class CamsodaModel extends AbstractModel {
|
|||
public void setNew(boolean isNew) {
|
||||
this.isNew = isNew;
|
||||
}
|
||||
|
||||
public String getGender() {
|
||||
return gender;
|
||||
}
|
||||
|
||||
public void setGender(String gender) {
|
||||
this.gender = gender;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,13 +11,12 @@ import ctbrec.StringUtil;
|
|||
import ctbrec.io.HttpException;
|
||||
import ctbrec.io.json.ObjectMapperFactory;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.EOFException;
|
||||
|
@ -32,10 +31,10 @@ import static ctbrec.Model.State.*;
|
|||
import static ctbrec.io.HttpConstants.*;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Slf4j
|
||||
public class ChaturbateModel extends AbstractModel {
|
||||
|
||||
private static final String PUBLIC = "public";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class);
|
||||
private int[] resolution = new int[2];
|
||||
private transient StreamInfo streamInfo;
|
||||
private transient Instant lastStreamInfoRequest = Instant.EPOCH;
|
||||
|
@ -60,11 +59,11 @@ public class ChaturbateModel extends AbstractModel {
|
|||
if (isOffline()) {
|
||||
roomStatus = "offline";
|
||||
onlineState = State.OFFLINE;
|
||||
LOG.trace("Model {} offline", getName());
|
||||
log.trace("Model {} offline", getName());
|
||||
} else {
|
||||
StreamInfo info = getStreamInfo();
|
||||
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 {
|
||||
StreamInfo info = getStreamInfo(true);
|
||||
|
@ -165,7 +164,7 @@ public class ChaturbateModel extends AbstractModel {
|
|||
case "away" -> onlineState = AWAY;
|
||||
case "group" -> onlineState = State.GROUP;
|
||||
default -> {
|
||||
LOG.debug("Unknown show type {}", roomStatus);
|
||||
log.debug("Unknown show type {}", roomStatus);
|
||||
onlineState = State.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
@ -203,16 +202,16 @@ public class ChaturbateModel extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(playlist.getStreamInfo().getResolution().height);
|
||||
String masterUrl = streamInfo.url;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
if (src.mediaPlaylistUrl.contains("?")) {
|
||||
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
|
||||
src.setMediaPlaylistUrl(segmentUri);
|
||||
if (src.getMediaPlaylistUrl().contains("?")) {
|
||||
src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
|
||||
}
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -261,10 +260,10 @@ public class ChaturbateModel extends AbstractModel {
|
|||
String responseBody = resp2.body().string();
|
||||
JSONObject json = new JSONObject(responseBody);
|
||||
if (!json.has("following")) {
|
||||
LOG.debug(responseBody);
|
||||
log.debug(responseBody);
|
||||
throw new IOException("Response was " + responseBody.substring(0, Math.min(responseBody.length(), 500)));
|
||||
} else {
|
||||
LOG.debug("Follow/Unfollow -> {}", responseBody);
|
||||
log.debug("Follow/Unfollow -> {}", responseBody);
|
||||
return json.getBoolean("following") == follow;
|
||||
}
|
||||
} else {
|
||||
|
@ -303,7 +302,7 @@ public class ChaturbateModel extends AbstractModel {
|
|||
lastStreamInfoRequest = Instant.now();
|
||||
if (response.isSuccessful()) {
|
||||
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);
|
||||
return streamInfo;
|
||||
} else {
|
||||
|
@ -355,7 +354,7 @@ public class ChaturbateModel extends AbstractModel {
|
|||
}
|
||||
|
||||
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()
|
||||
.url(streamInfo.url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -363,7 +362,7 @@ public class ChaturbateModel extends AbstractModel {
|
|||
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
LOG.trace(body);
|
||||
log.trace(body);
|
||||
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
|
|
|
@ -7,14 +7,15 @@ import com.iheartradio.m3u8.data.PlaylistData;
|
|||
import ctbrec.*;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -30,14 +31,17 @@ import static ctbrec.Model.State.ONLINE;
|
|||
import static ctbrec.io.HttpConstants.*;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Slf4j
|
||||
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>");
|
||||
|
||||
@Setter
|
||||
private boolean online = false;
|
||||
private int[] resolution;
|
||||
private String masterPlaylistUrl;
|
||||
@Getter
|
||||
@Setter
|
||||
private String id;
|
||||
|
||||
@Override
|
||||
|
@ -57,25 +61,25 @@ public class CherryTvModel extends AbstractModel {
|
|||
JSONObject json = new JSONObject(m.group(1));
|
||||
updateModelProperties(json);
|
||||
} else {
|
||||
LOG.error("NEXT_DATA not found in model page {}", getUrl());
|
||||
log.error("NEXT_DATA not found in model page {}", getUrl());
|
||||
return false;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
private void updateModelProperties(JSONObject json) {
|
||||
LOG.trace(json.toString(2));
|
||||
log.trace(json.toString(2));
|
||||
JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState");
|
||||
online = false;
|
||||
onlineState = OFFLINE;
|
||||
for (Iterator<String> iter = apolloState.keys(); iter.hasNext(); ) {
|
||||
String key = iter.next();
|
||||
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);
|
||||
setDisplayName(broadcast.optString("title"));
|
||||
online = broadcast.optString("showStatus").equalsIgnoreCase("Public")
|
||||
|
@ -89,10 +93,6 @@ public class CherryTvModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
public void setOnline(boolean online) {
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
if (!failFast) {
|
||||
|
@ -117,16 +117,16 @@ public class CherryTvModel extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(playlist.getStreamInfo().getResolution().height);
|
||||
String masterUrl = masterPlaylistUrl;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
if (src.mediaPlaylistUrl.contains("?")) {
|
||||
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
|
||||
src.setMediaPlaylistUrl(segmentUri);
|
||||
if (src.getMediaPlaylistUrl().contains("?")) {
|
||||
src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
|
||||
}
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -139,10 +139,10 @@ public class CherryTvModel extends AbstractModel {
|
|||
|
||||
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||
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());
|
||||
}
|
||||
LOG.trace("Loading master playlist {}", masterPlaylistUrl);
|
||||
log.trace("Loading master playlist {}", masterPlaylistUrl);
|
||||
Request req = new Request.Builder()
|
||||
.url(masterPlaylistUrl)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -150,7 +150,7 @@ public class CherryTvModel extends AbstractModel {
|
|||
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
String body = Objects.requireNonNull(response.body()).string();
|
||||
LOG.trace(body);
|
||||
log.trace(body);
|
||||
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
|
@ -185,13 +185,13 @@ public class CherryTvModel extends AbstractModel {
|
|||
List<StreamSource> sources = getStreamSources();
|
||||
Collections.sort(sources);
|
||||
StreamSource best = sources.get(sources.size() - 1);
|
||||
resolution = new int[]{best.width, best.height};
|
||||
resolution = new int[]{best.getWidth(), best.getHeight()};
|
||||
} catch (InterruptedException e) {
|
||||
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];
|
||||
} 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];
|
||||
}
|
||||
}
|
||||
|
@ -210,11 +210,11 @@ public class CherryTvModel extends AbstractModel {
|
|||
|
||||
private boolean followUnfollow(String action, String persistedQueryHash) throws IOException {
|
||||
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)) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseBody = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
|
||||
LOG.debug(responseBody);
|
||||
log.debug(responseBody);
|
||||
JSONObject resp = new JSONObject(responseBody);
|
||||
if (resp.has("data") && !resp.isNull("data")) {
|
||||
JSONObject data = resp.getJSONObject("data");
|
||||
|
@ -227,7 +227,7 @@ public class CherryTvModel extends AbstractModel {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
LOG.debug(resp.toString(2));
|
||||
log.debug(resp.toString(2));
|
||||
return false;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
|
@ -271,14 +271,6 @@ public class CherryTvModel extends AbstractModel {
|
|||
.build();
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSiteSpecificData(Map<String, String> data) {
|
||||
id = data.get("id");
|
||||
|
|
|
@ -80,7 +80,7 @@ public class DreamcamModel extends AbstractModel {
|
|||
List<StreamSource> sources = new ArrayList<>();
|
||||
try {
|
||||
StreamSource src = new StreamSource();
|
||||
src.mediaPlaylistUrl = getPlaylistUrl();
|
||||
src.setMediaPlaylistUrl(getPlaylistUrl());
|
||||
sources.add(src);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Can not get stream sources for {}: {}", getName(), e.getMessage());
|
||||
|
|
|
@ -10,12 +10,13 @@ import ctbrec.Config;
|
|||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.RecordingProcess;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import okio.ByteString;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -27,15 +28,19 @@ import java.util.function.BiConsumer;
|
|||
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
@Slf4j
|
||||
public class Fc2Model extends AbstractModel {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Fc2Model.class);
|
||||
@Getter
|
||||
@Setter
|
||||
private String id;
|
||||
@Getter
|
||||
@Setter
|
||||
private int viewerCount;
|
||||
private boolean online;
|
||||
private String version;
|
||||
private WebSocket ws;
|
||||
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 int messageId = 1;
|
||||
|
||||
|
@ -67,7 +72,7 @@ public class Fc2Model extends AbstractModel {
|
|||
if (resp.isSuccessful()) {
|
||||
String msg = resp.body().string();
|
||||
JSONObject json = new JSONObject(msg);
|
||||
// LOG.debug(json.toString(2));
|
||||
// log.debug(json.toString(2));
|
||||
JSONObject data = json.getJSONObject("data");
|
||||
JSONObject channelData = data.getJSONObject("channel_data");
|
||||
online = channelData.optInt("is_publish") == 1;
|
||||
|
@ -101,10 +106,8 @@ public class Fc2Model extends AbstractModel {
|
|||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
try {
|
||||
openWebsocket();
|
||||
List<StreamSource> sources = new ArrayList<>();
|
||||
LOG.debug("Paylist url {}", playlistUrl);
|
||||
sources.addAll(parseMasterPlaylist(playlistUrl));
|
||||
return sources;
|
||||
log.debug("Paylist url {}", playlistUrl);
|
||||
return parseMasterPlaylist(playlistUrl);
|
||||
} catch (InterruptedException e1) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ExecutionException(e1);
|
||||
|
@ -129,23 +132,22 @@ public class Fc2Model extends AbstractModel {
|
|||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
sources.clear();
|
||||
for (PlaylistData playlistData : master.getPlaylists()) {
|
||||
StreamSource streamsource = new StreamSource();
|
||||
streamsource.mediaPlaylistUrl = playlistData.getUri();
|
||||
streamsource.setMediaPlaylistUrl(playlistData.getUri());
|
||||
if (playlistData.hasStreamInfo()) {
|
||||
StreamInfo info = playlistData.getStreamInfo();
|
||||
streamsource.bandwidth = info.getBandwidth();
|
||||
streamsource.width = info.hasResolution() ? info.getResolution().width : 0;
|
||||
streamsource.height = info.hasResolution() ? info.getResolution().height : 0;
|
||||
streamsource.setBandwidth(info.getBandwidth());
|
||||
streamsource.setWidth(info.hasResolution() ? info.getResolution().width : 0);
|
||||
streamsource.setHeight(info.hasResolution() ? info.getResolution().height : 0);
|
||||
} else {
|
||||
streamsource.bandwidth = 0;
|
||||
streamsource.width = 0;
|
||||
streamsource.height = 0;
|
||||
streamsource.setBandwidth(0);
|
||||
streamsource.setWidth(0);
|
||||
streamsource.setHeight(0);
|
||||
}
|
||||
sources.add(streamsource);
|
||||
}
|
||||
LOG.debug(sources.toString());
|
||||
log.debug(sources.toString());
|
||||
return sources;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
|
@ -172,7 +174,7 @@ public class Fc2Model extends AbstractModel {
|
|||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||
.build();
|
||||
LOG.debug("Fetching page {}", url);
|
||||
log.debug("Fetching page {}", url);
|
||||
try (Response resp = getSite().getHttpClient().execute(req)) {
|
||||
if (resp.isSuccessful()) {
|
||||
String msg = resp.body().string();
|
||||
|
@ -236,48 +238,28 @@ public class Fc2Model extends AbstractModel {
|
|||
JSONObject json = new JSONObject(content);
|
||||
return json.optInt("status") == 1;
|
||||
} else {
|
||||
LOG.error("Login failed {} {}", resp.code(), resp.message());
|
||||
log.error("Login failed {} {}", resp.code(), resp.message());
|
||||
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
|
||||
* "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.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void openWebsocket() throws InterruptedException, IOException {
|
||||
messageId = 1;
|
||||
int usage = websocketUsage.incrementAndGet();
|
||||
LOG.debug("{} objects using the websocket for {}", usage, this);
|
||||
if (ws != null) {
|
||||
return;
|
||||
} else {
|
||||
log.debug("{} objects using the websocket for {}", usage, this);
|
||||
if (ws == null) {
|
||||
Object monitor = new Object();
|
||||
loadModelInfo();
|
||||
getControlToken((token, url) -> {
|
||||
url = url + "?control_token=" + token;
|
||||
LOG.debug("Session token: {}", token);
|
||||
LOG.debug("Getting playlist token over websocket {}", url);
|
||||
log.debug("Session token: {}", token);
|
||||
log.debug("Getting playlist token over websocket {}", url);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
|
@ -302,18 +284,19 @@ public class Fc2Model extends AbstractModel {
|
|||
JSONArray playlists = args.getJSONArray("playlists_high_latency");
|
||||
JSONObject playlist = playlists.getJSONObject(0);
|
||||
playlistUrl = playlist.getString("url");
|
||||
LOG.debug("Master Playlist: {}", playlistUrl);
|
||||
log.debug("Master Playlist: {}", playlistUrl);
|
||||
synchronized (monitor) {
|
||||
monitor.notifyAll();
|
||||
}
|
||||
} else {
|
||||
LOG.trace(json.toString());
|
||||
log.trace(json.toString());
|
||||
}
|
||||
}
|
||||
} else if (json.optString("name").equals("user_count") || json.optString("name").equals("comment")) {
|
||||
// ignore
|
||||
log.trace("WS <-- {}: {}", getName(), text);
|
||||
} else {
|
||||
LOG.trace("WS <-- {}: {}", getName(), text);
|
||||
log.trace("WS <-- {}: {}", getName(), text);
|
||||
}
|
||||
|
||||
// send heartbeat every now and again
|
||||
|
@ -321,24 +304,24 @@ public class Fc2Model extends AbstractModel {
|
|||
if ((now - lastHeartBeat) > TimeUnit.SECONDS.toMillis(30)) {
|
||||
webSocket.send("{\"name\":\"heartbeat\",\"arguments\":{},\"id\":" + messageId + "}");
|
||||
lastHeartBeat = now;
|
||||
LOG.trace("Sending heartbeat for {} (messageId: {})", getName(), messageId);
|
||||
log.trace("Sending heartbeat for {} (messageId: {})", getName(), messageId);
|
||||
messageId++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||
LOG.debug("ws btxt {}", bytes);
|
||||
log.debug("ws btxt {}", bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
LOG.debug("ws closed {} - {}", code, reason);
|
||||
log.debug("ws closed {} - {}", code, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
LOG.debug("ws failure", t);
|
||||
log.debug("ws failure", t);
|
||||
response.close();
|
||||
}
|
||||
});
|
||||
|
@ -355,9 +338,9 @@ public class Fc2Model extends AbstractModel {
|
|||
|
||||
public void closeWebsocket() {
|
||||
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) {
|
||||
LOG.debug("Closing the websocket for {}", this);
|
||||
log.debug("Closing the websocket for {}", this);
|
||||
ws.close(1000, "");
|
||||
ws = null;
|
||||
}
|
||||
|
|
|
@ -10,11 +10,12 @@ import ctbrec.Config;
|
|||
import ctbrec.Model;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -32,18 +33,22 @@ import static ctbrec.io.HttpConstants.*;
|
|||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Locale.ENGLISH;
|
||||
|
||||
@Slf4j
|
||||
public class Flirt4FreeModel extends AbstractModel {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class);
|
||||
@Setter
|
||||
private String id;
|
||||
private String chatHost;
|
||||
private String chatPort;
|
||||
private String chatToken;
|
||||
private String streamHost;
|
||||
@Setter
|
||||
private String streamUrl;
|
||||
int[] resolution = new int[2];
|
||||
private final transient Object monitor = new Object();
|
||||
@Getter
|
||||
private final transient List<String> categories = new LinkedList<>();
|
||||
@Setter
|
||||
private boolean online = false;
|
||||
private boolean isInteractiveShow = false;
|
||||
private boolean isNew = false;
|
||||
|
@ -93,7 +98,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
JSONObject json = new JSONObject(body);
|
||||
if (Objects.equals(json.optString("status"), "failed")) {
|
||||
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);
|
||||
}
|
||||
online = false;
|
||||
|
@ -115,17 +120,15 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
private void updateModelId(JSONObject json) {
|
||||
if (json.has(MODEL_ID)) {
|
||||
Object modelId = json.get(MODEL_ID);
|
||||
if (modelId instanceof Number n) {
|
||||
if (n.intValue() > 0) {
|
||||
id = String.valueOf(json.get(MODEL_ID));
|
||||
}
|
||||
if (modelId instanceof Number n && n.intValue() > 0) {
|
||||
id = String.valueOf(json.get(MODEL_ID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadModelInfo() throws IOException {
|
||||
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()
|
||||
.url(url)
|
||||
.header(ACCEPT, "*/*")
|
||||
|
@ -152,7 +155,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
JSONObject user = config.getJSONObject("user");
|
||||
userIp = user.getString("ip");
|
||||
} 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;
|
||||
onlineState = Model.State.OFFLINE;
|
||||
}
|
||||
|
@ -171,21 +174,14 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
try {
|
||||
if (withWebsocket) {
|
||||
acquireSlot();
|
||||
try {
|
||||
loadStreamUrl();
|
||||
} finally {
|
||||
releaseSlot();
|
||||
}
|
||||
acquireSlot();
|
||||
try {
|
||||
loadStreamUrl();
|
||||
} finally {
|
||||
releaseSlot();
|
||||
}
|
||||
masterPlaylist = getMasterPlaylist();
|
||||
} catch (InterruptedException e) {
|
||||
|
@ -197,12 +193,12 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
StreamInfo info = playlist.getStreamInfo();
|
||||
src.bandwidth = info.getBandwidth();
|
||||
src.height = (info.hasResolution()) ? info.getResolution().height : 0;
|
||||
src.width = (info.hasResolution()) ? info.getResolution().width : 0;
|
||||
src.setBandwidth(info.getBandwidth());
|
||||
src.setHeight((info.hasResolution()) ? info.getResolution().height : 0);
|
||||
src.setWidth((info.hasResolution()) ? info.getResolution().width : 0);
|
||||
HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl);
|
||||
src.mediaPlaylistUrl = "https://" + masterPlaylistUrl.host() + '/' + playlist.getUri();
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
src.setMediaPlaylistUrl("https://" + masterPlaylistUrl.host() + '/' + playlist.getUri());
|
||||
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +206,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
}
|
||||
|
||||
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()
|
||||
.url(streamUrl)
|
||||
.header(ACCEPT, "*/*")
|
||||
|
@ -240,7 +236,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
Objects.requireNonNull(chatHost, "chatHost is null");
|
||||
String h = chatHost.replace("chat", "chat-vip");
|
||||
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()
|
||||
.url(url)
|
||||
.header(ACCEPT, "*/*")
|
||||
|
@ -253,16 +249,16 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
getSite().getHttpClient().newWebSocket(req, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
LOG.trace("Chat websocket for {} opened", getName());
|
||||
log.trace("Chat websocket for {} opened", getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
if (json.optString("command").equals("8011")) {
|
||||
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");
|
||||
online = true;
|
||||
isInteractiveShow = data.optString("devices").equals("1");
|
||||
|
@ -277,7 +273,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
resolution[0] = Integer.parseInt(data.getString("stream_width"));
|
||||
resolution[1] = Integer.parseInt(data.getString("stream_height"));
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't determine stream resolution", e);
|
||||
log.warn("Couldn't determine stream resolution", e);
|
||||
}
|
||||
webSocket.close(1000, "");
|
||||
}
|
||||
|
@ -285,7 +281,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
monitor.notifyAll();
|
||||
}
|
||||
|
@ -296,7 +292,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
monitor.notifyAll();
|
||||
}
|
||||
|
@ -312,7 +308,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
+ "model_id=" + id
|
||||
+ "&video_host=" + streamHost
|
||||
+ "&t=" + System.currentTimeMillis();
|
||||
LOG.debug("Loading master playlist information: {}", url);
|
||||
log.debug("Loading master playlist information: {}", url);
|
||||
req = new Request.Builder()
|
||||
.url(url)
|
||||
.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());
|
||||
JSONArray hls = json.getJSONObject("data").getJSONArray("hls");
|
||||
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
|
||||
int giftId = isInteractiveShow ? 775 : 171;
|
||||
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?" +
|
||||
"gift_id=" + giftId +
|
||||
"&num_credits=" + amount +
|
||||
|
@ -355,7 +351,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
"&userIP=" + userIp +
|
||||
"&anonymous=N&response_type=json" +
|
||||
"&t=" + System.currentTimeMillis();
|
||||
LOG.debug("Trying to send tip: {}", url);
|
||||
log.debug("Trying to send tip: {}", url);
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header(ACCEPT, "*/*")
|
||||
|
@ -373,8 +369,8 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
if (json.has("error_message")) {
|
||||
msg = json.getString("error_message");
|
||||
}
|
||||
LOG.error("Sending tip failed: {}", msg);
|
||||
LOG.debug("Response: {}", json.toString(2));
|
||||
log.error("Sending tip failed: {}", msg);
|
||||
log.debug("Response: {}", json.toString(2));
|
||||
throw new IOException(msg);
|
||||
}
|
||||
} else {
|
||||
|
@ -434,10 +430,10 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||
if (!failFast && streamUrl != null && resolution[0] == 0) {
|
||||
try {
|
||||
List<StreamSource> streamSources = getStreamSources(true);
|
||||
List<StreamSource> streamSources = getStreamSources();
|
||||
Collections.sort(streamSources);
|
||||
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) {
|
||||
throw new ExecutionException("Couldn't determine stream resolution", e);
|
||||
}
|
||||
|
@ -485,7 +481,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
"&id=" + id +
|
||||
"&name=" + getName() +
|
||||
"&t=" + System.currentTimeMillis();
|
||||
LOG.debug("Sending follow/unfollow request: {}", url);
|
||||
log.debug("Sending follow/unfollow request: {}", url);
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header(ACCEPT, "*/*")
|
||||
|
@ -496,7 +492,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
LOG.debug("Follow/Unfollow response: {}", body);
|
||||
log.debug("Follow/Unfollow response: {}", body);
|
||||
return Objects.equals(body, "1");
|
||||
} else {
|
||||
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
|
||||
public void readSiteSpecificData(Map<String, String> data) {
|
||||
id = data.get("id");
|
||||
|
@ -518,14 +510,6 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
data.put("id", id);
|
||||
}
|
||||
|
||||
public void setStreamUrl(String streamUrl) {
|
||||
this.streamUrl = streamUrl;
|
||||
}
|
||||
|
||||
public void setOnline(boolean b) {
|
||||
online = b;
|
||||
}
|
||||
|
||||
public boolean isNew() {
|
||||
return isNew;
|
||||
}
|
||||
|
@ -557,8 +541,4 @@ public class Flirt4FreeModel extends AbstractModel {
|
|||
lastRequest = System.currentTimeMillis();
|
||||
requestThrottle.release();
|
||||
}
|
||||
|
||||
public List<String> getCategories() {
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ public class LiveJasminStreamRegistration {
|
|||
JSONObject data = message.getJSONArray("data").getJSONArray(0).getJSONObject(0);
|
||||
String streamId = data.getString("streamId");
|
||||
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) {
|
||||
awaitBarrier();
|
||||
}
|
||||
|
@ -230,10 +230,10 @@ public class LiveJasminStreamRegistration {
|
|||
.replace("{streamName}", URLEncoder.encode(streamName, UTF_8));
|
||||
|
||||
LiveJasminStreamSource streamSource = new LiveJasminStreamSource();
|
||||
streamSource.mediaPlaylistUrl = hlsUrl;
|
||||
streamSource.width = w;
|
||||
streamSource.height = h;
|
||||
streamSource.bandwidth = bitrate;
|
||||
streamSource.setMediaPlaylistUrl(hlsUrl);
|
||||
streamSource.setWidth(w);
|
||||
streamSource.setHeight(h);
|
||||
streamSource.setBandwidth(bitrate);
|
||||
streamSource.setRtmpUrl(rtmpUrl);
|
||||
streamSource.setStreamName(streamName);
|
||||
streamSource.setStreamId(streamId);
|
||||
|
|
|
@ -9,6 +9,8 @@ import ctbrec.io.HttpException;
|
|||
import ctbrec.recorder.download.RecordingProcess;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import ctbrec.sites.ModelOfflineException;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
@ -36,6 +38,8 @@ public class MVLiveModel extends AbstractModel {
|
|||
private transient JSONObject roomLocation;
|
||||
private transient Instant lastRoomLocationUpdate = Instant.EPOCH;
|
||||
private String roomNumber;
|
||||
@Getter
|
||||
@Setter
|
||||
private String id;
|
||||
|
||||
@Override
|
||||
|
@ -74,16 +78,16 @@ public class MVLiveModel extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(playlist.getStreamInfo().getResolution().height);
|
||||
String masterUrl = streamLocation.masterPlaylist;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
if (src.mediaPlaylistUrl.contains("?")) {
|
||||
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
|
||||
src.setMediaPlaylistUrl(segmentUri);
|
||||
if (src.getMediaPlaylistUrl().contains("?")) {
|
||||
src.setMediaPlaylistUrl((src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?'))));
|
||||
}
|
||||
log.debug("Media playlist {}", src.mediaPlaylistUrl);
|
||||
log.debug("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -247,14 +251,6 @@ public class MVLiveModel extends AbstractModel {
|
|||
id = data.get("id");
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void updateStateFromJson(JSONObject creator) {
|
||||
setId(creator.getString("id"));
|
||||
setDisplayName(creator.optString("display_name", null));
|
||||
|
|
|
@ -1,22 +1,5 @@
|
|||
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.recorder.download.StreamSource;
|
||||
import ctbrec.recorder.download.dash.AdaptationSetType;
|
||||
|
@ -26,14 +9,28 @@ import ctbrec.recorder.download.dash.RepresentationType;
|
|||
import ctbrec.sites.Site;
|
||||
import okhttp3.Request;
|
||||
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 {
|
||||
|
||||
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) {
|
||||
this.config = config;
|
||||
|
@ -65,12 +62,12 @@ public class DashStreamSourceProvider implements StreamSourceProvider {
|
|||
return videoStreams.stream().map(ast -> {
|
||||
RepresentationType representation = ast.getRepresentation().get(0);
|
||||
StreamSource src = new StreamSource();
|
||||
src.width = ast.getWidth().intValue();
|
||||
src.height = ast.getHeight().intValue();
|
||||
src.bandwidth = (int)representation.getBandwidth();
|
||||
src.mediaPlaylistUrl = streamUrl;
|
||||
src.setWidth(ast.getWidth().intValue());
|
||||
src.setHeight(ast.getHeight().intValue());
|
||||
src.setBandwidth((int) representation.getBandwidth());
|
||||
src.setMediaPlaylistUrl(streamUrl);
|
||||
return src;
|
||||
}).collect(Collectors.toList());
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
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.InputStream;
|
||||
|
@ -10,25 +21,7 @@ import java.util.Locale;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
public record HlsStreamSourceProvider(HttpClient httpClient) implements StreamSourceProvider {
|
||||
|
||||
|
@ -41,19 +34,19 @@ public record HlsStreamSourceProvider(HttpClient httpClient) implements StreamSo
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
if (playlist.getStreamInfo().getResolution() != null) {
|
||||
src.width = playlist.getStreamInfo().getResolution().width;
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.setWidth(playlist.getStreamInfo().getResolution().width);
|
||||
src.setHeight(playlist.getStreamInfo().getResolution().height);
|
||||
} else {
|
||||
src.width = StreamSource.UNKNOWN;
|
||||
src.height = StreamSource.UNKNOWN;
|
||||
src.setWidth(StreamSource.UNKNOWN);
|
||||
src.setHeight(StreamSource.UNKNOWN);
|
||||
}
|
||||
String masterUrl = streamUrl;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
src.setMediaPlaylistUrl(segmentUri);
|
||||
LOG.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import ctbrec.recorder.download.HttpHeaderFactory;
|
|||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||
import ctbrec.recorder.download.RecordingProcess;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
|
@ -32,9 +34,17 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private int uid = -1; // undefined
|
||||
@Getter
|
||||
@Setter
|
||||
private String streamUrl;
|
||||
@Getter
|
||||
@Setter
|
||||
private double camScore;
|
||||
@Getter
|
||||
@Setter
|
||||
private int viewerCount;
|
||||
private ctbrec.sites.mfc.State state;
|
||||
private int[] resolution = new int[2];
|
||||
|
@ -84,21 +94,17 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case ONLINE, RECORDING:
|
||||
return ctbrec.Model.State.ONLINE;
|
||||
case AWAY:
|
||||
return ctbrec.Model.State.AWAY;
|
||||
case PRIVATE:
|
||||
return ctbrec.Model.State.PRIVATE;
|
||||
case GROUP_SHOW:
|
||||
return ctbrec.Model.State.GROUP;
|
||||
case OFFLINE, CAMOFF, UNKNOWN:
|
||||
return ctbrec.Model.State.OFFLINE;
|
||||
default:
|
||||
return switch (state) {
|
||||
case ONLINE, RECORDING -> State.ONLINE;
|
||||
case AWAY -> State.AWAY;
|
||||
case PRIVATE -> State.PRIVATE;
|
||||
case GROUP_SHOW -> State.GROUP;
|
||||
case OFFLINE, CAMOFF, UNKNOWN -> State.OFFLINE;
|
||||
default -> {
|
||||
LOG.debug("State {} is not mapped", this.state);
|
||||
return ctbrec.Model.State.UNKNOWN;
|
||||
}
|
||||
yield State.UNKNOWN;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -182,7 +188,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
List<StreamSource> streamSources = getStreamSources();
|
||||
Collections.sort(streamSources);
|
||||
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) {
|
||||
LOG.warn("Couldn't determine stream resolution - {}", e.getMessage());
|
||||
} catch (ExecutionException | IOException e) {
|
||||
|
@ -192,22 +198,6 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
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() {
|
||||
MyFreeCams mfc = (MyFreeCams) getSite();
|
||||
SessionState sessionState = mfc.getClient().getSessionState(this);
|
||||
|
@ -292,7 +282,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
}
|
||||
|
||||
private int toCamServ(String server) {
|
||||
return Integer.parseInt(server.replaceAll("[^0-9]+", ""));
|
||||
return Integer.parseInt(server.replaceAll("\\D+", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -305,22 +295,6 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
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
|
||||
public void readSiteSpecificData(Map<String, String> data) {
|
||||
uid = Integer.parseInt(data.get("uid"));
|
||||
|
@ -337,11 +311,6 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
updateStreamUrl();
|
||||
}
|
||||
return super.createDownload();
|
||||
// if(isHlsStream()) {
|
||||
// return super.createDownload();
|
||||
// } else {
|
||||
// return new MyFreeCamsWebrtcDownload(uid, streamUrl, ((MyFreeCams)site).getClient());
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -90,9 +90,9 @@ public class SecretFriendsModel extends AbstractModel {
|
|||
.build();
|
||||
|
||||
StreamSource src = new StreamSource();
|
||||
src.width = 1280;
|
||||
src.height = 720;
|
||||
src.mediaPlaylistUrl = wsUrl.toString();
|
||||
src.setWidth(1280);
|
||||
src.setHeight(720);
|
||||
src.setMediaPlaylistUrl(wsUrl.toString());
|
||||
return Collections.singletonList(src);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import ctbrec.recorder.download.HttpHeaderFactory;
|
|||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||
import ctbrec.recorder.download.RecordingProcess;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.IOException;
|
||||
|
@ -21,11 +23,18 @@ import static ctbrec.Model.State.*;
|
|||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
public class ShowupModel extends AbstractModel {
|
||||
private static final Random RNG = new Random();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private String uid;
|
||||
@Getter
|
||||
@Setter
|
||||
private String streamId;
|
||||
@Getter
|
||||
@Setter
|
||||
private String streamTranscoderAddr;
|
||||
private int[] resolution = new int[2];
|
||||
private final int[] resolution = new int[2];
|
||||
|
||||
@Override
|
||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||
|
@ -54,8 +63,8 @@ public class ShowupModel extends AbstractModel {
|
|||
@Override
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
|
||||
StreamSource src = new StreamSource();
|
||||
src.width = 480;
|
||||
src.height = 360;
|
||||
src.setWidth(480);
|
||||
src.setHeight(360);
|
||||
|
||||
if (streamId == null || streamTranscoderAddr == null) {
|
||||
List<Model> modelList = getShowupSite().getModelList();
|
||||
|
@ -68,11 +77,11 @@ public class ShowupModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
int cdnHost = 1 + new Random().nextInt(5);
|
||||
int cid = 100_000 + new Random().nextInt(900_000);
|
||||
long pid = 10_000_000_000L + new Random().nextInt();
|
||||
int cdnHost = 1 + RNG.nextInt(5);
|
||||
int cid = 100_000 + RNG.nextInt(900_000);
|
||||
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}";
|
||||
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<>();
|
||||
sources.add(src);
|
||||
return sources;
|
||||
|
@ -107,30 +116,6 @@ public class ShowupModel extends AbstractModel {
|
|||
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
|
||||
public RecordingProcess createDownload() {
|
||||
return new ShowupWebrtcDownload(getSite().getHttpClient());
|
||||
|
@ -165,9 +150,9 @@ public class ShowupModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
int cdnHost = 1 + new Random().nextInt(5);
|
||||
int cid = 100_000 + new Random().nextInt(900_000);
|
||||
long pid = 10_000_000_000L + new Random().nextInt();
|
||||
int cdnHost = 1 + RNG.nextInt(5);
|
||||
int cid = 100_000 + RNG.nextInt(900_000);
|
||||
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,#}";
|
||||
return MessageFormat.format(urlTemplate, cdnHost, streamTranscoderAddr, streamId, cid, pid);
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ import ctbrec.Config;
|
|||
import ctbrec.NotImplementedExcetion;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
@ -26,17 +27,18 @@ import static ctbrec.io.HttpConstants.*;
|
|||
import static ctbrec.sites.streamate.StreamateHttpClient.JSON;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Slf4j
|
||||
public class StreamateModel extends AbstractModel {
|
||||
|
||||
private static final String ORIGIN = "origin";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StreamateModel.class);
|
||||
|
||||
private static final Long MODEL_ID_UNDEFINED = -1L;
|
||||
|
||||
@Setter
|
||||
private boolean online = false;
|
||||
private final transient List<StreamSource> streamSources = new ArrayList<>();
|
||||
private int[] resolution;
|
||||
@Getter
|
||||
@Setter
|
||||
private Long id = MODEL_ID_UNDEFINED;
|
||||
|
||||
@Override
|
||||
|
@ -57,10 +59,6 @@ public class StreamateModel extends AbstractModel {
|
|||
return online;
|
||||
}
|
||||
|
||||
public void setOnline(boolean online) {
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
if (!failFast && onlineState == UNKNOWN) {
|
||||
|
@ -92,10 +90,10 @@ public class StreamateModel extends AbstractModel {
|
|||
for (int i = 0; i < encodings.length(); i++) {
|
||||
JSONObject encoding = encodings.getJSONObject(i);
|
||||
StreamSource src = new StreamSource();
|
||||
src.mediaPlaylistUrl = encoding.getString("location");
|
||||
src.width = encoding.optInt("videoWidth");
|
||||
src.height = encoding.optInt("videoHeight");
|
||||
src.bandwidth = (encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024;
|
||||
src.setMediaPlaylistUrl(encoding.getString("location"));
|
||||
src.setWidth(encoding.optInt("videoWidth"));
|
||||
src.setHeight(encoding.optInt("videoHeight"));
|
||||
src.setBandwidth((encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024);
|
||||
streamSources.add(src);
|
||||
}
|
||||
|
||||
|
@ -103,11 +101,10 @@ public class StreamateModel extends AbstractModel {
|
|||
if (hls.has(ORIGIN) && !hls.isNull(ORIGIN)) {
|
||||
JSONObject origin = hls.getJSONObject(ORIGIN);
|
||||
StreamSource src = new StreamSource();
|
||||
src.mediaPlaylistUrl = origin.getString("location");
|
||||
src.width = origin.optInt("videoWidth");
|
||||
src.height = origin.optInt("videoHeight");
|
||||
src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024;
|
||||
src.height = StreamSource.ORIGIN;
|
||||
src.setMediaPlaylistUrl(origin.getString("location"));
|
||||
src.setWidth(origin.optInt("videoWidth"));
|
||||
src.setBandwidth((origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024);
|
||||
src.setHeight(StreamSource.ORIGIN);
|
||||
streamSources.add(src);
|
||||
}
|
||||
|
||||
|
@ -155,15 +152,15 @@ public class StreamateModel extends AbstractModel {
|
|||
List<StreamSource> sources = getStreamSources();
|
||||
Collections.sort(sources);
|
||||
StreamSource best = sources.get(sources.size() - 1);
|
||||
if (best.height == StreamSource.ORIGIN) {
|
||||
if (best.getHeight() == StreamSource.ORIGIN) {
|
||||
best = sources.get(sources.size() - 2);
|
||||
}
|
||||
resolution = new int[]{best.width, best.height};
|
||||
resolution = new int[]{best.getWidth(), best.getHeight()};
|
||||
} 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();
|
||||
} 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;
|
||||
|
@ -213,14 +210,6 @@ public class StreamateModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSiteSpecificData(Map<String, String> data) {
|
||||
id = Long.parseLong(data.get("id"));
|
||||
|
@ -232,7 +221,7 @@ public class StreamateModel extends AbstractModel {
|
|||
try {
|
||||
loadModelId();
|
||||
} 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));
|
||||
|
|
|
@ -89,10 +89,10 @@ public class StreamrayModel extends AbstractModel {
|
|||
try {
|
||||
String url = getMasterPlaylistUrl();
|
||||
StreamSource src = new StreamSource();
|
||||
src.mediaPlaylistUrl = url;
|
||||
src.height = 0;
|
||||
src.width = 0;
|
||||
src.bandwidth = 0;
|
||||
src.setMediaPlaylistUrl(url);
|
||||
src.setHeight(0);
|
||||
src.setWidth(0);
|
||||
src.setBandwidth(0);
|
||||
sources.add(src);
|
||||
} catch (IOException e) {
|
||||
log.error("Can not get stream sources for {}", getName());
|
||||
|
|
|
@ -11,13 +11,12 @@ import ctbrec.recorder.download.RecordingProcess;
|
|||
import ctbrec.recorder.download.StreamSource;
|
||||
import ctbrec.recorder.download.hls.HlsdlDownload;
|
||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -33,9 +32,9 @@ import static ctbrec.io.HttpConstants.*;
|
|||
import static ctbrec.sites.stripchat.StripchatHttpClient.JSON;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Slf4j
|
||||
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 modelId = 0;
|
||||
private boolean isVr = false;
|
||||
|
@ -52,7 +51,7 @@ public class StripchatModel extends AbstractModel {
|
|||
String status = user.optString("status");
|
||||
mapOnlineState(status);
|
||||
if (isBanned(user)) {
|
||||
LOG.debug("Model inactive or deleted: {}", getName());
|
||||
log.debug("Model inactive or deleted: {}", getName());
|
||||
setMarkedForLaterRecording(true);
|
||||
}
|
||||
modelId = user.optInt("id");
|
||||
|
@ -75,7 +74,7 @@ public class StripchatModel extends AbstractModel {
|
|||
case "private", "p2p", "groupShow", "virtualPrivate" -> setOnlineState(PRIVATE);
|
||||
case "off" -> setOnlineState(OFFLINE);
|
||||
default -> {
|
||||
LOG.debug("Unknown online state {} for model {}", status, getName());
|
||||
log.debug("Unknown online state {} for model {}", status, getName());
|
||||
setOnlineState(OFFLINE);
|
||||
}
|
||||
}
|
||||
|
@ -122,14 +121,15 @@ public class StripchatModel extends AbstractModel {
|
|||
for (StreamSource original : extractStreamSources(masterPlaylist)) {
|
||||
boolean found = false;
|
||||
for (StreamSource source : streamSources) {
|
||||
if (source.height == original.height) {
|
||||
if (source.getHeight() == original.getHeight()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) streamSources.add(original);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Original stream quality not available", e);
|
||||
log.warn("Original stream quality not available", e);
|
||||
}
|
||||
return streamSources;
|
||||
}
|
||||
|
@ -139,13 +139,13 @@ public class StripchatModel extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.mediaPlaylistUrl = playlist.getUri();
|
||||
if (src.mediaPlaylistUrl.contains("?")) {
|
||||
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(playlist.getStreamInfo().getResolution().height);
|
||||
src.setMediaPlaylistUrl(playlist.getUri());
|
||||
if (src.getMediaPlaylistUrl().contains("?")) {
|
||||
src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
|
||||
}
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ public class StripchatModel extends AbstractModel {
|
|||
}
|
||||
|
||||
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()
|
||||
.url(url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -161,7 +161,7 @@ public class StripchatModel extends AbstractModel {
|
|||
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
LOG.trace(body);
|
||||
log.trace(body);
|
||||
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
|
@ -174,9 +174,9 @@ public class StripchatModel extends AbstractModel {
|
|||
}
|
||||
|
||||
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 vrSuffix = (VR && isVr) ? "_vr" : "";
|
||||
String vrSuffix = (isVirtualRealityStream && isVr) ? "_vr" : "";
|
||||
if (modelId > 0) {
|
||||
return MessageFormat.format(hlsUrlTemplate, String.valueOf(modelId), vrSuffix);
|
||||
}
|
||||
|
@ -193,12 +193,12 @@ public class StripchatModel extends AbstractModel {
|
|||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
LOG.trace(body);
|
||||
log.trace(body);
|
||||
JSONObject jsonResponse = new JSONObject(body);
|
||||
String streamName = jsonResponse.optString("streamName");
|
||||
JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings");
|
||||
String vrBroadcastServer = broadcastSettings.optString("vrBroadcastServer");
|
||||
vrSuffix = (!VR || vrBroadcastServer.isEmpty()) ? "" : "_vr";
|
||||
vrSuffix = (!isVirtualRealityStream || vrBroadcastServer.isEmpty()) ? "" : "_vr";
|
||||
return MessageFormat.format(hlsUrlTemplate, streamName, vrSuffix);
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
|
@ -238,12 +238,12 @@ public class StripchatModel extends AbstractModel {
|
|||
@Override
|
||||
public boolean follow() throws IOException {
|
||||
getSite().getHttpClient().login();
|
||||
JSONObject modelInfo = getModelInfo();
|
||||
JSONObject user = modelInfo.getJSONObject("user");
|
||||
long modelId = user.optLong("id");
|
||||
JSONObject info = getModelInfo();
|
||||
JSONObject user = info.getJSONObject("user");
|
||||
long id = user.optLong("id");
|
||||
|
||||
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();
|
||||
requestParams.put("csrfToken", client.getCsrfToken());
|
||||
requestParams.put("csrfTimestamp", client.getCsrfTimestamp());
|
||||
|
@ -272,11 +272,11 @@ public class StripchatModel extends AbstractModel {
|
|||
@Override
|
||||
public boolean unfollow() throws IOException {
|
||||
getSite().getHttpClient().login();
|
||||
JSONObject modelInfo = getModelInfo();
|
||||
JSONObject user = modelInfo.getJSONObject("user");
|
||||
long modelId = user.optLong("id");
|
||||
JSONObject info = getModelInfo();
|
||||
JSONObject user = info.getJSONObject("user");
|
||||
long id = user.optLong("id");
|
||||
JSONArray favoriteIds = new JSONArray();
|
||||
favoriteIds.put(modelId);
|
||||
favoriteIds.put(id);
|
||||
|
||||
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
|
||||
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites";
|
||||
|
@ -320,7 +320,7 @@ public class StripchatModel extends AbstractModel {
|
|||
if (jsonResponse.has("user")) {
|
||||
JSONObject user = jsonResponse.getJSONObject("user");
|
||||
if (isBanned(user)) {
|
||||
LOG.debug("Model inactive or deleted: {}", getName());
|
||||
log.debug("Model inactive or deleted: {}", getName());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -337,11 +337,10 @@ public class StripchatModel extends AbstractModel {
|
|||
}
|
||||
|
||||
protected String getUniq() {
|
||||
Random r = new Random();
|
||||
String dict = "0123456789abcdefghijklmnopqarstvwxyz";
|
||||
char[] text = new char[16];
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||
@Slf4j
|
||||
public class WinkTvModel extends AbstractModel {
|
||||
|
||||
private static final String KEY_MEDIA = "media";
|
||||
private int[] resolution = new int[]{0, 0};
|
||||
|
||||
@Getter
|
||||
|
@ -49,8 +50,8 @@ public class WinkTvModel extends AbstractModel {
|
|||
if (ignoreCache) {
|
||||
try {
|
||||
JSONObject json = getModelInfo();
|
||||
if (json.has("media")) {
|
||||
JSONObject media = json.getJSONObject("media");
|
||||
if (json.has(KEY_MEDIA)) {
|
||||
JSONObject media = json.getJSONObject(KEY_MEDIA);
|
||||
boolean isLive = media.optBoolean("isLive");
|
||||
String meType = media.optString("type");
|
||||
if (isLive && meType.equals("free")) {
|
||||
|
@ -70,9 +71,7 @@ public class WinkTvModel extends AbstractModel {
|
|||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
if (failFast && onlineState != UNKNOWN) {
|
||||
return onlineState;
|
||||
} else {
|
||||
if (!failFast || onlineState == UNKNOWN) {
|
||||
try {
|
||||
onlineState = isOnline(true) ? ONLINE : OFFLINE;
|
||||
} catch (InterruptedException e) {
|
||||
|
@ -81,8 +80,8 @@ public class WinkTvModel extends AbstractModel {
|
|||
} catch (IOException | ExecutionException e) {
|
||||
onlineState = OFFLINE;
|
||||
}
|
||||
return onlineState;
|
||||
}
|
||||
return onlineState;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -98,10 +97,10 @@ public class WinkTvModel extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
src.mediaPlaylistUrl = playlist.getUri();
|
||||
log.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(playlist.getStreamInfo().getResolution().height);
|
||||
src.setMediaPlaylistUrl(playlist.getUri());
|
||||
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +189,7 @@ public class WinkTvModel extends AbstractModel {
|
|||
String url = "https://api.winktv.co.kr/v1/member/bj";
|
||||
FormBody body = new FormBody.Builder()
|
||||
.add("userId", getName())
|
||||
.add("info", "media")
|
||||
.add("info", KEY_MEDIA)
|
||||
.build();
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
package ctbrec.sites.xlovecam;
|
||||
|
||||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
import static java.nio.charset.StandardCharsets.*;
|
||||
import com.iheartradio.m3u8.*;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -14,31 +23,13 @@ import java.util.concurrent.ExecutionException;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.ParsingMode;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
|
||||
import ctbrec.AbstractModel;
|
||||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpConstants.USER_AGENT;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
@Slf4j
|
||||
public class XloveCamModel extends AbstractModel {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XloveCamModel.class);
|
||||
private static final Pattern HLS_PLAYLIST_PATTERN = Pattern.compile("\"hlsPlaylist\":\"(.*?)\",");
|
||||
|
||||
private boolean online = false;
|
||||
|
@ -56,9 +47,7 @@ public class XloveCamModel extends AbstractModel {
|
|||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
if (failFast && onlineState != UNKNOWN) {
|
||||
return onlineState;
|
||||
} else {
|
||||
if (!failFast || onlineState == UNKNOWN) {
|
||||
try {
|
||||
onlineState = isOnline(true) ? ONLINE : OFFLINE;
|
||||
} catch (InterruptedException e) {
|
||||
|
@ -67,8 +56,8 @@ public class XloveCamModel extends AbstractModel {
|
|||
} catch (IOException | ExecutionException e) {
|
||||
onlineState = OFFLINE;
|
||||
}
|
||||
return onlineState;
|
||||
}
|
||||
return onlineState;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -78,10 +67,10 @@ public class XloveCamModel extends AbstractModel {
|
|||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = Optional.ofNullable(playlist.getStreamInfo().getResolution()).map(r -> r.height).orElse(0);
|
||||
src.mediaPlaylistUrl = playlist.getUri();
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
src.setBandwidth(playlist.getStreamInfo().getBandwidth());
|
||||
src.setHeight(Optional.ofNullable(playlist.getStreamInfo().getResolution()).map(r -> r.height).orElse(0));
|
||||
src.setMediaPlaylistUrl(playlist.getUri());
|
||||
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +89,7 @@ public class XloveCamModel extends AbstractModel {
|
|||
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
LOG.trace(body);
|
||||
log.trace(body);
|
||||
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
|
@ -142,7 +131,7 @@ public class XloveCamModel extends AbstractModel {
|
|||
|
||||
@Override
|
||||
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||
return new int[] {0, 0};
|
||||
return new int[]{0, 0};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Loading…
Reference in New Issue