Merge branch 'dev' into v4

This commit is contained in:
0xb00bface 2021-01-10 17:31:49 +01:00
commit e709e2d45d
27 changed files with 718 additions and 148 deletions

View File

@ -1,7 +1,16 @@
NEXT
3.12.1
========================
* Fix: "Resume all" started the recordings of models marked for later recording
3.12.0
========================
* Added "record later" tab to "bookmark" models
* Added config option to show the total number of models in the title bar
* Added support for hlsdl. Some sites (MV Live, LiveJasmin, Showup) are
excluded, because they need some special behavior while the download is
running. hlsdl can be activated in the settings under "Advanced" or with
the config properties "useHlsdl", "hlsdlExecutable" and "loghlsdlOutput".
The used bandwidth calculation does not work with hlsdl.
* Fixed problem with Cam4 playlist URLs, thanks @gohufrapoc
3.11.0

View File

@ -106,7 +106,7 @@
<plugin>
<groupId>com.akathist.maven.plugins.launch4j</groupId>
<artifactId>launch4j-maven-plugin</artifactId>
<version>1.7.22</version>
<version>1.7.25</version>
<executions>
<execution>
<id>l4j-win</id>
@ -132,7 +132,9 @@
<bundledJre64Bit>true</bundledJre64Bit>
<minVersion>15</minVersion>
<maxHeapSize>512</maxHeapSize>
<opt>-Dfile.encoding=utf-8</opt>
<opts>
<opt>-Dfile.encoding=utf-8</opt>
</opts>
</jre>
<versionInfo>
<fileVersion>4.0.0.0</fileVersion>

View File

@ -1,34 +0,0 @@
package ctbrec.ui.controls;
import ctbrec.ui.PauseIcon;
import javafx.scene.Cursor;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
public class PausedIndicator extends StackPane {
private PauseIcon pausedIcon;
private Rectangle clickPanel;
public PausedIndicator(int size, Color color) {
setMaxSize(size, size);
pausedIcon = new PauseIcon(color, size);
pausedIcon.setVisible(false);
clickPanel = new Rectangle(size, size);
clickPanel.setCursor(Cursor.HAND);
clickPanel.setFill(Paint.valueOf("#00000000"));
getChildren().add(pausedIcon);
getChildren().add(clickPanel);
pausedIcon.visibleProperty().bindBidirectional(visibleProperty());
clickPanel.onMouseClickedProperty().bindBidirectional(onMouseClickedProperty());
Tooltip tooltip = new Tooltip("Resume Recording");
Tooltip.install(clickPanel, tooltip);
}
}

View File

@ -0,0 +1,37 @@
package ctbrec.ui.controls;
import javafx.scene.Cursor;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
public class RecordingIndicator extends StackPane {
private ImageView icon;
private Rectangle clickPanel;
public RecordingIndicator(int size) {
setMaxSize(size, size);
icon = new ImageView();
icon.setVisible(false);
icon.prefHeight(size);
icon.prefWidth(size);
icon.maxHeight(size);
icon.maxWidth(size);
clickPanel = new Rectangle(size, size);
clickPanel.setCursor(Cursor.HAND);
clickPanel.setFill(Paint.valueOf("#00000000"));
getChildren().add(icon);
getChildren().add(clickPanel);
icon.visibleProperty().bindBidirectional(visibleProperty());
}
public void setImage(Image img) {
icon.setImage(img);
}
}

View File

