forked from j62/ctbrec
1
0
Fork 0

Get rid of moshi

This commit is contained in:
0xb00bface 2023-06-18 20:49:31 +02:00
parent b53be222fb
commit 619d888bfa
73 changed files with 1587 additions and 1956 deletions

View File

@ -1,9 +1,8 @@
package ctbrec.ui;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
@ -19,6 +18,7 @@ import ctbrec.io.BandwidthMeter;
import ctbrec.io.ByteUnitFormatter;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.notes.LocalModelNotesService;
import ctbrec.notes.ModelNotesService;
import ctbrec.notes.RemoteModelNotesService;
@ -63,6 +63,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import lombok.Data;
import okhttp3.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -72,7 +73,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@ -549,7 +549,6 @@ public class CamrecApplication extends Application {
private void createRecorder() {
if (config.getSettings().localRecording) {
try {
//recorder = new NextGenLocalRecorder(config, sites);
recorder = new SimplifiedLocalRecorder(config, sites);
} catch (IOException e) {
LOG.error("Couldn't initialize recorder", e);
@ -597,10 +596,8 @@ public class CamrecApplication extends Application {
var body = response.body().string();
LOG.trace("Version check respone: {}", body);
if (response.isSuccessful()) {
var moshi = new Moshi.Builder().build();
Type type = Types.newParameterizedType(List.class, Release.class);
JsonAdapter<List<Release>> adapter = moshi.adapter(type);
List<Release> releases = adapter.fromJson(body);
List<Release> releases = ObjectMapperFactory.getMapper().readValue(body, new TypeReference<>() {
});
var latest = releases.get(0);
var latestVersion = latest.getVersion();
var ctbrecVersion = Version.getVersion();
@ -622,38 +619,16 @@ public class CamrecApplication extends Application {
updateCheck.start();
}
@Data
public static class Release {
private String name;
private String tag_name; // NOSONAR - name pattern is needed by moshi
private String html_url; // NOSONAR - name pattern is needed by moshi
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTagName() {
return tag_name;
}
public void setTagName(String tagName) {
this.tag_name = tagName;
}
public String getHtmlUrl() {
return html_url;
}
@SuppressWarnings("unused")
public void setHtmlUrl(String htmlUrl) {
this.html_url = htmlUrl;
}
@JsonProperty("tag_name")
private String tagName;
@JsonProperty("html_url")
private String htmlUrl;
public Version getVersion() {
return Version.of(tag_name);
return Version.of(tagName);
}
}
}

View File

@ -2,8 +2,6 @@ package ctbrec.ui;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.Model;
import ctbrec.SubsequentAction;
import ctbrec.recorder.download.HttpHeaderFactory;
@ -19,20 +17,21 @@ import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
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
*/
public class JavaFxModel implements Model {
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private transient BooleanProperty recordingProperty = new SimpleBooleanProperty();
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
private transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>();
private transient SimpleObjectProperty<Instant> lastRecordedProperty = new SimpleObjectProperty<>();
private final transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private final transient BooleanProperty recordingProperty = new SimpleBooleanProperty();
private final transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private final transient SimpleIntegerProperty priorityProperty = new SimpleIntegerProperty();
private final transient SimpleObjectProperty<Instant> lastSeenProperty = new SimpleObjectProperty<>();
private final transient SimpleObjectProperty<Instant> lastRecordedProperty = new SimpleObjectProperty<>();
private Model delegate;
private final Model delegate;
public JavaFxModel(Model delegate) {
this.delegate = delegate;
@ -188,13 +187,13 @@ public class JavaFxModel implements Model {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
delegate.readSiteSpecificData(reader);
public void readSiteSpecificData(Map<String, String> data) {
delegate.readSiteSpecificData(data);
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
delegate.writeSiteSpecificData(writer);
public void writeSiteSpecificData(Map<String, String> data) {
delegate.writeSiteSpecificData(data);
}
@Override

View File

@ -1,11 +1,16 @@
package ctbrec.ui.event;
import java.time.Instant;
import java.util.Objects;
import ctbrec.Model;
import ctbrec.ui.JavaFxModel;
import lombok.*;
import java.time.Instant;
@Getter
@Setter
@EqualsAndHashCode(of = "timestamp")
@ToString
@NoArgsConstructor
public class PlayerStartedEvent {
private Model model;
@ -20,41 +25,16 @@ public class PlayerStartedEvent {
this.timestamp = timestamp;
}
public Model getModel() {
return model;
}
public Instant getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return Objects.hash(timestamp);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PlayerStartedEvent other = (PlayerStartedEvent) obj;
return Objects.equals(timestamp, other.timestamp);
}
@Override
public String toString() {
return "PlayerStartedEvent [model=" + model + ", timestamp=" + timestamp + "]";
public void setModel(Model model) {
this.model = unwrap(model);
}
private Model unwrap(Model model) {
if (model instanceof JavaFxModel) {
return ((JavaFxModel) model).getDelegate();
if (model instanceof JavaFxModel fxModel) {
return fxModel.getDelegate();
} else {
return model;
}
}
}

View File

@ -0,0 +1,18 @@
package ctbrec.ui.io.json.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.io.json.dto.converter.InstantToMillisConverter;
import ctbrec.io.json.dto.converter.MillisToInstantConverter;
import lombok.Data;
import java.time.Instant;
@Data
public class PlayerStartedEventDto {
private ModelDto model;
@JsonSerialize(converter = InstantToMillisConverter.class)
@JsonDeserialize(converter = MillisToInstantConverter.class)
private Instant timestamp;
}

View File

@ -0,0 +1,14 @@
package ctbrec.ui.io.json.mapper;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.ui.event.PlayerStartedEvent;
import ctbrec.ui.io.json.dto.PlayerStartedEventDto;
import org.mapstruct.Mapper;
@Mapper(uses = ModelMapper.class)
public interface PlayerStartedEventMapper {
PlayerStartedEventDto toDto(PlayerStartedEvent event);
PlayerStartedEvent toEvent(PlayerStartedEventDto dto);
}

View File

@ -1,190 +1,49 @@
package ctbrec.ui.news;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
import com.squareup.moshi.Json;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Account {
@Json(name = "emojis")
@JsonProperty("emojis")
private List<Object> emojis = null;
@Json(name = "note")
@JsonProperty("note")
private String note;
@Json(name = "bot")
@JsonProperty("bot")
private Boolean bot;
@Json(name = "created_at")
@JsonProperty("created_at")
private String createdAt;
@Json(name = "avatar")
@JsonProperty("avatar")
private String avatar;
@Json(name = "display_name")
@JsonProperty("display_name")
private String displayName;
@Json(name = "header_static")
@JsonProperty("header_static")
private String headerStatic;
@Json(name = "url")
@JsonProperty("url")
private String url;
@Json(name = "following_count")
@JsonProperty("following_count")
private Integer followingCount;
@Json(name = "statuses_count")
@JsonProperty("statuses_count")
private Integer statusesCount;
@Json(name = "followers_count")
@JsonProperty("followers_count")
private Integer followersCount;
@Json(name = "header")
@JsonProperty("header")
private String header;
@Json(name = "id")
@JsonProperty("id")
private String id;
@Json(name = "locked")
@JsonProperty("locked")
private Boolean locked;
@Json(name = "avatar_static")
@JsonProperty("avatar_static")
private String avatarStatic;
@Json(name = "fields")
@JsonProperty("fields")
private List<Object> fields = null;
@Json(name = "acct")
@JsonProperty("acct")
private String acct;
@Json(name = "username")
@JsonProperty("username")
private String username;
public List<Object> getEmojis() {
return emojis;
}
public void setEmojis(List<Object> emojis) {
this.emojis = emojis;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public Boolean getBot() {
return bot;
}
public void setBot(Boolean bot) {
this.bot = bot;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getHeaderStatic() {
return headerStatic;
}
public void setHeaderStatic(String headerStatic) {
this.headerStatic = headerStatic;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getFollowingCount() {
return followingCount;
}
public void setFollowingCount(Integer followingCount) {
this.followingCount = followingCount;
}
public Integer getStatusesCount() {
return statusesCount;
}
public void setStatusesCount(Integer statusesCount) {
this.statusesCount = statusesCount;
}
public Integer getFollowersCount() {
return followersCount;
}
public void setFollowersCount(Integer followersCount) {
this.followersCount = followersCount;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Boolean getLocked() {
return locked;
}
public void setLocked(Boolean locked) {
this.locked = locked;
}
public String getAvatarStatic() {
return avatarStatic;
}
public void setAvatarStatic(String avatarStatic) {
this.avatarStatic = avatarStatic;
}
public List<Object> getFields() {
return fields;
}
public void setFields(List<Object> fields) {
this.fields = fields;
}
public String getAcct() {
return acct;
}
public void setAcct(String acct) {
this.acct = acct;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@ -1,11 +1,11 @@
package ctbrec.ui.news;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Version;
import ctbrec.io.HttpException;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.tabs.TabSelectionListener;
@ -15,6 +15,7 @@ import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.layout.VBox;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import org.json.JSONObject;
@ -24,12 +25,15 @@ import java.util.Objects;
import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.io.HttpConstants.USER_AGENT;
@Slf4j
public class NewsTab extends Tab implements TabSelectionListener {
private static final String ACCESS_TOKEN = "a2804d73a89951a22e0f8483a6fcec8943afd88b7ba17c459c095aa9e6f94fd0";
private static final String URL = "https://mastodon.cloud/api/v1/accounts/480960/statuses?limit=20&exclude_replies=true";
private final Config config;
private final VBox layout = new VBox();
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
public NewsTab(Config config) {
this.config = config;
setText("News");
@ -53,6 +57,7 @@ public class NewsTab extends Tab implements TabSelectionListener {
try (var response = CamrecApplication.httpClient.execute(request)) {
if (response.isSuccessful()) {
var body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
log.debug(body);
if (body.startsWith("[")) {
onSuccess(body);
} else if (body.startsWith("{")) {
@ -65,6 +70,7 @@ public class NewsTab extends Tab implements TabSelectionListener {
}
}
} catch (IOException e) {
log.info("Error while loading news", e);
Dialogs.showError(getTabPane().getScene(), "News", "Couldn't load news from mastodon", e);
}
}
@ -79,9 +85,7 @@ public class NewsTab extends Tab implements TabSelectionListener {
}
private void onSuccess(String body) throws IOException {
var moshi = new Moshi.Builder().build();
JsonAdapter<Status[]> statusListAdapter = moshi.adapter(Status[].class);
Status[] statusArray = Objects.requireNonNull(statusListAdapter.fromJson(body));
Status[] statusArray = mapper.readValue(body, Status[].class);
Platform.runLater(() -> {
layout.getChildren().clear();
for (Status status : statusArray) {

View File

@ -1,275 +1,71 @@
package ctbrec.ui.news;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import com.squareup.moshi.Json;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Status {
@Json(name = "pinned")
@JsonProperty("pinned")
private Boolean pinned;
@Json(name = "in_reply_to_id")
@JsonProperty("in_reply_to_id")
private Object inReplyToId;
@Json(name = "favourites_count")
@JsonProperty("favourites_count")
private Integer favouritesCount;
@Json(name = "media_attachments")
@JsonProperty("media_attachments")
private List<Object> mediaAttachments = null;
@Json(name = "created_at")
@JsonProperty("created_at")
private String createdAt;
@Json(name = "replies_count")
@JsonProperty("replies_count")
private Integer repliesCount;
@Json(name = "language")
@JsonProperty("language")
private String language;
@Json(name = "in_reply_to_account_id")
@JsonProperty("in_reply_to_account_id")
private Object inReplyToAccountId;
@Json(name = "content")
@JsonProperty("content")
private String content;
@Json(name = "reblog")
@JsonProperty("reblog")
private Object reblog;
@Json(name = "spoiler_text")
@JsonProperty("spoiler_text")
private String spoilerText;
@Json(name = "id")
@JsonProperty("id")
private String id;
@Json(name = "reblogged")
@JsonProperty("reblogged")
private Boolean reblogged;
@Json(name = "muted")
@JsonProperty("muted")
private Boolean muted;
@Json(name = "emojis")
@JsonProperty("emojis")
private List<Object> emojis = null;
@Json(name = "reblogs_count")
@JsonProperty("reblogs_count")
private Integer reblogsCount;
@Json(name = "visibility")
@JsonProperty("visibility")
private String visibility;
@Json(name = "sensitive")
@JsonProperty("sensitive")
private Boolean sensitive;
@Json(name = "uri")
@JsonProperty("uri")
private String uri;
@Json(name = "url")
@JsonProperty("url")
private String url;
@Json(name = "tags")
@JsonProperty("tags")
private List<Object> tags = null;
@Json(name = "application")
@JsonProperty("application")
private Object application;
@Json(name = "favourited")
@JsonProperty("favourited")
private Boolean favourited;
@Json(name = "mentions")
@JsonProperty("mentions")
private List<Object> mentions = null;
@Json(name = "account")
@JsonProperty("account")
private Account account;
@Json(name = "card")
@JsonProperty("card")
private Object card;
public Boolean getPinned() {
return pinned;
}
public void setPinned(Boolean pinned) {
this.pinned = pinned;
}
public Object getInReplyToId() {
return inReplyToId;
}
public void setInReplyToId(Object inReplyToId) {
this.inReplyToId = inReplyToId;
}
public Integer getFavouritesCount() {
return favouritesCount;
}
public void setFavouritesCount(Integer favouritesCount) {
this.favouritesCount = favouritesCount;
}
public List<Object> getMediaAttachments() {
return mediaAttachments;
}
public void setMediaAttachments(List<Object> mediaAttachments) {
this.mediaAttachments = mediaAttachments;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public Integer getRepliesCount() {
return repliesCount;
}
public void setRepliesCount(Integer repliesCount) {
this.repliesCount = repliesCount;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public Object getInReplyToAccountId() {
return inReplyToAccountId;
}
public void setInReplyToAccountId(Object inReplyToAccountId) {
this.inReplyToAccountId = inReplyToAccountId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Object getReblog() {
return reblog;
}
public void setReblog(Object reblog) {
this.reblog = reblog;
}
public String getSpoilerText() {
return spoilerText;
}
public void setSpoilerText(String spoilerText) {
this.spoilerText = spoilerText;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Boolean getReblogged() {
return reblogged;
}
public void setReblogged(Boolean reblogged) {
this.reblogged = reblogged;
}
public Boolean getMuted() {
return muted;
}
public void setMuted(Boolean muted) {
this.muted = muted;
}
public List<Object> getEmojis() {
return emojis;
}
public void setEmojis(List<Object> emojis) {
this.emojis = emojis;
}
public Integer getReblogsCount() {
return reblogsCount;
}
public void setReblogsCount(Integer reblogsCount) {
this.reblogsCount = reblogsCount;
}
public String getVisibility() {
return visibility;
}
public void setVisibility(String visibility) {
this.visibility = visibility;
}
public Boolean getSensitive() {
return sensitive;
}
public void setSensitive(Boolean sensitive) {
this.sensitive = sensitive;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<Object> getTags() {
return tags;
}
public void setTags(List<Object> tags) {
this.tags = tags;
}
public Object getApplication() {
return application;
}
public void setApplication(Object application) {
this.application = application;
}
public Boolean getFavourited() {
return favourited;
}
public void setFavourited(Boolean favourited) {
this.favourited = favourited;
}
public List<Object> getMentions() {
return mentions;
}
public void setMentions(List<Object> mentions) {
this.mentions = mentions;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public Object getCard() {
return card;
}
public void setCard(Object card) {
this.card = card;
}
public ZonedDateTime getCreationTime() {
String timestamp = getCreatedAt();
var instant = Instant.parse(timestamp);

View File

@ -1,52 +1,40 @@
package ctbrec.ui.settings;
import static javafx.scene.control.ButtonType.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.sites.Site;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.controls.Dialogs;
import javafx.geometry.Insets;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import lombok.extern.slf4j.Slf4j;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import static javafx.scene.control.ButtonType.NO;
import static javafx.scene.control.ButtonType.YES;
@Slf4j
public class IgnoreList extends GridPane {
private static final Logger LOG = LoggerFactory.getLogger(IgnoreList.class);
private ListView<String> ignoreListView;
private List<Site> sites;
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
public IgnoreList(List<Site> sites) {
this.sites = sites;
public IgnoreList() {
createGui();
loadIgnoredModels();
}
@ -83,16 +71,14 @@ public class IgnoreList extends GridPane {
private void removeSelectedModels() {
List<String> selectedModels = ignoreListView.getSelectionModel().getSelectedItems();
if (selectedModels.isEmpty()) {
return; // NOSONAR
} else {
if (!selectedModels.isEmpty()) {
Config.getInstance().getSettings().ignoredModels.removeAll(selectedModels);
ignoreListView.getItems().removeAll(selectedModels);
LOG.debug(Config.getInstance().getSettings().ignoredModels.toString());
log.debug(Config.getInstance().getSettings().ignoredModels.toString());
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.warn("Couldn't save config", e);
log.warn("Couldn't save config", e);
}
}
}
@ -114,12 +100,8 @@ public class IgnoreList extends GridPane {
chooser.setInitialFileName("ctbrec-ignorelist.json");
var file = chooser.showSaveDialog(null);
if (file != null) {
var moshi = new Moshi.Builder().add(Model.class, new ModelJsonAdapter(sites)).build();
Type modelListType = Types.newParameterizedType(List.class, String.class);
JsonAdapter<List<String>> adapter = moshi.adapter(modelListType);
adapter = adapter.indent(" ");
try (var out = new FileOutputStream(file)) {
String json = adapter.toJson(Config.getInstance().getSettings().ignoredModels);
String json = mapper.writeValueAsString(Config.getInstance().getSettings().ignoredModels);
out.write(json.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
Dialogs.showError(getScene(), "Couldn't export ignore list", e.getLocalizedMessage(), e);
@ -132,12 +114,10 @@ public class IgnoreList extends GridPane {
chooser.setTitle("Import ignore list");
var file = chooser.showOpenDialog(null);
if (file != null) {
var moshi = new Moshi.Builder().add(Model.class, new ModelJsonAdapter(sites)).build();
Type modelListType = Types.newParameterizedType(List.class, String.class);
JsonAdapter<List<String>> adapter = moshi.adapter(modelListType);
try {
byte[] fileContent = Files.readAllBytes(file.toPath());
List<String> ignoredModels = adapter.fromJson(new String(fileContent, StandardCharsets.UTF_8));
String fileContent = Files.readString(file.toPath());
List<String> ignoredModels = mapper.readValue(fileContent, new TypeReference<>() {
});
var confirmed = true;
if (!Config.getInstance().getSettings().ignoredModels.isEmpty()) {
var msg = "This will replace the existing ignore list! Continue?";

View File

@ -1,6 +1,7 @@
package ctbrec.ui.settings;
import ctbrec.Config;
import ctbrec.io.json.mapper.PostProcessorMapper;
import ctbrec.recorder.postprocessing.*;
import ctbrec.ui.controls.Dialogs;
import javafx.beans.property.SimpleBooleanProperty;
@ -16,11 +17,14 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.mapstruct.factory.Mappers;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class PostProcessingStepPanel extends GridPane {
@ -64,7 +68,11 @@ public class PostProcessingStepPanel extends GridPane {
edit = createEditButton();
var buttons = new VBox(5, add, edit, up, down, remove);
stepList = FXCollections.observableList(config.getSettings().postProcessors);
List<PostProcessor> postProcessors = config.getSettings().postProcessors
.stream()
.map(Mappers.getMapper(PostProcessorMapper.class)::toPostProcessor)
.collect(Collectors.toList()); // NOSONAR - toList returns an unmodifiable list
stepList = FXCollections.observableList(postProcessors);
stepList.addListener((ListChangeListener<PostProcessor>) change -> safelySaveConfig());
stepView = new TableView<>(stepList);
stepView.setEditable(true);
@ -107,6 +115,9 @@ public class PostProcessingStepPanel extends GridPane {
private void safelySaveConfig() {
try {
config.getSettings().postProcessors = stepList.stream()
.map(Mappers.getMapper(PostProcessorMapper.class)::toDto)
.collect(Collectors.toList()); // NOSONAR - toList returns an unmodifiable list
config.save();
} catch (IOException e) {
Dialogs.showError(getScene(), "Couldn't save configuration", "An error occurred while saving the configuration", e);
@ -114,7 +125,7 @@ public class PostProcessingStepPanel extends GridPane {
}
private Button createUpButton() {
var button = createButton("\u25B4", "Move step up");
var button = createButton("", "Move step up");
button.setOnAction(evt -> {
int idx = stepView.getSelectionModel().getSelectedIndex();
PostProcessor selectedItem = stepView.getSelectionModel().getSelectedItem();
@ -126,7 +137,7 @@ public class PostProcessingStepPanel extends GridPane {
}
private Button createDownButton() {
var button = createButton("\u25BE", "Move step down");
var button = createButton("", "Move step down");
button.setOnAction(evt -> {
int idx = stepView.getSelectionModel().getSelectedIndex();
PostProcessor selectedItem = stepView.getSelectionModel().getSelectedItem();
@ -197,7 +208,7 @@ public class PostProcessingStepPanel extends GridPane {
}
private Button createEditButton() {
var button = createButton("\u270E", "Edit selected step");
var button = createButton("", "Edit selected step");
button.setOnAction(evt -> {
PostProcessor selectedItem = stepView.getSelectionModel().getSelectedItem();
PostProcessingDialogFactory.openEditDialog(selectedItem, getScene(), stepList);

View File

@ -203,7 +203,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private void createGui() {
var postProcessingStepPanel = new PostProcessingStepPanel(config);
var variablesHelpButton = createHelpButton("Variables", "http://localhost:5689/docs/PostProcessing.md#variables");
ignoreList = new IgnoreList(sites);
ignoreList = new IgnoreList();
List<Category> siteCategories = new ArrayList<>();
for (Site site : sites) {
ofNullable(SiteUiFactory.getUi(site)).map(SiteUI::getConfigUI).map(ConfigUI::createConfigPanel)

View File

@ -1,40 +1,24 @@
package ctbrec.ui.tabs;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.event.EventBusHolder;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.sites.SiteUtil;
import ctbrec.ui.ShutdownListener;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.event.PlayerStartedEvent;
import ctbrec.ui.io.json.dto.PlayerStartedEventDto;
import ctbrec.ui.io.json.mapper.PlayerStartedEventMapper;
import ctbrec.ui.menu.ModelMenuContributor;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@ -42,36 +26,38 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
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.input.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.*;
@Slf4j
public class RecentlyWatchedTab extends Tab implements ShutdownListener {
private static final Logger LOG = LoggerFactory.getLogger(RecentlyWatchedTab.class);
private ObservableList<PlayerStartedEvent> filteredModels = FXCollections.observableArrayList();
private ObservableList<PlayerStartedEvent> observableModels = FXCollections.observableArrayList();
private TableView<PlayerStartedEvent> table = new TableView<>();
private final ObservableList<PlayerStartedEvent> filteredModels = FXCollections.observableArrayList();
private final ObservableList<PlayerStartedEvent> observableModels = FXCollections.observableArrayList();
private final TableView<PlayerStartedEvent> table = new TableView<>();
private final ReentrantLock lock = new ReentrantLock();
private final Recorder recorder;
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
private ContextMenu popup;
private ReentrantLock lock = new ReentrantLock();
private Recorder recorder;
private List<Site> sites;
public RecentlyWatchedTab(Recorder recorder, List<Site> sites) {
@ -91,7 +77,7 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
var filterInput = new SearchBox(false);
filterInput.setPromptText("Filter");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
filterInput.textProperty().addListener((observableValue, oldValue, newValue) -> {
String filter = filterInput.getText();
lock.lock();
try {
@ -186,7 +172,7 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
var sb = new StringBuilder();
for (TableColumn<PlayerStartedEvent, ?> tc : table.getColumns()) {
Object cellData = tc.getCellData(i);
if(cellData != null) {
if (cellData != null) {
var content = cellData.toString();
sb.append(content).append(' ');
}
@ -195,12 +181,12 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
var tokensMissing = false;
for (String token : tokens) {
if(!searchText.toLowerCase().contains(token.toLowerCase())) {
if (!searchText.toLowerCase().contains(token.toLowerCase())) {
tokensMissing = true;
break;
}
}
if(tokensMissing) {
if (tokensMissing) {
PlayerStartedEvent sessionState = table.getItems().get(i);
filteredModels.add(sessionState);
}
@ -221,9 +207,9 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
ContextMenu menu = new CustomMouseBehaviorContextMenu();
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
.afterwards(table::refresh)
.contributeToMenu(selectedModels, menu);
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
.afterwards(table::refresh)
.contributeToMenu(selectedModels, menu);
menu.getItems().add(new SeparatorMenuItem());
var delete = new MenuItem("Delete");
@ -267,7 +253,7 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
var selectedModel = table.getSelectionModel().getSelectedItem().getModel();
if(selectedModel != null) {
if (selectedModel != null) {
new PlayAction(table, selectedModel).execute();
}
}
@ -277,37 +263,29 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
}
private void saveHistory() throws IOException {
var moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.build();
Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
String json = adapter.indent(" ").toJson(observableModels);
String json = mapper.writeValueAsString(observableModels.stream().map(Mappers.getMapper(PlayerStartedEventMapper.class)::toDto).toList());
var recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
LOG.debug("Saving recently watched models to {}", recentlyWatched.getAbsolutePath());
log.debug("Saving recently watched models to {}", recentlyWatched.getAbsolutePath());
Files.createDirectories(recentlyWatched.getParentFile().toPath());
Files.write(recentlyWatched.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
Files.writeString(recentlyWatched.toPath(), json, CREATE, WRITE, TRUNCATE_EXISTING);
}
private void loadHistory() {
var recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
if(!recentlyWatched.exists()) {
if (!recentlyWatched.exists()) {
return;
}
LOG.debug("Loading recently watched models from {}", recentlyWatched.getAbsolutePath());
var moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.build();
Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
log.debug("Loading recently watched models from {}", recentlyWatched.getAbsolutePath());
try {
List<PlayerStartedEvent> fromJson = adapter.fromJson(Files.readString(recentlyWatched.toPath(), UTF_8));
observableModels.addAll(fromJson);
List<PlayerStartedEventDto> fromJson = mapper.readValue(Files.readString(recentlyWatched.toPath(), UTF_8), new TypeReference<List<PlayerStartedEventDto>>() {
});
observableModels.addAll(fromJson.stream()
.map(Mappers.getMapper(PlayerStartedEventMapper.class)::toEvent)
.toList());
observableModels.forEach(evt -> SiteUtil.getSiteForModel(sites, evt.getModel()).ifPresent(evt.getModel()::setSite));
} catch (IOException e) {
LOG.error("Couldn't load recently watched models", e);
log.error("Couldn't load recently watched models", e);
}
}
@ -316,7 +294,7 @@ public class RecentlyWatchedTab extends Tab implements ShutdownListener {
try {
saveHistory();
} catch (IOException e) {
LOG.error("Couldn't safe recently watched models", e);
log.error("Couldn't safe recently watched models", e);
}
}
}

View File

@ -8,7 +8,6 @@ import ctbrec.io.UrlUtil;
import ctbrec.notes.ModelNotesService;
import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RecordingPinnedException;
import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.postprocessing.PostProcessingContext;
import ctbrec.ui.*;
@ -844,7 +843,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown
try {
recorder.delete(r.getDelegate());
deleted.add(r);
} catch (RecordingPinnedException | IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
} catch (Exception e1) {
exceptions.add(e1);
LOG.error("Error while deleting recording", e1);
}

View File

@ -273,6 +273,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
try {
List<Model> models = ModelImportExport.importFrom(target, sites, config);
importModelList(models);
portraitCache.invalidateAll();
} catch (IOException e) {
String msg = "An error occurred while importing the model list";
Dialogs.showError(getTabPane().getScene(), "Import models", msg, e);

View File

@ -1,32 +1,36 @@
package ctbrec.ui.tabs.recorded;
import com.squareup.moshi.*;
import com.squareup.moshi.JsonReader.Token;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.image.LocalPortraitStore;
import ctbrec.image.PortraitStore;
import ctbrec.image.RemotePortraitStore;
import ctbrec.io.*;
import ctbrec.io.HttpClient;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.io.json.mapper.MappingException;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.sites.Site;
import okio.Buffer;
import okio.Okio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.sites.SiteUtil;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONWriter;
import org.mapstruct.factory.Mappers;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
import static com.squareup.moshi.JsonReader.Token.*;
@Slf4j
public class ModelImportExport {
private static final Logger LOG = LoggerFactory.getLogger(ModelImportExport.class);
enum ExportIncludes {
NOTES,
@ -34,6 +38,8 @@ public class ModelImportExport {
PORTRAITS
}
private static final ObjectMapper mapper = ObjectMapperFactory.getMapper();
record ExportOptions(Set<ExportIncludes> includes, File targetFile) {
}
@ -41,98 +47,98 @@ public class ModelImportExport {
}
public static void exportTo(List<Model> models, Set<ModelGroup> groups, Config config, ExportOptions exportOptions) throws IOException {
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.add(LocalTime.class, new LocalTimeJsonAdapter())
.build();
JsonAdapter<Map<String, String>> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class));
JsonAdapter<List<Model>> modelListAdapter = moshi.adapter(Types.newParameterizedType(List.class, Model.class));
JsonAdapter<Set<ModelGroup>> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class));
StringBuilder sb = new StringBuilder();
JSONWriter writer = new JSONWriter(sb);
try (JsonWriter writer = JsonWriter.of(Okio.buffer(Okio.sink(exportOptions.targetFile())))) {
writer.setIndent(" ");
writer.beginObject();
writer.name("models");
modelListAdapter.toJson(writer, models);
if (exportOptions.includes().contains(ExportIncludes.NOTES)) {
writer.name("notes");
notesAdapter.toJson(writer, config.getSettings().modelNotes);
}
if (exportOptions.includes().contains(ExportIncludes.GROUPS)) {
writer.name("groups");
modelGroupAdapter.toJson(writer, groups);
}
if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
var portraits = config.getSettings().modelPortraits;
PortraitStore portraitLoader;
if (config.getSettings().localRecording) {
portraitLoader = new LocalPortraitStore(config);
} else {
var httpClient = new HttpClient("camrec", config) {
@Override
public boolean login() {
return false;
}
};
portraitLoader = new RemotePortraitStore(httpClient, config);
}
if (portraits != null && !portraits.isEmpty()) {
writer.name("portraits");
writer.beginArray();
for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
String modelUrl = entry.getKey();
String portraitId = entry.getValue();
Optional<byte[]> portrait = portraitLoader.loadModelPortraitByModelUrl(modelUrl);
if (portrait.isPresent()) {
writer.beginObject();
writer.name("url").value(modelUrl);
writer.name("id").value(portraitId);
writer.name("data").value(Base64.getEncoder().encodeToString(portrait.get()));
writer.endObject();
}
writer.object();
writer.key("models");
writer.array();
var modelArray = models.stream()
.map(Mappers.getMapper(ModelMapper.class)::toDto)
.map(dto -> {
try {
return mapper.writeValueAsString(dto);
} catch (JsonProcessingException e) {
log.error("Error while serializing model {}", dto);
throw new MappingException(e);
}
writer.endArray();
}
}
writer.endObject();
})
.collect(Collectors.joining(","));
sb.append(modelArray);
writer.endArray();
if (exportOptions.includes().contains(ExportIncludes.NOTES)) {
writer.key("notes");
writer.value(config.getSettings().modelNotes);
}
if (exportOptions.includes().contains(ExportIncludes.GROUPS)) {
writer.key("groups");
writer.array();
var groupArray = groups
.stream().map(grp -> {
try {
return mapper.writeValueAsString(grp);
} catch (JsonProcessingException e) {
log.error("Error while serializing model group {}", grp);
throw new MappingException(e);
}
})
.collect(Collectors.joining(","));
sb.append(groupArray);
writer.endArray();
}
if (exportOptions.includes().contains(ExportIncludes.PORTRAITS)) {
var portraits = config.getSettings().modelPortraits;
PortraitStore portraitLoader;
if (config.getSettings().localRecording) {
portraitLoader = new LocalPortraitStore(config);
} else {
var httpClient = new HttpClient("camrec", config) {
@Override
public boolean login() {
return false;
}
};
portraitLoader = new RemotePortraitStore(httpClient, config);
}
if (portraits != null && !portraits.isEmpty()) {
writer.key("portraits");
writer.array();
for (Map.Entry<String, String> entry : config.getSettings().modelPortraits.entrySet()) {
String modelUrl = entry.getKey();
String portraitId = entry.getValue();
Optional<byte[]> portrait = portraitLoader.loadModelPortraitByModelUrl(modelUrl);
if (portrait.isPresent()) {
writer.object();
writer.key("url").value(modelUrl);
writer.key("id").value(portraitId);
writer.key("data").value(Base64.getEncoder().encodeToString(portrait.get()));
writer.endObject();
}
}
writer.endArray();
}
}
writer.endObject();
Files.writeString(exportOptions.targetFile().toPath(), new JSONObject(sb.toString()).toString(2));
}
public static List<Model> importFrom(File target, List<Site> sites, Config config) throws IOException {
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.add(LocalTime.class, new LocalTimeJsonAdapter())
.build();
JsonAdapter<Model> modelAdapter = moshi.adapter(Model.class);
JsonAdapter<Map<String, String>> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class));
JsonAdapter<Set<ModelGroup>> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class));
List<Model> models = null;
String json = Files.readString(target.toPath(), StandardCharsets.UTF_8);
try (Buffer buffer = new Buffer()) {
JsonReader reader = JsonReader.of(buffer.writeUtf8(json));
reader.setLenient(true);
reader.beginObject();
while (reader.hasNext()) {
var next = reader.nextName();
switch (next) {
case "models" -> models = readModels(modelAdapter, reader);
case "notes" -> importNotes(reader, notesAdapter, config);
case "groups" -> importGroups(reader, modelGroupAdapter, config);
case "portraits" -> importPortraits(reader, config);
default -> LOG.warn("Element {} unknown", next);
}
}
reader.endObject();
JSONObject json = new JSONObject(Files.readString(target.toPath(), StandardCharsets.UTF_8));
List<Model> models = readModels(json.getJSONArray("models"));
models.forEach(m -> SiteUtil.getSiteForModel(sites, m).ifPresent(m::setSite));
if (json.has("notes")) {
importNotes(json.getJSONObject("notes"), config);
}
if (json.has("groups")) {
importGroups(json.getJSONArray("groups"), config);
}
if (json.has("portraits")) {
importPortraits(json.getJSONArray("portraits"), config);
}
return models;
}
private static void importPortraits(JsonReader reader, Config config) throws IOException {
private static void importPortraits(JSONArray portraits, Config config) throws IOException {
PortraitStore portraitStore;
if (config.getSettings().localRecording) {
portraitStore = new LocalPortraitStore(config);
@ -145,89 +151,50 @@ public class ModelImportExport {
};
portraitStore = new RemotePortraitStore(httpClient, config);
}
reader.beginArray();
while (reader.hasNext()) {
reader.beginObject();
String url = null;
String id = null;
String dataBase64 = null;
while (reader.hasNext()) {
var name = reader.nextName();
switch (name) {
case "url" -> url = reader.nextString();
case "id" -> id = reader.nextString();
case "data" -> dataBase64 = reader.nextString();
default -> {
LOG.warn("Portrait element {} unknown", name);
reader.skipValue();
}
}
}
portraitStore.writePortrait(id, Base64.getDecoder().decode(dataBase64));
config.getSettings().modelPortraits.put(url, id);
reader.endObject();
}
reader.endArray();
}
private static void importGroups(JsonReader reader, JsonAdapter<Set<ModelGroup>> modelGroupAdapter, Config config) throws IOException {
var groups = modelGroupAdapter.fromJson(reader);
if (groups != null) {
config.getSettings().modelGroups.addAll(groups);
for (int i = 0; i < portraits.length(); i++) {
JSONObject portrait = portraits.getJSONObject(i);
String url = portrait.getString("url");
String id = portrait.getString("id");
String dataBase64 = portrait.getString("data");
portraitStore.writePortrait(url, Base64.getDecoder().decode(dataBase64));
}
}
private static void importNotes(JsonReader reader, JsonAdapter<Map<String, String>> notesAdapter, Config config) throws IOException {
var notes = notesAdapter.fromJson(reader);
if (notes != null) {
config.getSettings().modelNotes.putAll(notes);
}
}
private static List<Model> readModels(JsonAdapter<Model> modelAdapter, JsonReader reader) throws IOException {
List<Model> result = new LinkedList<>();
reader.beginArray();
while (reader.hasNext()) {
private static void importGroups(JSONArray groups, Config config) {
for (int i = 0; i < groups.length(); i++) {
JSONObject group = groups.getJSONObject(i);
try {
Token token = reader.peek();
if (token == BEGIN_OBJECT) {
Model model = modelAdapter.fromJson(reader);
result.add(model);
} else {
skipToNextModel(reader);
}
} catch (Exception e) {
LOG.error("Couldn't parse model json", e);
ModelGroup modelGroup = mapper.readValue(group.toString(), ModelGroup.class);
config.getSettings().modelGroups.add(modelGroup);
} catch (JsonProcessingException e) {
log.error("Error while deserializing model group {}", group);
}
}
}
private static void importNotes(JSONObject notes, Config config) {
var modelNotes = new HashMap<String, String>();
JSONArray urls = notes.names();
for (int i = 0; i < urls.length(); i++) {
String url = urls.getString(i);
String note = notes.getString(url);
modelNotes.put(url, note);
}
config.getSettings().modelNotes.putAll(modelNotes);
}
private static List<Model> readModels(JSONArray models) {
List<Model> result = new LinkedList<>();
for (int i = 0; i < models.length(); i++) {
JSONObject model = models.getJSONObject(i);
try {
ModelDto dto = mapper.readValue(model.toString(), ModelDto.class);
result.add(Mappers.getMapper(ModelMapper.class).toModel(dto));
} catch (Exception e) {
log.error("Error while deserializing model {}", model.toString(2), e);
}
}
reader.endArray();
return result;
}
private static void skipToNextModel(JsonReader reader) throws IOException {
while (true) {
Token token = reader.peek();
if (token == BEGIN_OBJECT) {
reader.beginObject();
} else if (token == END_OBJECT) {
reader.endObject();
if (reader.getPath().matches("\\$\\.models\\[\\d+]")) {
break;
}
} else if (token == NAME) {
reader.skipName();
Token next = reader.peek();
if (List.of(NULL, NUMBER, STRING, BOOLEAN).contains(next)) {
reader.skipValue();
}
} else if (token == BEGIN_ARRAY) {
reader.beginArray();
} else if (token == END_ARRAY) {
reader.endArray();
} else if (List.of(NULL, NUMBER, STRING, BOOLEAN).contains(token)) {
reader.skipValue();
}
}
}
}

View File

@ -29,14 +29,22 @@
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>com.iheartradio.m3u8</groupId>
<artifactId>open-m3u8</artifactId>
@ -96,6 +104,32 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -1,7 +1,6 @@
package ctbrec;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.RecordingProcess;
@ -14,10 +13,7 @@ import okhttp3.Response;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.ExecutionException;
import static ctbrec.io.HttpConstants.USER_AGENT;
@ -32,7 +28,7 @@ public abstract class AbstractModel implements Model {
private String description;
private List<String> tags = new ArrayList<>();
private int streamUrlIndex = -1;
private int priority = -1;
private int priority = new Settings().defaultPriority;
private boolean suspended = false;
private boolean markedForLaterRecording = false;
protected transient Site site;
@ -129,12 +125,12 @@ public abstract class AbstractModel implements Model {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
public void readSiteSpecificData(Map<String, String> data) {
// noop default implementation, can be overriden by concrete models
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
public void writeSiteSpecificData(Map<String, String> data) {
// noop default implementation, can be overriden by concrete models
}
@ -222,9 +218,6 @@ public abstract class AbstractModel implements Model {
@Override
public int getPriority() {
if (priority == -1) {
priority = Config.getInstance().getSettings().defaultPriority;
}
return priority;
}

View File

@ -1,34 +1,30 @@
package ctbrec;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.io.*;
import ctbrec.recorder.postprocessing.PostProcessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.io.IoUtils;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.sites.Site;
import ctbrec.sites.chaturbate.ChaturbateModel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.*;
// TODO don't use singleton pattern
@Slf4j
public class Config {
private static final Logger LOG = LoggerFactory.getLogger(Config.class);
private static final String SYSPROP_CONFIG_DIR = "ctbrec.config.dir";
private static final String V_4_7_5 = "4.7.5";
@ -45,6 +41,8 @@ public class Config {
private boolean savingDisabled = false;
public static final String RECORDING_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss_SSS";
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
public Config(List<Site> sites) throws IOException {
this.sites = sites;
@ -70,7 +68,7 @@ public class Config {
File src = currentConfigDir;
if (src.exists()) {
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);
deleteOldBackups(currentConfigDir);
}
@ -83,10 +81,10 @@ public class Config {
for (int i = 0; i < backupDirectories.length - 5; i++) {
File dirToDelete = backupDirectories[i];
try {
LOG.info("Delete old config backup {}", dirToDelete);
log.info("Delete old config backup {}", dirToDelete);
IoUtils.deleteDirectory(dirToDelete);
} catch (IOException e) {
LOG.error("Couldn't delete old config backup {}. You might have to delete it manually.", dirToDelete, e);
log.error("Couldn't delete old config backup {}. You might have to delete it manually.", dirToDelete, e);
}
}
}
@ -118,8 +116,8 @@ public class Config {
return;
}
if (!Objects.equals(previousVersion, currentVersion)) {
LOG.debug("Version update {} -> {}", previousVersion, currentVersion);
LOG.debug("Copying config from {} to {}", src, target);
log.debug("Version update {} -> {}", previousVersion, currentVersion);
log.debug("Copying config from {} to {}", src, target);
FileUtils.copyDirectory(src, target, pathname -> !(pathname.toString().contains("minimal-browser") && pathname.toString().contains("Cache")), true);
}
}
@ -138,28 +136,20 @@ public class Config {
}
private void load() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(PostProcessor.class, new PostProcessorJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.add(LocalTime.class, new LocalTimeJsonAdapter())
.build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
File configFile = new File(configDir, filename);
LOG.info("Loading config from {}", configFile.getAbsolutePath());
log.info("Loading config from {}", configFile.getAbsolutePath());
if (configFile.exists()) {
try {
byte[] fileContent = Files.readAllBytes(configFile.toPath());
if (fileContent[0] == -17 && fileContent[1] == -69 && fileContent[2] == -65) {
// found BOM (byte order mark)
LOG.debug("Removing BOM from config file");
log.debug("Removing BOM from config file");
fileContent[0] = ' ';
fileContent[1] = ' ';
fileContent[2] = ' ';
}
String json = new String(fileContent, UTF_8).trim();
settings = Objects.requireNonNull(adapter.fromJson(json));
settings = Objects.requireNonNull(mapper.readValue(json, Settings.class));
settings.httpTimeout = Math.max(settings.httpTimeout, 10_000);
if (settings.recordingsDir.endsWith("/")) {
settings.recordingsDir = settings.recordingsDir.substring(0, settings.recordingsDir.length() - 1);
@ -172,7 +162,7 @@ public class Config {
throw e;
}
} else {
LOG.error("Config file does not exist. Falling back to default values.");
log.error("Config file does not exist. Falling back to default values.");
settings = OS.getDefaultSettings();
}
for (Site site : sites) {
@ -183,66 +173,6 @@ public class Config {
}
private void migrateOldSettings() {
// 5.0.0
convertChaturbateModelNamesToLowerCase();
}
private void convertChaturbateModelNamesToLowerCase() {
final String CTB = "chaturbate.com";
// convert model notes
Map<String, String> convertedModelNotes = new HashMap<>();
getSettings().modelNotes.forEach((key, value) -> {
if (key.contains(CTB)) {
convertedModelNotes.put(key.toLowerCase(), value);
} else {
convertedModelNotes.put(key, value);
}
});
getSettings().modelNotes.clear();
getSettings().modelNotes.putAll(convertedModelNotes);
// convert model portraits
Map<String, String> convertedModelPortraits = new HashMap<>();
getSettings().modelPortraits.forEach((key, value) -> {
if (key.contains(CTB)) {
convertedModelPortraits.put(key.toLowerCase(), value);
} else {
convertedModelPortraits.put(key, value);
}
});
getSettings().modelPortraits.clear();
getSettings().modelPortraits.putAll(convertedModelPortraits);
// convert model groups
getSettings().modelGroups.forEach(mg -> mg.setModelUrls(mg.getModelUrls().stream()
.filter(Objects::nonNull)
.map(url -> url.contains(CTB) ? url.toLowerCase() : url)
.collect(Collectors.toList()))); // NOSONAR - has to be mutable
// convert ignored models
getSettings().ignoredModels = getSettings().ignoredModels.stream()
.filter(Objects::nonNull)
.map(url -> url.contains(CTB) ? url.toLowerCase() : url)
.collect(Collectors.toList()); // NOSONAR - has to be mutable
// change the model objects
getSettings().models.stream()
.filter(ChaturbateModel.class::isInstance)
.filter(m -> m.getUrl() != null)
.forEach(m -> {
m.setDisplayName(m.getName());
m.setName(m.getName().toLowerCase());
m.setUrl(m.getUrl().toLowerCase());
});
getSettings().recordLater.stream()
.filter(ChaturbateModel.class::isInstance)
.filter(m -> m.getUrl() != null)
.forEach(m -> {
m.setDisplayName(m.getName());
m.setName(m.getName().toLowerCase());
m.setUrl(m.getUrl().toLowerCase());
});
}
public static synchronized void init(List<Site> sites) throws IOException {
@ -267,17 +197,9 @@ public class Config {
if (savingDisabled) {
return;
}
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter())
.add(PostProcessor.class, new PostProcessorJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.add(LocalTime.class, new LocalTimeJsonAdapter())
.build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
String json = adapter.toJson(settings);
String json = mapper.writeValueAsString(settings);
File configFile = new File(configDir, filename);
LOG.debug("Saving config to {}", configFile.getAbsolutePath());
log.debug("Saving config to {}", configFile.getAbsolutePath());
Files.createDirectories(configDir.toPath());
Files.writeString(configFile.toPath(), json, CREATE, WRITE, TRUNCATE_EXISTING);
}

View File

@ -2,8 +2,6 @@ package ctbrec;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
@ -14,6 +12,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
public interface Model extends Comparable<Model>, Serializable {
@ -116,9 +115,9 @@ public interface Model extends Comparable<Model>, Serializable {
Site getSite();
void writeSiteSpecificData(JsonWriter writer) throws IOException;
void writeSiteSpecificData(Map<String, String> data);
void readSiteSpecificData(JsonReader reader) throws IOException;
void readSiteSpecificData(Map<String, String> data);
boolean isSuspended();

View File

@ -6,6 +6,7 @@ import ctbrec.io.IoUtils;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.VideoLengthDetector;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
@ -28,6 +29,7 @@ import static ctbrec.Recording.State.*;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
@Slf4j
@NoArgsConstructor
public class Recording implements Serializable {
private String id;
@ -48,7 +50,11 @@ public class Recording implements Serializable {
private File postProcessedFile = null;
private int selectedResolution = -1;
private long lastSizeUpdate = 0;
private String recordingsDir;
public Recording(String recordingsDir) {
this.recordingsDir = recordingsDir;
}
public enum State {
RECORDING("recording"),
@ -117,7 +123,6 @@ public class Recording implements Serializable {
public File getAbsoluteFile() {
if (absoluteFile == null) {
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
File recordingsFile = new File(recordingsDir, path);
absoluteFile = recordingsFile;
}
@ -140,6 +145,9 @@ public class Recording implements Serializable {
}
public long getSizeInByte() {
if (sizeInByte == -1) {
refresh();
}
return sizeInByte;
}
@ -211,6 +219,10 @@ public class Recording implements Serializable {
return selectedResolution;
}
public void setSelectedResolution(int selectedResolution) {
this.selectedResolution = selectedResolution;
}
public Duration getLength() {
File ppFile = getPostProcessedFile();
if (ppFile.isDirectory()) {

View File

@ -7,6 +7,8 @@ import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;
@ -19,6 +21,7 @@ public class RecordingSizeMonitor {
protected final Map<WatchKey, Recording> recordingByKey;
protected final Map<Recording, List<WatchKey>> keysByRecording;
protected final Set<Path> registeredPaths;
private final Lock lock = new ReentrantLock();
public RecordingSizeMonitor() throws IOException {
this.service = FileSystems.getDefault().newWatchService();
@ -54,13 +57,18 @@ public class RecordingSizeMonitor {
});
}
private void register(Path path, Recording rec) throws IOException {
if (!registeredPaths.contains(path)) {
WatchKey key = path.register(service, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
keys.put(key, path);
recordingByKey.put(key, rec);
keysByRecording.computeIfAbsent(rec, r -> new ArrayList<>()).add(key);
registeredPaths.add(path);
private synchronized void register(Path path, Recording rec) throws IOException {
lock.lock();
try {
if (!registeredPaths.contains(path)) {
WatchKey key = path.register(service, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
keys.put(key, path);
recordingByKey.put(key, rec);
keysByRecording.computeIfAbsent(rec, r -> new ArrayList<>()).add(key);
registeredPaths.add(path);
}
} finally {
lock.unlock();
}
}
@ -75,15 +83,20 @@ public class RecordingSizeMonitor {
}
public void uninstall(Recording rec) {
List<WatchKey> keysForRecording = this.keysByRecording.getOrDefault(rec, Collections.emptyList());
keysForRecording.forEach(key -> {
Path path = keys.get(key);
key.cancel();
keys.remove(key);
recordingByKey.remove(key);
registeredPaths.remove(path);
});
this.keysByRecording.remove(rec);
lock.lock();
try {
List<WatchKey> keysForRecording = this.keysByRecording.getOrDefault(rec, Collections.emptyList());
keysForRecording.forEach(key -> {
Path path = keys.get(key);
key.cancel();
keys.remove(key);
recordingByKey.remove(key);
registeredPaths.remove(path);
});
this.keysByRecording.remove(rec);
} finally {
lock.unlock();
}
}
public void processEvents() {
@ -97,6 +110,7 @@ public class RecordingSizeMonitor {
continue;
}
lock.lock();
Path dir = keys.get(key);
if (dir == null) {
log.error("WatchKey not recognized");
@ -136,6 +150,7 @@ public class RecordingSizeMonitor {
} catch (Exception e) {
log.error("Error while processing file system events", e);
} finally {
lock.unlock();
if (key != null) {
key.reset();
}

View File

@ -1,7 +1,8 @@
package ctbrec;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.io.json.dto.PostProcessorDto;
import java.io.File;
import java.time.LocalTime;
@ -115,12 +116,12 @@ public class Settings {
@Deprecated
public int minimumLengthInSeconds = 0;
public long minimumSpaceLeftInBytes = 0;
public List<Model> models = new ArrayList<>();
public List<ModelDto> models = new ArrayList<>();
public Set<ModelGroup> modelGroups = new HashSet<>();
public Map<String, String> modelNotes = new HashMap<>();
public Map<String, String> modelPortraits = new HashMap<>();
@Deprecated
public List<Model> modelsIgnored = new ArrayList<>();
public List<ModelDto> modelsIgnored = new ArrayList<>();
public boolean monitorClipboard = false;
public int onlineCheckIntervalInSecs = 60;
public boolean onlineCheckSkipsPausedModels = false;
@ -131,7 +132,7 @@ public class Settings {
public String postProcessing = "";
public int playlistRequestTimeout = 2000;
public int postProcessingThreads = 2;
public List<PostProcessor> postProcessors = new ArrayList<>();
public List<PostProcessorDto> postProcessors = new ArrayList<>();
public String proxyHost;
public String proxyPassword;
public String proxyPort;
@ -156,7 +157,7 @@ public class Settings {
public String recordingsTableSortType = "";
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
public List<Model> recordLater = new ArrayList<>();
public List<ModelDto> recordLater = new ArrayList<>();
public boolean recordSingleFile = false;
public long recordUntilDefaultDurationInMinutes = 24 * 60L;
public boolean removeRecordingAfterPostProcessing = false;

View File

@ -1,64 +0,0 @@
package ctbrec.io;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonReader.Token;
import com.squareup.moshi.JsonWriter;
import ctbrec.io.HttpClient.CookieContainer;
import okhttp3.Cookie;
public class CookieContainerJsonAdapter extends JsonAdapter<CookieContainer> {
private CookieJsonAdapter cookieAdapter = new CookieJsonAdapter();
@Override
public CookieContainer fromJson(JsonReader reader) throws IOException {
CookieContainer cookies = new CookieContainer();
reader.beginArray();
while(reader.hasNext()) {
reader.beginObject();
reader.nextName(); // "domain"
String domain = reader.nextString();
reader.nextName(); // "cookies"
reader.beginArray();
List<Cookie> cookieList = new ArrayList<>();
while(reader.hasNext()) {
Token token = reader.peek();
if(token == Token.END_ARRAY) {
break;
}
Cookie cookie = cookieAdapter.fromJson(reader);
cookieList.add(cookie);
}
reader.endArray();
reader.endObject();
cookies.put(domain, cookieList);
}
reader.endArray();
return cookies;
}
@Override
public void toJson(JsonWriter writer, CookieContainer cookieContainer) throws IOException {
writer.beginArray();
for (Entry<String, List<Cookie>> entry : cookieContainer.entrySet()) {
writer.beginObject();
writer.name("domain").value(entry.getKey());
writer.name("cookies");
writer.beginArray();
for (Cookie cookie : entry.getValue()) {
cookieAdapter.toJson(writer, cookie);
}
writer.endArray();
writer.endObject();
}
writer.endArray();
}
}

View File

@ -1,81 +0,0 @@
package ctbrec.io;
import java.io.IOException;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import okhttp3.Cookie;
import okhttp3.Cookie.Builder;
public class CookieJsonAdapter extends JsonAdapter<Cookie> {
@Override
public Cookie fromJson(JsonReader reader) throws IOException {
reader.beginObject();
Builder builder = new Cookie.Builder();
// domain
reader.nextName();
String domain = reader.nextString();
builder.domain(domain);
// expiresAt
reader.nextName();
builder.expiresAt(reader.nextLong());
// host only
reader.nextName();
if(reader.nextBoolean()) {
builder.hostOnlyDomain(domain);
}
// http only
reader.nextName();
if(reader.nextBoolean()) {
builder.httpOnly();
}
// name
reader.nextName();
builder.name(reader.nextString());
// path
reader.nextName();
builder.path(reader.nextString());
// persistent
reader.nextName();
if(reader.nextBoolean()) {
// noop
}
// secure
reader.nextName();
if(reader.nextBoolean()) {
builder.secure();
}
// value
reader.nextName();
builder.value(reader.nextString());
reader.endObject();
return builder.build();
}
@Override
public void toJson(JsonWriter writer, Cookie cookie) throws IOException {
writer.beginObject();
writer.name("domain").value(cookie.domain());
writer.name("expiresAt").value(cookie.expiresAt());
writer.name("hostOnly").value(cookie.hostOnly());
writer.name("httpOnly").value(cookie.httpOnly());
writer.name("name").value(cookie.name());
writer.name("path").value(cookie.path());
writer.name("persistent").value(cookie.persistent());
writer.name("secure").value(cookie.secure());
writer.name("value").value(cookie.value());
writer.endObject();
}
}

View File

@ -1,30 +0,0 @@
package ctbrec.io;
import java.io.File;
import java.io.IOException;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
public class FileJsonAdapter extends JsonAdapter<File> {
@Override
public File fromJson(JsonReader reader) throws IOException {
String path = reader.nextString();
if (path != null) {
return new File(path);
} else {
return null;
}
}
@Override
public void toJson(JsonWriter writer, File value) throws IOException {
if (value != null) {
writer.value(value.getCanonicalPath());
} else {
writer.nullValue();
}
}
}

View File

@ -1,30 +1,31 @@
package ctbrec.io;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.fasterxml.jackson.core.type.TypeReference;
import ctbrec.Config;
import ctbrec.LoggingInterceptor;
import ctbrec.Settings.ProxyType;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.io.json.dto.CookieDto;
import ctbrec.io.json.mapper.CookieMapper;
import lombok.Data;
import okhttp3.*;
import okhttp3.OkHttpClient.Builder;
import org.mapstruct.factory.Mappers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
@ -59,47 +60,47 @@ public abstract class HttpClient {
private void loadProxySettings() {
ProxyType proxyType = config.getSettings().proxyType;
switch (proxyType) {
case HTTP:
System.setProperty("http.proxyHost", config.getSettings().proxyHost);
System.setProperty("http.proxyPort", config.getSettings().proxyPort);
System.setProperty("https.proxyHost", config.getSettings().proxyHost);
System.setProperty("https.proxyPort", config.getSettings().proxyPort);
if(config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
String username = config.getSettings().proxyUser;
String password = config.getSettings().proxyPassword;
System.setProperty("http.proxyUser", username);
System.setProperty("http.proxyPassword", password);
}
break;
case SOCKS4:
System.setProperty("socksProxyVersion", "4");
System.setProperty("socksProxyHost", config.getSettings().proxyHost);
System.setProperty("socksProxyPort", config.getSettings().proxyPort);
break;
case SOCKS5:
System.setProperty("socksProxyVersion", "5");
System.setProperty("socksProxyHost", config.getSettings().proxyHost);
System.setProperty("socksProxyPort", config.getSettings().proxyPort);
if(config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
String username = config.getSettings().proxyUser;
String password = config.getSettings().proxyPassword;
Authenticator.setDefault(new SocksProxyAuth(username, password));
}
break;
case DIRECT:
default:
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyVersion");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
System.clearProperty("java.net.socks.username");
System.clearProperty("java.net.socks.password");
System.clearProperty("http.proxyUser");
System.clearProperty("http.proxyPassword");
break;
case HTTP:
System.setProperty("http.proxyHost", config.getSettings().proxyHost);
System.setProperty("http.proxyPort", config.getSettings().proxyPort);
System.setProperty("https.proxyHost", config.getSettings().proxyHost);
System.setProperty("https.proxyPort", config.getSettings().proxyPort);
if (config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
String username = config.getSettings().proxyUser;
String password = config.getSettings().proxyPassword;
System.setProperty("http.proxyUser", username);
System.setProperty("http.proxyPassword", password);
}
break;
case SOCKS4:
System.setProperty("socksProxyVersion", "4");
System.setProperty("socksProxyHost", config.getSettings().proxyHost);
System.setProperty("socksProxyPort", config.getSettings().proxyPort);
break;
case SOCKS5:
System.setProperty("socksProxyVersion", "5");
System.setProperty("socksProxyHost", config.getSettings().proxyHost);
System.setProperty("socksProxyPort", config.getSettings().proxyPort);
if (config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
String username = config.getSettings().proxyUser;
String password = config.getSettings().proxyPassword;
Authenticator.setDefault(new SocksProxyAuth(username, password));
}
break;
case DIRECT:
default:
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyVersion");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
System.clearProperty("java.net.socks.username");
System.clearProperty("java.net.socks.password");
System.clearProperty("http.proxyUser");
System.clearProperty("http.proxyPassword");
break;
}
}
@ -156,12 +157,16 @@ public abstract class HttpClient {
X509Certificate[] x509Certificates = new X509Certificate[0];
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 {
final TrustManager[] trustManagers = new TrustManager[] { x509TrustManager };
final TrustManager[] trustManagers = new TrustManager[]{x509TrustManager};
final String PROTOCOL = "TLSv1.2";
SSLContext sslContext = SSLContext.getInstance(PROTOCOL);
KeyManager[] keyManagers = null;
@ -183,18 +188,17 @@ public abstract class HttpClient {
private void persistCookies() {
try {
CookieContainer cookies = new CookieContainer();
cookies.putAll(cookieJar.getCookies());
Moshi moshi = new Moshi.Builder()
.add(CookieContainer.class, new CookieContainerJsonAdapter())
.build();
JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent(" ");
String json = adapter.toJson(cookies);
List<CookieContainer> containers = new ArrayList<>();
cookieJar.getCookies().forEach((domain, cookieList) -> {
CookieContainer cookies = new CookieContainer();
cookies.setDomain(domain);
List<CookieDto> dtos = cookieList.stream().map(Mappers.getMapper(CookieMapper.class)::toDto).toList();
cookies.setCookies(dtos);
containers.add(cookies);
});
String json = ObjectMapperFactory.getMapper().writeValueAsString(containers);
File cookieFile = new File(config.getConfigDir(), "cookies-" + name + ".json");
try(FileOutputStream fout = new FileOutputStream(cookieFile)) {
fout.write(json.getBytes(UTF_8));
}
Files.writeString(cookieFile.toPath(), json);
} catch (Exception e) {
LOG.error("Couldn't persist cookies for {}", name, e);
}
@ -203,33 +207,29 @@ public abstract class HttpClient {
private void loadCookies() {
try {
File cookieFile = new File(config.getConfigDir(), "cookies-" + name + ".json");
if(!cookieFile.exists()) {
if (!cookieFile.exists()) {
return;
}
byte[] jsonBytes = Files.readAllBytes(cookieFile.toPath());
String json = new String(jsonBytes, UTF_8);
String json = Files.readString(cookieFile.toPath());
Map<String, List<Cookie>> cookies = cookieJar.getCookies();
Moshi moshi = new Moshi.Builder()
.add(CookieContainer.class, new CookieContainerJsonAdapter())
.build();
JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent(" ");
CookieContainer fromJson = adapter.fromJson(json);
Set<Entry<String, List<Cookie>>> entries = fromJson.entrySet();
for (Entry<String, List<Cookie>> entry : entries) {
List<Cookie> filteredCookies = entry.getValue().stream()
.filter(c -> !Objects.equals("deleted", c.value()))
List<CookieContainer> fromJson = ObjectMapperFactory.getMapper().readValue(json, new TypeReference<>() {
});
for (CookieContainer container : fromJson) {
List<Cookie> filteredCookies = container.getCookies().stream()
.filter(c -> !Objects.equals("deleted", c.getValue()))
.map(Mappers.getMapper(CookieMapper.class)::toCookie)
.collect(Collectors.toList());
cookies.put(entry.getKey(), filteredCookies);
cookies.put(container.getDomain(), filteredCookies);
}
} catch (Exception e) {
LOG.error("Couldn't load cookies for {}", name, e);
}
}
public static class CookieContainer extends HashMap<String, List<Cookie>> {
@Data
public static class CookieContainer {
private String domain;
private List<CookieDto> cookies;
}
private okhttp3.Authenticator createHttpProxyAuthenticator(String username, String password) {
@ -310,7 +310,7 @@ public abstract class HttpClient {
while ((len = gzipIn.read(b)) >= 0) {
bos.write(b, 0, len);
}
return bos.toString(StandardCharsets.UTF_8.toString());
return bos.toString(UTF_8.toString());
} else {
return Objects.requireNonNull(response.body()).string();
}

View File

@ -1,21 +0,0 @@
package ctbrec.io;
import java.io.IOException;
import java.time.Instant;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
public class InstantJsonAdapter extends JsonAdapter<Instant> {
@Override
public Instant fromJson(JsonReader reader) throws IOException {
long timeInEpochMillis = reader.nextLong();
return Instant.ofEpochMilli(timeInEpochMillis);
}
@Override
public void toJson(JsonWriter writer, Instant time) throws IOException {
writer.value(time.toEpochMilli());
}
}

View File

@ -1,21 +0,0 @@
package ctbrec.io;
import java.io.IOException;
import java.time.LocalTime;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
public class LocalTimeJsonAdapter extends JsonAdapter<LocalTime> {
@Override
public LocalTime fromJson(JsonReader reader) throws IOException {
String timeString = reader.nextString();
return LocalTime.parse(timeString);
}
@Override
public void toJson(JsonWriter writer, LocalTime time) throws IOException {
writer.value(time.toString());
}
}

View File

@ -1,134 +0,0 @@
package ctbrec.io;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonReader.Token;
import com.squareup.moshi.JsonWriter;
import ctbrec.Model;
import ctbrec.SubsequentAction;
import ctbrec.sites.Site;
import ctbrec.sites.chaturbate.ChaturbateModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
public class ModelJsonAdapter extends JsonAdapter<Model> {
private static final Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class);
private List<Site> sites;
public ModelJsonAdapter() {
}
public ModelJsonAdapter(List<Site> sites) {
this.sites = sites;
}
@Override
public Model fromJson(JsonReader reader) throws IOException {
reader.beginObject();
Object type = null;
Model model = null;
while (reader.hasNext()) {
try {
Token token = reader.peek();
if (token == Token.NAME) {
String key = reader.nextName();
if (key.equals("type")) {
type = reader.readJsonValue();
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString());
model = (Model) modelClass.getDeclaredConstructor().newInstance();
} else if (key.equals("name")) {
model.setName(reader.nextString());
} else if (key.equals("displayName")) {
model.setDisplayName(reader.nextString());
} else if (key.equals("description")) {
model.setDescription(reader.nextString());
} else if (key.equals("url")) {
model.setUrl(reader.nextString());
} else if (key.equals("priority")) {
model.setPriority(reader.nextInt());
} else if (key.equals("streamUrlIndex")) {
model.setStreamUrlIndex(reader.nextInt());
} else if (key.equals("suspended")) {
model.setSuspended(reader.nextBoolean());
} else if (key.equals("markedForLater")) {
model.setMarkedForLaterRecording(reader.nextBoolean());
} else if (key.equals("lastSeen")) {
model.setLastSeen(Instant.ofEpochMilli(reader.nextLong()));
} else if (key.equals("lastRecorded")) {
model.setLastRecorded(Instant.ofEpochMilli(reader.nextLong()));
} else if (key.equals("addedTimestamp")) {
model.setAddedTimestamp(Instant.ofEpochMilli(reader.nextLong()));
} else if (key.equals("recordUntil")) {
model.setRecordUntil(Instant.ofEpochMilli(reader.nextLong()));
} else if (key.equals("recordUntilSubsequentAction")) {
model.setRecordUntilSubsequentAction(SubsequentAction.valueOf(reader.nextString()));
} else if (key.equals("siteSpecific")) {
reader.beginObject();
try {
model.readSiteSpecificData(reader);
} catch (Exception e) {
LOG.error("Couldn't read site specific data for model {}", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
throw e;
}
reader.endObject();
}
} else {
reader.skipValue();
}
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
}
}
reader.endObject();
if (sites != null) {
for (Site site : sites) {
if (site.isSiteForModel(model)) {
model.setSite(site);
}
}
}
return model;
}
@Override
public void toJson(JsonWriter writer, Model model) throws IOException {
writer.beginObject();
writer.name("type").value(model.getClass().getName());
writeValueIfSet(writer, "name", model.getName());
writeValueIfSet(writer, "displayName", model.getDisplayName());
writeValueIfSet(writer, "description", model.getDescription());
writeValueIfSet(writer, "url", model.getUrl());
writer.name("priority").value(model.getPriority());
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
writer.name("suspended").value(model.isSuspended());
writer.name("markedForLater").value(model.isMarkedForLaterRecording());
writer.name("lastSeen").value(model.getLastSeen().toEpochMilli());
writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli());
writer.name("addedTimestamp").value(model.getAddedTimestamp().toEpochMilli());
writer.name("recordUntil").value(model.getRecordUntil().toEpochMilli());
writer.name("recordUntilSubsequentAction").value(model.getRecordUntilSubsequentAction().name());
writer.name("siteSpecific");
writer.beginObject();
model.writeSiteSpecificData(writer);
writer.endObject();
writer.endObject();
}
private void writeValueIfSet(JsonWriter writer, String name, String value) throws IOException {
if (value != null) {
writer.name(name).value(value);
}
}
}

View File

@ -1,65 +0,0 @@
package ctbrec.io;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonReader.Token;
import com.squareup.moshi.JsonWriter;
import ctbrec.recorder.postprocessing.PostProcessor;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Map.Entry;
public class PostProcessorJsonAdapter extends JsonAdapter<PostProcessor> {
@Override
public PostProcessor fromJson(JsonReader reader) throws IOException {
reader.beginObject();
Object type = null;
PostProcessor postProcessor = null;
while (reader.hasNext()) {
try {
Token token = reader.peek();
if (token == Token.NAME) {
String key = reader.nextName();
if (key.equals("type")) {
type = reader.readJsonValue();
Class<?> modelClass = Class.forName(type.toString());
postProcessor = (PostProcessor) modelClass.getDeclaredConstructor().newInstance();
} else if (key.equals("enabled")) {
postProcessor.setEnabled(reader.nextBoolean());
} else if (key.equals("config")) {
reader.beginObject();
} else {
String value = reader.nextString();
postProcessor.getConfig().put(key, value);
}
} else {
reader.skipValue();
}
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException |
NoSuchMethodException | SecurityException e) {
throw new IOException("Couldn't instantiate post-processor class [" + type + "]", e);
}
}
reader.endObject();
reader.endObject();
return postProcessor;
}
@Override
public void toJson(JsonWriter writer, PostProcessor pp) throws IOException {
writer.beginObject();
writer.name("type").value(pp.getClass().getName());
writer.name("enabled").value(pp.isEnabled());
writer.name("config");
writer.beginObject();
for (Entry<String, String> entry : pp.getConfig().entrySet()) {
writer.name(entry.getKey()).value(entry.getValue());
}
writer.endObject();
writer.endObject();
}
}

View File

@ -1,22 +0,0 @@
package ctbrec.io;
import java.io.IOException;
import java.util.UUID;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
public class UuidJSonAdapter extends JsonAdapter<UUID> {
@Override
public UUID fromJson(JsonReader reader) throws IOException {
return UUID.fromString(reader.nextString());
}
@Override
public void toJson(JsonWriter writer, UUID value) throws IOException {
writer.value(value.toString());
}
}

View File

@ -0,0 +1,25 @@
package ctbrec.io.json;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ObjectMapperFactory {
private static final ObjectMapper mapper;
static {
mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
public static ObjectMapper getMapper() {
return mapper;
}
}

View File

@ -0,0 +1,16 @@
package ctbrec.io.json.dto;
import lombok.Data;
@Data
public class CookieDto {
private String domain;
private long expiresAt;
private boolean hostOnly;
private boolean httpOnly;
private String name;
private String path;
private boolean persistent;
private boolean secure;
private String value;
}

View File

@ -0,0 +1,42 @@
package ctbrec.io.json.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import ctbrec.SubsequentAction;
import ctbrec.io.json.dto.converter.InstantToMillisConverter;
import ctbrec.io.json.dto.converter.MillisToInstantConverter;
import lombok.Data;
import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@Data
public class ModelDto {
private String type;
private String name;
private String displayName;
private String description;
private URI url;
private URI preview;
private int priority = -1;
private int streamUrlIndex = -1;
private boolean suspended = false;
private boolean bookmarked = false;
@JsonSerialize(converter = InstantToMillisConverter.class)
@JsonDeserialize(converter = MillisToInstantConverter.class)
private Instant lastSeen;
@JsonSerialize(converter = InstantToMillisConverter.class)
@JsonDeserialize(converter = MillisToInstantConverter.class)
private Instant lastRecorded;
@JsonSerialize(converter = InstantToMillisConverter.class)
@JsonDeserialize(converter = MillisToInstantConverter.class)
private Instant addedAt;
@JsonSerialize(converter = InstantToMillisConverter.class)
@JsonDeserialize(converter = MillisToInstantConverter.class)
private Instant recordUntil;
private SubsequentAction recordUntilSubsequentAction;
private Map<String, String> siteSpecific = new HashMap<>();
}

View File

@ -0,0 +1,12 @@
package ctbrec.io.json.dto;
import lombok.Data;
import java.util.Map;
@Data
public class PostProcessorDto {
private String type;
private boolean enabled;
private Map<String, String> config;
}

View File

@ -0,0 +1,28 @@
package ctbrec.io.json.dto;
import ctbrec.Recording;
import lombok.Data;
import java.io.File;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
@Data
public class RecordingDto {
private String id;
private ModelDto model;
private Instant startDate;
private Recording.State status = Recording.State.UNKNOWN;
private int progress = -1;
private long sizeInByte = -1;
private String metaDataFile;
private boolean singleFile = false;
private boolean pinned = false;
private String note;
private Set<String> associatedFiles = new HashSet<>();
private File absoluteFile = null;
private File postProcessedFile = null;
private int selectedResolution = -1;
private long lastSizeUpdate = 0;
}

View File

@ -0,0 +1,25 @@
package ctbrec.io.json.dto.converter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
import java.time.Instant;
public class InstantToMillisConverter implements Converter<Instant, Long> {
@Override
public Long convert(Instant instant) {
return instant.toEpochMilli();
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(Instant.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(long.class);
}
}

View File

@ -0,0 +1,24 @@
package ctbrec.io.json.dto.converter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
import java.time.Instant;
public class MillisToInstantConverter implements Converter<Long, Instant> {
@Override
public Instant convert(Long aLong) {
return Instant.ofEpochMilli(aLong);
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(long.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(Instant.class);
}
}

View File

@ -0,0 +1,39 @@
package ctbrec.io.json.mapper;
import ctbrec.io.json.dto.CookieDto;
import okhttp3.Cookie;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ObjectFactory;
@Mapper
public interface CookieMapper {
default Cookie toCookie(CookieDto dto) {
var builder = new Cookie.Builder()
.name(dto.getName())
.domain(dto.getDomain())
.path(dto.getPath())
.value(dto.getValue())
.expiresAt(dto.getExpiresAt());
if (dto.isHostOnly()) builder.hostOnlyDomain(dto.getDomain());
if (dto.isHttpOnly()) builder.httpOnly();
if (dto.isSecure()) builder.secure();
return builder.build();
}
@Mapping(target = "name", expression = "java(cookie.name())")
@Mapping(target = "domain", expression = "java(cookie.domain())")
@Mapping(target = "path", expression = "java(cookie.path())")
@Mapping(target = "value", expression = "java(cookie.value())")
@Mapping(target = "expiresAt", expression = "java(cookie.expiresAt())")
@Mapping(target = "hostOnly", expression = "java(cookie.hostOnly())")
@Mapping(target = "httpOnly", expression = "java(cookie.httpOnly())")
@Mapping(target = "persistent", expression = "java(cookie.persistent())")
@Mapping(target = "secure", expression = "java(cookie.secure())")
CookieDto toDto(Cookie cookie);
@ObjectFactory
default Cookie.Builder builder() {
return new Cookie.Builder();
}
}

View File

@ -0,0 +1,7 @@
package ctbrec.io.json.mapper;
public class MappingException extends RuntimeException {
public MappingException(Exception cause) {
super(cause);
}
}

View File

@ -0,0 +1,19 @@
package ctbrec.io.json.mapper;
import ctbrec.Model;
import ctbrec.io.json.dto.ModelDto;
import org.mapstruct.ObjectFactory;
public class ModelFactory {
@SuppressWarnings("unchecked")
@ObjectFactory
Model toModel(ModelDto dto) {
try {
Class<? extends Model> modelClass = (Class<? extends Model>) Class.forName(dto.getType());
return modelClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new MappingException(e);
}
}
}

View File

@ -0,0 +1,32 @@
package ctbrec.io.json.mapper;
import ctbrec.Model;
import ctbrec.io.json.dto.ModelDto;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
@Mapper(uses = {ModelFactory.class, UriMapper.class})
public interface ModelMapper {
@Mapping(target = "addedAt", source = "addedTimestamp")
@Mapping(target = "bookmarked", source = "markedForLaterRecording")
@Mapping(target = "type", expression = "java(model.getClass().getName())")
ModelDto toDto(Model model);
@Mapping(target = "addedTimestamp", source = "addedAt")
@Mapping(target = "markedForLaterRecording", source = "bookmarked")
Model toModel(ModelDto dto) throws MappingException;
@AfterMapping
default void afterToDto(Model model, @MappingTarget ModelDto dto) {
model.writeSiteSpecificData(dto.getSiteSpecific());
}
@AfterMapping
default void afterToModel(ModelDto dto, @MappingTarget Model model) {
model.readSiteSpecificData(dto.getSiteSpecific());
}
}

View File

@ -0,0 +1,19 @@
package ctbrec.io.json.mapper;
import ctbrec.io.json.dto.PostProcessorDto;
import ctbrec.recorder.postprocessing.PostProcessor;
import org.mapstruct.ObjectFactory;
public class PostProcessorFactory {
@SuppressWarnings("unchecked")
@ObjectFactory
PostProcessor toPostProcessor(PostProcessorDto dto) {
try {
Class<? extends PostProcessor> ppClass = (Class<? extends PostProcessor>) Class.forName(dto.getType());
return ppClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new MappingException(e);
}
}
}

View File

@ -0,0 +1,14 @@
package ctbrec.io.json.mapper;
import ctbrec.io.json.dto.PostProcessorDto;
import ctbrec.recorder.postprocessing.PostProcessor;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(uses = {PostProcessorFactory.class})
public interface PostProcessorMapper {
@Mapping(target = "type", expression = "java(model.getClass().getName())")
PostProcessorDto toDto(PostProcessor model);
PostProcessor toPostProcessor(PostProcessorDto dto) throws MappingException;
}

View File

@ -0,0 +1,13 @@
package ctbrec.io.json.mapper;
import ctbrec.Recording;
import ctbrec.io.json.dto.RecordingDto;
import org.mapstruct.Mapper;
@Mapper(uses = ModelMapper.class)
public interface RecordingMapper {
RecordingDto toDto(Recording recording);
Recording toRecording(RecordingDto dto);
}

View File

@ -0,0 +1,17 @@
package ctbrec.io.json.mapper;
import org.mapstruct.Mapper;
import java.net.URI;
import java.util.Optional;
@Mapper
public interface UriMapper {
default URI map(String uri) {
return Optional.ofNullable(uri).map(URI::create).orElse(null);
}
default String map(URI uri) {
return Optional.ofNullable(uri).map(Object::toString).orElse(null);
}
}

View File

@ -1,23 +1,21 @@
package ctbrec.recorder;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.RecordingSizeMonitor;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.io.json.dto.RecordingDto;
import ctbrec.io.json.mapper.RecordingMapper;
import ctbrec.sites.Site;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.sites.SiteUtil;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
@ -27,11 +25,12 @@ import static ctbrec.io.IoUtils.deleteDirectory;
import static ctbrec.io.IoUtils.deleteEmptyParents;
import static java.nio.file.StandardOpenOption.*;
@Slf4j
public class RecordingManager {
private static final Logger LOG = LoggerFactory.getLogger(RecordingManager.class);
private static final ObjectMapper mapper = ObjectMapperFactory.getMapper();
private final Config config;
private final JsonAdapter<Recording> adapter;
private final List<Site> sites;
private final List<Recording> recordings = new ArrayList<>();
private final ReentrantLock recordingsLock = new ReentrantLock();
@ -39,12 +38,7 @@ public class RecordingManager {
public RecordingManager(Config config, List<Site> sites) throws IOException {
this.config = config;
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.add(File.class, new FileJsonAdapter())
.build();
adapter = moshi.adapter(Recording.class).indent(" ");
this.sites = sites;
sizeMonitor = new RecordingSizeMonitor();
Executors.newSingleThreadExecutor().submit(sizeMonitor::processEvents);
@ -61,16 +55,16 @@ public class RecordingManager {
recordingsLock.lock();
try {
recordings.add(rec);
sizeMonitor.monitor(rec);
} finally {
recordingsLock.unlock();
}
sizeMonitor.monitor(rec);
}
public void saveRecording(Recording rec) throws IOException {
if (rec.getMetaDataFile() != null) {
File recordingMetaData = new File(rec.getMetaDataFile());
String json = adapter.toJson(rec);
String json = mapper.writeValueAsString(Mappers.getMapper(RecordingMapper.class).toDto(rec));
rec.setMetaDataFile(recordingMetaData.getAbsolutePath());
Files.createDirectories(recordingMetaData.getParentFile().toPath());
Files.writeString(recordingMetaData.toPath(), json, CREATE, WRITE, TRUNCATE_EXISTING);
@ -84,11 +78,12 @@ public class RecordingManager {
for (File file : metaFiles) {
String json = Files.readString(file.toPath());
try {
Recording recording = adapter.fromJson(json);
Recording recording = Mappers.getMapper(RecordingMapper.class).toRecording(mapper.readValue(json, RecordingDto.class));
recording.setMetaDataFile(file.getCanonicalPath());
SiteUtil.getSiteForModel(sites, recording.getModel()).ifPresent(s -> recording.getModel().setSite(s));
loadRecording(recording);
} catch (Exception e) {
LOG.error("Couldn't load recording {}", file, e);
log.error("Couldn't load recording {}", file, e);
}
}
}
@ -106,7 +101,7 @@ public class RecordingManager {
recordings.add(recording);
sizeMonitor.monitor(recording);
} else {
LOG.info("Recording {} does not exist anymore -> ignoring recording", recording);
log.info("Recording {} does not exist anymore -> ignoring recording", recording);
}
}
@ -139,7 +134,10 @@ public class RecordingManager {
recording.setStatus(State.DELETING);
File path = recording.getAbsoluteFile();
boolean isFile = path.isFile();
LOG.debug("Deleting {}", path);
log.debug("Deleting {}", path);
// uninstall file monitor
sizeMonitor.uninstall(recording);
// delete the video files
if (isFile) {
@ -177,7 +175,6 @@ public class RecordingManager {
} finally {
recordingsLock.unlock();
}
sizeMonitor.uninstall(recording);
}
/**
@ -191,6 +188,8 @@ public class RecordingManager {
try {
int idx = recordings.indexOf(recording);
recording = recordings.get(idx);
// uninstall file monitor
sizeMonitor.uninstall(recording);
// delete the meta data
Files.deleteIfExists(new File(recording.getMetaDataFile()).toPath());
// remove from data structure
@ -198,7 +197,6 @@ public class RecordingManager {
} finally {
recordingsLock.unlock();
}
sizeMonitor.uninstall(recording);
}
public List<Recording> getAll() {

View File

@ -1,19 +1,29 @@
package ctbrec.recorder;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.*;
import ctbrec.event.EventBusHolder;
import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.*;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.io.json.dto.RecordingDto;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.io.json.mapper.RecordingMapper;
import ctbrec.sites.Site;
import lombok.AllArgsConstructor;
import lombok.Data;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.mapstruct.factory.Mappers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -25,6 +35,7 @@ import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class RemoteRecorder implements Recorder {
@ -35,25 +46,14 @@ public class RemoteRecorder implements Recorder {
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String LOG_MSG_SENDING_REQUERST = "Sending request to recording server: {}";
private final Moshi moshi = new Moshi.Builder()
.add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter())
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build();
private final JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
private final JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
private final JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
private final JsonAdapter<ModelGroupRequest> modelGroupRequestAdapter = moshi.adapter(ModelGroupRequest.class);
private final JsonAdapter<ModelGroupListResponse> modelGroupListResponseAdapter = moshi.adapter(ModelGroupListResponse.class);
private final JsonAdapter<RecordingRequest> recordingRequestAdapter = moshi.adapter(RecordingRequest.class);
private final JsonAdapter<SimpleResponse> simpleResponseAdapter = moshi.adapter(SimpleResponse.class);
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
private List<Model> models = Collections.emptyList();
private List<Model> onlineModels = Collections.emptyList();
private List<Recording> recordings = Collections.emptyList();
private final ReentrantLock modelGroupLock = new ReentrantLock();
private final Set<ModelGroup> modelGroups = new HashSet<>();
private final ReentrantLock modelGroupLock = new ReentrantLock();
private final List<Site> sites;
private long spaceTotal = -1;
private long spaceFree = -1;
@ -123,7 +123,7 @@ public class RemoteRecorder implements Recorder {
}
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
String payload = mapper.writeValueAsString(new ModelRequest(action, model));
LOG.trace(LOG_MSG_SENDING_REQUERST, payload);
RequestBody body = RequestBody.Companion.create(payload, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
@ -132,7 +132,7 @@ public class RemoteRecorder implements Recorder {
try (Response response = client.execute(request)) {
String json = response.body().string();
if (response.isSuccessful()) {
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
ModelListResponse resp = mapper.readValue(json, ModelListResponse.class);
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Server returned error " + resp.status + " " + resp.msg);
}
@ -144,7 +144,7 @@ public class RemoteRecorder implements Recorder {
private void sendRequest(String action, Recording recording, Runnable... onSuccess) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
RecordingRequest recReq = new RecordingRequest(action, recording);
String msg = recordingRequestAdapter.toJson(recReq);
String msg = mapper.writeValueAsString(recReq);
RequestBody body = RequestBody.Companion.create(msg, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
LOG.trace(LOG_MSG_SENDING_REQUERST, msg);
@ -152,7 +152,7 @@ public class RemoteRecorder implements Recorder {
Request request = builder.build();
try (Response response = client.execute(request)) {
String json = response.body().string();
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
RecordingListResponse resp = mapper.readValue(json, RecordingListResponse.class);
if (response.isSuccessful()) {
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Request failed: " + resp.msg);
@ -167,8 +167,8 @@ public class RemoteRecorder implements Recorder {
}
}
private void sendRequest(String action, ModelGroup model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = modelGroupRequestAdapter.toJson(new ModelGroupRequest(action, model));
private void sendRequest(String action, ModelGroup modelGroup) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
String payload = mapper.writeValueAsString(new ModelGroupRequest(action, modelGroup));
LOG.trace(LOG_MSG_SENDING_REQUERST, payload);
RequestBody body = RequestBody.Companion.create(payload, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
@ -185,7 +185,7 @@ public class RemoteRecorder implements Recorder {
}
private void updateModelGroups(String responseBody) throws IOException {
ModelGroupListResponse resp = modelGroupListResponseAdapter.fromJson(responseBody);
ModelGroupListResponse resp = mapper.readValue(responseBody, ModelGroupListResponse.class);
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Server returned error " + resp.status + " " + resp.msg);
}
@ -330,9 +330,9 @@ public class RemoteRecorder implements Recorder {
try (Response response = client.execute(request)) {
String json = response.body().string();
if (response.isSuccessful()) {
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
ModelListResponse resp = mapper.readValue(json, ModelListResponse.class);
if (resp.status.equals(SUCCESS)) {
models = resp.models;
models = resp.models.stream().map(Mappers.getMapper(ModelMapper.class)::toModel).collect(Collectors.toList());
for (Model model : models) {
for (Site site : sites) {
if (site.isSiteForModel(model)) {
@ -363,9 +363,9 @@ public class RemoteRecorder implements Recorder {
try (Response response = client.execute(request)) {
String json = response.body().string();
if (response.isSuccessful()) {
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
ModelListResponse resp = mapper.readValue(json, ModelListResponse.class);
if (resp.status.equals(SUCCESS)) {
onlineModels = resp.models;
onlineModels = resp.models.stream().map(Mappers.getMapper(ModelMapper.class)::toModel).collect(Collectors.toList());
for (Model model : models) {
for (Site site : sites) {
if (site.isSiteForModel(model)) {
@ -395,12 +395,11 @@ public class RemoteRecorder implements Recorder {
try (Response response = client.execute(request)) {
String json = response.body().string();
if (response.isSuccessful()) {
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
RecordingListResponse resp = mapper.readValue(json, RecordingListResponse.class);
if (resp.status.equals(SUCCESS)) {
List<Recording> newRecordings = resp.recordings;
List<Recording> newRecordings = resp.recordings.stream().map(Mappers.getMapper(RecordingMapper.class)::toRecording).collect(Collectors.toList());
// fire changed events
for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext(); ) {
Recording recording = iterator.next();
for (Recording recording : recordings) {
if (newRecordings.contains(recording)) {
int idx = newRecordings.indexOf(recording);
Recording newRecording = newRecordings.get(idx);
@ -462,27 +461,31 @@ public class RemoteRecorder implements Recorder {
}
}
@Data
private static class ModelListResponse {
public String status;
public String msg;
public List<Model> models;
String status;
String msg;
List<ModelDto> models;
}
@Data
private static class ModelGroupListResponse {
public String status;
public String msg;
public List<ModelGroup> groups;
String status;
String msg;
List<ModelGroup> groups;
}
@Data
private static class SimpleResponse {
public String status;
public String msg;
String status;
String msg;
}
@Data
private static class RecordingListResponse {
public String status;
public String msg;
public List<Recording> recordings;
String status;
String msg;
List<RecordingDto> recordings;
}
@Override
@ -495,80 +498,34 @@ public class RemoteRecorder implements Recorder {
sendRequest("delete", recording, () -> recordings.remove(recording));
}
@Data
public static class ModelRequest {
private String action;
private Model model;
private ModelDto model;
public ModelRequest(String action, Model model) {
super();
this.action = action;
this.model = model;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public Model getModel() {
return model;
}
public void setModel(Model model) {
this.model = model;
this.model = Mappers.getMapper(ModelMapper.class).toDto(model);
}
}
@Data
@AllArgsConstructor
public static class ModelGroupRequest {
private String action;
private final String action;
private final ModelGroup modelGroup;
public ModelGroupRequest(String action, ModelGroup modelGroup) {
super();
this.action = action;
this.modelGroup = modelGroup;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public ModelGroup getModelGroup() {
return modelGroup;
}
}
@Data
public static class RecordingRequest {
private String action;
private Recording recording;
private RecordingDto recording;
public RecordingRequest(String action, Recording recording) {
super();
this.action = action;
this.recording = recording;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public Recording getRecording() {
return recording;
}
public void setRecording(Recording recording) {
this.recording = recording;
this.recording = Mappers.getMapper(RecordingMapper.class).toDto(recording);
}
}
@ -612,7 +569,7 @@ public class RemoteRecorder implements Recorder {
}
@Override
public long getTotalSpaceBytes() throws IOException {
public long getTotalSpaceBytes() {
return spaceTotal;
}
@ -634,7 +591,7 @@ public class RemoteRecorder implements Recorder {
@Override
public void rerunPostProcessing(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
RecordingRequest recReq = new RecordingRequest("rerunPostProcessing", recording);
String msg = recordingRequestAdapter.toJson(recReq);
String msg = mapper.writeValueAsString(recReq);
LOG.debug(msg);
RequestBody body = RequestBody.Companion.create(msg, JSON);
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
@ -642,7 +599,7 @@ public class RemoteRecorder implements Recorder {
Request request = builder.build();
try (Response response = client.execute(request)) {
String json = response.body().string();
SimpleResponse resp = simpleResponseAdapter.fromJson(json);
SimpleResponse resp = mapper.readValue(json, SimpleResponse.class);
if (response.isSuccessful()) {
if (!resp.status.equals(SUCCESS)) {
throw new IOException("Couldn't start post-processing for recording: " + resp.msg);

View File

@ -5,12 +5,16 @@ import ctbrec.*;
import ctbrec.Recording.State;
import ctbrec.event.*;
import ctbrec.io.HttpClient;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.io.json.mapper.PostProcessorMapper;
import ctbrec.notes.LocalModelNotesService;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.postprocessing.PostProcessingContext;
import ctbrec.recorder.postprocessing.PostProcessor;
import ctbrec.sites.Site;
import ctbrec.sites.SiteUtil;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.factory.Mappers;
import java.io.File;
import java.io.IOException;
@ -24,6 +28,7 @@ import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static ctbrec.Recording.State.WAITING;
import static ctbrec.SubsequentAction.*;
@ -35,7 +40,7 @@ import static java.lang.Thread.MIN_PRIORITY;
public class SimplifiedLocalRecorder implements Recorder {
public static final boolean IGNORE_CACHE = true;
private final List<Model> models = Collections.synchronizedList(new ArrayList<>());
private List<Model> models = Collections.synchronizedList(new ArrayList<>());
private final Config config;
private volatile boolean running;
private final ReentrantLock recorderLock = new ReentrantLock();
@ -62,7 +67,7 @@ public class SimplifiedLocalRecorder implements Recorder {
scheduler = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY));
threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) scheduler, 5);
recordingManager = new RecordingManager(config, sites);
loadModels();
loadModels(sites);
int ppThreads = config.getSettings().postProcessingThreads;
BlockingQueue<Runnable> ppQueue = new LinkedBlockingQueue<>();
postProcessing = new ThreadPoolExecutor(ppThreads, ppThreads, 5, TimeUnit.MINUTES, ppQueue, createThreadFactory("PP", MIN_PRIORITY));
@ -178,18 +183,22 @@ public class SimplifiedLocalRecorder implements Recorder {
recordings.add(recording);
}
private void loadModels() {
config.getSettings().models.forEach(m -> {
if (m.getSite() != null) {
if (m.getSite().isEnabled()) {
models.add(m);
} else {
log.info("{} disabled -> ignoring {}", m.getSite().getName(), m.getName());
}
} else {
log.info("Site for model {} is unknown -> ignoring", m.getName());
}
});
private void loadModels(List<Site> sites) {
config.getSettings().models
.stream()
.map(Mappers.getMapper(ModelMapper.class)::toModel)
.forEach(m -> {
SiteUtil.getSiteForModel(sites, m).ifPresent(m::setSite);
if (m.getSite() != null) {
if (m.getSite().isEnabled()) {
models.add(m);
} else {
log.info("{} disabled -> ignoring {}", m.getSite().getName(), m.getName());
}
} else {
log.info("Site for model {} is unknown -> ignoring", m.getName());
}
});
}
private void shutdownPool(String name, ExecutorService executorService, int secondsToWaitForTermination) throws InterruptedException {
@ -238,7 +247,10 @@ public class SimplifiedLocalRecorder implements Recorder {
recording.refresh();
recordingManager.saveRecording(recording);
recording.postprocess();
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
List<PostProcessor> postProcessors = config.getSettings().postProcessors
.stream()
.map(Mappers.getMapper(PostProcessorMapper.class)::toPostProcessor)
.toList();
PostProcessingContext ctx = createPostProcessingContext(recording);
for (PostProcessor postProcessor : postProcessors) {
if (postProcessor.isEnabled()) {
@ -281,7 +293,7 @@ public class SimplifiedLocalRecorder implements Recorder {
ctx.setRecorder(this);
ctx.setRecording(recording);
ctx.setRecordingManager(recordingManager);
ctx.setModelNotesService(new LocalModelNotesService(config)); // TODO
ctx.setModelNotesService(new LocalModelNotesService(config));
return ctx;
}
@ -307,7 +319,7 @@ public class SimplifiedLocalRecorder implements Recorder {
if (Objects.equals(model.getAddedTimestamp(), Instant.EPOCH)) {
model.setAddedTimestamp(Instant.now());
}
config.getSettings().models.add(model);
config.getSettings().models.add(Mappers.getMapper(ModelMapper.class).toDto(model));
config.save();
} catch (IOException e) {
log.error("Couldn't save config", e);
@ -364,7 +376,7 @@ public class SimplifiedLocalRecorder implements Recorder {
private Recording createRecording(RecordingProcess download) throws IOException {
Model model = download.getModel();
Recording rec = new Recording();
Recording rec = new Recording(config.getSettings().recordingsDir);
rec.setId(UUID.randomUUID().toString());
rec.setRecordingProcess(download);
String recordingFile = download.getPath(model).replace('\\', '/');
@ -405,9 +417,8 @@ public class SimplifiedLocalRecorder implements Recorder {
try {
if (models.contains(model)) {
models.remove(model);
config.getSettings().models.remove(model);
saveConfig();
log.info("Model {} removed", model);
config.save();
} else {
throw new NoSuchElementException("Model " + model.getName() + " [" + model.getUrl() + "] not found in list of recorded models");
}
@ -433,7 +444,7 @@ public class SimplifiedLocalRecorder implements Recorder {
if (models.contains(model)) {
int index = models.indexOf(model);
models.get(index).setStreamUrlIndex(model.getStreamUrlIndex());
config.save();
saveConfig();
log.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
recorderLock.lock();
try {
@ -517,7 +528,7 @@ public class SimplifiedLocalRecorder implements Recorder {
int index = models.indexOf(model);
models.get(index).setSuspended(true);
model.setSuspended(true);
config.save();
saveConfig();
} else {
log.warn("Couldn't suspend model {}. Not found in list", model.getName());
return;
@ -531,6 +542,11 @@ public class SimplifiedLocalRecorder implements Recorder {
}
}
private void saveConfig() throws IOException {
config.getSettings().models = models.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
config.save();
}
@Override
public void resumeRecording(Model model) throws IOException {
recorderLock.lock();
@ -542,7 +558,7 @@ public class SimplifiedLocalRecorder implements Recorder {
m.setMarkedForLaterRecording(false);
model.setSuspended(false);
model.setMarkedForLaterRecording(false);
config.save();
saveConfig();
startRecordingProcess(m);
} else {
log.warn("Couldn't resume model {}. Not found in list", model.getName());
@ -591,6 +607,7 @@ public class SimplifiedLocalRecorder implements Recorder {
addModel(model);
}
}
saveConfig();
}
private Optional<Model> findModel(Model m) {
@ -758,7 +775,7 @@ public class SimplifiedLocalRecorder implements Recorder {
if (models.contains(model)) {
int index = models.indexOf(model);
models.get(index).setPriority(model.getPriority());
config.save();
saveConfig();
} else {
log.warn("Couldn't change priority for model {}. Not found in list", model.getName());
}
@ -794,7 +811,7 @@ public class SimplifiedLocalRecorder implements Recorder {
m.setRecordUntil(model.getRecordUntil());
m.setRecordUntilSubsequentAction(model.getRecordUntilSubsequentAction());
log.debug("Stopping recording of model {} at {} and then {}", m, model.getRecordUntil(), m.getRecordUntilSubsequentAction());
config.save();
saveConfig();
} else {
throw new NoSuchElementException("Model " + model.getName() + " [" + model.getUrl() + "] not found in list of recorded models");
}

View File

@ -1,17 +1,15 @@
package ctbrec.recorder.postprocessing;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
@Slf4j
public class DeleteTooShort extends AbstractPostProcessor {
private static final transient Logger LOG = LoggerFactory.getLogger(DeleteTooShort.class);
public static final String MIN_LEN_IN_SECS = "minimumLengthInSeconds";
@Override
@ -27,10 +25,10 @@ public class DeleteTooShort extends AbstractPostProcessor {
if (minimumLengthInSeconds.getSeconds() > 0) {
Duration recordingLength = rec.getLength();
if (recordingLength.isNegative()) {
LOG.info("Video length couldn't be determined. Keeping the file!");
log.info("Video length couldn't be determined. Keeping the file!");
} else {
if (!recordingLength.isZero() && recordingLength.compareTo(minimumLengthInSeconds) < 0) {
LOG.info("Deleting too short recording {} [{} < {}]", rec, recordingLength, minimumLengthInSeconds);
log.info("Deleting too short recording {} [{} < {}]", rec, recordingLength, minimumLengthInSeconds);
recordingManager.delete(rec);
return false;
}

View File

@ -0,0 +1,19 @@
package ctbrec.sites;
import ctbrec.Model;
import lombok.experimental.UtilityClass;
import java.util.List;
import java.util.Optional;
@UtilityClass
public class SiteUtil {
public static Optional<Site> getSiteForModel(List<Site> sites, Model model) {
for (Site site : sites) {
if (site.isSiteForModel(model)) {
return Optional.of(site);
}
}
return Optional.empty();
}
}

View File

@ -1,14 +1,14 @@
package ctbrec.sites.chaturbate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HttpException;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request;
@ -38,7 +38,7 @@ public class ChaturbateModel extends AbstractModel { // NOSONAR
private int[] resolution = new int[2];
private transient StreamInfo streamInfo;
private transient Instant lastStreamInfoRequest = Instant.EPOCH;
private final transient ObjectMapper mapper = ObjectMapperFactory.getMapper();
/**
* This constructor exists only for deserialization. Please don't call it directly
@ -251,9 +251,7 @@ public class ChaturbateModel extends AbstractModel { // NOSONAR
if (response.isSuccessful()) {
String content = response.body().string();
LOG.trace("Raw stream info: {}", content);
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<StreamInfo> adapter = moshi.adapter(StreamInfo.class);
streamInfo = adapter.fromJson(content);
streamInfo = mapper.readValue(content, StreamInfo.class);
return streamInfo;
} else {
int code = response.code();

View File

@ -4,8 +4,6 @@ import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.*;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
@ -287,15 +285,12 @@ public class CherryTvModel extends AbstractModel {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
if (reader.hasNext()) {
reader.nextName();
id = reader.nextString();
}
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("id").value(id);
public void writeSiteSpecificData(Map<String, String> data) {
data.put("id", id);
}
}

View File

@ -5,8 +5,6 @@ import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StreamInfo;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HttpException;
@ -21,10 +19,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@ -382,14 +377,13 @@ public class Fc2Model extends AbstractModel {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
id = reader.nextString();
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("id").value(id);
public void writeSiteSpecificData(Map<String, String> data) {
data.put("id", id);
}
@Override

View File

@ -4,8 +4,6 @@ import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.Model;
@ -499,16 +497,13 @@ public class Flirt4FreeModel extends AbstractModel {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
if (reader.hasNext()) {
reader.nextName();
id = reader.nextString();
}
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("id").value(id);
public void writeSiteSpecificData(Map<String, String> data) {
data.put("id", id);
}
public void setStreamUrl(String streamUrl) {

View File

@ -2,8 +2,6 @@ package ctbrec.sites.jasmin;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.StringUtil;
@ -229,13 +227,12 @@ public class LiveJasminModel extends AbstractModel {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
id = reader.nextString();
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
public void writeSiteSpecificData(Map<String, String> data) {
if (id == null) {
try {
loadModelInfo();
@ -243,7 +240,7 @@ public class LiveJasminModel extends AbstractModel {
LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName());
}
}
writer.name("id").value(id);
data.put("id", id);
}
public void setOnline(boolean online) {

View File

@ -4,8 +4,6 @@ import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.*;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess;
@ -240,17 +238,13 @@ public class MVLiveModel extends AbstractModel {
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("id").value(id);
public void writeSiteSpecificData(Map<String, String> data) {
data.put("id", id);
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
if (reader.hasNext()) {
//noinspection ResultOfMethodCallIgnored
reader.nextName();
id = reader.nextString();
}
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
public void setId(String id) {

View File

@ -2,8 +2,6 @@ package ctbrec.sites.mfc;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.StringUtil;
import ctbrec.io.HttpException;
@ -39,7 +37,6 @@ public class MyFreeCamsClient {
private MyFreeCams mfc;
private WebSocket ws;
private Thread keepAlive;
private final Moshi moshi;
private volatile boolean running = false;
private final Cache<Integer, SessionState> sessionStates = CacheBuilder.newBuilder().maximumSize(4000).build();
@ -61,7 +58,6 @@ public class MyFreeCamsClient {
private final Queue<String> receivedTextHistory = new LinkedList<>();
private MyFreeCamsClient() {
moshi = new Moshi.Builder().build();
}
public static synchronized MyFreeCamsClient getInstance() {
@ -228,16 +224,16 @@ public class MyFreeCamsClient {
case MYWEBCAM:
case JOINCHAN:
case SESSIONSTATE:
if (!message.getMessage().isEmpty()) {
//LOG.debug("SessionState: {}", message.getMessage());
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class);
try {
SessionState sessionState = adapter.fromJson(message.getMessage());
updateSessionState(sessionState);
} catch (IOException e) {
LOG.error("Couldn't parse session state message {}", message, e);
}
}
// if (!message.getMessage().isEmpty()) {
// //LOG.debug("SessionState: {}", message.getMessage());
// JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class);
// try {
// SessionState sessionState = adapter.fromJson(message.getMessage());
// updateSessionState(sessionState);
// } catch (IOException e) {
// LOG.error("Couldn't parse session state message {}", message, e);
// }
// }
break;
case USERNAMELOOKUP:
// LOG.debug("{}", message.getType());
@ -680,13 +676,13 @@ public class MyFreeCamsClient {
if (StringUtil.isNotBlank(msg.getMessage()) && !Objects.equals(msg.getMessage(), q)) {
JSONObject json = new JSONObject(msg.getMessage());
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class);
try {
SessionState sessionState = Objects.requireNonNull(adapter.fromJson(msg.getMessage()));
updateSessionState(sessionState);
} catch (Exception e) {
LOG.error("Couldn't parse session state message {}", msg, e);
}
// JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class);
// try {
// SessionState sessionState = Objects.requireNonNull(adapter.fromJson(msg.getMessage()));
// updateSessionState(sessionState);
// } catch (Exception e) {
// LOG.error("Couldn't parse session state message {}", msg, e);
// }
String name = json.getString("nm");
MyFreeCamsModel model = mfc.createModel(name);

View File

@ -2,8 +2,6 @@ package ctbrec.sites.mfc;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HtmlParser;
@ -324,14 +322,13 @@ public class MyFreeCamsModel extends AbstractModel {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
uid = reader.nextInt();
public void readSiteSpecificData(Map<String, String> data) {
uid = Integer.parseInt(data.get("uid"));
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("uid").value(uid);
public void writeSiteSpecificData(Map<String, String> data) {
data.put("uid", Integer.toString(uid));
}
@Override

View File

@ -2,8 +2,6 @@ package ctbrec.sites.streamate;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion;
@ -224,13 +222,12 @@ public class StreamateModel extends AbstractModel {
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
id = reader.nextLong();
public void readSiteSpecificData(Map<String, String> data) {
id = Long.parseLong(data.get("id"));
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
public void writeSiteSpecificData(Map<String, String> data) {
if (id == null || Objects.equals(id, MODEL_ID_UNDEFINED)) {
try {
loadModelId();
@ -238,7 +235,7 @@ public class StreamateModel extends AbstractModel {
LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName(), e);
}
}
writer.name("id").value(id);
data.put("id", Long.toString(id));
}
@Override

View File

@ -0,0 +1,85 @@
package ctbrec.io.json.mapper;
import ctbrec.SubsequentAction;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.chaturbate.ChaturbateModel;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import java.net.URI;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ModelMapperTest {
@Test
void mapsAllFieldsFromModelToDto() {
var model = new Chaturbate().createModel("foobarina");
model.setDescription("description");
model.setAddedTimestamp(Instant.now());
model.setRecordUntil(Instant.now().plus(1, ChronoUnit.DAYS));
model.setLastRecorded(Instant.now().minusSeconds(3600));
model.setLastSeen(Instant.now().minusSeconds(60));
model.setPriority(51);
model.setSuspended(true);
model.setMarkedForLaterRecording(true);
model.setRecordUntilSubsequentAction(SubsequentAction.REMOVE);
model.setDisplayName("whatever");
var mapper = Mappers.getMapper(ModelMapper.class);
var mapped = mapper.toDto(model);
assertEquals(model.getName(), mapped.getName());
assertEquals(model.getUrl(), mapped.getUrl().toString());
assertEquals(model.getDescription(), mapped.getDescription());
assertEquals(model.getAddedTimestamp(), mapped.getAddedAt());
assertEquals(model.getLastSeen(), mapped.getLastSeen());
assertEquals(model.getLastRecorded(), mapped.getLastRecorded());
assertEquals(model.getRecordUntil(), mapped.getRecordUntil());
assertEquals(model.getRecordUntilSubsequentAction(), mapped.getRecordUntilSubsequentAction());
assertEquals(model.getDisplayName(), mapped.getDisplayName());
assertEquals(model.getPriority(), mapped.getPriority());
assertEquals(model.getPreview(), mapped.getPreview().toString());
assertEquals(model.isMarkedForLaterRecording(), mapped.isBookmarked());
assertEquals(model.isSuspended(), mapped.isSuspended());
}
@Test
void mapsAllFieldsFromDtoToModel() {
var dto = new ModelDto();
dto.setType(ChaturbateModel.class.getName());
dto.setName("foobarina");
dto.setUrl(URI.create("https://foobarina.com"));
dto.setPreview(URI.create("https://foobarina.com/portrait.jpg"));
dto.setDescription("description");
dto.setAddedAt(Instant.now());
dto.setRecordUntil(Instant.now().plus(1, ChronoUnit.DAYS));
dto.setLastRecorded(Instant.now().minusSeconds(3600));
dto.setLastSeen(Instant.now().minusSeconds(60));
dto.setPriority(51);
dto.setSuspended(true);
dto.setBookmarked(true);
dto.setRecordUntilSubsequentAction(SubsequentAction.REMOVE);
dto.setDisplayName("whatever");
var mapper = Mappers.getMapper(ModelMapper.class);
var mapped = mapper.toModel(dto);
assertEquals(dto.getName(), mapped.getName());
assertEquals(dto.getUrl().toString(), mapped.getUrl());
assertEquals(dto.getDescription(), mapped.getDescription());
assertEquals(dto.getAddedAt(), mapped.getAddedTimestamp());
assertEquals(dto.getLastSeen(), mapped.getLastSeen());
assertEquals(dto.getLastRecorded(), mapped.getLastRecorded());
assertEquals(dto.getRecordUntil(), mapped.getRecordUntil());
assertEquals(dto.getRecordUntilSubsequentAction(), mapped.getRecordUntilSubsequentAction());
assertEquals(dto.getDisplayName(), mapped.getDisplayName());
assertEquals(dto.getPriority(), mapped.getPriority());
assertEquals(dto.getPreview().toString(), mapped.getPreview());
assertEquals(dto.isBookmarked(), mapped.isMarkedForLaterRecording());
assertEquals(dto.isSuspended(), mapped.isSuspended());
}
}

View File

@ -0,0 +1,106 @@
package ctbrec.io.json.mapper;
import ctbrec.Recording;
import ctbrec.UnknownModel;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.io.json.dto.RecordingDto;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import java.io.File;
import java.net.URI;
import java.time.Instant;
import java.util.Set;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RecordingMapperTest {
@Test
void mapsAllFieldsFromRecordingToDto() {
var model = new UnknownModel();
model.setName("unknown");
model.setUrl("https://site/model/unknown");
var recording = new Recording();
recording.setModel(model);
recording.setId(UUID.randomUUID().toString());
recording.setAbsoluteFile(new File("/tmp/recording"));
recording.setStatus(Recording.State.RECORDING);
recording.setSelectedResolution(1080);
recording.setProgress(23);
recording.setStartDate(Instant.now());
recording.setNote("note");
recording.setPinned(true);
recording.setSingleFile(true);
recording.setSizeInByte(87346587);
recording.setPostProcessedFile(new File("/tmp/pp"));
recording.setMetaDataFile("/tmp/meta");
recording.setAssociatedFiles(Set.of("a", "b", "c"));
var mapper = Mappers.getMapper(RecordingMapper.class);
var mapped = mapper.toDto(recording);
assertEquals(recording.getId(), mapped.getId());
assertEquals(recording.getAbsoluteFile(), mapped.getAbsoluteFile());
assertEquals(recording.getStatus(), mapped.getStatus());
assertEquals(recording.getSelectedResolution(), mapped.getSelectedResolution());
assertEquals(recording.getProgress(), mapped.getProgress());
assertEquals(recording.getStartDate(), mapped.getStartDate());
assertEquals(recording.getNote(), mapped.getNote());
assertEquals(recording.isPinned(), mapped.isPinned());
assertEquals(recording.isSingleFile(), mapped.isSingleFile());
assertEquals(recording.getSizeInByte(), mapped.getSizeInByte());
assertEquals(recording.getPostProcessedFile(), mapped.getPostProcessedFile());
assertEquals(recording.getMetaDataFile(), mapped.getMetaDataFile());
assertEquals(recording.getAssociatedFiles(), mapped.getAssociatedFiles());
assertEquals(recording.getModel().getName(), mapped.getModel().getName());
assertEquals(recording.getModel().getUrl(), mapped.getModel().getUrl().toString());
}
@Test
void mapsAllFieldsFromDtoToRecording() {
ModelDto model = new ModelDto();
model.setType(UnknownModel.class.getName());
model.setName("unknown");
model.setUrl(URI.create("http://site/model/unknown"));
var dto = new RecordingDto();
dto.setId(UUID.randomUUID().toString());
dto.setModel(model);
dto.setAbsoluteFile(new File("/tmp/recording"));
dto.setStatus(Recording.State.RECORDING);
dto.setSelectedResolution(1080);
dto.setProgress(23);
dto.setStartDate(Instant.now());
dto.setNote("note");
dto.setPinned(true);
dto.setSingleFile(true);
dto.setSizeInByte(87346587);
dto.setPostProcessedFile(new File("/tmp/pp"));
dto.setMetaDataFile("/tmp/meta");
dto.setAssociatedFiles(Set.of("a", "b", "c"));
var mapper = Mappers.getMapper(RecordingMapper.class);
var mapped = mapper.toRecording(dto);
assertEquals(dto.getId(), mapped.getId());
assertEquals(dto.getAbsoluteFile(), mapped.getAbsoluteFile());
assertEquals(dto.getStatus(), mapped.getStatus());
assertEquals(dto.getSelectedResolution(), mapped.getSelectedResolution());
assertEquals(dto.getProgress(), mapped.getProgress());
assertEquals(dto.getStartDate(), mapped.getStartDate());
assertEquals(dto.getNote(), mapped.getNote());
assertEquals(dto.isPinned(), mapped.isPinned());
assertEquals(dto.isSingleFile(), mapped.isSingleFile());
assertEquals(dto.getSizeInByte(), mapped.getSizeInByte());
assertEquals(dto.getPostProcessedFile(), mapped.getPostProcessedFile());
assertEquals(dto.getMetaDataFile(), mapped.getMetaDataFile());
assertEquals(dto.getAssociatedFiles(), mapped.getAssociatedFiles());
assertEquals(dto.getModel().getName(), mapped.getModel().getName());
assertEquals(dto.getModel().getUrl().toString(), mapped.getModel().getUrl());
}
}

View File

@ -1,9 +1,11 @@
package ctbrec.recorder;
import ctbrec.*;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.recorder.download.RecordingProcess;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import org.mockito.MockedStatic;
import java.io.IOException;
@ -15,6 +17,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static java.time.temporal.ChronoUnit.HOURS;
import static org.junit.jupiter.api.Assertions.*;
@ -169,9 +172,9 @@ class RecordingPreconditionsTest {
var recorder = mock(SimplifiedLocalRecorder.class);
List<Model> modelsToRecord = new LinkedList<>();
settings.models = modelsToRecord;
modelsToRecord.add(theOtherOne);
modelsToRecord.add(mockita);
settings.models = modelsToRecord.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
when(recorder.isRunning()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.notEnoughSpaceForRecording()).thenReturn(false);
@ -197,8 +200,8 @@ class RecordingPreconditionsTest {
var recorder = mock(SimplifiedLocalRecorder.class);
List<Model> modelsToRecord = new LinkedList<>();
settings.models = modelsToRecord;
modelsToRecord.add(mockita);
settings.models = modelsToRecord.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
when(recorder.isRunning()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.notEnoughSpaceForRecording()).thenReturn(false);
@ -217,8 +220,8 @@ class RecordingPreconditionsTest {
var recorder = mock(SimplifiedLocalRecorder.class);
List<Model> modelsToRecord = new LinkedList<>();
settings.models = modelsToRecord;
modelsToRecord.add(mockita);
settings.models = modelsToRecord.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
when(recorder.isRunning()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.notEnoughSpaceForRecording()).thenReturn(false);
@ -252,8 +255,8 @@ class RecordingPreconditionsTest {
var recorder = mock(SimplifiedLocalRecorder.class);
List<Model> modelsToRecord = new LinkedList<>();
settings.models = modelsToRecord;
modelsToRecord.add(mockita);
settings.models = modelsToRecord.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
when(recorder.isRunning()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.notEnoughSpaceForRecording()).thenReturn(false);
@ -291,8 +294,8 @@ class RecordingPreconditionsTest {
var recorder = mock(SimplifiedLocalRecorder.class);
List<Model> modelsToRecord = new LinkedList<>();
settings.models = modelsToRecord;
modelsToRecord.add(mockita);
settings.models = modelsToRecord.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
when(recorder.isRunning()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.notEnoughSpaceForRecording()).thenReturn(false);
@ -334,10 +337,10 @@ class RecordingPreconditionsTest {
when(mockita.getPriority()).thenReturn(100);
var recorder = mock(SimplifiedLocalRecorder.class);
List<Model> modelsToRecord = new LinkedList<>();
settings.models = modelsToRecord;
settings.timeoutRecordingStartingAt = LocalTime.now().minusHours(1).truncatedTo(ChronoUnit.MINUTES);
settings.timeoutRecordingEndingAt = LocalTime.now().plusHours(1).truncatedTo(ChronoUnit.MINUTES);
modelsToRecord.add(mockita);
settings.models = modelsToRecord.stream().map(Mappers.getMapper(ModelMapper.class)::toDto).collect(Collectors.toList());
when(recorder.isRunning()).thenReturn(true);
when(recorder.getModels()).thenReturn(modelsToRecord);
when(recorder.notEnoughSpaceForRecording()).thenReturn(false);

View File

@ -46,7 +46,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
@Test
void testModelNameReplacement() {
String input = "asdf_${modelName}_asdf";
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
assertEquals("asdf_Mockita_Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
input = "asdf_${modelDisplayName}_asdf";
assertEquals("asdf_Mockita Boobilicious_asdf", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
input = "asdf_$sanitize(${modelName})_asdf";
@ -170,7 +170,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
@Test
void testFunctionCalls() {
String input = "$upper(${modelName})";
assertEquals("MOCKITA BOOBILICIOUS", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
assertEquals("MOCKITA_BOOBILICIOUS", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));
input = "$upper($orElse(${doesNotExist},mockita boobilicious))";
assertEquals("MOCKITA BOOBILICIOUS", placeHolderAwarePp.fillInPlaceHolders(input, createPostProcessingContext(rec, null, config)));

View File

@ -76,7 +76,7 @@ public abstract class AbstractPpTest {
Model mockModel() {
Site site = new Stripchat();
Model model = site.createModel("Mockita Boobilicious");
Model model = site.createModel("Mockita_Boobilicious");
model.setDisplayName("Mockita Boobilicious");
return model;
}

View File

@ -20,12 +20,12 @@ import static org.mockito.Mockito.*;
class DeleteTooShortTest extends AbstractPpTest {
@Test
void tooShortSingleFileRecShouldBeDeleted() throws IOException, InterruptedException {
void tooShortSingleFileRecShouldBeDeleted() throws IOException {
testProcess(original);
}
@Test
void tooShortDirectoryRecShouldBeDeleted() throws IOException, InterruptedException {
void tooShortDirectoryRecShouldBeDeleted() throws IOException {
testProcess(originalDir);
}
@ -56,7 +56,7 @@ class DeleteTooShortTest extends AbstractPpTest {
}
@Test
void testDisabledWithSingleFile() throws IOException, InterruptedException {
void testDisabledWithSingleFile() throws IOException {
Recording rec = createRec(original);
Config config = mockConfig();
RecordingManager recordingManager = new RecordingManager(config, Collections.emptyList());
@ -73,7 +73,7 @@ class DeleteTooShortTest extends AbstractPpTest {
}
@Test
void longEnoughVideoShouldStay() throws IOException, InterruptedException {
void longEnoughVideoShouldStay() throws IOException {
Recording rec = createRec(original);
Config config = mockConfig();
RecordingManager recordingManager = new RecordingManager(config, Collections.emptyList());

View File

@ -1,15 +1,15 @@
package ctbrec.recorder.postprocessing;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.recorder.RecordingManager;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RemoveKeepFileTest extends AbstractPpTest {

View File

@ -20,6 +20,9 @@
<maven.compiler.target>17</maven.compiler.target>
<version.javafx>20.0.1</version.javafx>
<version.junit>5.7.2</version.junit>
<jackson.version>2.15.1</jackson.version>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
<lombok.version>1.18.24</lombok.version>
</properties>
<build>
@ -67,11 +70,6 @@
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.13.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
@ -123,6 +121,21 @@
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
@ -159,7 +172,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
@ -186,6 +199,12 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -31,6 +31,7 @@ import ctbrec.sites.stripchat.Stripchat;
import ctbrec.sites.xlovecam.XloveCam;
import org.eclipse.jetty.security.*;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.HandlerList;

View File

@ -1,50 +1,42 @@
package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*;
import java.io.File;
import com.fasterxml.jackson.databind.ObjectMapper;
import ctbrec.*;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.json.ObjectMapperFactory;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.io.json.mapper.RecordingMapper;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.server.io.json.dto.RequestDto;
import ctbrec.recorder.server.io.json.mapper.RequestMapper;
import ctbrec.sites.Site;
import ctbrec.sites.SiteUtil;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.mapstruct.factory.Mappers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import static javax.servlet.http.HttpServletResponse.*;
@Slf4j
public class RecorderServlet extends AbstractCtbrecServlet {
private static final transient Logger LOG = LoggerFactory.getLogger(RecorderServlet.class);
private final ObjectMapper mapper = ObjectMapperFactory.getMapper();
private final ModelMapper modelMapper = Mappers.getMapper(ModelMapper.class);
private final RecordingMapper recordingMapper = Mappers.getMapper(RecordingMapper.class);
private Recorder recorder;
private final Recorder recorder;
private final List<Site> sites;
private List<Site> sites;
public RecorderServlet(Recorder recorder, List<Site> sites) {
this.recorder = recorder;
@ -52,7 +44,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setStatus(SC_OK);
resp.setContentType("application/json");
@ -66,221 +58,208 @@ public class RecorderServlet extends AbstractCtbrecServlet {
return;
}
LOG.debug("Request: {}", json);
Moshi moshi = new Moshi.Builder()
.add(Instant.class, new InstantJsonAdapter())
.add(Model.class, new ModelJsonAdapter(sites))
.add(File.class, new FileJsonAdapter())
.add(UUID.class, new UuidJSonAdapter())
.build();
JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
Request request = requestAdapter.fromJson(json);
if (request.action == null) {
log.debug("Request: {}", json);
Request request = Mappers.getMapper(RequestMapper.class).toRequest(mapper.readValue(json, RequestDto.class));
Model model = request.getModel();
Optional.ofNullable(model).ifPresent(m -> SiteUtil.getSiteForModel(sites, m).ifPresent(m::setSite));
Recording recording = request.getRecording();
if (request.getAction() == null) {
sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"action is missing\"}");
return;
}
switch (request.action) {
case "start":
LOG.debug("Starting recording for model {} - {}", request.model.getName(), request.model.getUrl());
LOG.trace("Model marked: {}", request.model.isMarkedForLaterRecording());
LOG.trace("Model paused: {}", request.model.isSuspended());
LOG.trace("Model until: {}", request.model.getRecordUntil().equals(Instant.ofEpochMilli(Model.RECORD_INDEFINITELY)) ? "no limit" : request.model.getRecordUntil());
LOG.trace("Model after: {}", request.model.getRecordUntilSubsequentAction());
recorder.addModel(request.model);
String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
responseWriter.write(response);
break;
case "startByUrl":
LOG.debug("Starting recording for model {}", request.model.getUrl());
startByUrl(request);
response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
responseWriter.write(response);
break;
case "startByName":
LOG.debug("Starting recording for model {}", request.model.getUrl());
startByName(request);
response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
responseWriter.write(response);
break;
case "stop":
GlobalThreadPool.submit(() -> {
try {
recorder.stopRecording(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't stop recording for model {}", request.model, e);
switch (request.getAction()) {
case "start":
log.debug("Starting recording for model {} - {}", model.getName(), model.getUrl());
log.trace("Model marked: {}", model.isMarkedForLaterRecording());
log.trace("Model paused: {}", model.isSuspended());
log.trace("Model until: {}", model.getRecordUntil().equals(Instant.ofEpochMilli(Model.RECORD_INDEFINITELY)) ? "no limit" : model.getRecordUntil());
log.trace("Model after: {}", model.getRecordUntilSubsequentAction());
recorder.addModel(model);
String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
responseWriter.write(response);
break;
case "startByUrl":
log.debug("Starting recording for model {}", model.getUrl());
startByUrl(request);
response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
responseWriter.write(response);
break;
case "startByName":
log.debug("Starting recording for model {}", model.getUrl());
startByName(request);
response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
responseWriter.write(response);
break;
case "stop":
GlobalThreadPool.submit(() -> {
try {
recorder.stopRecording(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
log.error("Couldn't stop recording for model {}", model, e);
}
});
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
responseWriter.write(response);
break;
case "stopAt":
GlobalThreadPool.submit(() -> {
try {
recorder.stopRecordingAt(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
log.error("Couldn't stop recording for model {}", model, e);
}
});
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
responseWriter.write(response);
break;
case "list":
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
for (Iterator<Model> iterator = recorder.getModels().iterator(); iterator.hasNext(); ) {
Model m = iterator.next();
responseWriter.write(mapper.writeValueAsString(modelMapper.toDto(m)));
if (iterator.hasNext()) {
responseWriter.write(',');
}
}
});
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
responseWriter.write(response);
break;
case "stopAt":
GlobalThreadPool.submit(() -> {
try {
recorder.stopRecordingAt(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't stop recording for model {}", request.model, e);
responseWriter.write("]}");
break;
case "listOnline":
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of online models\", \"models\": [");
for (Iterator<Model> iterator = recorder.getOnlineModels().iterator(); iterator.hasNext(); ) {
Model m = iterator.next();
responseWriter.write(mapper.writeValueAsString(modelMapper.toDto(m)));
if (iterator.hasNext()) {
responseWriter.write(',');
}
}
});
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
responseWriter.write(response);
break;
case "list":
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();
List<Model> models = recorder.getModels();
for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
Model model = iterator.next();
responseWriter.write(modelAdapter.toJson(model));
if (iterator.hasNext()) {
responseWriter.write(',');
responseWriter.write("]}");
break;
case "recordings":
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
List<Recording> recordings = recorder.getRecordings();
for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext(); ) {
Recording rec = iterator.next();
String recJSON = mapper.writeValueAsString(recordingMapper.toDto(rec));
log.debug("Rec: {}", recJSON);
responseWriter.write(recJSON);
if (iterator.hasNext()) {
responseWriter.write(',');
}
}
}
responseWriter.write("]}");
break;
case "listOnline":
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of online models\", \"models\": [");
modelAdapter = new ModelJsonAdapter();
models = recorder.getOnlineModels();
for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
Model model = iterator.next();
responseWriter.write(modelAdapter.toJson(model));
if (iterator.hasNext()) {
responseWriter.write(',');
}
}
responseWriter.write("]}");
break;
case "recordings":
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
JsonAdapter<Recording> recAdapter = moshi.adapter(Recording.class);
List<Recording> recordings = recorder.getRecordings();
for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
Recording recording = iterator.next();
String recJSON = recAdapter.toJson(recording);
LOG.debug("Rec: {}", recJSON);
responseWriter.write(recJSON);
if (iterator.hasNext()) {
responseWriter.write(',');
}
}
responseWriter.write("]}");
break;
case "delete":
recorder.delete(request.recording);
recAdapter = moshi.adapter(Recording.class);
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
responseWriter.write(recAdapter.toJson(request.recording));
responseWriter.write("]}");
break;
case "pin":
recorder.pin(request.recording);
recAdapter = moshi.adapter(Recording.class);
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
responseWriter.write(recAdapter.toJson(request.recording));
responseWriter.write("]}");
break;
case "unpin":
recorder.unpin(request.recording);
recAdapter = moshi.adapter(Recording.class);
responseWriter.write("{\"status\": \"success\", \"msg\": \"Note saved\", \"recordings\": [");
responseWriter.write(recAdapter.toJson(request.recording));
responseWriter.write("]}");
break;
case "setNote":
recorder.setNote(request.recording, request.recording.getNote());
recAdapter = moshi.adapter(Recording.class);
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
responseWriter.write(recAdapter.toJson(request.recording));
responseWriter.write("]}");
break;
case "rerunPostProcessing":
recorder.rerunPostProcessing(request.recording);
responseWriter.write("{\"status\": \"success\", \"msg\": \"Post-Processing triggered\"}");
break;
case "switch":
recorder.switchStreamSource(request.model);
response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}";
responseWriter.write(response);
break;
case "suspend":
LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl());
GlobalThreadPool.submit(() -> {
try {
recorder.suspendRecording(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't suspend recording for model {}", request.model, e);
}
});
response = "{\"status\": \"success\", \"msg\": \"Suspending recording\"}";
responseWriter.write(response);
break;
case "resume":
LOG.debug("Resume recording for model {} - {}", request.model.getName(), request.model.getUrl());
recorder.resumeRecording(request.model);
response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
responseWriter.write(response);
break;
case "space":
JSONObject jsonResponse = new JSONObject();
jsonResponse.put("status", "success");
jsonResponse.put("spaceTotal", recorder.getTotalSpaceBytes());
jsonResponse.put("spaceFree", recorder.getFreeSpaceBytes());
jsonResponse.put("throughput", BandwidthMeter.getThroughput());
jsonResponse.put("throughputTimeframe", BandwidthMeter.getTimeframe().toMillis());
jsonResponse.put("minimumSpaceLeftInBytes", Config.getInstance().getSettings().minimumSpaceLeftInBytes);
responseWriter.write(jsonResponse.toString());
break;
case "changePriority":
recorder.priorityChanged(request.model);
response = "{\"status\": \"success\"}";
responseWriter.write(response);
break;
case "pauseRecorder":
recorder.pause();
response = "{\"status\": \"success\"}";
responseWriter.write(response);
break;
case "resumeRecorder":
recorder.resume();
response = "{\"status\": \"success\"}";
responseWriter.write(response);
break;
case "saveModelGroup":
recorder.saveModelGroup(request.modelGroup);
sendModelGroups(resp, recorder.getModelGroups());
break;
case "deleteModelGroup":
recorder.deleteModelGroup(request.modelGroup);
sendModelGroups(resp, recorder.getModelGroups());
break;
case "listModelGroups":
sendModelGroups(resp, recorder.getModelGroups());
break;
case "markForLater":
LOG.debug("Mark model {} for later recording", request.model.getName());
response = "{\"status\": \"success\", \"msg\": \"Model marked for later recording\"}";
responseWriter.write(response);
recorder.markForLaterRecording(request.model, true);
break;
case "unmarkForLater":
LOG.debug("Unmark model {} for later recording", request.model.getName());
response = "{\"status\": \"success\", \"msg\": \"Model has been unmarked\"}";
responseWriter.write(response);
recorder.markForLaterRecording(request.model, false);
break;
default:
sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"Unknown action [" + request.action + "]\"}");
break;
responseWriter.write("]}");
break;
case "delete":
recorder.delete(recording);
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
responseWriter.write(mapper.writeValueAsString(recordingMapper.toDto(recording)));
responseWriter.write("]}");
break;
case "pin":
recorder.pin(recording);
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
responseWriter.write(mapper.writeValueAsString(recordingMapper.toDto(recording)));
responseWriter.write("]}");
break;
case "unpin":
recorder.unpin(recording);
responseWriter.write("{\"status\": \"success\", \"msg\": \"Note saved\", \"recordings\": [");
responseWriter.write(mapper.writeValueAsString(recordingMapper.toDto(recording)));
responseWriter.write("]}");
break;
case "setNote":
recorder.setNote(recording, recording.getNote());
responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
responseWriter.write(mapper.writeValueAsString(recordingMapper.toDto(recording)));
responseWriter.write("]}");
break;
case "rerunPostProcessing":
recorder.rerunPostProcessing(recording);
responseWriter.write("{\"status\": \"success\", \"msg\": \"Post-Processing triggered\"}");
break;
case "switch":
recorder.switchStreamSource(model);
response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}";
responseWriter.write(response);
break;
case "suspend":
log.debug("Suspend recording for model {} - {}", model.getName(), model.getUrl());
GlobalThreadPool.submit(() -> {
try {
recorder.suspendRecording(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
log.error("Couldn't suspend recording for model {}", model, e);
}
});
response = "{\"status\": \"success\", \"msg\": \"Suspending recording\"}";
responseWriter.write(response);
break;
case "resume":
log.debug("Resume recording for model {} - {}", model.getName(), model.getUrl());
recorder.resumeRecording(model);
response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
responseWriter.write(response);
break;
case "space":
JSONObject jsonResponse = new JSONObject();
jsonResponse.put("status", "success");
jsonResponse.put("spaceTotal", recorder.getTotalSpaceBytes());
jsonResponse.put("spaceFree", recorder.getFreeSpaceBytes());
jsonResponse.put("throughput", BandwidthMeter.getThroughput());
jsonResponse.put("throughputTimeframe", BandwidthMeter.getTimeframe().toMillis());
jsonResponse.put("minimumSpaceLeftInBytes", Config.getInstance().getSettings().minimumSpaceLeftInBytes);
responseWriter.write(jsonResponse.toString());
break;
case "changePriority":
recorder.priorityChanged(model);
response = "{\"status\": \"success\"}";
responseWriter.write(response);
break;
case "pauseRecorder":
recorder.pause();
response = "{\"status\": \"success\"}";
responseWriter.write(response);
break;
case "resumeRecorder":
recorder.resume();
response = "{\"status\": \"success\"}";
responseWriter.write(response);
break;
case "saveModelGroup":
recorder.saveModelGroup(request.getModelGroup());
sendModelGroups(resp, recorder.getModelGroups());
break;
case "deleteModelGroup":
recorder.deleteModelGroup(request.getModelGroup());
sendModelGroups(resp, recorder.getModelGroups());
break;
case "listModelGroups":
sendModelGroups(resp, recorder.getModelGroups());
break;
case "markForLater":
log.debug("Mark model {} for later recording", model.getName());
response = "{\"status\": \"success\", \"msg\": \"Model marked for later recording\"}";
responseWriter.write(response);
recorder.markForLaterRecording(model, true);
break;
case "unmarkForLater":
log.debug("Unmark model {} for later recording", model.getName());
response = "{\"status\": \"success\", \"msg\": \"Model has been unmarked\"}";
responseWriter.write(response);
recorder.markForLaterRecording(model, false);
break;
default:
sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"Unknown action [" + request.getAction() + "]\"}");
break;
}
} catch(Exception e) {
} catch (Exception e) {
resp.setStatus(SC_INTERNAL_SERVER_ERROR);
JSONObject response = new JSONObject();
response.put("status", "error");
response.put("msg", e.getMessage());
resp.getWriter().write(response.toString());
LOG.error("Unexpected error", e);
log.error("Unexpected error", e);
if (json != null) {
LOG.debug("Request: {}", json);
log.debug("Request: {}", json);
}
}
}
@ -298,7 +277,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
}
private void startByUrl(Request request) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
String url = request.model.getUrl();
String url = request.getModel().getUrl();
for (Site site : sites) {
Model model = site.createModelFromUrl(url);
if (model != null) {
@ -310,7 +289,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
}
private void startByName(Request request) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
String[] input = request.model.getUrl().split(":");
String[] input = request.getModel().getUrl().split(":");
if (input.length != 2) {
throw new IllegalArgumentException("Invalid input. Should be site:model");
}
@ -323,13 +302,6 @@ public class RecorderServlet extends AbstractCtbrecServlet {
return;
}
}
throw new IllegalArgumentException("No site found to record " + request.model.getUrl());
}
private static class Request {
public String action;
public Model model;
public Recording recording;
public ModelGroup modelGroup;
throw new IllegalArgumentException("No site found to record " + request.getModel().getUrl());
}
}

View File

@ -0,0 +1,14 @@
package ctbrec.recorder.server;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import lombok.Data;
@Data
public class Request {
private String action;
private Model model;
private Recording recording;
private ModelGroup modelGroup;
}

View File

@ -0,0 +1,14 @@
package ctbrec.recorder.server.io.json.dto;
import ctbrec.ModelGroup;
import ctbrec.io.json.dto.ModelDto;
import ctbrec.io.json.dto.RecordingDto;
import lombok.Data;
@Data
public class RequestDto {
private String action;
private ModelDto model;
private RecordingDto recording;
private ModelGroup modelGroup;
}

View File

@ -0,0 +1,14 @@
package ctbrec.recorder.server.io.json.mapper;
import ctbrec.io.json.mapper.ModelMapper;
import ctbrec.io.json.mapper.RecordingMapper;
import ctbrec.recorder.server.Request;
import ctbrec.recorder.server.io.json.dto.RequestDto;
import org.mapstruct.Mapper;
@Mapper(uses = {ModelMapper.class, RecordingMapper.class})
public interface RequestMapper {
RequestDto toDto(Request recording);
Request toRequest(RequestDto dto);
}