Add setting to restrict recording by bit rate

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

View File

@ -12,8 +12,8 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.event.PlayerStartedEvent;
import ctbrec.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();

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"));

View File

@ -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")

View File

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

View File

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

View File

@ -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();

View File

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

View File

@ -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();

View File

@ -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");

View File

@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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());

View File

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

View File

@ -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)

View File

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