@ -290,6 +290,7 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
ComboBox<Object> comboBox = new ComboBox(listProp);
Field field = Settings.class.getField(setting.getKey());
Object value = field.get(settings);
LOG.debug("{} {} {}", setting.getName(), value, setting.getProperty().getValue());
if (StringUtil.isNotBlank(value.toString())) {
if (setting.getConverter() != null) {
comboBox.getSelectionModel().select(setting.getConverter().convertTo(value));

View File

@ -9,6 +9,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -117,6 +118,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleLongProperty leaveSpaceOnDevice;
private SimpleStringProperty ffmpegParameters;
private SimpleBooleanProperty logFFmpegOutput;
private SimpleBooleanProperty loghlsdlOutput;
private SimpleStringProperty fileExtension;
private SimpleStringProperty server;
private SimpleIntegerProperty port;
@ -126,6 +128,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleBooleanProperty totalModelCountInTitle;
private SimpleBooleanProperty transportLayerSecurity;
private SimpleBooleanProperty fastScrollSpeed;
private SimpleBooleanProperty useHlsdl;
private SimpleFileProperty hlsdlExecutable;
private ExclusiveSelectionProperty recordLocal;
private SimpleIntegerProperty postProcessingThreads;
private IgnoreList ignoreList;
@ -170,6 +174,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
loghlsdlOutput = new SimpleBooleanProperty(null, "loghlsdlOutput", settings.loghlsdlOutput);
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
@ -184,6 +189,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
fastScrollSpeed = new SimpleBooleanProperty(null, "fastScrollSpeed", settings.fastScrollSpeed);
confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
useHlsdl = new SimpleBooleanProperty(null, "useHlsdl", settings.useHlsdl);
hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
}
private void createGui() {
@ -269,6 +276,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Category.of("Advanced / Devtools",
Group.of("Logging",
Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory")
),
Group.of("hlsdl (experimental)",
Setting.of("Use hlsdl (if possible)", useHlsdl, "Use hlsdl to record the live streams. Some features might not work correctly."),
Setting.of("hlsdl executable", hlsdlExecutable, "Path to the hlsdl executable"),
Setting.of("Log hlsdl output", loghlsdlOutput, "Log hlsdl output to files in the system's temp directory")
)
)
);
@ -320,13 +332,17 @@ public class SettingsTab extends Tab implements TabSelectionListener {
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("concurrentRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
prefs.getSetting("hlsdlExecutable").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
prefs.getSetting("loghlsdlOutput").ifPresent(s -> bindEnabledProperty(s, useHlsdl.not()));
postProcessingStepPanel.disableProperty().bind(recordLocal.not());
variablesHelpButton.disableProperty().bind(recordLocal);
}
private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
boolean splitAfterSet = settings.splitRecordingsAfterSecs > 0;
LOG.debug("after {}", settings.splitRecordingsAfterSecs);
boolean splitBiggerThanSet = settings.splitRecordingsBiggerThanBytes > 0;
LOG.debug("bigger {}", settings.splitRecordingsBiggerThanBytes);
if (splitAfterSet && splitBiggerThanSet) {
settings.splitStrategy = TIME_OR_SIZE;
} else if (splitAfterSet) {
@ -336,6 +352,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
} else {
settings.splitStrategy = DONT;
}
LOG.debug("strats {}", settings.splitStrategy);
saveConfig();
}
@ -422,11 +439,13 @@ public class SettingsTab extends Tab implements TabSelectionListener {
}
public void saveConfig() {
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.error("Couldn't save config", e);
}
CompletableFuture.runAsync(() -> {
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.error("Couldn't save config", e);
}
});
}
@Override

View File

@ -1,13 +1,58 @@
package ctbrec.ui.tabs;
import static ctbrec.Recording.State.*;
import static ctbrec.ui.UnicodeEmoji.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.*;
import ctbrec.ui.action.*;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.EditNotesAction;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.action.ToggleRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Dialogs;
@ -26,13 +71,33 @@ import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
@ -41,30 +106,6 @@ import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static ctbrec.Recording.State.RECORDING;
import static ctbrec.ui.UnicodeEmoji.CLOCK;
import static ctbrec.ui.UnicodeEmoji.HEAVY_CHECK_MARK;
public class RecordedModelsTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
@ -409,14 +450,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void pauseAll(ActionEvent evt) {
boolean yes = Dialogs.showConfirmDialog("Pause all models", "", "Pause the recording of all models?", getTabPane().getScene());
if (yes) {
new PauseAction(getTabPane(), recorder.getModels(), recorder).execute();
List<Model> models = recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).collect(Collectors.toList());
new PauseAction(getTabPane(), models, recorder).execute();
}
}
private void resumeAll(ActionEvent evt) {
boolean yes = Dialogs.showConfirmDialog("Resume all models", "", "Resume the recording of all models?", getTabPane().getScene());
if (yes) {
new ResumeAction(getTabPane(), recorder.getModels(), recorder).execute();
List<Model> models = recorder.getModels().stream().filter(Predicate.not(Model::isMarkedForLaterRecording)).collect(Collectors.toList());
new ResumeAction(getTabPane(), models, recorder).execute();
}
}

