forked from j62/ctbrec
1
0
Fork 0

Merge almost all changes by @winkru

This commit is contained in:
0xb00bface 2023-10-29 19:24:16 +01:00
parent addbeab76e
commit 224bb27003
57 changed files with 1513 additions and 641 deletions

View File

@ -1,6 +1,8 @@
package ctbrec.ui; package ctbrec.ui;
import ctbrec.Config;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.StringUtil;
import ctbrec.io.StreamRedirector; import ctbrec.io.StreamRedirector;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import javafx.application.Platform; import javafx.application.Platform;
@ -31,6 +33,20 @@ public class DesktopIntegration {
private static TrayIcon trayIcon; private static TrayIcon trayIcon;
public static void open(String uri) { public static void open(String uri) {
Config cfg = Config.getInstance();
Runtime rt = Runtime.getRuntime();
String[] cmdline = createCmdline(uri);
if (!cfg.getSettings().browserOverride.isEmpty()) {
try {
rt.exec(cmdline);
return;
} catch (Exception e) {
LOG.debug("Couldn't open URL with user-defined {} {}", cmdline, uri);
}
}
if (!cfg.getSettings().forceBrowserOverride) {
try { try {
CamrecApplication.hostServices.showDocument(uri); CamrecApplication.hostServices.showDocument(uri);
return; return;
@ -48,7 +64,6 @@ public class DesktopIntegration {
// try external helpers // try external helpers
var externalHelpers = new String[]{"kde-open5", "kde-open", "gnome-open", "xdg-open"}; var externalHelpers = new String[]{"kde-open5", "kde-open", "gnome-open", "xdg-open"};
var rt = Runtime.getRuntime();
for (String helper : externalHelpers) { for (String helper : externalHelpers) {
try { try {
rt.exec(helper + " " + uri); rt.exec(helper + " " + uri);
@ -57,6 +72,7 @@ public class DesktopIntegration {
LOG.debug("Couldn't open URL with {} {}", helper, uri); LOG.debug("Couldn't open URL with {} {}", helper, uri);
} }
} }
}
// all attempts failed, show a dialog with URL at least // all attempts failed, show a dialog with URL at least
Alert info = new AutosizeAlert(Alert.AlertType.ERROR); Alert info = new AutosizeAlert(Alert.AlertType.ERROR);
@ -73,6 +89,23 @@ public class DesktopIntegration {
info.show(); info.show();
} }
private static String[] createCmdline(String streamUrl) {
Config cfg = Config.getInstance();
String params = cfg.getSettings().browserParams.trim();
String[] cmdline;
if (params.isEmpty()) {
cmdline = new String[2];
} else {
String[] playerArgs = StringUtil.splitParams(params);
cmdline = new String[playerArgs.length + 2];
System.arraycopy(playerArgs, 0, cmdline, 1, playerArgs.length);
}
cmdline[0] = cfg.getSettings().browserOverride;
cmdline[cmdline.length - 1] = streamUrl;
return cmdline;
}
public static void open(File f) { public static void open(File f) {
try { try {
Desktop.getDesktop().open(f); Desktop.getDesktop().open(f);

View File

@ -1,17 +1,9 @@
package ctbrec.ui; package ctbrec.ui;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter; import com.squareup.moshi.JsonWriter;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.SubsequentAction; import ctbrec.SubsequentAction;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
@ -23,6 +15,12 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;
/** /**
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly * Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
*/ */
@ -230,6 +228,16 @@ public class JavaFxModel implements Model {
pausedProperty.set(suspended); pausedProperty.set(suspended);
} }
@Override
public void delay() {
delegate.delay();
}
@Override
public boolean isDelayed() {
return delegate.isDelayed();
}
@Override @Override
public String getDisplayName() { public String getDisplayName() {
return delegate.getDisplayName(); return delegate.getDisplayName();

View File

@ -189,7 +189,7 @@ public class Player {
private void expandPlaceHolders(String[] cmdline) { private void expandPlaceHolders(String[] cmdline) {
ModelVariableExpander expander = new ModelVariableExpander(model, Config.getInstance(), null, null); ModelVariableExpander expander = new ModelVariableExpander(model, Config.getInstance(), null, null);
for (int i = 0; i < cmdline.length; i++) { for (int i = 1; i < cmdline.length; i++) {
var param = cmdline[i]; var param = cmdline[i];
param = expander.expand(param); param = expander.expand(param);
cmdline[i] = param; cmdline[i] = param;

View File

@ -0,0 +1,51 @@
package ctbrec.ui.action;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Node;
public class LaterGroupAction extends ModelMassEditAction {
private Recorder recorder;
private Model model;
public LaterGroupAction(Node source, Recorder recorder, Model model) {
super.source = source;
this.recorder = recorder;
this.model = model;
action = m -> {
try {
if (recorder.isMarkedForLaterRecording(m) == false) {
recorder.markForLaterRecording(m, true);
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Platform.runLater(() -> Dialogs.showError(source.getScene(), "Couldn't change model state", "Mark for later recording of " + m.getName() + " failed", e));
}
};
}
@Override
protected List<Model> getModels() {
Optional<ModelGroup> optionalGroup = recorder.getModelGroup(model);
if (optionalGroup.isPresent()) {
ModelGroup group = optionalGroup.get();
return recorder.getModels().stream() //
.filter(m -> group.getModelUrls().contains(m.getUrl())) //
.collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
}

View File

@ -31,17 +31,10 @@
*/ */
package ctbrec.ui.controls; package ctbrec.ui.controls;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool; import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.SetThumbAsPortraitAction; import ctbrec.ui.action.SetThumbAsPortraitAction;
@ -52,19 +45,20 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Skin;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/** /**
* Popover page that displays a list of samples and sample categories for a given SampleCategory. * Popover page that displays a list of samples and sample categories for a given SampleCategory.
@ -85,7 +79,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
@Override @Override
protected void itemClicked(Model model) { protected void itemClicked(Model model) {
if(model == null) { if (model == null) {
return; return;
} }
@ -253,7 +247,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
this.model = model; this.model = model;
URL anonymousPng = getClass().getResource("/anonymous.png"); URL anonymousPng = getClass().getResource("/anonymous.png");
String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString()); String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString());
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1") && StringUtil.isNotBlank(previewUrl)) {
Image img = new Image(previewUrl, true); Image img = new Image(previewUrl, true);
thumb.setImage(img); thumb.setImage(img);
} else { } else {

View File

@ -1,19 +1,16 @@
package ctbrec.ui.menu; package ctbrec.ui.menu;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.action.*;
import javafx.scene.Node;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.action.EditGroupAction;
import ctbrec.ui.action.PauseGroupAction;
import ctbrec.ui.action.ResumeGroupAction;
import ctbrec.ui.action.StopGroupAction;
import javafx.scene.Node;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
public class ModelGroupMenuBuilder { public class ModelGroupMenuBuilder {
private Model model; private Model model;
@ -45,7 +42,8 @@ public class ModelGroupMenuBuilder {
Objects.requireNonNull(model, "Model has to be set"); Objects.requireNonNull(model, "Model has to be set");
Objects.requireNonNull(recorder, "Recorder has to be set"); Objects.requireNonNull(recorder, "Recorder has to be set");
Objects.requireNonNull(source, "Node has to be set"); Objects.requireNonNull(source, "Node has to be set");
callback = Optional.ofNullable(callback).orElse(m -> {}); callback = Optional.ofNullable(callback).orElse(m -> {
});
var menu = new Menu("Group"); var menu = new Menu("Group");
@ -61,7 +59,10 @@ public class ModelGroupMenuBuilder {
var stopAllOfGroup = new MenuItem("Stop all in group"); var stopAllOfGroup = new MenuItem("Stop all in group");
stopAllOfGroup.setOnAction(e -> new StopGroupAction(source, recorder, model).execute(callback)); stopAllOfGroup.setOnAction(e -> new StopGroupAction(source, recorder, model).execute(callback));
menu.getItems().addAll(editGroup, resumeAllOfGroup, pauseAllOfGroup, stopAllOfGroup); var laterAllOfGroup = new MenuItem("Record later all in group");
laterAllOfGroup.setOnAction(e -> new LaterGroupAction(source, recorder, model).execute(callback));
menu.getItems().addAll(editGroup, resumeAllOfGroup, pauseAllOfGroup, stopAllOfGroup, laterAllOfGroup);
return menu; return menu;
} }
} }

View File

@ -1,33 +1,13 @@
package ctbrec.ui.menu; package ctbrec.ui.menu;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import ctbrec.ui.controls.Dialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ModelGroup; import ctbrec.ModelGroup;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.DesktopIntegration; import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.action.AbstractModelAction.Result; import ctbrec.ui.action.AbstractModelAction.Result;
import ctbrec.ui.action.AddToGroupAction; import ctbrec.ui.action.*;
import ctbrec.ui.action.EditNotesAction; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.MarkForLaterRecordingAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.SetPortraitAction;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.action.SwitchStreamResolutionAction;
import ctbrec.ui.action.TipAction;
import ctbrec.ui.action.TriConsumer;
import ctbrec.ui.tabs.FollowedTab; import ctbrec.ui.tabs.FollowedTab;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
@ -38,6 +18,15 @@ import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent; import javafx.scene.input.ClipboardContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URLEncoder;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static java.nio.charset.StandardCharsets.UTF_8;
public class ModelMenuContributor { public class ModelMenuContributor {
@ -90,11 +79,16 @@ public class ModelMenuContributor {
} }
public void contributeToMenu(List<Model> selectedModels, ContextMenu menu) { public void contributeToMenu(List<Model> selectedModels, ContextMenu menu) {
startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {}); startStopCallback = Optional.ofNullable(startStopCallback).orElse(m -> {
followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {}); });
ignoreCallback = Optional.ofNullable(ignoreCallback).orElse(m -> {}); followCallback = Optional.ofNullable(followCallback).orElse((m, f, s) -> {
portraitCallback = Optional.ofNullable(portraitCallback).orElse(m -> {}); });
callback = Optional.ofNullable(callback).orElse(() -> {}); ignoreCallback = Optional.ofNullable(ignoreCallback).orElse(m -> {
});
portraitCallback = Optional.ofNullable(portraitCallback).orElse(m -> {
});
callback = Optional.ofNullable(callback).orElse(() -> {
});
addOpenInPlayer(menu, selectedModels); addOpenInPlayer(menu, selectedModels);
addOpenInBrowser(menu, selectedModels); addOpenInBrowser(menu, selectedModels);
addCopyUrl(menu, selectedModels); addCopyUrl(menu, selectedModels);
@ -116,6 +110,7 @@ public class ModelMenuContributor {
addOpenRecDir(menu, selectedModels); addOpenRecDir(menu, selectedModels);
addNotes(menu, selectedModels); addNotes(menu, selectedModels);
addPortrait(menu, selectedModels); addPortrait(menu, selectedModels);
addOpenOnCamGirlFinder(menu, selectedModels);
} }
public ModelMenuContributor afterwards(Runnable callback) { public ModelMenuContributor afterwards(Runnable callback) {
@ -157,6 +152,23 @@ public class ModelMenuContributor {
menu.getItems().add(openInBrowser); menu.getItems().add(openInBrowser);
} }
private void addOpenOnCamGirlFinder(ContextMenu menu, List<Model> selectedModels) {
var openOnCamGirlFinder = new MenuItem("Search on CamGirlFinder");
openOnCamGirlFinder.setOnAction(e -> {
for (Model model : selectedModels) {
String preview = model.getPreview();
if (preview != null && !preview.isEmpty()) {
String query = URLEncoder.encode(preview, UTF_8);
DesktopIntegration.open("https://camgirlfinder.net/search?url=" + query);
} else {
String query = URLEncoder.encode(model.getName(), UTF_8);
DesktopIntegration.open("https://camgirlfinder.net/models?m=" + query + "&p=a&g=a");
}
}
});
menu.getItems().add(openOnCamGirlFinder);
}
private void addCopyUrl(ContextMenu menu, List<Model> selectedModels) { private void addCopyUrl(ContextMenu menu, List<Model> selectedModels) {
if (selectedModels == null || selectedModels.isEmpty()) { if (selectedModels == null || selectedModels.isEmpty()) {
return; return;
@ -213,8 +225,7 @@ public class ModelMenuContributor {
} }
private boolean isFollowedTab() { private boolean isFollowedTab() {
if (source instanceof TabPane) { if (source instanceof TabPane tabPane) {
var tabPane = (TabPane) source;
return tabPane.getSelectionModel().getSelectedItem() instanceof FollowedTab; return tabPane.getSelectionModel().getSelectedItem() instanceof FollowedTab;
} }
return false; return false;

View File

@ -0,0 +1,53 @@
package ctbrec.ui.settings;
import ctbrec.Config;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.HBox;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.List;
@Slf4j
public class CacheSettingsPane extends HBox {
private ComboBox<String> cacheSizeCombo;
private final SettingsTab settingsTab;
private final Config config;
private static final List<String> names = List.of("disabled", "16 MiB", "64 MiB", "128 MiB", "256 MiB", "512 MiB");
private static final List<Integer> values = List.of(0, 16, 64, 128, 256, 512);
public CacheSettingsPane(SettingsTab settingsTab, Config config) {
this.settingsTab = settingsTab;
this.config = config;
setSpacing(5);
getChildren().addAll(buildCacheSizeCombo());
}
private ComboBox<String> buildCacheSizeCombo() {
ObservableList<String> lst = FXCollections.observableList(names);
cacheSizeCombo = new ComboBox<>(lst);
cacheSizeCombo.setOnAction(evt -> saveCacheConfig());
int size = config.getSettings().thumbCacheSize;
int selectedIndex = values.indexOf(size);
if (selectedIndex < 0) {
selectedIndex = 1;
}
cacheSizeCombo.getSelectionModel().select(selectedIndex);
return cacheSizeCombo;
}
private void saveCacheConfig() {
int index = cacheSizeCombo.getSelectionModel().getSelectedIndex();
int size = values.get(index);
config.getSettings().thumbCacheSize = size;
try {
config.save();
settingsTab.showRestartRequired();
} catch (IOException e) {
log.error("Can't save config", e);
}
}
}

View File

@ -71,6 +71,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleListProperty<String> startTab; private SimpleListProperty<String> startTab;
private SimpleFileProperty mediaPlayer; private SimpleFileProperty mediaPlayer;
private SimpleStringProperty mediaPlayerParams; private SimpleStringProperty mediaPlayerParams;
private SimpleFileProperty browserOverride;
private SimpleStringProperty browserParams;
private SimpleBooleanProperty forceBrowserOverride;
private SimpleIntegerProperty maximumResolutionPlayer; private SimpleIntegerProperty maximumResolutionPlayer;
private SimpleBooleanProperty showPlayerStarting; private SimpleBooleanProperty showPlayerStarting;
private SimpleBooleanProperty singlePlayer; private SimpleBooleanProperty singlePlayer;
@ -124,6 +127,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleLongProperty recordUntilDefaultDurationInMinutes; private SimpleLongProperty recordUntilDefaultDurationInMinutes;
private SimpleStringProperty dateTimeFormat; private SimpleStringProperty dateTimeFormat;
private final VariablePlayGroundDialogFactory variablePlayGroundDialogFactory = new VariablePlayGroundDialogFactory(); private final VariablePlayGroundDialogFactory variablePlayGroundDialogFactory = new VariablePlayGroundDialogFactory();
private SimpleBooleanProperty checkForUpdates;
public SettingsTab(List<Site> sites, Recorder recorder) { public SettingsTab(List<Site> sites, Recorder recorder) {
this.sites = sites; this.sites = sites;
@ -146,6 +150,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
startTab = new SimpleListProperty<>(null, "startTab", FXCollections.observableList(getTabNames())); startTab = new SimpleListProperty<>(null, "startTab", FXCollections.observableList(getTabNames()));
mediaPlayer = new SimpleFileProperty(null, "mediaPlayer", settings.mediaPlayer); mediaPlayer = new SimpleFileProperty(null, "mediaPlayer", settings.mediaPlayer);
mediaPlayerParams = new SimpleStringProperty(null, "mediaPlayerParams", settings.mediaPlayerParams); mediaPlayerParams = new SimpleStringProperty(null, "mediaPlayerParams", settings.mediaPlayerParams);
browserOverride = new SimpleFileProperty(null, "browserOverride", settings.browserOverride);
browserParams = new SimpleStringProperty(null, "browserParams", settings.browserParams);
forceBrowserOverride = new SimpleBooleanProperty(null, "forceBrowserOverride", settings.forceBrowserOverride);
maximumResolutionPlayer = new SimpleIntegerProperty(null, "maximumResolutionPlayer", settings.maximumResolutionPlayer); maximumResolutionPlayer = new SimpleIntegerProperty(null, "maximumResolutionPlayer", settings.maximumResolutionPlayer);
showPlayerStarting = new SimpleBooleanProperty(null, "showPlayerStarting", settings.showPlayerStarting); showPlayerStarting = new SimpleBooleanProperty(null, "showPlayerStarting", settings.showPlayerStarting);
singlePlayer = new SimpleBooleanProperty(null, "singlePlayer", settings.singlePlayer); singlePlayer = new SimpleBooleanProperty(null, "singlePlayer", settings.singlePlayer);
@ -156,7 +163,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
proxyPassword = new SimpleStringProperty(null, "proxyPassword", settings.proxyPassword); proxyPassword = new SimpleStringProperty(null, "proxyPassword", settings.proxyPassword);
recordingsDir = new SimpleDirectoryProperty(null, "recordingsDir", settings.recordingsDir); recordingsDir = new SimpleDirectoryProperty(null, "recordingsDir", settings.recordingsDir);
directoryStructure = new SimpleListProperty<>(null, "recordingsDirStructure", directoryStructure = new SimpleListProperty<>(null, "recordingsDirStructure",
FXCollections.observableList(List.of(FLAT, ONE_PER_MODEL, ONE_PER_RECORDING))); FXCollections.observableList(List.of(FLAT, ONE_PER_MODEL, ONE_PER_GROUP, ONE_PER_RECORDING)));
splitAfter = new SimpleListProperty<>(null, "splitRecordingsAfterSecs", FXCollections.observableList(getSplitAfterSecsOptions())); splitAfter = new SimpleListProperty<>(null, "splitRecordingsAfterSecs", FXCollections.observableList(getSplitAfterSecsOptions()));
splitBiggerThan = new SimpleListProperty<>(null, "splitRecordingsBiggerThanBytes", FXCollections.observableList(getSplitBiggerThanOptions())); splitBiggerThan = new SimpleListProperty<>(null, "splitRecordingsBiggerThanBytes", FXCollections.observableList(getSplitBiggerThanOptions()));
resolutionRange = new SimpleRangeProperty<>(rangeValues, "minimumResolution", "maximumResolution", settings.minimumResolution, resolutionRange = new SimpleRangeProperty<>(rangeValues, "minimumResolution", "maximumResolution", settings.minimumResolution,
@ -198,6 +205,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
recordUntilDefaultDurationInMinutes = new SimpleLongProperty(null, "recordUntilDefaultDurationInMinutes", settings.recordUntilDefaultDurationInMinutes); recordUntilDefaultDurationInMinutes = new SimpleLongProperty(null, "recordUntilDefaultDurationInMinutes", settings.recordUntilDefaultDurationInMinutes);
dateTimeFormat = new SimpleStringProperty(null, "dateTimeFormat", settings.dateTimeFormat); dateTimeFormat = new SimpleStringProperty(null, "dateTimeFormat", settings.dateTimeFormat);
tabsSortable = new SimpleBooleanProperty(null, "tabsSortable", settings.tabsSortable); tabsSortable = new SimpleBooleanProperty(null, "tabsSortable", settings.tabsSortable);
checkForUpdates = new SimpleBooleanProperty(null, "checkForUpdates", settings.checkForUpdates);
} }
private void createGui() { private void createGui() {
@ -219,6 +227,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds").needsRestart(), Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds").needsRestart(),
Setting.of("Update thumbnails", updateThumbnails, Setting.of("Update thumbnails", updateThumbnails,
"The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."), "The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."),
Setting.of("Cache size", new CacheSettingsPane(this, config)).needsRestart(),
Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"), Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"),
Setting.of("Enable live previews (experimental)", livePreviews), Setting.of("Enable live previews (experimental)", livePreviews),
Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(), Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(),
@ -227,6 +236,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(), Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(),
Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"), Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"),
Setting.of("Recording tab per site", recordedModelsPerSite, "Add a Recording tab for each site").needsRestart(), Setting.of("Recording tab per site", recordedModelsPerSite, "Add a Recording tab for each site").needsRestart(),
Setting.of("Check for new versions at startup", checkForUpdates, "Search for updates every startup"),
Setting.of("Start Tab", startTab)), Setting.of("Start Tab", startTab)),
Group.of("Player", Group.of("Player",
@ -234,7 +244,12 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Start parameters", mediaPlayerParams), Setting.of("Start parameters", mediaPlayerParams),
Setting.of("Maximum resolution (0 = unlimited)", maximumResolutionPlayer, "video height, e.g. 720 or 1080"), Setting.of("Maximum resolution (0 = unlimited)", maximumResolutionPlayer, "video height, e.g. 720 or 1080"),
Setting.of("Show \"Player Starting\" Message", showPlayerStarting), Setting.of("Show \"Player Starting\" Message", showPlayerStarting),
Setting.of("Start only one player at a time", singlePlayer))), Setting.of("Start only one player at a time", singlePlayer)),
Group.of("Browser",
Setting.of("Browser", browserOverride),
Setting.of("Start parameters", browserParams),
Setting.of("Force use (ignore default browser)", forceBrowserOverride, "Default behaviour will fallback to OS default if the above browser fails"))),
Category.of("Look & Feel", Category.of("Look & Feel",
Group.of("Look & Feel", Group.of("Look & Feel",
Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart(), Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart(),

View File

@ -24,32 +24,32 @@ public class AmateurTvTabProvider extends AbstractTabProvider {
List<Tab> tabs = new ArrayList<>(); List<Tab> tabs = new ArrayList<>();
// all // all
var url = AmateurTv.BASE_URL + "/v3/readmodel/cache/cams/A"; var url = AmateurTv.BASE_URL + "/v3/readmodel/cache/onlinecamlist";
var updateService = new AmateurTvUpdateService((AmateurTv) site, url); var updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("All", updateService)); tabs.add(createTab("All", updateService));
// female // female
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/cams/W"; url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22w%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url); updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Female", updateService)); tabs.add(createTab("Female", updateService));
// male // male
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/cams/M"; url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22m%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url); updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Male", updateService)); tabs.add(createTab("Male", updateService));
// couples // couples
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/cams/C"; url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22c%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url); updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Couples", updateService)); tabs.add(createTab("Couples", updateService));
// trans // trans
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/cams/T"; url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22t%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url); updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Trans", updateService)); tabs.add(createTab("Trans", updateService));
// followed // followed
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/cams/F"; url = AmateurTv.BASE_URL + "/v3/readmodel/cache/favorites";
updateService = new AmateurTvUpdateService((AmateurTv) site, url); updateService = new AmateurTvUpdateService((AmateurTv) site, url);
updateService.requiresLogin(true); updateService.requiresLogin(true);
followedTab = new AmateurTvFollowedTab("Followed", updateService, site); followedTab = new AmateurTvFollowedTab("Followed", updateService, site);

View File

@ -1,17 +1,5 @@
package ctbrec.ui.sites.amateurtv; package ctbrec.ui.sites.amateurtv;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.sites.amateurtv.AmateurTv; import ctbrec.sites.amateurtv.AmateurTv;
@ -20,15 +8,33 @@ import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService; import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import okhttp3.Request; import okhttp3.Request;
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.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.*;
public class AmateurTvUpdateService extends PaginatedScheduledService { public class AmateurTvUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvUpdateService.class); private static final Logger LOG = LoggerFactory.getLogger(AmateurTvUpdateService.class);
private static final int ITEMS_PER_PAGE = 50; private static final int ITEMS_PER_PAGE = 48;
private AmateurTv site; private AmateurTv site;
private String url; private String url;
private boolean requiresLogin = false; private boolean requiresLogin = false;
private List<Model> modelsList;
private Instant lastListInfoRequest = Instant.EPOCH;
public AmateurTvUpdateService(AmateurTv site, String url) { public AmateurTvUpdateService(AmateurTv site, String url) {
this.site = site; this.site = site;
@ -41,31 +47,62 @@ public class AmateurTvUpdateService extends PaginatedScheduledService {
@Override @Override
public List<Model> call() throws IOException { public List<Model> call() throws IOException {
if (requiresLogin) { if (requiresLogin) {
SiteUiFactory.getUi(site).login(); if (!SiteUiFactory.getUi(site).login()) {
throw new IOException("- Login is required");
} }
return loadModelList(); ;
}
return getModelList().stream()
.skip((page - 1) * (long) ITEMS_PER_PAGE)
.limit(ITEMS_PER_PAGE)
.collect(Collectors.toList()); // NOSONAR
} }
}; };
} }
private List<Model> getModelList() throws IOException {
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
return modelsList;
}
lastListInfoRequest = Instant.now();
modelsList = loadModelList();
if (modelsList == null) {
modelsList = Collections.emptyList();
}
return modelsList;
}
private List<Model> loadModelList() throws IOException { private List<Model> loadModelList() throws IOException {
int offset = page - 1; LOG.debug("Fetching page {}", url);
String pageUrl = url + '/' + offset * ITEMS_PER_PAGE + '/' + ITEMS_PER_PAGE + "/en"; Request request = new Request.Builder()
LOG.debug("Fetching page {}", pageUrl); .url(url)
var request = new Request.Builder()
.url(pageUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT, Locale.ENGLISH.getLanguage()) .header(ACCEPT, Locale.ENGLISH.getLanguage())
.header(REFERER, site.getBaseUrl() + "/following") .header(REFERER, site.getBaseUrl() + "/following")
.build(); .build();
try (var response = site.getHttpClient().execute(request)) { try (Response response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
var content = response.body().string(); String content = response.body().string();
List<Model> models = new ArrayList<>(); List<Model> models = new ArrayList<>();
var json = new JSONObject(content); JSONObject json = new JSONObject(content);
var modelNodes = json.getJSONObject("cams").getJSONArray("nodes"); if (json.has("body")) {
parseModels(modelNodes, models); JSONObject body = json.getJSONObject("body");
if (body.has("cams")) {
JSONArray cams = body.getJSONArray("cams");
parseModels(cams, models);
}
if (body.has("list") && body.has("total")) {
if (body.optInt("total") > 0) {
JSONArray list = body.getJSONArray("list");
parseModels(list, models);
}
}
}
if (json.has("cams")) {
JSONArray cams = json.getJSONArray("cams");
parseModels(cams, models);
}
return models; return models;
} else { } else {
int code = response.code(); int code = response.code();
@ -76,12 +113,15 @@ public class AmateurTvUpdateService extends PaginatedScheduledService {
private void parseModels(JSONArray jsonModels, List<Model> models) { private void parseModels(JSONArray jsonModels, List<Model> models) {
for (var i = 0; i < jsonModels.length(); i++) { for (var i = 0; i < jsonModels.length(); i++) {
var m = jsonModels.getJSONObject(i); JSONObject m = jsonModels.getJSONObject(i);
var user = m.getJSONObject("user"); String name = m.optString("username");
var name = user.optString("username");
AmateurTvModel model = (AmateurTvModel) site.createModel(name); AmateurTvModel model = (AmateurTvModel) site.createModel(name);
model.setPreview(m.optString("imageURL")); if (m.optBoolean("capturesEnabled", true) && m.has("capture")) {
model.setDescription(m.optJSONObject("topic").optString("text")); model.setPreview(m.optString("capture"));
} else {
model.setPreview(site.getBaseUrl() + m.optString("avatar"));
}
model.setDescription(m.optString("topic"));
models.add(model); models.add(model);
} }
} }

View File

@ -2,6 +2,7 @@ package ctbrec.ui.sites.bonga;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.bonga.BongaCamsModel; import ctbrec.sites.bonga.BongaCamsModel;
import ctbrec.ui.SiteUiFactory; import ctbrec.ui.SiteUiFactory;
@ -84,13 +85,19 @@ public class BongaCamsUpdateService extends PaginatedScheduledService {
for (var i = 0; i < jsonModels.length(); i++) { for (var i = 0; i < jsonModels.length(); i++) {
var m = jsonModels.getJSONObject(i); var m = jsonModels.getJSONObject(i);
var name = m.optString("username"); var name = m.optString("username");
if (name.isEmpty()) { if (StringUtil.isBlank(name)) {
continue; continue;
} }
BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name); BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name);
model.mapOnlineState(m.optString("room")); model.mapOnlineState(m.optString("room"));
model.setOnline(m.optInt("viewers") > 0); model.setOnline(m.optInt("viewers") > 0);
model.setPreview("https:" + m.getString("thumb_image").replace("{ext}", "jpg")); model.setPreview("https://en.bongacams.com/images/default/thumb_m_female.png");
if (m.has("thumb_image")) {
String thumb = m.optString("thumb_image");
if (StringUtil.isNotBlank(thumb)) {
model.setPreview("https:" + thumb.replace("{ext}", "jpg"));
}
}
if (m.has("display_name")) { if (m.has("display_name")) {
model.setDisplayName(m.getString("display_name")); model.setDisplayName(m.getString("display_name"));
} }

View File

@ -56,7 +56,7 @@ public class ChaturbateElectronLoginDialog {
var url = json.getString("url"); var url = json.getString("url");
if (url.endsWith("/auth/login/")) { if (url.endsWith("/auth/login/")) {
try { try {
Thread.sleep(500); Thread.sleep(2000);
String username = Config.getInstance().getSettings().chaturbateUsername; String username = Config.getInstance().getSettings().chaturbateUsername;
if (username != null && !username.trim().isEmpty()) { if (username != null && !username.trim().isEmpty()) {
browser.executeJavaScript("document.getElementById('id_username').value = '" + username + "'"); browser.executeJavaScript("document.getElementById('id_username').value = '" + username + "'");

View File

@ -20,9 +20,8 @@ public class ChaturbateFollowedTab extends ThumbOverviewTab implements FollowedT
public ChaturbateFollowedTab(String title, String url, Chaturbate chaturbate) { public ChaturbateFollowedTab(String title, String url, Chaturbate chaturbate) {
super(title, new ChaturbateUpdateService(url, true, chaturbate), chaturbate); super(title, new ChaturbateUpdateService(url, true, chaturbate), chaturbate);
onlineUrl = url; onlineUrl = url.replace("offline=true", "offline=false");
offlineUrl = url + "offline/"; offlineUrl = url.replace("offline=false", "offline=true");
status = new Label("Logging in..."); status = new Label("Logging in...");
grid.getChildren().add(status); grid.getChildren().add(status);
} }
@ -41,14 +40,14 @@ public class ChaturbateFollowedTab extends ThumbOverviewTab implements FollowedT
offline.setToggleGroup(group); offline.setToggleGroup(group);
pagination.getChildren().add(online); pagination.getChildren().add(online);
pagination.getChildren().add(offline); pagination.getChildren().add(offline);
HBox.setMargin(online, new Insets(5,5,5,40)); HBox.setMargin(online, new Insets(5, 5, 5, 40));
HBox.setMargin(offline, new Insets(5,5,5,5)); HBox.setMargin(offline, new Insets(5, 5, 5, 5));
online.setSelected(true); online.setSelected(true);
group.selectedToggleProperty().addListener(e -> { group.selectedToggleProperty().addListener(e -> {
if(online.isSelected()) { if (online.isSelected()) {
((ChaturbateUpdateService)updateService).setUrl(onlineUrl); ((ChaturbateUpdateService) updateService).setUrl(onlineUrl);
} else { } else {
((ChaturbateUpdateService)updateService).setUrl(offlineUrl); ((ChaturbateUpdateService) updateService).setUrl(offlineUrl);
} }
queue.clear(); queue.clear();
updateService.restart(); updateService.restart();

View File

@ -12,29 +12,36 @@ import java.util.List;
public class ChaturbateTabProvider extends AbstractTabProvider { public class ChaturbateTabProvider extends AbstractTabProvider {
private final ChaturbateFollowedTab followedTab; private ChaturbateFollowedTab followedTab;
private Chaturbate site;
private String API_URL;
public ChaturbateTabProvider(Chaturbate chaturbate) { public ChaturbateTabProvider(Chaturbate chaturbate) {
super(chaturbate); super(chaturbate);
this.followedTab = new ChaturbateFollowedTab("Followed", chaturbate.getBaseUrl() + "/followed-cams/", chaturbate); this.site = chaturbate;
API_URL = site.getBaseUrl() + "/api/ts";
} }
@Override @Override
protected List<Tab> getSiteTabs(Scene scene) { protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>(); List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Featured", site.getBaseUrl() + "/")); tabs.add(createTab("All", API_URL + "/roomlist/room-list/?enable_recommendations=false"));
tabs.add(createTab("Female", site.getBaseUrl() + "/female-cams/")); tabs.add(createTab("Girls", API_URL + "/roomlist/room-list/?enable_recommendations=false&genders=f"));
tabs.add(createTab("New Female", site.getBaseUrl() + "/new-cams/female/")); tabs.add(createTab("New Girls", API_URL + "/roomlist/room-list/?enable_recommendations=false&genders=f&new_cams=true"));
tabs.add(createTab("Male", site.getBaseUrl() + "/male-cams/")); tabs.add(createTab("Boys", API_URL + "/roomlist/room-list/?enable_recommendations=false&genders=m"));
tabs.add(createTab("Couples", site.getBaseUrl() + "/couple-cams/")); tabs.add(createTab("New Boys", API_URL + "/roomlist/room-list/?enable_recommendations=false&genders=m&new_cams=true"));
tabs.add(createTab("Trans", site.getBaseUrl() + "/trans-cams/")); tabs.add(createTab("Couples", API_URL + "/roomlist/room-list/?enable_recommendations=false&genders=c"));
tabs.add(createTab("Trans", API_URL + "/roomlist/room-list/?enable_recommendations=false&genders=t"));
tabs.add(createTab("Private", API_URL + "/roomlist/room-list/?enable_recommendations=false&private=true"));
tabs.add(createTab("Hidden", API_URL + "/roomlist/room-list/?enable_recommendations=false&hidden=true"));
followedTab = new ChaturbateFollowedTab("Followed", API_URL + "/roomlist/room-list/?enable_recommendations=false&follow=true&offline=false", site);
followedTab.setScene(scene); followedTab.setScene(scene);
followedTab.setRecorder(recorder); followedTab.setRecorder(recorder);
followedTab.setImageAspectRatio(9.0 / 16.0); followedTab.setImageAspectRatio(9.0 / 16.0);
tabs.add(followedTab); tabs.add(followedTab);
tabs.add(createApiTab("Top Rated", site.getBaseUrl() + "/api/ts/discover/carousels/top-rated/")); tabs.add(createApiTab("Top Rated", API_URL + "/discover/carousels/top-rated/"));
tabs.add(createApiTab("Trending", site.getBaseUrl() + "/api/ts/discover/carousels/trending/")); tabs.add(createApiTab("Trending", API_URL + "/ts/discover/carousels/trending/"));
tabs.add(createApiTab("Recommended", site.getBaseUrl() + "/api/ts/discover/carousels/recommended/")); tabs.add(createApiTab("Recommended", API_URL + "/discover/carousels/recommended/"));
return tabs; return tabs;
} }

View File

@ -2,20 +2,24 @@ package ctbrec.ui.sites.chaturbate;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.chaturbate.ChaturbateModelParser;
import ctbrec.ui.SiteUiFactory; import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService; import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.Locale;
import java.util.concurrent.Executors;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
@ -25,19 +29,12 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
private String url; private String url;
private final boolean loginRequired; private final boolean loginRequired;
private final Chaturbate chaturbate; private final Chaturbate chaturbate;
private final int modelsPerPage = 90;
public ChaturbateUpdateService(String url, boolean loginRequired, Chaturbate chaturbate) { public ChaturbateUpdateService(String url, boolean loginRequired, Chaturbate chaturbate) {
this.url = url; this.url = url;
this.loginRequired = loginRequired; this.loginRequired = loginRequired;
this.chaturbate = chaturbate; this.chaturbate = chaturbate;
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
var t = new Thread(r);
t.setDaemon(true);
t.setName("ThumbOverviewTab UpdateService");
return t;
});
setExecutor(executor);
} }
@Override @Override
@ -48,29 +45,73 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
if (loginRequired && !chaturbate.credentialsAvailable()) { if (loginRequired && !chaturbate.credentialsAvailable()) {
return Collections.emptyList(); return Collections.emptyList();
} else { } else {
String pageUrl = ChaturbateUpdateService.this.url + "?page=" + page + "&keywords=&_=" + System.currentTimeMillis(); int offset = (getPage() - 1) * modelsPerPage;
LOG.debug("Fetching page {}", pageUrl); int limit = modelsPerPage;
String paginatedUrl = url + "&offset=" + offset + "&limit=" + limit;
LOG.debug("Fetching page {}", paginatedUrl);
if (loginRequired) { if (loginRequired) {
SiteUiFactory.getUi(chaturbate).login(); SiteUiFactory.getUi(chaturbate).login();
} }
var request = new Request.Builder() Request request = new Request.Builder()
.url(pageUrl) .url(paginatedUrl)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_TEXT_HTML)
.build(); .build();
try (var response = chaturbate.getHttpClient().execute(request)) { try (Response response = chaturbate.getHttpClient().execute(request)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
List<Model> models = ChaturbateModelParser.parseModels(chaturbate, response.body().string()); return parseModels(response.body().string());
} else {
throw new HttpException(response.code(), response.message());
}
} // try
} // if
} // call
};
}
private List<Model> parseModels(String body) {
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(body);
if (json.has("rooms")) {
JSONArray jsonModels = json.getJSONArray("rooms");
for (int i = 0; i < jsonModels.length(); i++) {
var jsonModel = jsonModels.getJSONObject(i);
try {
String name = jsonModel.getString("username");
Model model = chaturbate.createModel(name);
model.setDisplayName(name);
model.setPreview(jsonModel.optString("img"));
if (jsonModel.has("tags")) {
JSONArray tags = jsonModel.getJSONArray("tags");
for (int j = 0; j < tags.length(); j++) {
model.getTags().add(tags.optString(j));
}
}
if (jsonModel.has("subject")) {
String html = jsonModel.optString("subject");
model.setDescription(html2text(html));
}
models.add(model);
} catch (Exception e) {
LOG.warn("Couldn't parse one of the models: {}", jsonModel, e);
}
}
return models; return models;
} else { } else {
int code = response.code(); LOG.debug("Response was not successful: {}", json);
throw new IOException("HTTP status " + code); return Collections.emptyList();
} }
} }
public static String html2text(String html) {
try {
return Jsoup.parse(html).text();
} catch (Exception ex) {
return "";
} }
} }
};
}
public void setUrl(String url) { public void setUrl(String url) {
this.url = url; this.url = url;

View File

@ -25,11 +25,11 @@ public class Flirt4FreeTabProvider extends AbstractTabProvider {
@Override @Override
protected List<Tab> getSiteTabs(Scene scene) { protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>(); List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Girls", site.getBaseUrl() + "/live/girls/", m -> true)); tabs.add(createTab("Girls", site.getBaseUrl() + "/live/girls/?tpl=index2&model=json", m -> true));
tabs.add(createTab("New Girls", site.getBaseUrl() + "/live/girls/", Flirt4FreeModel::isNew)); tabs.add(createTab("New Girls", site.getBaseUrl() + "/live/girls/?tpl=index2&model=json", Flirt4FreeModel::isNew));
tabs.add(createTab("Boys", site.getBaseUrl() + "/live/guys/", m -> true)); tabs.add(createTab("Boys", site.getBaseUrl() + "/live/guys/?tpl=index2&model=json", m -> true));
tabs.add(createTab("Couples", site.getBaseUrl() + "/live/couples/", m -> m.getCategories().contains("2"))); tabs.add(createTab("Couples", site.getBaseUrl() + "/live/couples/?tpl=index2&model=json", m -> m.getCategories().contains("2")));
tabs.add(createTab("Trans", site.getBaseUrl() + "/live/trans/", m -> true)); tabs.add(createTab("Trans", site.getBaseUrl() + "/live/trans/?tpl=index2&model=json", m -> true));
tabs.add(followedTab); tabs.add(followedTab);
return tabs; return tabs;
} }

View File

@ -26,7 +26,7 @@ import static ctbrec.io.HttpConstants.*;
public class Flirt4FreeUpdateService extends PaginatedScheduledService { public class Flirt4FreeUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(Flirt4FreeUpdateService.class); private static final Logger LOG = LoggerFactory.getLogger(Flirt4FreeUpdateService.class);
private static final int MODELS_PER_PAGE = 40; private static final int MODELS_PER_PAGE = 50;
private final String url; private final String url;
private final Flirt4Free flirt4Free; private final Flirt4Free flirt4Free;
private final Predicate<Flirt4FreeModel> filter; private final Predicate<Flirt4FreeModel> filter;

View File

@ -15,22 +15,22 @@ public class LiveJasminTabProvider extends AbstractTabProvider {
private final LiveJasminFollowedTab followedTab; private final LiveJasminFollowedTab followedTab;
public LiveJasminTabProvider(LiveJasmin liveJasmin) { public LiveJasminTabProvider(LiveJasmin site) {
super(liveJasmin); super(site);
followedTab = new LiveJasminFollowedTab(liveJasmin); followedTab = new LiveJasminFollowedTab(site);
followedTab.setRecorder(liveJasmin.getRecorder()); followedTab.setRecorder(recorder);
followedTab.setImageAspectRatio(9.0 / 16.0); followedTab.setImageAspectRatio(9.0 / 16.0);
} }
@Override @Override
protected List<Tab> getSiteTabs(Scene scene) { protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>(); List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Girls", site.getBaseUrl() + "/en/girl/?listPageOrderType=most_popular")); tabs.add(createTab("Girls", site.getBaseUrl() + "/en/girls/?listPageOrderType=most_popular"));
tabs.add(createTab("Girls HD", site.getBaseUrl() + "/en/girl/hd/?listPageOrderType=most_popular")); tabs.add(createTab("New Girls", site.getBaseUrl() + "/en/girls/new-models/?listPageOrderType=most_popular"));
tabs.add(createTab("New Girls", site.getBaseUrl() + "/en/girls/newbie/?listPageOrderType=most_popular")); tabs.add(createTab("Boys", site.getBaseUrl() + "/en/boys/?listPageOrderType=most_popular"));
tabs.add(createTab("New Boys", site.getBaseUrl() + "/en/boys/new-models/?listPageOrderType=most_popular"));
tabs.add(createTab("Couples", site.getBaseUrl() + "/en/girls/couple/?listPageOrderType=most_popular")); tabs.add(createTab("Couples", site.getBaseUrl() + "/en/girls/couple/?listPageOrderType=most_popular"));
tabs.add(createTab("Boys", site.getBaseUrl() + "/en/boy/?listPageOrderType=most_popular")); tabs.add(createTab("Trans", site.getBaseUrl() + "/en/boys/transboy/?listPageOrderType=most_popular"));
tabs.add(createTab("Boys HD", site.getBaseUrl() + "/en/boy/hd/?listPageOrderType=most_popular"));
tabs.add(followedTab); tabs.add(followedTab);
return tabs; return tabs;
} }
@ -43,8 +43,8 @@ public class LiveJasminTabProvider extends AbstractTabProvider {
private ThumbOverviewTab createTab(String title, String url) { private ThumbOverviewTab createTab(String title, String url) {
var s = new LiveJasminUpdateService((LiveJasmin) site, url); var s = new LiveJasminUpdateService((LiveJasmin) site, url);
s.setPeriod(Duration.seconds(60)); s.setPeriod(Duration.seconds(60));
ThumbOverviewTab tab = new LiveJasminTab(title, s, site); ThumbOverviewTab tab = new ThumbOverviewTab(title, s, site);
tab.setRecorder(site.getRecorder()); tab.setRecorder(recorder);
tab.setImageAspectRatio(9.0 / 16.0); tab.setImageAspectRatio(9.0 / 16.0);
return tab; return tab;
} }

View File

@ -1,17 +1,5 @@
package ctbrec.ui.sites.jasmin; package ctbrec.ui.sites.jasmin;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
@ -23,16 +11,40 @@ import javafx.concurrent.Task;
import okhttp3.Cookie; import okhttp3.Cookie;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.Request; import okhttp3.Request;
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.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.*;
public class LiveJasminUpdateService extends PaginatedScheduledService { public class LiveJasminUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminUpdateService.class); private static final Logger LOG = LoggerFactory.getLogger(LiveJasminUpdateService.class);
private String url; private String url;
private String listPageId = "";
private LiveJasmin liveJasmin; private LiveJasmin liveJasmin;
private List<Model> modelsList;
private int modelsPerPage = 60;
private int lastPageLoaded = 0;
private transient Instant lastListInfoRequest = Instant.EPOCH;
public LiveJasminUpdateService(LiveJasmin liveJasmin, String url) { public LiveJasminUpdateService(LiveJasmin liveJasmin, String url) {
this.liveJasmin = liveJasmin; this.liveJasmin = liveJasmin;
this.url = url; this.url = url;
this.lastPageLoaded = 0;
} }
@Override @Override
@ -40,18 +52,48 @@ public class LiveJasminUpdateService extends PaginatedScheduledService {
return new Task<List<Model>>() { return new Task<List<Model>>() {
@Override @Override
public List<Model> call() throws IOException { public List<Model> call() throws IOException {
// sort by popularity return getModelList().stream()
var cookieJar = liveJasmin.getHttpClient().getCookieJar(); .skip((page - 1) * (long) modelsPerPage)
var sortCookie = new Cookie.Builder() .limit(modelsPerPage)
.domain(LiveJasmin.baseDomain) .collect(Collectors.toList()); // NOSONAR
.name("listPageOrderType") }
.value("most_popular") };
.build(); }
cookieJar.saveFromResponse(HttpUrl.parse("https://" + LiveJasmin.baseDomain), Collections.singletonList(sortCookie));
private List<Model> getModelList() throws IOException {
page = Math.min(page, 99);
if ((lastPageLoaded > 0) && Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 60) {
while (page > lastPageLoaded) {
lastPageLoaded++;
modelsList.addAll(loadMore());
}
return modelsList;
}
lastPageLoaded = 1;
modelsList = loadModelList();
while (page > lastPageLoaded) {
lastPageLoaded++;
modelsList.addAll(loadMore());
}
if (modelsList == null) {
return Collections.emptyList();
}
return modelsList;
}
private List<Model> loadModelList() throws IOException {
lastListInfoRequest = Instant.now();
var cookieJar = liveJasmin.getHttpClient().getCookieJar();
var sortCookie = new Cookie.Builder().domain(LiveJasmin.baseDomain).name("listPageOrderType").value("most_popular").build();
cookieJar.saveFromResponse(HttpUrl.parse(liveJasmin.getBaseUrl()), Collections.singletonList(sortCookie));
String category = (url.indexOf("boys") > -1) ? "boys" : "girls";
var categoryCookie = new Cookie.Builder().domain(LiveJasmin.baseDomain).name("category").value(category).build();
cookieJar.saveFromResponse(HttpUrl.parse(liveJasmin.getBaseUrl()), Collections.singletonList(categoryCookie));
// TODO find out how to switch pages
LOG.debug("Fetching page {}", url); LOG.debug("Fetching page {}", url);
var request = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) .addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON)
@ -59,18 +101,18 @@ public class LiveJasminUpdateService extends PaginatedScheduledService {
.addHeader(REFERER, liveJasmin.getBaseUrl()) .addHeader(REFERER, liveJasmin.getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build(); .build();
try (var response = liveJasmin.getHttpClient().execute(request)) { try (Response response = liveJasmin.getHttpClient().execute(req)) {
LOG.debug("Response {} {}", response.code(), response.message()); LOG.debug("Response {} {}", response.code(), response.message());
if (response.isSuccessful()) { if (response.isSuccessful()) {
var body = response.body().string(); String body = response.body().string();
List<Model> models = new ArrayList<>(); List<Model> models = new ArrayList<>();
var json = new JSONObject(body); JSONObject json = new JSONObject(body);
if(json.optBoolean("success")) { if (json.optBoolean("success")) {
parseModels(models, json); parseModels(models, json);
} else if(json.optString("error").equals("Please login.")) { } else if (json.optString("error").equals("Please login.")) {
var siteUI = SiteUiFactory.getUi(liveJasmin); var siteUI = SiteUiFactory.getUi(liveJasmin);
if(siteUI.login()) { if (siteUI.login()) {
return call(); return loadModelList();
} else { } else {
LOG.error("Request failed:\n{}", body); LOG.error("Request failed:\n{}", body);
throw new IOException("Response was not successful"); throw new IOException("Response was not successful");
@ -85,24 +127,61 @@ public class LiveJasminUpdateService extends PaginatedScheduledService {
} }
} }
} }
};
private List<Model> loadMore() throws IOException {
lastListInfoRequest = Instant.now();
String moreURL = liveJasmin.getBaseUrl() + MessageFormat.format("/en/list-page-ajax/show-more-json/{0}?wide=true&layout=layout-big&_dc={1}", listPageId, String.valueOf(System.currentTimeMillis()));
LOG.debug("Fetching page {}", moreURL);
Request req = new Request.Builder()
.url(moreURL)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON)
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, liveJasmin.getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = liveJasmin.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(body);
if (json.optBoolean("success")) {
parseModels(models, json);
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
} }
private void parseModels(List<Model> models, JSONObject json) { private void parseModels(List<Model> models, JSONObject json) {
var data = json.getJSONObject("data"); if (json.has("data")) {
var content = data.getJSONObject("content"); JSONObject data = json.getJSONObject("data");
var performers = content.getJSONArray("performers"); if (data.optInt("isLast") > 0) {
lastPageLoaded = 999;
}
if (data.has("content")) {
JSONObject content = data.getJSONObject("content");
if (content.optInt("isLastPage") > 0) {
lastPageLoaded = 999;
}
listPageId = content.optString("listPageId");
JSONArray performers = content.getJSONArray("performers");
for (var i = 0; i < performers.length(); i++) { for (var i = 0; i < performers.length(); i++) {
var m = performers.getJSONObject(i); var m = performers.getJSONObject(i);
var name = m.optString("pid"); var name = m.optString("pid");
if(name.isEmpty()) { if (name.isEmpty()) {
continue; continue;
} }
LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name); LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name);
model.setId(m.getString("id")); model.setId(m.getString("id"));
model.setPreview(m.getString("profilePictureUrl")); model.setPreview(m.optString("profilePictureUrl"));
model.setOnlineState(LiveJasminModel.mapStatus(m.optInt("status"))); model.setOnlineState(LiveJasminModel.mapStatus(m.optInt("status")));
model.setDisplayName(m.optString("display_name", null));
models.add(model); models.add(model);
} }
} // if content
} // if data
} }
} }

View File

@ -6,42 +6,67 @@ import ctbrec.sites.manyvids.MVLive;
import ctbrec.sites.manyvids.MVLiveModel; import ctbrec.sites.manyvids.MVLiveModel;
import ctbrec.ui.tabs.PaginatedScheduledService; import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
@Slf4j
@RequiredArgsConstructor
public class MVLiveUpdateService extends PaginatedScheduledService { public class MVLiveUpdateService extends PaginatedScheduledService {
private final MVLive mvlive; private final MVLive mvlive;
private final String url; private final String url;
private final int modelsPerPage = 48;
private static List<Model> modelsList;
private static Instant lastListInfoRequest = Instant.EPOCH;
private static final Logger LOG = LoggerFactory.getLogger(MVLiveUpdateService.class);
public MVLiveUpdateService(MVLive site, String url) {
this.mvlive = site;
this.url = url;
}
@Override @Override
protected Task<List<Model>> createTask() { protected Task<List<Model>> createTask() {
return new Task<>() { return new Task<List<Model>>() {
@Override @Override
public List<Model> call() throws IOException { public List<Model> call() throws IOException {
List<Model> models = loadModels(url); return getModelList().stream()
return models; .skip((page - 1) * (long) modelsPerPage)
.limit(modelsPerPage)
.collect(Collectors.toList()); // NOSONAR
} }
}; };
} }
private List<Model> getModelList() throws IOException {
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
return modelsList;
}
lastListInfoRequest = Instant.now();
modelsList = loadModels(url);
if (modelsList == null) {
return Collections.emptyList();
}
return modelsList;
}
protected List<Model> loadModels(String url) throws IOException { protected List<Model> loadModels(String url) throws IOException {
List<Model> models = new ArrayList<>(); List<Model> models = new ArrayList<>();
log.debug("Loading live models from {}", url); LOG.debug("Loading live models from {}", url);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
@ -52,11 +77,11 @@ public class MVLiveUpdateService extends PaginatedScheduledService {
.build(); .build();
try (Response response = mvlive.getHttpClient().execute(req)) { try (Response response = mvlive.getHttpClient().execute(req)) {
String body = response.body().string(); String body = response.body().string();
log.trace("response body: {}", body); LOG.trace("response body: {}", body);
if (response.isSuccessful()) { if (response.isSuccessful()) {
JSONObject json = new JSONObject(body); JSONObject json = new JSONObject(body);
if (!json.has("live_creators")) { if (!json.has("live_creators")) {
log.debug("Unexpected response:\n{}", json.toString(2)); LOG.debug("Unexpected response:\n{}", json.toString(2));
return Collections.emptyList(); return Collections.emptyList();
} }
JSONArray creators = json.getJSONArray("live_creators"); JSONArray creators = json.getJSONArray("live_creators");

View File

@ -9,7 +9,9 @@ import ctbrec.sites.secretfriends.SecretFriendsModelParser;
import ctbrec.ui.SiteUiFactory; import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService; import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import okhttp3.HttpUrl;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -43,19 +45,23 @@ public class SecretFriendsUpdateService extends PaginatedScheduledService {
if (loginRequired && !site.credentialsAvailable()) { if (loginRequired && !site.credentialsAvailable()) {
return Collections.emptyList(); return Collections.emptyList();
} else { } else {
String paginatedUrl = url + (url.indexOf('?') >= 0 ? '&' : '?') + "Friend_page=" + page; String paginatedUrl = url;
if (page > 1) {
String pager = (url.indexOf("/users") > 0) ? "Friend_page" : "AModel_page";
paginatedUrl = HttpUrl.parse(url).newBuilder().addQueryParameter(pager, String.valueOf(page)).build().toString();
}
LOG.debug("Fetching page {}", paginatedUrl); LOG.debug("Fetching page {}", paginatedUrl);
if (loginRequired) { if (loginRequired) {
SiteUiFactory.getUi(site).login(); SiteUiFactory.getUi(site).login();
} }
var request = new Request.Builder() Request request = new Request.Builder()
.url(paginatedUrl) .url(paginatedUrl)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, SecretFriends.BASE_URI)
.build(); .build();
try (var response = site.getHttpClient().execute(request)) { try (Response response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
return parseModels(Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string()); return parseModels(Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string());
} else { } else {

View File

@ -1,18 +1,21 @@
package ctbrec.ui.sites.showup; package ctbrec.ui.sites.showup;
import java.io.IOException;
import java.util.List;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.sites.showup.Showup; import ctbrec.sites.showup.Showup;
import ctbrec.sites.showup.ShowupHttpClient; import ctbrec.sites.showup.ShowupHttpClient;
import ctbrec.ui.tabs.PaginatedScheduledService; import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class ShowupUpdateService extends PaginatedScheduledService { public class ShowupUpdateService extends PaginatedScheduledService {
private final Showup showup; private final Showup showup;
private final String category; private final String category;
protected int modelsPerPage = 48;
public ShowupUpdateService(Showup showup, String category) { public ShowupUpdateService(Showup showup, String category) {
this.showup = showup; this.showup = showup;
@ -24,11 +27,22 @@ public class ShowupUpdateService extends PaginatedScheduledService {
return new Task<>() { return new Task<>() {
@Override @Override
public List<Model> call() throws IOException { public List<Model> call() throws IOException {
ShowupHttpClient httpClient = (ShowupHttpClient) showup.getHttpClient(); return getModelList().stream()
httpClient.setCookie("category", category); .skip((page - 1) * (long) modelsPerPage)
return showup.getModelList(true); .limit(modelsPerPage)
.collect(Collectors.toList()); // NOSONAR
} }
}; };
} }
private List<Model> getModelList() throws IOException {
ShowupHttpClient httpClient = (ShowupHttpClient) showup.getHttpClient();
httpClient.setCookie("category", category);
var modelsList = showup.getModelList(true);
if (modelsList == null) {
return Collections.emptyList();
}
return modelsList;
}
} }

View File

@ -7,19 +7,13 @@ import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI; import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
public class StripchatConfigUI extends AbstractConfigUI { public class StripchatConfigUI extends AbstractConfigUI {
private Stripchat stripchat; private final Stripchat stripchat;
public StripchatConfigUI(Stripchat stripchat) { public StripchatConfigUI(Stripchat stripchat) {
this.stripchat = stripchat; this.stripchat = stripchat;
@ -36,7 +30,7 @@ public class StripchatConfigUI extends AbstractConfigUI {
var enabled = new CheckBox(); var enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(stripchat.getName())); enabled.setSelected(!settings.disabledSites.contains(stripchat.getName()));
enabled.setOnAction(e -> { enabled.setOnAction(e -> {
if(enabled.isSelected()) { if (enabled.isSelected()) {
settings.disabledSites.remove(stripchat.getName()); settings.disabledSites.remove(stripchat.getName());
} else { } else {
settings.disabledSites.add(stripchat.getName()); settings.disabledSites.add(stripchat.getName());
@ -69,7 +63,7 @@ public class StripchatConfigUI extends AbstractConfigUI {
layout.add(new Label("Stripchat User"), 0, row); layout.add(new Label("Stripchat User"), 0, row);
var username = new TextField(Config.getInstance().getSettings().stripchatUsername); var username = new TextField(Config.getInstance().getSettings().stripchatUsername);
username.textProperty().addListener((ob, o, n) -> { username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().stripchatUsername)) { if (!n.equals(Config.getInstance().getSettings().stripchatUsername)) {
Config.getInstance().getSettings().stripchatUsername = username.getText(); Config.getInstance().getSettings().stripchatUsername = username.getText();
stripchat.getHttpClient().logout(); stripchat.getHttpClient().logout();
save(); save();
@ -84,7 +78,7 @@ public class StripchatConfigUI extends AbstractConfigUI {
var password = new PasswordField(); var password = new PasswordField();
password.setText(Config.getInstance().getSettings().stripchatPassword); password.setText(Config.getInstance().getSettings().stripchatPassword);
password.textProperty().addListener((ob, o, n) -> { password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().stripchatPassword)) { if (!n.equals(Config.getInstance().getSettings().stripchatPassword)) {
Config.getInstance().getSettings().stripchatPassword = password.getText(); Config.getInstance().getSettings().stripchatPassword = password.getText();
stripchat.getHttpClient().logout(); stripchat.getHttpClient().logout();
save(); save();
@ -102,9 +96,21 @@ public class StripchatConfigUI extends AbstractConfigUI {
var deleteCookies = new Button("Delete Cookies"); var deleteCookies = new Button("Delete Cookies");
deleteCookies.setOnAction(e -> stripchat.getHttpClient().clearCookies()); deleteCookies.setOnAction(e -> stripchat.getHttpClient().clearCookies());
layout.add(deleteCookies, 1, row); layout.add(deleteCookies, 1, row++);
GridPane.setColumnSpan(deleteCookies, 2); GridPane.setColumnSpan(deleteCookies, 2);
row++;
l = new Label("Get VR stream if available");
layout.add(l, 0, row);
var vr = new CheckBox();
vr.setSelected(settings.stripchatVR);
vr.setOnAction(e -> {
settings.stripchatVR = vr.isSelected();
save();
});
GridPane.setMargin(vr, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(vr, 1, row);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));

View File

@ -14,6 +14,7 @@ import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -22,7 +23,7 @@ import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
public class StripchatFollowedUpdateService extends AbstractStripchatUpdateService { public class StripchatFollowedUpdateService extends AbstractStripchatUpdateService {
private static final int PAGE_SIZE = 30; private static final int PAGE_SIZE = 48;
private static final String FAVORITES = "/favorites"; private static final String FAVORITES = "/favorites";
private final Stripchat stripchat; private final Stripchat stripchat;
@ -81,6 +82,7 @@ public class StripchatFollowedUpdateService extends AbstractStripchatUpdateServi
model.setDescription(user.optString("description")); model.setDescription(user.optString("description"));
model.setPreview(getPreviewUrl(user)); model.setPreview(getPreviewUrl(user));
model.setOnlineState(mapStatus(user.optString("status"))); model.setOnlineState(mapStatus(user.optString("status")));
model.setLastSeen(Instant.now());
models.add(model); models.add(model);
} }
} }

View File

@ -13,20 +13,24 @@ import java.util.List;
public class StripchatTabProvider extends AbstractTabProvider { public class StripchatTabProvider extends AbstractTabProvider {
private final String urlTemplate; private final String urlTemplate;
private final String urlFilterTemplate;
private final StripchatFollowedTab followedTab; private final StripchatFollowedTab followedTab;
public StripchatTabProvider(Stripchat stripchat) { public StripchatTabProvider(Stripchat stripchat) {
super(stripchat); super(stripchat);
followedTab = new StripchatFollowedTab("Followed", stripchat); followedTab = new StripchatFollowedTab("Followed", stripchat);
urlTemplate = stripchat.getBaseUrl() + "/api/front/models?primaryTag={0}&sortBy=viewersRating&withMixedTags=true&parentTag="; urlTemplate = stripchat.getBaseUrl() + "/api/front/models?primaryTag={0}&sortBy=viewersRating&withMixedTags=true&parentTag=";
urlFilterTemplate = stripchat.getBaseUrl() + "/api/front/models?primaryTag=girls&filterGroupTags=%5B%5B%22{0}%22%5D%5D&parentTag={0}";
} }
@Override @Override
protected List<Tab> getSiteTabs(Scene scene) { protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>(); List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Girls", MessageFormat.format(urlTemplate, "girls"))); tabs.add(createTab("Girls", MessageFormat.format(urlTemplate, "girls")));
tabs.add(createTab("Girls HD", site.getBaseUrl() + "/api/front/models?primaryTag=girls&filterGroupTags=%5B%5B%22autoTagHd%22%5D%5D&parentTag=autoTagHd")); tabs.add(createTab("Girls New", MessageFormat.format(urlFilterTemplate, "autoTagNew")));
tabs.add(createTab("New Girls", site.getBaseUrl() + "/api/front/models?primaryTag=girls&filterGroupTags=%5B%5B%22autoTagNew%22%5D%5D&parentTag=autoTagNew")); tabs.add(createTab("Girls HD", MessageFormat.format(urlFilterTemplate, "autoTagHd")));
tabs.add(createTab("Girls VR", MessageFormat.format(urlFilterTemplate, "autoTagVr")));
tabs.add(createTab("Mobile", MessageFormat.format(urlFilterTemplate, "mobile")));
tabs.add(createTab("Couples", MessageFormat.format(urlTemplate, "couples"))); tabs.add(createTab("Couples", MessageFormat.format(urlTemplate, "couples")));
tabs.add(createTab("Boys", MessageFormat.format(urlTemplate, "men"))); tabs.add(createTab("Boys", MessageFormat.format(urlTemplate, "men")));
tabs.add(createTab("Trans", MessageFormat.format(urlTemplate, "trans"))); tabs.add(createTab("Trans", MessageFormat.format(urlTemplate, "trans")));

View File

@ -27,7 +27,7 @@ public class StripchatUpdateService extends AbstractStripchatUpdateService {
private final String url; private final String url;
private final boolean loginRequired; private final boolean loginRequired;
private final Stripchat stripchat; private final Stripchat stripchat;
int modelsPerPage = 60; int modelsPerPage = 48;
public StripchatUpdateService(String url, boolean loginRequired, Stripchat stripchat) { public StripchatUpdateService(String url, boolean loginRequired, Stripchat stripchat) {
this.url = url; this.url = url;

View File

@ -17,6 +17,7 @@ public class XloveCamTabProvider extends AbstractTabProvider {
private final XloveCam xloveCam; private final XloveCam xloveCam;
private static final String FILTER_PARAM = "config[filter][10][]"; private static final String FILTER_PARAM = "config[filter][10][]";
private static final String FILTER_PARAM_NEW = "config[filter][100522][]";
public XloveCamTabProvider(XloveCam xloveCam) { public XloveCamTabProvider(XloveCam xloveCam) {
super(xloveCam); super(xloveCam);
@ -31,6 +32,10 @@ public class XloveCamTabProvider extends AbstractTabProvider {
var updateService = new XloveCamUpdateService(xloveCam, Collections.emptyMap()); var updateService = new XloveCamUpdateService(xloveCam, Collections.emptyMap());
tabs.add(createTab("All", updateService)); tabs.add(createTab("All", updateService));
// new
updateService = new XloveCamUpdateService(xloveCam, Map.of(FILTER_PARAM_NEW, "3"));
tabs.add(createTab("New", updateService));
// Young Women // Young Women
updateService = new XloveCamUpdateService(xloveCam, Map.of(FILTER_PARAM, "1")); updateService = new XloveCamUpdateService(xloveCam, Map.of(FILTER_PARAM, "1"));
tabs.add(createTab("Young Women", updateService)); tabs.add(createTab("Young Women", updateService));

View File

@ -149,6 +149,15 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
resolution.setPrefWidth(100); resolution.setPrefWidth(100);
resolution.setCellValueFactory(cdf -> new SimpleIntegerProperty(cdf.getValue().getSelectedResolution())); resolution.setCellValueFactory(cdf -> new SimpleIntegerProperty(cdf.getValue().getSelectedResolution()));
resolution.setCellFactory(tc -> createResolutionCell()); resolution.setCellFactory(tc -> createResolutionCell());
TableColumn<JavaFxRecording, String> siteName = new TableColumn<>("Site");
siteName.setId("siteName");
siteName.setPrefWidth(200);
siteName.setCellValueFactory(cdf -> {
var sname = cdf.getValue().getModel().getSite().getName();
return new SimpleObjectProperty<>(sname);
});
TableColumn<JavaFxRecording, String> notes = new TableColumn<>("Notes"); TableColumn<JavaFxRecording, String> notes = new TableColumn<>("Notes");
notes.setId("notes"); notes.setId("notes");
notes.setPrefWidth(400); notes.setPrefWidth(400);
@ -158,7 +167,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
modelNotes.setPrefWidth(400); modelNotes.setPrefWidth(400);
modelNotes.setCellValueFactory(cdf -> new SimpleStringProperty(config.getModelNotes(cdf.getValue().getModel()))); modelNotes.setCellValueFactory(cdf -> new SimpleStringProperty(config.getModelNotes(cdf.getValue().getModel())));
table.getColumns().addAll(name, date, status, progress, size, resolution, notes, modelNotes); table.getColumns().addAll(siteName, name, date, status, progress, size, resolution, notes, modelNotes);
table.setItems(observableRecordings); table.setItems(observableRecordings);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::onContextMenuRequested); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::onContextMenuRequested);
table.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed); table.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
@ -261,7 +270,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
Recording recording = table.getSelectionModel().getSelectedItem(); Recording recording = table.getSelectionModel().getSelectedItem();
if (recording != null) { if (recording != null) {
var state = recording.getStatus(); var state = recording.getStatus();
if(state == FINISHED || state == RECORDING) { if (state == FINISHED || state == RECORDING) {
play(recording); play(recording);
} }
} }

View File

@ -51,8 +51,6 @@
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/> <logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/> <logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
<logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/> <logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/>
<!-- <logger name="ctbrec.sites.cam4.Cam4Model" level="DEBUG"/> -->
<!-- <logger name="ctbrec.sites.showup.Showup" level="TRACE"/> -->
<logger name="ctbrec.ui.ExternalBrowser" level="DEBUG"/> <logger name="ctbrec.ui.ExternalBrowser" level="DEBUG"/>
<logger name="ctbrec.ui.ThumbOverviewTab" level="DEBUG"/> <logger name="ctbrec.ui.ThumbOverviewTab" level="DEBUG"/>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>

View File

@ -71,11 +71,18 @@ public class Config {
if (src.exists()) { if (src.exists()) {
File target = new File(src.getParentFile(), src.getName() + "_backup_" + dateTimeFormatter.format(LocalDateTime.now())); File target = new File(src.getParentFile(), src.getName() + "_backup_" + dateTimeFormatter.format(LocalDateTime.now()));
LOG.info("Creating a backup of {} the config in {}", src, target); LOG.info("Creating a backup of {} the config in {}", src, target);
FileUtils.copyDirectory(src, target, pathname -> !(pathname.toString().contains("minimal-browser") && pathname.toString().contains("Cache")), true); FileUtils.copyDirectory(src, target, pathname -> includeDir(pathname), true);
deleteOldBackups(currentConfigDir); deleteOldBackups(currentConfigDir);
} }
} }
private boolean includeDir(File pathname) {
String name = pathname.getName();
if (name.contains("minimal-browser") && name.contains("Cache")) return false;
if (name.contains("cache")) return false;
return true;
}
private void deleteOldBackups(File currentConfigDir) { private void deleteOldBackups(File currentConfigDir) {
File parent = currentConfigDir.getParentFile(); File parent = currentConfigDir.getParentFile();
File[] backupDirectories = parent.listFiles(file -> file.isDirectory() && file.getName().matches(".*?_backup_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}_\\d{3}")); File[] backupDirectories = parent.listFiles(file -> file.isDirectory() && file.getName().matches(".*?_backup_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}_\\d{3}"));

View File

@ -41,6 +41,7 @@ public class Recording implements Serializable, Callable<Recording> {
private File absoluteFile = null; private File absoluteFile = null;
private File postProcessedFile = null; private File postProcessedFile = null;
private int selectedResolution = -1; private int selectedResolution = -1;
private long lastSizeUpdate = 0;
/** /**
* Signals, if the recording has been changed and it has to be refreshed * Signals, if the recording has been changed and it has to be refreshed
@ -291,11 +292,15 @@ public class Recording implements Serializable, Callable<Recording> {
} }
public void refresh() { public void refresh() {
long now = System.currentTimeMillis();
if (now - lastSizeUpdate > 2500) {
if ((status != FINISHED && status != FAILED) || dirtyFlag) { if ((status != FINISHED && status != FAILED) || dirtyFlag) {
sizeInByte = getSize(); sizeInByte = getSize();
lastSizeUpdate = now;
dirtyFlag = false; dirtyFlag = false;
} }
} }
}
public boolean canBePostProcessed() { public boolean canBePostProcessed() {
return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED; return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED;

View File

@ -12,6 +12,7 @@ public class Settings {
public enum DirectoryStructure { public enum DirectoryStructure {
FLAT("all recordings in one directory"), FLAT("all recordings in one directory"),
ONE_PER_MODEL("one directory for each model"), ONE_PER_MODEL("one directory for each model"),
ONE_PER_GROUP("one directory for each group"),
ONE_PER_RECORDING("one directory for each recording"); ONE_PER_RECORDING("one directory for each recording");
private final String description; private final String description;
@ -102,6 +103,9 @@ public class Settings {
public int maximumResolutionPlayer = 0; public int maximumResolutionPlayer = 0;
public String mediaPlayer = "/usr/bin/mpv"; public String mediaPlayer = "/usr/bin/mpv";
public String mediaPlayerParams = ""; public String mediaPlayerParams = "";
public String browserOverride = "";
public String browserParams = "";
public boolean forceBrowserOverride = false;
public String mfcBaseUrl = "https://www.myfreecams.com"; public String mfcBaseUrl = "https://www.myfreecams.com";
public List<String> mfcDisabledModelsTableColumns = new ArrayList<>(); public List<String> mfcDisabledModelsTableColumns = new ArrayList<>();
public String[] mfcModelsTableColumnIds = new String[0]; public String[] mfcModelsTableColumnIds = new String[0];

View File

@ -5,10 +5,9 @@ import com.squareup.moshi.Moshi;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.LoggingInterceptor; import ctbrec.LoggingInterceptor;
import ctbrec.Settings.ProxyType; import ctbrec.Settings.ProxyType;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import okhttp3.OkHttpClient.Builder; import okhttp3.OkHttpClient.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*; import javax.net.ssl.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -17,12 +16,12 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.Authenticator; import java.net.Authenticator;
import java.net.PasswordAuthentication; import java.net.PasswordAuthentication;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.text.NumberFormat;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -33,12 +32,12 @@ import static ctbrec.io.HttpConstants.ACCEPT_ENCODING_GZIP;
import static ctbrec.io.HttpConstants.CONTENT_ENCODING; import static ctbrec.io.HttpConstants.CONTENT_ENCODING;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public abstract class HttpClient { public abstract class HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES); private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES);
protected OkHttpClient client; protected OkHttpClient client;
protected Cache cache;
protected CookieJarImpl cookieJar; protected CookieJarImpl cookieJar;
protected Config config; protected Config config;
protected boolean loggedIn = false; protected boolean loggedIn = false;
@ -64,7 +63,7 @@ public abstract class HttpClient {
System.setProperty("http.proxyPort", config.getSettings().proxyPort); System.setProperty("http.proxyPort", config.getSettings().proxyPort);
System.setProperty("https.proxyHost", config.getSettings().proxyHost); System.setProperty("https.proxyHost", config.getSettings().proxyHost);
System.setProperty("https.proxyPort", config.getSettings().proxyPort); System.setProperty("https.proxyPort", config.getSettings().proxyPort);
if(config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) { if (config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
String username = config.getSettings().proxyUser; String username = config.getSettings().proxyUser;
String password = config.getSettings().proxyPassword; String password = config.getSettings().proxyPassword;
System.setProperty("http.proxyUser", username); System.setProperty("http.proxyUser", username);
@ -80,7 +79,7 @@ public abstract class HttpClient {
System.setProperty("socksProxyVersion", "5"); System.setProperty("socksProxyVersion", "5");
System.setProperty("socksProxyHost", config.getSettings().proxyHost); System.setProperty("socksProxyHost", config.getSettings().proxyHost);
System.setProperty("socksProxyPort", config.getSettings().proxyPort); System.setProperty("socksProxyPort", config.getSettings().proxyPort);
if(config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) { if (config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
String username = config.getSettings().proxyUser; String username = config.getSettings().proxyUser;
String password = config.getSettings().proxyPassword; String password = config.getSettings().proxyPassword;
Authenticator.setDefault(new SocksProxyAuth(username, password)); Authenticator.setDefault(new SocksProxyAuth(username, password));
@ -104,11 +103,13 @@ public abstract class HttpClient {
} }
public Response execute(Request req) throws IOException { public Response execute(Request req) throws IOException {
log.trace("Cache hit ratio {}/{} = {}", cache.hitCount(), cache.requestCount(), NumberFormat.getPercentInstance().format(cache.hitCount() / (double) cache.requestCount()));
Response resp = client.newCall(req).execute(); Response resp = client.newCall(req).execute();
return resp; return resp;
} }
public Response execute(Request request, int timeoutInMillis) throws IOException { public Response execute(Request request, int timeoutInMillis) throws IOException {
log.trace("Cache hit ratio {}/{} = {}", cache.hitCount(), cache.requestCount(), NumberFormat.getPercentInstance().format(cache.hitCount() / (double) cache.requestCount()));
return client.newBuilder() // return client.newBuilder() //
.connectTimeout(timeoutInMillis, TimeUnit.MILLISECONDS) // .connectTimeout(timeoutInMillis, TimeUnit.MILLISECONDS) //
.readTimeout(timeoutInMillis, TimeUnit.MILLISECONDS).build() // .readTimeout(timeoutInMillis, TimeUnit.MILLISECONDS).build() //
@ -120,9 +121,14 @@ public abstract class HttpClient {
public void reconfigure() { public void reconfigure() {
loadProxySettings(); loadProxySettings();
loadCookies(); loadCookies();
long cacheSize = (long) config.getSettings().thumbCacheSize * 1024 * 1024;
File configDir = config.getConfigDir();
File cacheDir = new File(configDir, "cache");
cache = new Cache(cacheDir, cacheSize);
Builder builder = new OkHttpClient.Builder() Builder builder = new OkHttpClient.Builder()
.cookieJar(cookieJar) .cookieJar(cookieJar)
.connectionPool(GLOBAL_HTTP_CONN_POOL) .connectionPool(GLOBAL_HTTP_CONN_POOL)
.cache(cache)
.connectTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS) .connectTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS)
.readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS) .readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS)
.addNetworkInterceptor(new LoggingInterceptor()); .addNetworkInterceptor(new LoggingInterceptor());
@ -156,12 +162,16 @@ public abstract class HttpClient {
X509Certificate[] x509Certificates = new X509Certificate[0]; X509Certificate[] x509Certificates = new X509Certificate[0];
return x509Certificates; return x509Certificates;
} }
@Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ }
@Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ } @Override
public void checkServerTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ }
@Override
public void checkClientTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ }
}; };
try { try {
final TrustManager[] trustManagers = new TrustManager[] { x509TrustManager }; final TrustManager[] trustManagers = new TrustManager[]{x509TrustManager};
final String PROTOCOL = "TLSv1.2"; final String PROTOCOL = "TLSv1.2";
SSLContext sslContext = SSLContext.getInstance(PROTOCOL); SSLContext sslContext = SSLContext.getInstance(PROTOCOL);
KeyManager[] keyManagers = null; KeyManager[] keyManagers = null;
@ -171,7 +181,7 @@ public abstract class HttpClient {
builder.sslSocketFactory(sslSocketFactory, x509TrustManager); builder.sslSocketFactory(sslSocketFactory, x509TrustManager);
builder.hostnameVerifier((hostname, sslSession) -> true); builder.hostnameVerifier((hostname, sslSession) -> true);
} catch (KeyManagementException | NoSuchAlgorithmException e) { } catch (KeyManagementException | NoSuchAlgorithmException e) {
LOG.error("Couldn't install trust managers for TLS connections"); log.error("Couldn't install trust managers for TLS connections");
} }
} }
@ -192,18 +202,18 @@ public abstract class HttpClient {
String json = adapter.toJson(cookies); String json = adapter.toJson(cookies);
File cookieFile = new File(config.getConfigDir(), "cookies-" + name + ".json"); File cookieFile = new File(config.getConfigDir(), "cookies-" + name + ".json");
try(FileOutputStream fout = new FileOutputStream(cookieFile)) { try (FileOutputStream fout = new FileOutputStream(cookieFile)) {
fout.write(json.getBytes(UTF_8)); fout.write(json.getBytes(UTF_8));
} }
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't persist cookies for {}", name, e); log.error("Couldn't persist cookies for {}", name, e);
} }
} }
private void loadCookies() { private void loadCookies() {
try { try {
File cookieFile = new File(config.getConfigDir(), "cookies-" + name + ".json"); File cookieFile = new File(config.getConfigDir(), "cookies-" + name + ".json");
if(!cookieFile.exists()) { if (!cookieFile.exists()) {
return; return;
} }
byte[] jsonBytes = Files.readAllBytes(cookieFile.toPath()); byte[] jsonBytes = Files.readAllBytes(cookieFile.toPath());
@ -224,7 +234,7 @@ public abstract class HttpClient {
} }
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't load cookies for {}", name, e); log.error("Couldn't load cookies for {}", name, e);
} }
} }
@ -310,7 +320,7 @@ public abstract class HttpClient {
while ((len = gzipIn.read(b)) >= 0) { while ((len = gzipIn.read(b)) >= 0) {
bos.write(b, 0, len); bos.write(b, 0, len);
} }
return bos.toString(StandardCharsets.UTF_8.toString()); return bos.toString(UTF_8);
} else { } else {
return Objects.requireNonNull(response.body()).string(); return Objects.requireNonNull(response.body()).string();
} }

View File

@ -78,6 +78,7 @@ public class RecordingManager {
String json = Files.readString(file.toPath()); String json = Files.readString(file.toPath());
try { try {
Recording recording = adapter.fromJson(json); Recording recording = adapter.fromJson(json);
recording.setMetaDataFile(file.getCanonicalPath());
loadRecording(recording); loadRecording(recording);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't load recording {}", file, e); LOG.error("Couldn't load recording {}", file, e);

View File

@ -39,6 +39,7 @@ public class RecordingPreconditions {
ensureRecorderIsActive(); ensureRecorderIsActive();
ensureNotInTimeoutPeriod(); ensureNotInTimeoutPeriod();
ensureModelIsNotSuspended(model); ensureModelIsNotSuspended(model);
ensureModelIsNotDelayed(model);
ensureModelIsNotMarkedForLaterRecording(model); ensureModelIsNotMarkedForLaterRecording(model);
ensureRecordUntilIsInFuture(model); ensureRecordUntilIsInFuture(model);
ensureNoRecordingRunningForModel(model); ensureNoRecordingRunningForModel(model);
@ -141,6 +142,13 @@ public class RecordingPreconditions {
} }
} }
private void ensureModelIsNotDelayed(Model model) {
Optional<ModelGroup> modelGroup = recorder.getModelGroup(model);
if (modelGroup.isPresent() && model.isDelayed()) {
throw new PreconditionNotMetException("Recording for model " + model + " is delayed");
}
}
private void ensureModelIsNotMarkedForLaterRecording(Model model) { private void ensureModelIsNotMarkedForLaterRecording(Model model) {
if (model.isMarkedForLaterRecording()) { if (model.isMarkedForLaterRecording()) {
throw new PreconditionNotMetException("Model " + model + " is marked for later recording"); throw new PreconditionNotMetException("Model " + model + " is marked for later recording");
@ -204,6 +212,7 @@ public class RecordingPreconditions {
try { try {
ensureRecorderIsActive(); ensureRecorderIsActive();
ensureModelIsNotSuspended(model); ensureModelIsNotSuspended(model);
ensureModelIsNotDelayed(model);
ensureModelIsNotMarkedForLaterRecording(model); ensureModelIsNotMarkedForLaterRecording(model);
ensureRecordUntilIsInFuture(model); ensureRecordUntilIsInFuture(model);
ensureModelShouldBeRecorded(model); ensureModelShouldBeRecorded(model);

View File

@ -136,12 +136,15 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
} }
} catch (ParseException e) { } catch (ParseException e) {
LOG.error("Couldn't parse HLS playlist for model {}\n{}", model, e.getInput(), e); LOG.error("Couldn't parse HLS playlist for model {}\n{}", model, e.getInput(), e);
model.delay();
stop(); stop();
} catch (PlaylistException e) { } catch (PlaylistException e) {
LOG.error("Couldn't parse HLS playlist for model {}", model, e); LOG.error("Couldn't parse HLS playlist for model {}", model, e);
model.delay();
stop(); stop();
} catch (PlaylistTimeoutException e) { } catch (PlaylistTimeoutException e) {
if (consecutivePlaylistTimeouts >= 5) { if (consecutivePlaylistTimeouts >= 5) {
model.delay();
stop(); stop();
} else { } else {
rescheduleTime = beforeLastPlaylistRequest; // try again as fast as possible rescheduleTime = beforeLastPlaylistRequest; // try again as fast as possible
@ -151,9 +154,11 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
LOG.debug("Reached end of playlist for model {}", model); LOG.debug("Reached end of playlist for model {}", model);
stop(); stop();
} catch (HttpException e) { } catch (HttpException e) {
consecutivePlaylistErrors++;
handleHttpException(e); handleHttpException(e);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't download segment for model {}", model, e); LOG.error("Couldn't download segment for model {}", model, e);
model.delay();
stop(); stop();
} finally { } finally {
if (consecutivePlaylistErrors > 0) { if (consecutivePlaylistErrors > 0) {
@ -218,6 +223,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
} }
if (consecutivePlaylistErrors >= 3) { if (consecutivePlaylistErrors >= 3) {
LOG.info("Playlist could not be downloaded for model {} {} times. Stopping recording", model, consecutivePlaylistErrors, e); LOG.info("Playlist could not be downloaded for model {} {} times. Stopping recording", model, consecutivePlaylistErrors, e);
model.delay();
stop(); stop();
} else { } else {
LOG.info("Playlist could not be downloaded for model {} {} times: {}", model, consecutivePlaylistErrors, e.getLocalizedMessage()); LOG.info("Playlist could not be downloaded for model {} {} times: {}", model, consecutivePlaylistErrors, e.getLocalizedMessage());
@ -252,31 +258,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
for (StreamSource streamSource : streamSources) { for (StreamSource streamSource : streamSources) {
LOG.debug("{}:{} src {}", model.getSite().getName(), model.getName(), streamSource); LOG.debug("{}:{} src {}", model.getSite().getName(), model.getName(), streamSource);
} }
String url; StreamSource selectedStreamSource = selectStreamSource(streamSources);
if (model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) { String url = selectedStreamSource.getMediaPlaylistUrl();
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one selectedResolution = selectedStreamSource.height;
StreamSource source = streamSources.get(model.getStreamUrlIndex());
LOG.debug("{} selected {}", model.getName(), source);
url = source.getMediaPlaylistUrl();
selectedResolution = source.height;
} 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;
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)
.toList();
if (filteredStreamSources.isEmpty()) {
throw new ExecutionException(new NoStreamFoundException("No stream left in playlist"));
} else {
StreamSource source = filteredStreamSources.get(filteredStreamSources.size() - 1);
LOG.debug("{} selected {}", model.getName(), source);
url = source.getMediaPlaylistUrl();
selectedResolution = source.height;
}
}
LOG.debug("Segment playlist url {}", url); LOG.debug("Segment playlist url {}", url);
return url; return url;
} }
@ -297,7 +281,6 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
// no segments, empty playlist // no segments, empty playlist
return new SegmentPlaylist(segmentPlaylistUrl); return new SegmentPlaylist(segmentPlaylistUrl);
} }
byte[] bytes = body.getBytes(UTF_8); byte[] bytes = body.getBytes(UTF_8);
BandwidthMeter.add(bytes.length); BandwidthMeter.add(bytes.length);
InputStream inputStream = new ByteArrayInputStream(bytes); InputStream inputStream = new ByteArrayInputStream(bytes);
@ -375,6 +358,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
} }
if (playlistEmptyCount == 10) { if (playlistEmptyCount == 10) {
LOG.info("Last 10 playlists were empty for {}. Stopping recording!", getModel()); LOG.info("Last 10 playlists were empty for {}. Stopping recording!", getModel());
model.delay();
internalStop(); internalStop();
} }
} }

View File

@ -1,14 +1,26 @@
package ctbrec.sites.amateurtv; package ctbrec.sites.amateurtv;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import ctbrec.Model; import static ctbrec.io.HttpConstants.*;
import ctbrec.io.HttpClient; import static java.nio.charset.StandardCharsets.UTF_8;
import ctbrec.sites.AbstractSite;
public class AmateurTv extends AbstractSite { public class AmateurTv extends AbstractSite {
@ -33,7 +45,7 @@ public class AmateurTv extends AbstractSite {
} }
@Override @Override
public Model createModel(String name) { public AmateurTvModel createModel(String name) {
AmateurTvModel model = new AmateurTvModel(); AmateurTvModel model = new AmateurTvModel();
model.setName(name); model.setName(name);
model.setUrl(BASE_URL + '/' + name); model.setUrl(BASE_URL + '/' + name);
@ -84,18 +96,41 @@ public class AmateurTv extends AbstractSite {
@Override @Override
public boolean supportsSearch() { public boolean supportsSearch() {
return false; return true;
}
@Override
public boolean searchRequiresLogin() {
return false;
} }
@Override @Override
public List<Model> search(String q) throws IOException, InterruptedException { public List<Model> search(String q) throws IOException, InterruptedException {
if (StringUtil.isBlank(q)) {
return Collections.emptyList(); return Collections.emptyList();
} }
String url = getBaseUrl() + "/v3/readmodel/cache/filterbyusername/" + URLEncoder.encode(q, UTF_8);
var req = new Request.Builder()
.url(url)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT, Locale.ENGLISH.getLanguage())
.header(REFERER, getBaseUrl())
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
List<Model> models = new ArrayList<>();
JSONArray results = json.getJSONObject("cams").getJSONArray("nodes");
int maxResults = Math.min(30, results.length());
for (int i = 0; i < maxResults; i++) {
JSONObject result = results.getJSONObject(i);
var user = result.getJSONObject("user");
AmateurTvModel model = createModel(user.optString("username"));
model.setPreview(result.optString("imageURL"));
models.add(model);
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
@Override @Override
public boolean isSiteForModel(Model m) { public boolean isSiteForModel(Model m) {
@ -111,7 +146,7 @@ public class AmateurTv extends AbstractSite {
@Override @Override
public Model createModelFromUrl(String url) { public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://.*?amateur.tv/(.*)").matcher(url); Matcher m = Pattern.compile("https?://.*?amateur.tv/(.*)").matcher(url);
if(m.matches()) { if (m.matches()) {
String modelName = m.group(1); String modelName = m.group(1);
return createModel(modelName); return createModel(modelName);
} else { } else {

View File

@ -1,25 +1,25 @@
package ctbrec.sites.amateurtv; package ctbrec.sites.amateurtv;
import com.iheartradio.m3u8.*; import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.data.MediaPlaylist; import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.data.Playlist;
import ctbrec.AbstractModel; import ctbrec.AbstractModel;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.hls.FfmpegHlsDownload;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -32,17 +32,25 @@ import static ctbrec.io.HttpConstants.*;
public class AmateurTvModel extends AbstractModel { public class AmateurTvModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class); private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class);
private JSONArray qualities = new JSONArray();
private boolean online = false; private int[] resolution = new int[2];
@Override @Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache) { if (ignoreCache) {
JSONObject json = getModelInfo(); JSONObject json = getModelInfo();
online = json.optString("status").equalsIgnoreCase("online"); setOnlineState(OFFLINE);
onlineState = online ? ONLINE : OFFLINE;
boolean online = json.optString("status").equalsIgnoreCase("online");
if (online) setOnlineState(ONLINE);
boolean brb = json.optBoolean("brb");
if (brb) setOnlineState(AWAY);
boolean privateChat = json.optString("privateChatStatus").equalsIgnoreCase("exclusive_private");
if (privateChat) setOnlineState(PRIVATE);
} }
return online; return onlineState == ONLINE;
} }
@Override @Override
@ -65,46 +73,30 @@ public class AmateurTvModel extends AbstractModel {
@Override @Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
List<StreamSource> streamSources = new ArrayList<>(); List<StreamSource> streamSources = new ArrayList<>();
String streamUrl = getStreamUrl(); String mediaPlaylistUrl = getMasterPlaylistUrl();
Request req = new Request.Builder().url(streamUrl).build(); qualities.forEach(item -> {
try (Response response = site.getHttpClient().execute(req)) { String value = (String) item;
if (response.isSuccessful()) { String[] res = value.split("x");
InputStream inputStream = Objects.requireNonNull(response.body()).byteStream(); StreamSource src = new StreamSource();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); src.mediaPlaylistUrl = MessageFormat.format("{0}&variant={1}", mediaPlaylistUrl, res[1]);
Playlist playlist = parser.parse(); src.width = Integer.parseInt(res[0]);
MediaPlaylist media = playlist.getMediaPlaylist(); src.height = Integer.parseInt(res[1]);
String vodUri; src.bandwidth = 0;
String trackUri = media.getTracks().get(0).getUri(); streamSources.add(src);
if (trackUri.startsWith("http")) { });
vodUri = trackUri;
} else if (trackUri.startsWith("/")) {
String baseUrl = streamUrl.substring(0, streamUrl.indexOf("/", 8));
vodUri = baseUrl + trackUri;
} else {
String baseUrl = streamUrl.substring(0, streamUrl.lastIndexOf('/') + 1);
vodUri = baseUrl + trackUri;
}
StreamSource streamsource = new StreamSource();
streamsource.mediaPlaylistUrl = vodUri;
streamsource.width = 0;
streamsource.height = 0;
streamSources.add(streamsource);
} else {
throw new HttpException(response.code(), response.message());
}
}
return streamSources; return streamSources;
} }
private String getStreamUrl() throws IOException { private String getMasterPlaylistUrl() throws IOException {
JSONObject json = getModelInfo(); JSONObject json = getModelInfo();
JSONObject videoTech = json.getJSONObject("videoTechnologies"); JSONObject videoTech = json.getJSONObject("videoTechnologies");
qualities = json.getJSONArray("qualities");
return videoTech.getString("fmp4-hls"); return videoTech.getString("fmp4-hls");
} }
@Override @Override
public void invalidateCacheEntries() { public void invalidateCacheEntries() {
// nothing to do resolution = new int[2];
} }
@Override @Override
@ -114,12 +106,19 @@ public class AmateurTvModel extends AbstractModel {
@Override @Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException { public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if (!failFast) {
try { try {
return new int[]{getStreamSources().get(0).width, getStreamSources().get(0).height}; List<StreamSource> sources = getStreamSources();
} catch (Exception e) { if (!sources.isEmpty()) {
StreamSource best = sources.get(0);
resolution = new int[]{best.getWidth(), best.getHeight()};
}
} catch (IOException | ParseException | PlaylistException | JAXBException e) {
throw new ExecutionException(e); throw new ExecutionException(e);
} }
} }
return resolution;
}
@Override @Override
public boolean follow() throws IOException { public boolean follow() throws IOException {
@ -176,14 +175,18 @@ public class AmateurTvModel extends AbstractModel {
.header(ACCEPT_LANGUAGE, "en") .header(ACCEPT_LANGUAGE, "en")
.header(REFERER, getSite().getBaseUrl() + '/' + getName()) .header(REFERER, getSite().getBaseUrl() + '/' + getName())
.build(); .build();
try (Response resp = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
JSONObject json = new JSONObject(HttpClient.bodyToJsonObject(resp)); if (response.isSuccessful()) {
return json; JSONObject jsonResponse = new JSONObject(response.body().string());
return jsonResponse;
} else {
throw new HttpException(response.code(), response.message());
}
} }
} }
@Override @Override
public Download createDownload() { public Download createDownload() {
return new AmateurTvDownload(getSite().getHttpClient()); return new FfmpegHlsDownload(getSite().getHttpClient());
} }
} }

View File

@ -1,6 +1,7 @@
package ctbrec.sites.bonga; package ctbrec.sites.bonga;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite; import ctbrec.sites.AbstractSite;
@ -21,7 +22,6 @@ import java.util.regex.Pattern;
import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL; import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public class BongaCams extends AbstractSite { public class BongaCams extends AbstractSite {
@ -133,19 +133,18 @@ public class BongaCams extends AbstractSite {
return true; return true;
} }
@Override
public boolean searchRequiresLogin() {
return true;
}
@Override @Override
public List<Model> search(String q) throws IOException, InterruptedException { public List<Model> search(String q) throws IOException, InterruptedException {
String url = baseUrl + "/tools/listing_v3.php?offset=0&model_search[display_name][text]=" + URLEncoder.encode(q, UTF_8); if (StringUtil.isBlank(q)) {
return Collections.emptyList();
}
String url = getBaseUrl() + "/tools/listing_v3.php?livetab=all&_suggest=1&model_search[display_name][text]=" + URLEncoder.encode(q, "utf-8");
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.addHeader(USER_AGENT, getConfig().getSettings().httpUserAgent) .addHeader(USER_AGENT, getConfig().getSettings().httpUserAgent)
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON) .addHeader("Accept", "*/*")
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .addHeader("Accept-Encoding", "deflate")
.addHeader("Accept-Language", "en,en-US;q=0.9")
.addHeader(REFERER, getBaseUrl()) .addHeader(REFERER, getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST) .addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build(); .build();
@ -153,7 +152,7 @@ public class BongaCams extends AbstractSite {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string(); String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
JSONObject json = new JSONObject(body); JSONObject json = new JSONObject(body);
if (json.optString("status").equals("success")) { if (json.has("models")) {
List<Model> models = new ArrayList<>(); List<Model> models = new ArrayList<>();
parseModelList(models, json); parseModelList(models, json);
return models; return models;
@ -169,16 +168,17 @@ public class BongaCams extends AbstractSite {
private void parseModelList(List<Model> models, JSONObject json) { private void parseModelList(List<Model> models, JSONObject json) {
JSONArray results = json.getJSONArray("models"); JSONArray results = json.getJSONArray("models");
for (int i = 0; i < results.length(); i++) { int maxResults = Math.min(30, results.length());
for (int i = 0; i < maxResults; i++) {
JSONObject result = results.getJSONObject(i); JSONObject result = results.getJSONObject(i);
if (result.has("username")) { if (result.has("username")) {
Model model = createModel(result.getString("username")); String username = result.getString("username");
String thumb = result.getString("thumb_image").replace("{ext}", "jpg"); Model model = createModel(username.toLowerCase());
if (thumb != null) { if (result.has("avatar")) {
model.setPreview("https:" + thumb); model.setPreview("https://i.bcicdn.com" + result.getString("avatar"));
} }
if (result.has("display_name")) { if (result.has("name")) {
model.setDisplayName(result.getString("display_name")); model.setDisplayName(result.getString("name"));
} }
models.add(model); models.add(model);
} }

View File

@ -42,7 +42,6 @@ public class BongaCamsHttpClient extends HttpClient {
.value("%7B%22limit%22%3A20%2C%22c_limit%22%3A10%2C%22th_type%22%3A%22live%22%2C%22sorting%22%3A%22popular%22%2C%22display%22%3A%22auto%22%7D") .value("%7B%22limit%22%3A20%2C%22c_limit%22%3A10%2C%22th_type%22%3A%22live%22%2C%22sorting%22%3A%22popular%22%2C%22display%22%3A%22auto%22%7D")
.build(); .build();
Map<String, List<Cookie>> cookies = cookieJar.getCookies(); Map<String, List<Cookie>> cookies = cookieJar.getCookies();
for (Entry<String, List<Cookie>> entry : cookies.entrySet()) { for (Entry<String, List<Cookie>> entry : cookies.entrySet()) {
List<Cookie> cookieList = entry.getValue(); List<Cookie> cookieList = entry.getValue();

View File

@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.text.MessageFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -37,28 +38,30 @@ public class Cam4Model extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(Cam4Model.class); private static final Logger LOG = LoggerFactory.getLogger(Cam4Model.class);
private String playlistUrl; private String playlistUrl;
private int[] resolution = null; private int[] resolution = null;
private boolean privateRoom = false; private JSONObject modelInfo;
@Override @Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache || onlineState == UNKNOWN) { if (ignoreCache) {
try { try {
loadModelDetails(); modelInfo = loadModelInfo();
getPlaylistUrl(); if (modelInfo.optBoolean("privateRoom")) {
onlineState = PRIVATE;
}
} catch (Exception e) { } catch (Exception e) {
onlineState = OFFLINE; onlineState = OFFLINE;
} }
} }
return onlineState == ONLINE && !privateRoom && StringUtil.isNotBlank(playlistUrl); return onlineState == ONLINE;
} }
private void loadModelDetails() throws IOException { private JSONObject loadModelInfo() throws IOException {
JSONObject roomState = new Cam4WsClient(Config.getInstance(), (Cam4) getSite(), this).getRoomState(); JSONObject roomState = new Cam4WsClient(Config.getInstance(), (Cam4) getSite(), this).getRoomState();
if (LOG.isTraceEnabled()) LOG.trace(roomState.toString(2)); LOG.trace(roomState.toString(2));
String state = roomState.optString("newShowsState"); String state = roomState.optString("newShowsState");
setOnlineStateByShowType(state); setOnlineStateByShowType(state);
privateRoom = roomState.optBoolean("privateRoom");
setDescription(roomState.optString("status")); setDescription(roomState.optString("status"));
return roomState;
} }
public void setOnlineStateByShowType(String showType) { public void setOnlineStateByShowType(String showType) {
@ -81,7 +84,7 @@ public class Cam4Model extends AbstractModel {
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (!failFast && onlineState == UNKNOWN) { if (!failFast && onlineState == UNKNOWN) {
try { try {
loadModelDetails(); modelInfo = loadModelInfo();
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Couldn't load model details {}", e.getMessage()); LOG.warn("Couldn't load model details {}", e.getMessage());
} }
@ -90,15 +93,39 @@ public class Cam4Model extends AbstractModel {
} }
private String getPlaylistUrl() throws IOException { private String getPlaylistUrl() throws IOException {
try {
getPlaylistUrlFromStreamUrl();
if (StringUtil.isNotBlank(playlistUrl)) {
return playlistUrl;
}
} catch (IOException e) {
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);
if (StringUtil.isNotBlank(hls) && hls.startsWith("http")) {
playlistUrl = hls;
return playlistUrl;
}
}
if (modelInfo != null && modelInfo.has("streamUUID")) {
String uuid = modelInfo.optString("streamUUID");
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";
playlistUrl = MessageFormat.format(urlTemplate, parts[1], uuid);
return playlistUrl;
}
}
String page = loadModelPage(); String page = loadModelPage();
Matcher m = Pattern.compile("hlsUrl\\s*:\\s*'(.*?)'", DOTALL | MULTILINE).matcher(page); Matcher m = Pattern.compile("hlsUrl\\s*:\\s*'(.*?)'", DOTALL | MULTILINE).matcher(page);
if (m.find()) { if (m.find()) {
playlistUrl = m.group(1); playlistUrl = m.group(1);
} else { return playlistUrl;
LOG.trace("hlsUrl not in page");
getPlaylistUrlFromStreamUrl();
} }
if (playlistUrl == null) { if (StringUtil.isBlank(playlistUrl)) {
throw new IOException("Couldn't determine playlist url"); throw new IOException("Couldn't determine playlist url");
} }
return playlistUrl; return playlistUrl;
@ -122,9 +149,9 @@ public class Cam4Model extends AbstractModel {
if (LOG.isTraceEnabled()) LOG.trace(json.toString(2)); if (LOG.isTraceEnabled()) LOG.trace(json.toString(2));
if (json.has("canUseCDN")) { if (json.has("canUseCDN")) {
if (json.getBoolean("canUseCDN")) { if (json.getBoolean("canUseCDN")) {
playlistUrl = json.getString("cdnURL"); playlistUrl = json.optString("cdnURL");
} else { } else {
playlistUrl = json.getString("edgeURL"); playlistUrl = json.optString("edgeURL");
} }
} }
} else { } else {
@ -164,8 +191,7 @@ public class Cam4Model extends AbstractModel {
if (playlist.getUri().startsWith("http")) { if (playlist.getUri().startsWith("http")) {
src.mediaPlaylistUrl = playlist.getUri(); src.mediaPlaylistUrl = playlist.getUri();
} else { } else {
String masterUrl = getPlaylistUrl(); String baseUrl = playlistUrl.substring(0, playlistUrl.lastIndexOf('/') + 1);
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
src.mediaPlaylistUrl = baseUrl + playlist.getUri(); src.mediaPlaylistUrl = baseUrl + playlist.getUri();
} }
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
@ -177,7 +203,8 @@ public class Cam4Model extends AbstractModel {
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
String masterPlaylistUrl = getPlaylistUrl(); String masterPlaylistUrl = getPlaylistUrl();
LOG.trace("Loading master playlist [{}]", masterPlaylistUrl); masterPlaylistUrl = masterPlaylistUrl.replace("_sfm4s", "");
LOG.debug("Loading master playlist [{}]", masterPlaylistUrl);
Request.Builder builder = new Request.Builder().url(masterPlaylistUrl); Request.Builder builder = new Request.Builder().url(masterPlaylistUrl);
getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header); getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header);
Request req = builder.build(); Request req = builder.build();

View File

@ -44,10 +44,10 @@ public class CamsodaModel extends AbstractModel {
public String getStreamUrl() throws IOException { public String getStreamUrl() throws IOException {
Request req = createJsonRequest(getTokenInfoUrl()); Request req = createJsonRequest(getTokenInfoUrl());
JSONObject response = executeJsonRequest(req); JSONObject response = executeJsonRequest(req);
if (response.optInt(STATUS) == 1 && response.optJSONArray(EDGE_SERVERS) != null && response.optJSONArray(EDGE_SERVERS).length() > 0) { if (response.optInt(STATUS) == 1 && response.optJSONArray(EDGE_SERVERS) != null && !response.optJSONArray(EDGE_SERVERS).isEmpty()) {
String edgeServer = response.getJSONArray(EDGE_SERVERS).getString(0); String edgeServer = response.getJSONArray(EDGE_SERVERS).getString(0);
String streamName = response.getString(STREAM_NAME); String streamName = response.getString(STREAM_NAME);
String token = response.getString("token"); String token = response.optString("token");
return constructStreamUrl(edgeServer, streamName, token); return constructStreamUrl(edgeServer, streamName, token);
} else { } else {
throw new JSONException("JSON response has not the expected structure"); throw new JSONException("JSON response has not the expected structure");
@ -185,7 +185,7 @@ public class CamsodaModel extends AbstractModel {
@Override @Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache || onlineState == UNKNOWN) { if (ignoreCache) {
loadModel(); loadModel();
} }
return onlineState == ONLINE; return onlineState == ONLINE;

View File

@ -49,13 +49,11 @@ public class ChaturbateHttpClient extends HttpClient {
if (loggedIn) { if (loggedIn) {
return true; return true;
} }
if (checkLogin()) { if (checkLogin()) {
loggedIn = true; loggedIn = true;
LOG.debug("Logged in with cookies"); LOG.debug("Logged in with cookies");
return true; return true;
} }
try { try {
Request login = new Request.Builder() Request login = new Request.Builder()
.url(Chaturbate.baseUrl + PATH) .url(Chaturbate.baseUrl + PATH)
@ -88,18 +86,9 @@ public class ChaturbateHttpClient extends HttpClient {
loggedIn = true; loggedIn = true;
extractCsrfToken(login); extractCsrfToken(login);
} }
} else {
if (loginTries++ < 3) {
login();
} else {
throw new IOException("Login failed: " + response.code() + " " + response.message());
} }
}
response.close();
} catch (Exception ex) { } catch (Exception ex) {
LOG.debug("Login failed: {}", ex.getMessage()); LOG.debug("Login failed: {}", ex.getMessage());
} finally {
loginTries = 0;
} }
return loggedIn; return loggedIn;
} }

View File

@ -94,7 +94,6 @@ public class ChaturbateModel extends AbstractModel { // NOSONAR
if (failFast) { if (failFast) {
return resolution; return resolution;
} }
try { try {
resolution = getResolution(); resolution = getResolution();
} catch (Exception e) { } catch (Exception e) {

View File

@ -4,6 +4,7 @@ import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist; import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData; import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StreamInfo;
import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter; import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel; import ctbrec.AbstractModel;
@ -92,6 +93,15 @@ public class Flirt4FreeModel extends AbstractModel {
return; return;
} }
JSONObject json = new JSONObject(body); 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());
setMarkedForLaterRecording(true);
}
online = false;
onlineState = Model.State.OFFLINE;
return;
}
online = Objects.equals(json.optString(STATUS), "online"); // online is true, even if the model is in private or away online = Objects.equals(json.optString(STATUS), "online"); // online is true, even if the model is in private or away
updateModelId(json); updateModelId(json);
if (online) { if (online) {
@ -188,8 +198,10 @@ public class Flirt4FreeModel extends AbstractModel {
for (PlaylistData playlist : masterPlaylist.getPlaylists()) { for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth(); StreamInfo info = playlist.getStreamInfo();
src.height = playlist.getStreamInfo().getResolution().height; src.bandwidth = info.getBandwidth();
src.height = (info.hasResolution()) ? info.getResolution().height : 0;
src.width = (info.hasResolution()) ? info.getResolution().width : 0;
HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl); HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl);
src.mediaPlaylistUrl = "https://" + masterPlaylistUrl.host() + '/' + playlist.getUri(); src.mediaPlaylistUrl = "https://" + masterPlaylistUrl.host() + '/' + playlist.getUri();
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
@ -473,7 +485,7 @@ public class Flirt4FreeModel extends AbstractModel {
String url = getSite().getBaseUrl() + "/external.php?a=" + String url = getSite().getBaseUrl() + "/external.php?a=" +
(add ? "add_favorite" : "delete_favorite") + (add ? "add_favorite" : "delete_favorite") +
"&id=" + id + "&id=" + id +
"&name=" + getDisplayName() + "&name=" + getName() +
"&t=" + System.currentTimeMillis(); "&t=" + System.currentTimeMillis();
LOG.debug("Sending follow/unfollow request: {}", url); LOG.debug("Sending follow/unfollow request: {}", url);
Request req = new Request.Builder() Request req = new Request.Builder()
@ -527,6 +539,16 @@ public class Flirt4FreeModel extends AbstractModel {
this.isNew = isNew; this.isNew = isNew;
} }
@Override
public String getName() {
String original = super.getName();
String fixed = original.toLowerCase().replace(" ", "-").replace("_", "-");
if (!fixed.equals(original)) {
setName(fixed);
}
return fixed;
}
private void acquireSlot() throws InterruptedException { private void acquireSlot() throws InterruptedException {
requestThrottle.acquire(); requestThrottle.acquire();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();

View File

@ -15,7 +15,10 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -24,8 +27,8 @@ import static ctbrec.io.HttpConstants.*;
public class LiveJasmin extends AbstractSite { public class LiveJasmin extends AbstractSite {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasmin.class); private static final Logger LOG = LoggerFactory.getLogger(LiveJasmin.class);
public static String baseUrl = ""; public static String baseUrl = "https://www.livejasmin.com";
public static String baseDomain = ""; public static String baseDomain = "www.livejasmin.com";
private HttpClient httpClient; private HttpClient httpClient;
@Override @Override
@ -41,7 +44,6 @@ public class LiveJasmin extends AbstractSite {
@Override @Override
public String getAffiliateLink() { public String getAffiliateLink() {
return "https://awejmp.com/?siteId=jasmin&categoryName=girl&pageName=listpage&performerName=&prm[psid]=0xb00bface&prm[pstool]=205_1&prm[psprogram]=revs&prm[campaign_id]=&subAffId={SUBAFFID}&filters="; return "https://awejmp.com/?siteId=jasmin&categoryName=girl&pageName=listpage&performerName=&prm[psid]=0xb00bface&prm[pstool]=205_1&prm[psprogram]=revs&prm[campaign_id]=&subAffId={SUBAFFID}&filters=";
// return "https://awejmp.com/?siteId=jasmin&categoryName=girl&pageName=listpage&performerName=&prm[psid]=0xb00bface&prm[pstool]=205_1&prm[psprogram]=pps&prm[campaign_id]=&subAffId={SUBAFFID}&filters=";
} }
@Override @Override
@ -196,12 +198,12 @@ public class LiveJasmin extends AbstractSite {
@Override @Override
public Model createModelFromUrl(String url) { public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("http.*?livejasmin\\.com.*?#!chat/(.*)").matcher(url); Matcher m = Pattern.compile("http.*?livejasmin\\.com.*?#!chat/(.*)").matcher(url);
if(m.find()) { if (m.find()) {
String name = m.group(1); String name = m.group(1);
return createModel(name); return createModel(name);
} }
m = Pattern.compile("http.*?livejasmin\\.com.*?/chat(?:-html5)?/(.*)").matcher(url); m = Pattern.compile("http.*?livejasmin\\.com.*?/chat(?:-html5)?/(.*)").matcher(url);
if(m.find()) { if (m.find()) {
String name = m.group(1); String name = m.group(1);
return createModel(name); return createModel(name);
} }

View File

@ -6,6 +6,7 @@ import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter; import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel; import ctbrec.AbstractModel;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.StringUtil;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
@ -41,53 +42,54 @@ public class LiveJasminModel extends AbstractModel {
} }
protected void loadModelInfo() throws IOException { protected void loadModelInfo() throws IOException {
String url = "https://m." + LiveJasmin.baseDomain + "/en/chat-html5/" + getName(); Request req = new Request.Builder().url(LiveJasmin.baseUrl)
Request req = new Request.Builder().url(url) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(USER_AGENT, .header(ACCEPT, "*/*")
"Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15") .header(ACCEPT_ENCODING, "deflate")
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, getSite().getBaseUrl()) .header(REFERER, getSite().getBaseUrl() + "/")
.header(ORIGIN, getSite().getBaseUrl())
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
// do nothing we just want the cookies
LOG.debug("Initial request succeeded: {} - {}", response.isSuccessful(), response.code());
}
String url = LiveJasmin.baseUrl + "/en/flash/get-performer-details/" + getName();
req = new Request.Builder().url(url)
.header(USER_AGENT, "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15")
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_ENCODING, "deflate")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, getSite().getBaseUrl() + "/")
.header(ORIGIN, getSite().getBaseUrl())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build(); .build();
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
JSONObject json = new JSONObject(body); JSONObject json = new JSONObject(body);
//LOG.debug(json.toString(2)); LOG.trace(json.toString(2));
//Files.writeString(Path.of("/tmp/model.json"), json.toString(2));
if (json.optBoolean("success")) { if (json.optBoolean("success")) {
JSONObject data = json.getJSONObject("data"); JSONObject data = json.getJSONObject("data");
JSONObject config = data.getJSONObject("config");
JSONObject chatRoom = config.getJSONObject("chatRoom");
JSONObject armageddonConfig = config.getJSONObject("armageddonConfig");
setId(chatRoom.getString("p_id"));
setName(chatRoom.getString("performer_id"));
setDisplayName(chatRoom.getString("display_name"));
if (chatRoom.has("profile_picture_url")) {
setPreview(chatRoom.getString("profile_picture_url"));
}
int status = chatRoom.optInt("status", -1);
onlineState = mapStatus(status);
if (chatRoom.optInt("is_on_private", 0) == 1) {
onlineState = State.PRIVATE;
}
if (chatRoom.optInt("is_video_call_enabled", 0) == 1) {
onlineState = State.PRIVATE;
}
resolution = new int[2];
resolution[0] = config.optInt("streamWidth");
resolution[1] = config.optInt("streamHeight");
modelInfo = new LiveJasminModelInfo.LiveJasminModelInfoBuilder() modelInfo = new LiveJasminModelInfo.LiveJasminModelInfoBuilder()
.sbIp(chatRoom.getString("sb_ip")) .sbIp(data.optString("sb_ip", null))
.sbHash(chatRoom.getString("sb_hash")) .sbHash(data.optString("sb_hash", null))
.sessionId(armageddonConfig.getString("sessionid")) .sessionId("m12345678901234567890123456789012")
.jsm2session(armageddonConfig.getString("jsm2session")) .jsm2session(getSite().getHttpClient().getCookiesByName("session").get(0).value())
.performerId(getName()) .performerId(data.optString("performer_id", getName()))
.displayName(data.optString("display_name", getName()))
.clientInstanceId(randomClientInstanceId()) .clientInstanceId(randomClientInstanceId())
.status(data.optInt("status", -1))
.build(); .build();
online = onlineState == State.ONLINE; if (data.has("channelsiteurl")) {
LOG.trace("{} - status:{} {} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl(), id); setUrl(LiveJasmin.baseUrl + data.getString("channelsiteurl"));
}
onlineState = mapStatus(modelInfo.getStatus());
online = onlineState == State.ONLINE
&& StringUtil.isNotBlank(modelInfo.getSbIp())
&& StringUtil.isNotBlank(modelInfo.getSbHash());
LOG.debug("{} - status:{} {} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl(), id);
} else { } else {
throw new IOException("Response was not successful: " + body); throw new IOException("Response was not successful: " + body);
} }
@ -107,17 +109,21 @@ public class LiveJasminModel extends AbstractModel {
public static State mapStatus(int status) { public static State mapStatus(int status) {
switch (status) { switch (status) {
case 0: case 0 -> {
return State.OFFLINE; return State.OFFLINE;
case 1: }
case 1 -> {
return State.ONLINE; return State.ONLINE;
case 2, 3: }
case 2, 3 -> {
return State.PRIVATE; return State.PRIVATE;
default: }
default -> {
LOG.debug("Unkown state {}", status); LOG.debug("Unkown state {}", status);
return State.UNKNOWN; return State.UNKNOWN;
} }
} }
}
@Override @Override
public void setOnlineState(State status) { public void setOnlineState(State status) {
@ -129,17 +135,17 @@ public class LiveJasminModel extends AbstractModel {
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
loadModelInfo(); loadModelInfo();
String websocketUrlTemplate = "wss://dss-relay-{ipWithDashes}.dditscdn.com/?random={clientInstanceId}"; String websocketUrlTemplate = "wss://dss-relay-{ipWithDashes}.dditscdn.com/memberChat/jasmin{modelName}{sb_hash}?random={clientInstanceId}";
String websocketUrl = websocketUrlTemplate String websocketUrl = websocketUrlTemplate
.replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-')) .replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-'))
.replace("{modelName}", getName())
.replace("{sb_hash}", modelInfo.getSbHash())
.replace("{clientInstanceId}", modelInfo.getClientInstanceId()); .replace("{clientInstanceId}", modelInfo.getClientInstanceId());
modelInfo.setWebsocketUrl(websocketUrl); modelInfo.setWebsocketUrl(websocketUrl);
LiveJasminStreamRegistration liveJasminStreamRegistration = new LiveJasminStreamRegistration(site, modelInfo); LiveJasminStreamRegistration liveJasminStreamRegistration = new LiveJasminStreamRegistration(site, modelInfo);
List<StreamSource> streamSources = liveJasminStreamRegistration.getStreamSources(); List<StreamSource> streamSources = liveJasminStreamRegistration.getStreamSources();
streamSources.stream().max(Comparator.naturalOrder()).ifPresent(ss -> { Collections.sort(streamSources);
new LiveJasminStreamStarter().start(site, modelInfo, (LiveJasminStreamSource) ss);
});
return streamSources; return streamSources;
} }
@ -174,10 +180,8 @@ public class LiveJasminModel extends AbstractModel {
} catch (IOException e) { } catch (IOException e) {
throw new ExecutionException(e); throw new ExecutionException(e);
} }
return resolution;
} else {
return resolution;
} }
return resolution;
} }
@Override @Override
@ -253,10 +257,6 @@ public class LiveJasminModel extends AbstractModel {
@Override @Override
public Download createDownload() { public Download createDownload() {
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) { return new LiveJasminWebrtcDownload(getSite().getHttpClient());
return new LiveJasminHlsDownload(getSite().getHttpClient());
} else {
return new LiveJasminMergedHlsDownload(getSite().getHttpClient());
}
} }
} }

View File

@ -12,5 +12,7 @@ public class LiveJasminModelInfo {
private String sessionId; private String sessionId;
private String jsm2session; private String jsm2session;
private String performerId; private String performerId;
private String displayName;
private String clientInstanceId; private String clientInstanceId;
private int status;
} }

View File

@ -16,12 +16,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.LinkedList; import java.util.*;
import java.util.List;
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.USER_AGENT; import static ctbrec.io.HttpConstants.USER_AGENT;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@ -29,6 +29,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
public class LiveJasminStreamRegistration { public class LiveJasminStreamRegistration {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminStreamRegistration.class); private static final Logger LOG = LoggerFactory.getLogger(LiveJasminStreamRegistration.class);
private static final String KEY_EVENT = "event"; private static final String KEY_EVENT = "event";
private static final String KEY_FUNC_NAME = "funcName"; private static final String KEY_FUNC_NAME = "funcName";
@ -36,61 +37,65 @@ public class LiveJasminStreamRegistration {
private final LiveJasminModelInfo modelInfo; private final LiveJasminModelInfo modelInfo;
private final CyclicBarrier barrier = new CyclicBarrier(2); private final CyclicBarrier barrier = new CyclicBarrier(2);
private int streamCount = 0;
private WebSocket webSocket;
public LiveJasminStreamRegistration(Site site, LiveJasminModelInfo modelInfo) { public LiveJasminStreamRegistration(Site site, LiveJasminModelInfo modelInfo) {
this.site = site; this.site = site;
this.modelInfo = modelInfo; this.modelInfo = modelInfo;
} }
List<StreamSource> getStreamSources() { List<StreamSource> getStreamSources() {
var streamSources = new LinkedList<StreamSource>(); var streamSources = new LinkedList<LiveJasminStreamSource>();
try { try {
Request webSocketRequest = new Request.Builder() Request webSocketRequest = new Request.Builder()
.url(modelInfo.getWebsocketUrl()) .url(modelInfo.getWebsocketUrl())
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile) .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile)
.build(); .build();
LOG.debug("Websocket: {}", modelInfo.getWebsocketUrl()); LOG.debug("Websocket: {}", modelInfo.getWebsocketUrl());
site.getHttpClient().newWebSocket(webSocketRequest, new WebSocketListener() { webSocket = site.getHttpClient().newWebSocket(webSocketRequest, new WebSocketListener() {
@Override @Override
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
LOG.debug("onOpen"); Thread.currentThread().setName("Stream registration for " + modelInfo.getPerformerId());
LOG.trace("onOpen");
JSONObject register = new JSONObject() JSONObject register = new JSONObject()
.put(KEY_EVENT, "register") .put(KEY_EVENT, "register")
.put("applicationId", "memberChat/jasmin" + modelInfo.getPerformerId() + modelInfo.getSbHash()) .put("applicationId", "memberChat/jasmin" + modelInfo.getPerformerId() + modelInfo.getSbHash())
.put("connectionData", new JSONObject() .put("connectionData", new JSONObject()
.put("jasmin2App", false)
.put("isMobileClient", true)
.put("platform", "mobile")
.put("chatID", "freechat")
.put("sessionID", modelInfo.getSessionId()) .put("sessionID", modelInfo.getSessionId())
.put("jasmin2App", true)
.put("isMobileClient", false)
.put("platform", "desktop")
.put("chatID", "freechat")
.put("jsm2SessionId", modelInfo.getJsm2session()) .put("jsm2SessionId", modelInfo.getJsm2session())
.put("userType", "user") .put("userType", "user")
.put("performerId", modelInfo.getPerformerId()) .put("performerId", modelInfo.getPerformerId())
.put("clientRevision", "") .put("clientRevision", "")
.put("playerVer", "nanoPlayerVersion: 4.12.1 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (iPad; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/15E148 Safari/605.1.15 platform: iPad")
.put("livejasminTvmember", false) .put("livejasminTvmember", false)
.put("newApplet", true) .put("newApplet", true)
.put("livefeedtype", JSONObject.NULL) .put("livefeedtype", JSONObject.NULL)
.put("gravityCookieId", "") .put("gravityCookieId", "")
.put("passparam", "") .put("passparam", "")
.put("clientInstanceId", modelInfo.getClientInstanceId())
.put("armaVersion", "39.158.0")
.put("isPassive", false)
.put("brandID", "jasmin") .put("brandID", "jasmin")
.put("cobrandId", "") .put("cobrandId", "livejasmin")
.put("subbrand", "livejasmin") .put("subbrand", "livejasmin")
.put("siteName", "LiveJasmin") .put("siteName", "LiveJasmin")
.put("siteUrl", "https://m." + LiveJasmin.baseDomain) .put("siteUrl", "https://www.livejasmin.com")
.put("chatHistoryRequired", false) .put("clientInstanceId", modelInfo.getClientInstanceId())
.put("armaVersion", "38.32.1-LIVEJASMIN-44016-1")
.put("isPassive", false)
.put("peekPatternJsm2", true) .put("peekPatternJsm2", true)
.put("chatHistoryRequired", true)
); );
webSocket.send(register.toString()); LOG.trace("Stream registration\n{}", register.toString(2));
webSocket.send(new JSONObject().put(KEY_EVENT, "ping").toString()); send(register.toString());
webSocket.send(new JSONObject() send(new JSONObject().put(KEY_EVENT, "ping").toString());
send(new JSONObject()
.put(KEY_EVENT, "call") .put(KEY_EVENT, "call")
.put(KEY_FUNC_NAME, "makeActive") .put(KEY_FUNC_NAME, "makeActive")
.put("data", new JSONArray()) .put("data", new JSONArray())
.toString()); .toString());
webSocket.send(new JSONObject() send(new JSONObject()
.put(KEY_EVENT, "call") .put(KEY_EVENT, "call")
.put(KEY_FUNC_NAME, "setVideo") .put(KEY_FUNC_NAME, "setVideo")
.put("data", new JSONArray() .put("data", new JSONArray()
@ -100,11 +105,10 @@ public class LiveJasminStreamRegistration {
.put(modelInfo.getJsm2session()) .put(modelInfo.getJsm2session())
) )
.toString()); .toString());
webSocket.send(new JSONObject() send(new JSONObject()
.put(KEY_EVENT, "connectSharedObject") .put(KEY_EVENT, "connectSharedObject")
.put("name", "data/chat_so") .put("name", "data/chat_so")
.toString()); .toString());
//webSocket.close(1000, "Good bye");
} }
@Override @Override
@ -116,6 +120,7 @@ public class LiveJasminStreamRegistration {
@Override @Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
LOG.trace("< {}", text);
JSONObject message = new JSONObject(text); JSONObject message = new JSONObject(text);
if (message.opt(KEY_EVENT).equals("pong")) { if (message.opt(KEY_EVENT).equals("pong")) {
new Thread(() -> { new Thread(() -> {
@ -124,7 +129,7 @@ public class LiveJasminStreamRegistration {
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
webSocket.send(new JSONObject().put(KEY_EVENT, "ping").toString()); send(new JSONObject().put(KEY_EVENT, "ping").toString());
}).start(); }).start();
} else if (message.optString(KEY_EVENT).equals("updateSharedObject") && message.optString("name").equals("data/chat_so")) { } else if (message.optString(KEY_EVENT).equals("updateSharedObject") && message.optString("name").equals("data/chat_so")) {
LOG.trace(message.toString(2)); LOG.trace(message.toString(2));
@ -140,9 +145,34 @@ public class LiveJasminStreamRegistration {
JSONObject stream = streams.getJSONObject(j); JSONObject stream = streams.getJSONObject(j);
addStreamSource(streamSources, freePattern, stream); addStreamSource(streamSources, freePattern, stream);
} }
webSocket.close(1000, ""); Collections.sort(streamSources);
Collections.reverse(streamSources);
for (LiveJasminStreamSource src : streamSources) {
JSONObject getVideoData = new JSONObject()
.put(KEY_EVENT, "call")
.put(KEY_FUNC_NAME, "getVideoData")
.put("data", new JSONArray()
.put(new JSONObject()
.put("protocols", new JSONArray()
.put("h5live")
)
.put("streamId", src.getStreamId())
.put("correlationId", UUID.randomUUID().toString().replace("-", "").substring(0, 16))
)
);
streamCount++;
send(getVideoData.toString());
} }
} }
}
} else if (message.optString(KEY_FUNC_NAME).equals("setVideoData")) {
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);
if (--streamCount == 0) {
awaitBarrier();
}
} else if (!message.optString(KEY_FUNC_NAME).equals("chatHistory")) { } else if (!message.optString(KEY_FUNC_NAME).equals("chatHistory")) {
LOG.trace("onMessageT {}", new JSONObject(text).toString(2)); LOG.trace("onMessageT {}", new JSONObject(text).toString(2));
} }
@ -156,7 +186,7 @@ public class LiveJasminStreamRegistration {
@Override @Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
LOG.debug("onClosed {} {}", code, reason); LOG.trace("onClosed {} {}", code, reason);
super.onClosed(webSocket, code, reason); super.onClosed(webSocket, code, reason);
} }
@ -165,6 +195,11 @@ public class LiveJasminStreamRegistration {
LOG.trace("onClosing {} {}", code, reason); LOG.trace("onClosing {} {}", code, reason);
awaitBarrier(); awaitBarrier();
} }
private void send(String msg) {
LOG.trace("Send > {}", msg);
webSocket.send(msg);
}
}); });
LOG.debug("Waiting for websocket to return"); LOG.debug("Waiting for websocket to return");
@ -173,15 +208,16 @@ public class LiveJasminStreamRegistration {
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't determine stream sources", e); LOG.error("Couldn't determine stream sources", e);
} }
return streamSources; return streamSources.stream().map(StreamSource.class::cast).collect(Collectors.toList()); // NOSONAR
} }
private void addStreamSource(LinkedList<StreamSource> streamSources, String pattern, JSONObject stream) { private void addStreamSource(LinkedList<LiveJasminStreamSource> streamSources, String pattern, JSONObject stream) {
int w = stream.getInt("width"); int w = stream.getInt("width");
int h = stream.getInt("height"); int h = stream.getInt("height");
int bitrate = stream.getInt("bitrate") * 1024; int bitrate = stream.getInt("bitrate") * 1024;
String name = stream.getString("name"); String name = stream.getString("name");
String streamName = pattern.replace("{$streamname}", name); String streamName = pattern.replace("{$streamname}", name);
String streamId = stream.getString("streamId");
String rtmpUrl = "rtmp://{ip}/memberChat/jasmin{modelName}{sb_hash}?sessionId-{sessionId}|clientInstanceId-{clientInstanceId}" String rtmpUrl = "rtmp://{ip}/memberChat/jasmin{modelName}{sb_hash}?sessionId-{sessionId}|clientInstanceId-{clientInstanceId}"
.replace("{ip}", modelInfo.getSbIp()) .replace("{ip}", modelInfo.getSbIp())
@ -200,8 +236,10 @@ public class LiveJasminStreamRegistration {
streamSource.width = w; streamSource.width = w;
streamSource.height = h; streamSource.height = h;
streamSource.bandwidth = bitrate; streamSource.bandwidth = bitrate;
streamSource.rtmpUrl = rtmpUrl; streamSource.setRtmpUrl(rtmpUrl);
streamSource.streamName = streamName; streamSource.setStreamName(streamName);
streamSource.setStreamId(streamId);
streamSource.setStreamRegistration(this);
streamSources.add(streamSource); streamSources.add(streamSource);
} }
@ -215,4 +253,8 @@ public class LiveJasminStreamRegistration {
LOG.error(e.getLocalizedMessage(), e); LOG.error(e.getLocalizedMessage(), e);
} }
} }
void close() {
webSocket.close(1000, "");
}
} }

View File

@ -1,8 +1,14 @@
package ctbrec.sites.jasmin; package ctbrec.sites.jasmin;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LiveJasminStreamSource extends StreamSource { public class LiveJasminStreamSource extends StreamSource {
public String rtmpUrl; public String rtmpUrl;
public String streamName; public String streamName;
public String streamId;
public LiveJasminStreamRegistration streamRegistration;
} }

View File

@ -0,0 +1,247 @@
package ctbrec.sites.jasmin;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
public class LiveJasminWebrtcDownload extends AbstractDownload {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminWebrtcDownload.class);
private static final int MAX_SECONDS_WITHOUT_TRANSFER = 20;
private final HttpClient httpClient;
private WebSocket ws;
private FileOutputStream fout;
private Instant timeOfLastTransfer = Instant.MAX;
private volatile boolean running;
private volatile boolean started;
private File targetFile;
public LiveJasminWebrtcDownload(HttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
this.config = config;
this.model = model;
this.startTime = startTime;
this.downloadExecutor = executorService;
splittingStrategy = initSplittingStrategy(config.getSettings());
targetFile = config.getFileForRecording(model, "mp4", startTime);
timeOfLastTransfer = Instant.now();
}
@Override
public void stop() {
running = false;
if (ws != null) {
ws.close(1000, "");
ws = null;
}
}
@Override
public void finalizeDownload() {
if (fout != null) {
try {
LOG.debug("Closing recording file {}", targetFile);
fout.close();
} catch (IOException e) {
LOG.error("Error while closing recording file {}", targetFile, e);
}
}
}
@Override
public boolean isRunning() {
return running;
}
@Override
public void postprocess(Recording recording) {
// nothing to do
}
@Override
public File getTarget() {
return targetFile;
}
@Override
public String getPath(Model model) {
String absolutePath = targetFile.getAbsolutePath();
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
return relativePath;
}
@Override
public boolean isSingleFile() {
return true;
}
@Override
public long getSizeInByte() {
return getTarget().length();
}
@Override
public Download call() throws Exception {
if (!started) {
started = true;
startDownload();
}
if (splittingStrategy.splitNecessary(this)) {
stop();
rescheduleTime = Instant.now();
} else {
rescheduleTime = Instant.now().plusSeconds(5);
}
if (!model.isOnline(true)) {
stop();
}
if (Duration.between(timeOfLastTransfer, Instant.now()).getSeconds() > MAX_SECONDS_WITHOUT_TRANSFER) {
LOG.info("No video data received for {} seconds. Stopping recording for model {}", MAX_SECONDS_WITHOUT_TRANSFER, model);
stop();
}
return this;
}
private void startDownload() throws IOException, PlaylistException, ParseException, ExecutionException {
LiveJasminModel liveJasminModel = (LiveJasminModel) model;
List<StreamSource> streamSources = liveJasminModel.getStreamSources();
LiveJasminStreamSource streamSource = (LiveJasminStreamSource) selectStreamSource(streamSources);
LiveJasminStreamRegistration streamRegistration = streamSource.getStreamRegistration();
Request request = new Request.Builder()
.url(streamSource.getMediaPlaylistUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en")
.header(REFERER, model.getSite().getBaseUrl() + "/")
.header(ORIGIN, model.getSite().getBaseUrl())
.build();
running = true;
LOG.debug("Opening webrtc connection {}", request.url());
ws = httpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
LOG.trace("onOpen {} {}", webSocket, response);
response.close();
try {
LOG.trace("Recording video stream to {}", targetFile);
Files.createDirectories(targetFile.getParentFile().toPath());
fout = new FileOutputStream(targetFile);
} catch (Exception e) {
LOG.error("Couldn't open file {} to save the video stream", targetFile, e);
stop();
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
LOG.trace("received video data with length {}", bytes.size());
timeOfLastTransfer = Instant.now();
try {
byte[] videoData = bytes.toByteArray();
fout.write(videoData);
BandwidthMeter.add(videoData.length);
} catch (IOException e) {
if (running) {
LOG.error("Couldn't write video stream to file", e);
stop();
}
}
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
LOG.trace("onMessageT {} {}", webSocket, text);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
stop();
if (t instanceof EOFException) {
LOG.info("End of stream detected for model {}", model);
} else {
LOG.error("Websocket failure for model {} {}", model, response, t);
}
if (response != null) {
response.close();
}
streamRegistration.close();
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
LOG.trace("Websocket closing for model {} {} {}", model, code, reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
LOG.debug("Websocket closed for model {} {} {}", model, code, reason);
stop();
streamRegistration.close();
}
});
}
@Override
public void awaitEnd() {
long secondsToWait = 30;
for (int i = 0; i < secondsToWait; i++) {
if (ws == null) {
return;
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warn("Interrupted while waiting for the download to terminate");
}
}
}
LOG.warn("Download didn't finish in {} seconds", secondsToWait);
}
}

View File

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter; import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel; import ctbrec.AbstractModel;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model;
import ctbrec.NotImplementedExcetion; import ctbrec.NotImplementedExcetion;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
@ -19,6 +18,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -26,6 +26,7 @@ import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static ctbrec.sites.streamate.StreamateHttpClient.JSON; import static ctbrec.sites.streamate.StreamateHttpClient.JSON;
import static java.nio.charset.StandardCharsets.UTF_8;
public class StreamateModel extends AbstractModel { public class StreamateModel extends AbstractModel {
@ -70,11 +71,6 @@ public class StreamateModel extends AbstractModel {
return onlineState; return onlineState;
} }
@Override
public void setOnlineState(State onlineState) {
this.onlineState = onlineState;
}
@Override @Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json"; String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json";
@ -129,10 +125,22 @@ public class StreamateModel extends AbstractModel {
resolution = null; resolution = null;
} }
void loadModelId() throws IOException, InterruptedException { void loadModelId() throws IOException {
List<Model> models = getSite().search(getName()); String url = "https://www.streamate.com/api/performer/lookup?nicknames" + URLEncoder.encode(getName(), UTF_8);
if (!models.isEmpty()) { Request req = new Request.Builder().url(url)
id = ((StreamateModel)models.get(0)).getId(); .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.addHeader(ACCEPT, "*/*")
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, Streamate.BASE_URL + '/' + getName())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
id = new JSONObject(body).getJSONObject("result").getLong(getName());
} else {
throw new HttpException(response.code(), response.message());
}
} }
} }
@ -228,8 +236,6 @@ public class StreamateModel extends AbstractModel {
loadModelId(); loadModelId();
} catch (IOException e) { } catch (IOException e) {
LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName(), e); LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName(), e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} }
} }
writer.name("id").value(id); writer.name("id").value(id);

View File

@ -29,6 +29,7 @@ import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public class StreamrayModel extends AbstractModel { public class StreamrayModel extends AbstractModel {
@ -145,7 +146,7 @@ public class StreamrayModel extends AbstractModel {
String lname = getName().toLowerCase(); String lname = getName().toLowerCase();
String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0, 1), lname.substring(lname.length() - 1), lname); String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0, 1), lname.substring(lname.length() - 1), lname);
try { try {
return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:320::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, "utf-8")); return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:320::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, UTF_8));
} catch (Exception ex) { } catch (Exception ex) {
return url; return url;
} }

View File

@ -1,6 +1,13 @@
package ctbrec.sites.stripchat; package ctbrec.sites.stripchat;
import static ctbrec.io.HttpConstants.*; import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -10,15 +17,8 @@ import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.json.JSONArray; import static ctbrec.io.HttpConstants.USER_AGENT;
import org.json.JSONObject; import static java.nio.charset.StandardCharsets.UTF_8;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.Request;
import okhttp3.Response;
public class Stripchat extends AbstractSite { public class Stripchat extends AbstractSite {
@ -70,8 +70,8 @@ public class Stripchat extends AbstractSite {
throw new IOException("Account settings not available"); throw new IOException("Account settings not available");
} }
String username = getConfig().getSettings().stripchatPassword; String username = getConfig().getSettings().stripchatUsername;
String url = baseUri + "/api/v1/user/" + username; String url = baseUri + "/api/front/users/username/" + username;
Request request = new Request.Builder().url(url).build(); Request request = new Request.Builder().url(url).build();
try (Response response = getHttpClient().execute(request)) { try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
@ -126,7 +126,7 @@ public class Stripchat extends AbstractSite {
@Override @Override
public List<Model> search(String q) throws IOException, InterruptedException { public List<Model> search(String q) throws IOException, InterruptedException {
String url = baseUri + "/api/front/v2/models/search?limit=20&query=" + URLEncoder.encode(q, "utf-8"); String url = baseUri + "/api/front/v2/models/search?limit=20&query=" + URLEncoder.encode(q, UTF_8);
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent) .header(USER_AGENT, getConfig().getSettings().httpUserAgent)

View File

@ -23,9 +23,12 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
@ -36,22 +39,38 @@ import static java.nio.charset.StandardCharsets.UTF_8;
public class StripchatModel extends AbstractModel { public class StripchatModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(StripchatModel.class); private static final Logger LOG = LoggerFactory.getLogger(StripchatModel.class);
private String status = null;
private int[] resolution = new int[]{0, 0}; private int[] resolution = new int[]{0, 0};
private int modelId = 0;
private boolean isVr = false;
private JSONObject modelInfo;
private transient Instant lastInfoRequest = Instant.EPOCH;
@Override @Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache || status == null) { if (ignoreCache) {
JSONObject jsonResponse = loadModelInfo(); JSONObject jsonResponse = getModelInfo();
if (jsonResponse.has("user")) { if (jsonResponse.has("user")) {
JSONObject user = jsonResponse.getJSONObject("user"); JSONObject user = jsonResponse.getJSONObject("user");
status = user.optString("status"); String status = user.optString("status");
mapOnlineState(status); mapOnlineState(status);
if (isBanned(user)) {
LOG.debug("Model inactive or deleted: {}", getName());
setMarkedForLaterRecording(true);
}
modelId = user.optInt("id");
isVr = user.optBoolean("isVr", false);
} }
} }
return onlineState == ONLINE; return onlineState == ONLINE;
} }
private boolean isBanned(JSONObject user) {
boolean isDeleted = user.optBoolean("isDeleted", false);
boolean isApprovedModel = user.optBoolean("isApprovedModel", true);
return (!isApprovedModel || isDeleted);
}
private void mapOnlineState(String status) { private void mapOnlineState(String status) {
switch (status) { switch (status) {
case "public" -> setOnlineState(ONLINE); case "public" -> setOnlineState(ONLINE);
@ -65,6 +84,15 @@ public class StripchatModel extends AbstractModel {
} }
} }
private JSONObject getModelInfo() throws IOException {
if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) {
return Optional.ofNullable(modelInfo).orElse(new JSONObject());
}
lastInfoRequest = Instant.now();
modelInfo = loadModelInfo();
return modelInfo;
}
private JSONObject loadModelInfo() throws IOException { private JSONObject loadModelInfo() throws IOException {
String name = getName(); String name = getName();
String url = getSite().getBaseUrl() + "/api/front/users/username/" + name; String url = getSite().getBaseUrl() + "/api/front/users/username/" + name;
@ -94,8 +122,15 @@ public class StripchatModel extends AbstractModel {
try { try {
String originalUrl = url.replace("_auto", ""); String originalUrl = url.replace("_auto", "");
masterPlaylist = getMasterPlaylist(originalUrl); masterPlaylist = getMasterPlaylist(originalUrl);
List<StreamSource> originalStreamSource = extractStreamSources(masterPlaylist); for (StreamSource original : extractStreamSources(masterPlaylist)) {
streamSources.addAll(originalStreamSource); boolean found = false;
for (StreamSource source : streamSources) {
if (source.height == original.height) {
found = true;
}
}
if (!found) streamSources.add(original);
}
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Original stream quality not available", e); LOG.warn("Original stream quality not available", e);
} }
@ -142,6 +177,12 @@ public class StripchatModel extends AbstractModel {
} }
private String getMasterPlaylistUrl() throws IOException { private String getMasterPlaylistUrl() throws IOException {
boolean VR = 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" : "";
if (modelId > 0) {
return MessageFormat.format(hlsUrlTemplate, String.valueOf(modelId), vrSuffix);
}
String name = getName(); String name = getName();
String url = getSite().getBaseUrl() + "/api/front/models/username/" + name + "/cam?triggerRequest=loadCam"; String url = getSite().getBaseUrl() + "/api/front/models/username/" + name + "/cam?triggerRequest=loadCam";
Request req = new Request.Builder() Request req = new Request.Builder()
@ -157,11 +198,11 @@ public class StripchatModel extends AbstractModel {
String body = response.body().string(); String body = response.body().string();
LOG.trace(body); LOG.trace(body);
JSONObject jsonResponse = new JSONObject(body); JSONObject jsonResponse = new JSONObject(body);
String streamName = jsonResponse.optString("streamName", jsonResponse.optString("")); String streamName = jsonResponse.optString("streamName");
JSONObject viewServers = jsonResponse.getJSONObject("viewServers"); JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings");
String serverName = viewServers.optString("flashphoner-hls"); String vrBroadcastServer = broadcastSettings.optString("vrBroadcastServer");
String hslUrlTemplate = "https://b-{0}.doppiocdn.com/hls/{1}/master/{1}_auto.m3u8"; vrSuffix = (!VR || vrBroadcastServer.isEmpty()) ? "" : "_vr";
return MessageFormat.format(hslUrlTemplate, serverName, streamName); return MessageFormat.format(hlsUrlTemplate, streamName, vrSuffix);
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
} }
@ -171,8 +212,9 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public void invalidateCacheEntries() { public void invalidateCacheEntries() {
status = null;
resolution = new int[]{0, 0}; resolution = new int[]{0, 0};
lastInfoRequest = Instant.EPOCH;
modelInfo = null;
} }
@Override @Override
@ -199,7 +241,7 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean follow() throws IOException { public boolean follow() throws IOException {
getSite().getHttpClient().login(); getSite().getHttpClient().login();
JSONObject modelInfo = loadModelInfo(); JSONObject modelInfo = getModelInfo();
JSONObject user = modelInfo.getJSONObject("user"); JSONObject user = modelInfo.getJSONObject("user");
long modelId = user.optLong("id"); long modelId = user.optLong("id");
@ -231,7 +273,7 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean unfollow() throws IOException { public boolean unfollow() throws IOException {
getSite().getHttpClient().login(); getSite().getHttpClient().login();
JSONObject modelInfo = loadModelInfo(); JSONObject modelInfo = getModelInfo();
JSONObject user = modelInfo.getJSONObject("user"); JSONObject user = modelInfo.getJSONObject("user");
long modelId = user.optLong("id"); long modelId = user.optLong("id");
JSONArray favoriteIds = new JSONArray(); JSONArray favoriteIds = new JSONArray();
@ -263,6 +305,28 @@ public class StripchatModel extends AbstractModel {
} }
} }
@Override
public boolean exists() throws IOException {
Request req = new Request.Builder() // @formatter:off
.url(getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build(); // @formatter:on
try (Response response = getSite().getHttpClient().execute(req)) {
if (!response.isSuccessful() && response.code() == 404) {
return false;
}
}
JSONObject jsonResponse = getModelInfo();
if (jsonResponse.has("user")) {
JSONObject user = jsonResponse.getJSONObject("user");
if (isBanned(user)) {
LOG.debug("Model inactive or deleted: {}", getName());
return false;
}
}
return true;
}
@Override @Override
public Download createDownload() { public Download createDownload() {
if (Config.getInstance().getSettings().useHlsdl) { if (Config.getInstance().getSettings().useHlsdl) {

View File

@ -18,7 +18,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
<version.javafx>20</version.javafx> <version.javafx>20.0.2</version.javafx>
<version.junit>5.7.2</version.junit> <version.junit>5.7.2</version.junit>
</properties> </properties>