View File

@ -31,7 +31,7 @@ import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.PausedIndicator;
import ctbrec.ui.controls.RecordingIndicator;
import ctbrec.ui.controls.StreamPreview;
import javafx.animation.FadeTransition;
import javafx.animation.FillTransition;
@ -51,6 +51,7 @@ import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
@ -72,6 +73,11 @@ public class ThumbCell extends StackPane {
private static final Logger LOG = LoggerFactory.getLogger(ThumbCell.class);
private static final Duration ANIMATION_DURATION = new Duration(250);
private static Image imgRecordIndicator = new Image(ThumbCell.class.getResource("/media-record-16.png").toExternalForm());
private static Image imgPauseIndicator = new Image(ThumbCell.class.getResource("/media-playback-pause-16.png").toExternalForm());
private static Image imgBookmarkIndicator = new Image(ThumbCell.class.getResource("/bookmark-new-16.png").toExternalForm());
private ModelRecordingState modelRecordingState = ModelRecordingState.NOT;
private Model model;
private StreamPreview streamPreview;
private ImageView iv;
@ -85,8 +91,8 @@ public class ThumbCell extends StackPane {
private Text topic;
private Text resolutionTag;
private Recorder recorder;
private Circle recordingIndicator;
private PausedIndicator pausedIndicator;
private RecordingIndicator recordingIndicator;
private Tooltip recordingIndicatorTooltip;
private StackPane previewTrigger;
private int index = 0;
ContextMenu popup;
@ -178,22 +184,15 @@ public class ThumbCell extends StackPane {
StackPane.setMargin(resolutionTag, new Insets(2, 4, 2, 2));
getChildren().add(resolutionTag);
recordingIndicator = new Circle(8);
recordingIndicator.setFill(colorRecording);
recordingIndicator = new RecordingIndicator(16);
recordingIndicator.setCursor(Cursor.HAND);
recordingIndicator.setOnMouseClicked(e -> pauseResumeAction(true));
Tooltip tooltip = new Tooltip("Pause Recording");
Tooltip.install(recordingIndicator, tooltip);
recordingIndicator.setOnMouseClicked(this::recordingInidicatorClicked);
recordingIndicatorTooltip = new Tooltip("Pause Recording");
Tooltip.install(recordingIndicator, recordingIndicatorTooltip);
StackPane.setMargin(recordingIndicator, new Insets(3));
StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT);
getChildren().add(recordingIndicator);
pausedIndicator = new PausedIndicator(16, colorRecording);
pausedIndicator.setOnMouseClicked(e -> pauseResumeAction(false));
StackPane.setMargin(pausedIndicator, new Insets(3));
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
getChildren().add(pausedIndicator);
if (Config.getInstance().getSettings().livePreviews) {
getChildren().add(createPreviewTrigger());
}
@ -231,6 +230,21 @@ public class ThumbCell extends StackPane {
update();
}
private void recordingInidicatorClicked(MouseEvent evt) {
switch(modelRecordingState) {
case RECORDING:
pauseResumeAction(true);
break;
case PAUSED:
pauseResumeAction(false);
break;
case BOOKMARKED:
recordLater(false);
break;
default:
}
}
private Node createPreviewTrigger() {
int s = 32;
previewTrigger = new StackPane();
@ -293,7 +307,6 @@ public class ThumbCell extends StackPane {
streamPreview.stop();
}
recordingIndicator.setVisible(!visible);
pausedIndicator.setVisible(!visible);
if (!visible) {
updateRecordingIndicator();
}
@ -445,17 +458,32 @@ public class ThumbCell extends StackPane {
c = mouseHovering ? colorHighlight : colorNormal;
}
nameBackground.setFill(c);
updateRecordingIndicator();
}
private void updateRecordingIndicator() {
if (recording) {
recordingIndicator.setVisible(!model.isSuspended());
pausedIndicator.setVisible(model.isSuspended());
recordingIndicator.setVisible(true);
if (model.isSuspended()) {
modelRecordingState = ModelRecordingState.PAUSED;
recordingIndicator.setImage(imgPauseIndicator);
recordingIndicatorTooltip.setText("Resume Recording");
} else {
modelRecordingState = ModelRecordingState.RECORDING;
recordingIndicator.setImage(imgRecordIndicator);
recordingIndicatorTooltip.setText("Pause Recording");
}
} else {
recordingIndicator.setVisible(false);
pausedIndicator.setVisible(false);
if (model.isMarkedForLaterRecording()) {
recordingIndicator.setVisible(true);
modelRecordingState = ModelRecordingState.BOOKMARKED;
recordingIndicator.setImage(imgBookmarkIndicator);
recordingIndicatorTooltip.setText("Forget Model");
} else {
recordingIndicator.setVisible(false);
modelRecordingState = ModelRecordingState.NOT;
recordingIndicator.setImage(null);
}
}
}
@ -562,9 +590,9 @@ public class ThumbCell extends StackPane {
});
}
void recordLater() {
model.setMarkedForLaterRecording(true);
startStopAction(true);
void recordLater(boolean recordLater) {
model.setMarkedForLaterRecording(recordLater);
startStopAction(recordLater);
}
public Model getModel() {
@ -577,7 +605,6 @@ public class ThumbCell extends StackPane {
this.model.setPreview(model.getPreview());
this.model.setTags(model.getTags());
this.model.setUrl(model.getUrl());
this.model.setSuspended(recorder.isSuspended(model));
update();
}
@ -591,9 +618,11 @@ public class ThumbCell extends StackPane {
private void update() {
model.setSuspended(recorder.isSuspended(model));
model.setMarkedForLaterRecording(recorder.isMarkedForLaterRecording(model));
setRecording(recorder.isTracked(model));
updateRecordingIndicator();
setImage(model.getPreview());
String txt = recording ? " " : "";
String txt = (modelRecordingState != ModelRecordingState.NOT) ? " " : "";
txt += model.getDescription() != null ? model.getDescription() : "";
topic.setText(txt);
@ -693,4 +722,11 @@ public class ThumbCell extends StackPane {
model.setMarkedForLaterRecording(false);
startStopAction(true);
}
private enum ModelRecordingState {
RECORDING,
PAUSED,
BOOKMARKED,
NOT
}
}

View File

@ -1,5 +1,31 @@
package ctbrec.ui.tabs;
import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.event.EventBusHolder;
@ -7,12 +33,24 @@ import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCamsClient;
import ctbrec.sites.mfc.MyFreeCamsModel;
import ctbrec.ui.*;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TipDialog;
import ctbrec.ui.TokenLabel;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.controls.*;
import javafx.animation.*;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.SearchPopover;
import ctbrec.ui.controls.SearchPopoverTreeList;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -28,24 +66,34 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.*;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.transform.Transform;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static ctbrec.ui.controls.Dialogs.showError;
public class ThumbOverviewTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
@ -441,7 +489,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
MenuItem addPaused = new MenuItem("Add in paused state");
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
MenuItem recordLater = new MenuItem("Record Later");
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell)));
recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), true));
MenuItem removeRecordLater = new MenuItem("Forget Model");
removeRecordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell), false));
MenuItem addRemoveBookmark = recorder.isMarkedForLaterRecording(model) ? removeRecordLater : recordLater;
MenuItem pause = new MenuItem("Pause Recording");
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
@ -476,10 +527,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
if (modelIsTrackedByRecorder) {
contextMenu.getItems().addAll(pauseResume, recordLater);
} else {
contextMenu.getItems().addAll(recordUntil, addPaused);
if (!recorder.isMarkedForLaterRecording(model)) {
contextMenu.getItems().add(recordLater);
}
contextMenu.getItems().addAll(recordUntil, addPaused, addRemoveBookmark);
}
contextMenu.getItems().add(new SeparatorMenuItem());
if (site.supportsFollow()) {
@ -500,9 +548,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
return contextMenu;
}
private void recordLater(List<ThumbCell> list) {
private void recordLater(List<ThumbCell> list, boolean recordLater) {
for (ThumbCell cell : list) {
cell.recordLater();
cell.recordLater(recordLater);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

@ -28,6 +28,8 @@ until a recording is finished. 0 means unlimited.
- **generatePlaylist** (server only) - [`true`,`false`] Generate a playlist once a recording terminates.
- **hlsdlExecutable** - Path to the hlsdl executable, which is used, if `useHlsdl` is set to true
- **httpPort** - [1 - 65536] The TCP port, the server listens on. In the server config, this will tell the server, which port to use. In the application this will set
the port ctbrec tries to connect to, if it is run in remote mode.
@ -43,7 +45,9 @@ the port ctbrec tries to connect to, if it is run in remote mode.
- **livePreviews** (app only) - Enables the live preview feature in the app.
- **logFFmpegOutput** - The output from FFmpeg (from recordings or post-processing steps) will be logged in temporary files.
- **logFFmpegOutput** - [`true`,`false`] The output from FFmpeg (from recordings or post-processing steps) will be logged in temporary files.
- **loghlsdlOutput** - [`true`,`false`] The output from hlsdl will be logged in temporary files. Only in effect, if `useHlsdl` is set to true
- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream.
@ -73,5 +77,7 @@ which have the defined length (roughly). Has to be activated with `splitStrategy
- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings,
which have the defined size (roughly). Has to be activated with `splitStrategy`.
- **useHlsdl** - [`true`,`false`] Use hlsdl to record the live streams. You also have to set `hlsdlExecutable`, if hlsdl is not globally available on your system. hlsdl won't be used for MV Live, LiveJasmin and Showup.
- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on
a machine, which can be accessed from the internet, because this is totally unprotected at the moment.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -17,6 +17,7 @@ import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.hls.HlsDownload;
import ctbrec.recorder.download.hls.HlsdlDownload;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import ctbrec.sites.Site;
import okhttp3.Request;
@ -275,10 +276,14 @@ public abstract class AbstractModel implements Model {
@Override
public Download createDownload() {
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new HlsDownload(getSite().getHttpClient());
if (Config.getInstance().getSettings().useHlsdl) {
return new HlsdlDownload();
} else {
return new MergedFfmpegHlsDownload(getSite().getHttpClient());
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new HlsDownload(getSite().getHttpClient());
} else {
return new MergedFfmpegHlsDownload(getSite().getHttpClient());
}
}
}

View File

@ -213,7 +213,7 @@ public class Config {
return settings;
}
public void save() throws IOException {
public synchronized void save() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter())
.add(PostProcessor.class, new PostProcessorJsonAdapter())

View File

@ -69,6 +69,7 @@ public class Settings {
public String flirt4freePassword;
public String flirt4freeUsername;
public boolean generatePlaylist = true;
public String hlsdlExecutable = "hlsdl";
public int httpPort = 8080;
public int httpSecurePort = 8443;
public String httpServer = "localhost";
@ -84,6 +85,7 @@ public class Settings {
public boolean livePreviews = false;
public boolean localRecording = true;
public boolean logFFmpegOutput = false;
public boolean loghlsdlOutput = false;
public int minimumResolution = 0;
public int maximumResolution = 8640;
public int maximumResolutionPlayer = 0;
@ -156,6 +158,7 @@ public class Settings {
public boolean transportLayerSecurity = true;
public int thumbWidth = 180;
public boolean updateThumbnails = true;
public boolean useHlsdl = false;
@Deprecated
public String username = "";
public int windowHeight = 800;

View File

@ -9,15 +9,15 @@ import java.util.concurrent.ScheduledExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FfmpegStreamRedirector implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(FfmpegStreamRedirector.class);
public class ProcessStreamRedirector implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(ProcessStreamRedirector.class);
private InputStream in;
private OutputStream out;
private boolean keepGoing = true;
private ScheduledExecutorService executor;
public FfmpegStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
public ProcessStreamRedirector(ScheduledExecutorService executor, InputStream in, OutputStream out) {
super();
this.executor = executor;
this.in = in;
@ -37,7 +37,7 @@ public class FfmpegStreamRedirector implements Runnable {
executor.schedule(this, 100, MILLISECONDS);
}
} catch (Exception e) {
LOG.debug("Error while reading from FFmpeg output stream: {}", e.getLocalizedMessage());
LOG.debug("Error while reading from process output stream: {}", e.getLocalizedMessage());
keepGoing = false;
}
}

View File

@ -15,7 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.DevNull;
import ctbrec.io.FfmpegStreamRedirector;
import ctbrec.io.ProcessStreamRedirector;
import ctbrec.recorder.download.ProcessExitedUncleanException;
public class FFmpeg {
@ -30,8 +30,8 @@ public class FFmpeg {
private Consumer<Integer> exitCallback;
private File ffmpegLog = null;
private OutputStream ffmpegLogStream;
private FfmpegStreamRedirector stdoutRedirector;
private FfmpegStreamRedirector stderrRedirector;
private ProcessStreamRedirector stdoutRedirector;
private ProcessStreamRedirector stderrRedirector;
private FFmpeg() {}
@ -45,7 +45,7 @@ public class FFmpeg {
};
}
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException, InterruptedException {
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException {
LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline));
process = Runtime.getRuntime().exec(cmdline, env, executionDir);
afterStart();
@ -83,8 +83,8 @@ public class FFmpeg {
} else {
ffmpegLogStream = new DevNull();
}
stdoutRedirector = new FfmpegStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
stderrRedirector = new FfmpegStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
processOutputReader.submit(stdoutRedirector);
processOutputReader.submit(stderrRedirector);
}

View File

@ -143,9 +143,9 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
segmentDownloadFinished(result.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
LOG.error("Error in segmentDownloadFinished", e);
} catch (ExecutionException e) {
e.printStackTrace();
LOG.error("Error in segmentDownloadFinished", e);
}
});
}
@ -173,7 +173,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
running = false;
}
} catch (ExecutionException e1) {
modelState = ctbrec.Model.State.UNKNOWN;
modelState = State.UNKNOWN;
}
LOG.info(errorMsg, model, modelState);
waitSomeTime(TEN_SECONDS);

View File

@ -0,0 +1,154 @@
package ctbrec.recorder.download.hls;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.DevNull;
import ctbrec.io.ProcessStreamRedirector;
import ctbrec.recorder.download.ProcessExitedUncleanException;
public class Hlsdl {
private static final Logger LOG = LoggerFactory.getLogger(Hlsdl.class);
private static ScheduledExecutorService processOutputReader = Executors.newScheduledThreadPool(2, createThreadFactory("hlsdl output stream reader"));
private Process process;
private boolean logOutput = false;
private Consumer<Process> startCallback;
private Consumer<Integer> exitCallback;
private File processLog = null;
private OutputStream processLogStream;
private ProcessStreamRedirector stdoutRedirector;
private ProcessStreamRedirector stderrRedirector;
private Hlsdl() {}
private static ThreadFactory createThreadFactory(String name) {
return r -> {
Thread t = new Thread(r);
t.setName(name);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
return t;
};
}
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException {
LOG.debug("hlsdl command line: {}", Arrays.toString(cmdline));
process = Runtime.getRuntime().exec(cmdline, env, executionDir);
afterStart();
}
private void afterStart() throws IOException {
notifyStartCallback(process);
setupLogging();
}
public void shutdown(int exitCode) throws IOException {
LOG.debug("hlsdl exit code was {}", exitCode);
processLogStream.flush();
processLogStream.close();
stdoutRedirector.setKeepGoing(false);
stderrRedirector.setKeepGoing(false);
notifyExitCallback(exitCode);
if (exitCode != 1) {
if (processLog != null && processLog.exists()) {
Files.delete(processLog.toPath());
}
} else {
throw new ProcessExitedUncleanException("hlsdl exit code was " + exitCode);
}
}
private void setupLogging() throws IOException {
if (logOutput) {
if (processLog == null) {
processLog = File.createTempFile("hlsdl_", ".log");
}
LOG.debug("Logging hlsdl output to {}", processLog);
processLog.deleteOnExit();
processLogStream = new FileOutputStream(processLog);
} else {
processLogStream = new DevNull();
}
stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), processLogStream);
stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), processLogStream);
processOutputReader.submit(stdoutRedirector);
processOutputReader.submit(stderrRedirector);
}
private void notifyStartCallback(Process process) {
try {
startCallback.accept(process);
} catch(Exception e) {
LOG.error("Exception in onStart callback", e);
}
}
private void notifyExitCallback(int exitCode) {
try {
exitCallback.accept(exitCode);
} catch(Exception e) {
LOG.error("Exception in onExit callback", e);
}
}
public int waitFor() throws InterruptedException {
int exitCode = process.waitFor();
try {
shutdown(exitCode);
} catch (IOException e) {
LOG.error("Error while shutting down hlsdl process", e);
}
return exitCode;
}
public static class Builder {
private boolean logOutput = false;
private File logFile;
private Consumer<Process> startCallback;
private Consumer<Integer> exitCallback;
public Builder logOutput(boolean logOutput) {
this.logOutput = logOutput;
return this;
}
public Builder logFile(File logFile) {
this.logFile = logFile;
return this;
}
public Builder onStarted(Consumer<Process> callback) {
this.startCallback = callback;
return this;
}
public Builder onExit(Consumer<Integer> callback) {
this.exitCallback = callback;
return this;
}
public Hlsdl build() {
Hlsdl instance = new Hlsdl();
instance.logOutput = logOutput;
instance.startCallback = startCallback != null ? startCallback : p -> {};
instance.exitCallback = exitCallback != null ? exitCallback : exitCode -> {};
instance.processLog = logFile;
return instance;
}
}
}

View File

@ -0,0 +1,205 @@
package ctbrec.recorder.download.hls;
import static ctbrec.recorder.download.StreamSource.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.ProcessExitedUncleanException;
import ctbrec.recorder.download.StreamSource;
public class HlsdlDownload extends AbstractDownload {
private static final transient Logger LOG = LoggerFactory.getLogger(HlsdlDownload.class);
protected File targetFile;
private transient Hlsdl hlsdl;
protected transient Process hlsdlProcess;
protected transient boolean running = true;
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
super.init(config, model, startTime, executorService);
String fileSuffix = config.getSettings().ffmpegFileSuffix;
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
createTargetDirectory();
startHlsdlProcess();
if (hlsdlProcess == null) {
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
}
}
protected void createTargetDirectory() throws IOException {
Files.createDirectories(targetFile.getParentFile().toPath());
}
@Override
public Download call() throws Exception {
try {
if (running && !hlsdlProcess.isAlive()) {
running = false;
int exitValue = hlsdlProcess.exitValue();
hlsdl.shutdown(exitValue);
}
rescheduleTime = Instant.now().plusSeconds(1);
if (splittingStrategy.splitNecessary(this)) {
stop();
}
} catch (ProcessExitedUncleanException e) {
LOG.error("hlsdl exited unclean", e);
}
return this;
}
private void startHlsdlProcess() {
try {
String[] cmdline = prepareCommandLine();
hlsdl = new Hlsdl.Builder()
.logOutput(config.getSettings().logFFmpegOutput)
.onStarted(p -> hlsdlProcess = p)
.build();
hlsdl.exec(cmdline, OS.getEnvironment(), targetFile.getParentFile());
} catch (Exception e) {
LOG.error("Error starting hlsdl", e);
}
}
private String[] prepareCommandLine() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
String playlistUrl = getSegmentPlaylistUrl(model);
Map<String, String> headers = model.getHttpHeaderFactory().createSegmentPlaylistHeaders();
String[] cmdline = new String[9 + headers.size() * 2];
int idx = 0;
cmdline[idx++] = config.getSettings().hlsdlExecutable;
cmdline[idx++] = "-c";
cmdline[idx++] = "-r";
cmdline[idx++] = "3";
cmdline[idx++] = "-w";
cmdline[idx++] = "3";
for (Entry<String, String> header : headers.entrySet()) {
cmdline[idx++] = "-h";
cmdline[idx++] = header.getKey() + ": " + header.getValue();
}
cmdline[idx++] = "-o";
cmdline[idx++] = targetFile.getCanonicalPath();
cmdline[idx] = playlistUrl;
return cmdline;
}
protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
List<StreamSource> streamSources = model.getStreamSources();
Collections.sort(streamSources);
for (StreamSource streamSource : streamSources) {
LOG.debug("{} src {}", model.getName(), streamSource);
}
String url = null;
if (model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
} 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)
.collect(Collectors.toList());
if (filteredStreamSources.isEmpty()) {
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
} else {
LOG.debug("{} selected {}", model.getName(), filteredStreamSources.get(filteredStreamSources.size() - 1));
url = filteredStreamSources.get(filteredStreamSources.size() - 1).getMediaPlaylistUrl();
}
}
LOG.debug("Segment playlist url {}", url);
return url;
}
@Override
public void stop() {
if (running) {
running = false;
if (hlsdlProcess != null) {
hlsdlProcess.destroy();
if (hlsdlProcess.isAlive()) {
LOG.info("hlsdl didn't terminate. Destroying the process with force!");
hlsdlProcess.destroyForcibly();
hlsdlProcess = null;
}
}
}
}
@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 Model getModel() {
return model;
}
@Override
public void finalizeDownload() {
// nothing to do
}
@Override
public boolean isRunning() {
return running;
}
}

View File

@ -127,11 +127,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
ffmpeg.exec(cmdline, new String[0], target.getParentFile());
} catch (IOException | ProcessExitedUncleanException e) {
LOG.error("Error in FFmpeg thread", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (running) {
LOG.info("Interrupted while waiting for ffmpeg", e);
}
}
}

View File

@ -0,0 +1,37 @@
package ctbrec.sites.fc2live;
import java.io.IOException;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.download.hls.HlsdlDownload;
public class Fc2HlsdlDownload extends HlsdlDownload {
private static final Logger LOG = LoggerFactory.getLogger(Fc2HlsdlDownload.class);
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
super.init(config, model, startTime, executorService);
Fc2Model fc2Model = (Fc2Model) model;
try {
fc2Model.openWebsocket();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Couldn't start download for {}", model, e);
}
}
@Override
public void finalizeDownload() {
Fc2Model fc2Model = (Fc2Model) model;
fc2Model.closeWebsocket();
super.finalizeDownload();
}
}

View File

@ -381,10 +381,14 @@ public class Fc2Model extends AbstractModel {
@Override
public Download createDownload() {
if(Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new Fc2HlsDownload(getSite().getHttpClient());
if (Config.getInstance().getSettings().useHlsdl) {
return new Fc2HlsdlDownload();
} else {
return new Fc2MergedHlsDownload(getSite().getHttpClient());
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new Fc2HlsDownload(getSite().getHttpClient());
} else {
return new Fc2MergedHlsDownload(getSite().getHttpClient());
}
}
}