From 619d888bfa18fd947c7e8514af0ab51dde3b64eb Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 18 Jun 2023 20:49:31 +0200 Subject: [PATCH] Get rid of moshi --- .../java/ctbrec/ui/CamrecApplication.java | 49 +- .../src/main/java/ctbrec/ui/JavaFxModel.java | 25 +- .../ctbrec/ui/event/PlayerStartedEvent.java | 46 +- .../ui/io/json/dto/PlayerStartedEventDto.java | 18 + .../json/mapper/PlayerStartedEventMapper.java | 14 + .../src/main/java/ctbrec/ui/news/Account.java | 189 +------ .../src/main/java/ctbrec/ui/news/NewsTab.java | 14 +- .../src/main/java/ctbrec/ui/news/Status.java | 268 ++-------- .../java/ctbrec/ui/settings/IgnoreList.java | 70 +-- .../ui/settings/PostProcessingStepPanel.java | 19 +- .../java/ctbrec/ui/settings/SettingsTab.java | 2 +- .../ctbrec/ui/tabs/RecentlyWatchedTab.java | 122 ++--- .../java/ctbrec/ui/tabs/RecordingsTab.java | 3 +- .../recorded/AbstractRecordedModelsTab.java | 1 + .../ui/tabs/recorded/ModelImportExport.java | 305 +++++------- common/pom.xml | 42 +- .../src/main/java/ctbrec/AbstractModel.java | 17 +- common/src/main/java/ctbrec/Config.java | 116 +---- common/src/main/java/ctbrec/Model.java | 7 +- common/src/main/java/ctbrec/Recording.java | 14 +- .../java/ctbrec/RecordingSizeMonitor.java | 47 +- common/src/main/java/ctbrec/Settings.java | 11 +- .../ctbrec/io/CookieContainerJsonAdapter.java | 64 --- .../java/ctbrec/io/CookieJsonAdapter.java | 81 --- .../main/java/ctbrec/io/FileJsonAdapter.java | 30 -- .../src/main/java/ctbrec/io/HttpClient.java | 156 +++--- .../java/ctbrec/io/InstantJsonAdapter.java | 21 - .../java/ctbrec/io/LocalTimeJsonAdapter.java | 21 - .../main/java/ctbrec/io/ModelJsonAdapter.java | 134 ----- .../ctbrec/io/PostProcessorJsonAdapter.java | 65 --- .../main/java/ctbrec/io/UuidJSonAdapter.java | 22 - .../ctbrec/io/json/ObjectMapperFactory.java | 25 + .../java/ctbrec/io/json/dto/CookieDto.java | 16 + .../java/ctbrec/io/json/dto/ModelDto.java | 42 ++ .../ctbrec/io/json/dto/PostProcessorDto.java | 12 + .../java/ctbrec/io/json/dto/RecordingDto.java | 28 ++ .../converter/InstantToMillisConverter.java | 25 + .../converter/MillisToInstantConverter.java | 24 + .../ctbrec/io/json/mapper/CookieMapper.java | 39 ++ .../io/json/mapper/MappingException.java | 7 + .../ctbrec/io/json/mapper/ModelFactory.java | 19 + .../ctbrec/io/json/mapper/ModelMapper.java | 32 ++ .../io/json/mapper/PostProcessorFactory.java | 19 + .../io/json/mapper/PostProcessorMapper.java | 14 + .../io/json/mapper/RecordingMapper.java | 13 + .../java/ctbrec/io/json/mapper/UriMapper.java | 17 + .../ctbrec/recorder/RecordingManager.java | 48 +- .../java/ctbrec/recorder/RemoteRecorder.java | 159 +++--- .../recorder/SimplifiedLocalRecorder.java | 67 ++- .../postprocessing/DeleteTooShort.java | 16 +- .../src/main/java/ctbrec/sites/SiteUtil.java | 19 + .../sites/chaturbate/ChaturbateModel.java | 10 +- .../ctbrec/sites/cherrytv/CherryTvModel.java | 13 +- .../java/ctbrec/sites/fc2live/Fc2Model.java | 16 +- .../sites/flirt4free/Flirt4FreeModel.java | 13 +- .../ctbrec/sites/jasmin/LiveJasminModel.java | 11 +- .../ctbrec/sites/manyvids/MVLiveModel.java | 14 +- .../ctbrec/sites/mfc/MyFreeCamsClient.java | 38 +- .../ctbrec/sites/mfc/MyFreeCamsModel.java | 11 +- .../sites/streamate/StreamateModel.java | 11 +- .../io/json/mapper/ModelMapperTest.java | 85 ++++ .../io/json/mapper/RecordingMapperTest.java | 106 ++++ .../recorder/RecordingPreconditionsTest.java | 15 +- ...ractPlaceholderAwarePostProcessorTest.java | 4 +- .../postprocessing/AbstractPpTest.java | 2 +- .../postprocessing/DeleteTooShortTest.java | 8 +- .../postprocessing/RemoveKeepFileTest.java | 14 +- master/pom.xml | 31 +- .../ctbrec/recorder/server/HttpServer.java | 1 + .../recorder/server/RecorderServlet.java | 464 ++++++++---------- .../java/ctbrec/recorder/server/Request.java | 14 + .../server/io/json/dto/RequestDto.java | 14 + .../server/io/json/mapper/RequestMapper.java | 14 + 73 files changed, 1587 insertions(+), 1956 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/io/json/dto/PlayerStartedEventDto.java create mode 100644 client/src/main/java/ctbrec/ui/io/json/mapper/PlayerStartedEventMapper.java delete mode 100644 common/src/main/java/ctbrec/io/CookieContainerJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/CookieJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/FileJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/InstantJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/ModelJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java delete mode 100644 common/src/main/java/ctbrec/io/UuidJSonAdapter.java create mode 100644 common/src/main/java/ctbrec/io/json/ObjectMapperFactory.java create mode 100644 common/src/main/java/ctbrec/io/json/dto/CookieDto.java create mode 100644 common/src/main/java/ctbrec/io/json/dto/ModelDto.java create mode 100644 common/src/main/java/ctbrec/io/json/dto/PostProcessorDto.java create mode 100644 common/src/main/java/ctbrec/io/json/dto/RecordingDto.java create mode 100644 common/src/main/java/ctbrec/io/json/dto/converter/InstantToMillisConverter.java create mode 100644 common/src/main/java/ctbrec/io/json/dto/converter/MillisToInstantConverter.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/CookieMapper.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/MappingException.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/ModelFactory.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/ModelMapper.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/PostProcessorFactory.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/PostProcessorMapper.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/RecordingMapper.java create mode 100644 common/src/main/java/ctbrec/io/json/mapper/UriMapper.java create mode 100644 common/src/main/java/ctbrec/sites/SiteUtil.java create mode 100644 common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java create mode 100644 common/src/test/java/ctbrec/io/json/mapper/RecordingMapperTest.java create mode 100644 server/src/main/java/ctbrec/recorder/server/Request.java create mode 100644 server/src/main/java/ctbrec/recorder/server/io/json/dto/RequestDto.java create mode 100644 server/src/main/java/ctbrec/recorder/server/io/json/mapper/RequestMapper.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 37335f4e..659b040f 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -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> adapter = moshi.adapter(type); - List releases = adapter.fromJson(body); + List 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); } } } diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index 0f7f844b..a86be8c5 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -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 lastSeenProperty = new SimpleObjectProperty<>(); - private transient SimpleObjectProperty 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 lastSeenProperty = new SimpleObjectProperty<>(); + private final transient SimpleObjectProperty 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 data) { + delegate.readSiteSpecificData(data); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { - delegate.writeSiteSpecificData(writer); + public void writeSiteSpecificData(Map data) { + delegate.writeSiteSpecificData(data); } @Override diff --git a/client/src/main/java/ctbrec/ui/event/PlayerStartedEvent.java b/client/src/main/java/ctbrec/ui/event/PlayerStartedEvent.java index 6b040f60..c06dd012 100644 --- a/client/src/main/java/ctbrec/ui/event/PlayerStartedEvent.java +++ b/client/src/main/java/ctbrec/ui/event/PlayerStartedEvent.java @@ -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; } } + } diff --git a/client/src/main/java/ctbrec/ui/io/json/dto/PlayerStartedEventDto.java b/client/src/main/java/ctbrec/ui/io/json/dto/PlayerStartedEventDto.java new file mode 100644 index 00000000..a5538f8a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/io/json/dto/PlayerStartedEventDto.java @@ -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; +} diff --git a/client/src/main/java/ctbrec/ui/io/json/mapper/PlayerStartedEventMapper.java b/client/src/main/java/ctbrec/ui/io/json/mapper/PlayerStartedEventMapper.java new file mode 100644 index 00000000..7001896f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/io/json/mapper/PlayerStartedEventMapper.java @@ -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); +} diff --git a/client/src/main/java/ctbrec/ui/news/Account.java b/client/src/main/java/ctbrec/ui/news/Account.java index 786755dc..8b5db643 100644 --- a/client/src/main/java/ctbrec/ui/news/Account.java +++ b/client/src/main/java/ctbrec/ui/news/Account.java @@ -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 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 fields = null; - @Json(name = "acct") + @JsonProperty("acct") private String acct; - @Json(name = "username") + @JsonProperty("username") private String username; - - public List getEmojis() { - return emojis; - } - - public void setEmojis(List 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 getFields() { - return fields; - } - - public void setFields(List 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; - } - } diff --git a/client/src/main/java/ctbrec/ui/news/NewsTab.java b/client/src/main/java/ctbrec/ui/news/NewsTab.java index dbaf38f4..152cd693 100644 --- a/client/src/main/java/ctbrec/ui/news/NewsTab.java +++ b/client/src/main/java/ctbrec/ui/news/NewsTab.java @@ -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 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) { diff --git a/client/src/main/java/ctbrec/ui/news/Status.java b/client/src/main/java/ctbrec/ui/news/Status.java index 5404c6b3..8bf4c97d 100644 --- a/client/src/main/java/ctbrec/ui/news/Status.java +++ b/client/src/main/java/ctbrec/ui/news/Status.java @@ -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 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 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 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 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 getMediaAttachments() { - return mediaAttachments; - } - - public void setMediaAttachments(List 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 getEmojis() { - return emojis; - } - - public void setEmojis(List 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 getTags() { - return tags; - } - - public void setTags(List 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 getMentions() { - return mentions; - } - - public void setMentions(List 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); diff --git a/client/src/main/java/ctbrec/ui/settings/IgnoreList.java b/client/src/main/java/ctbrec/ui/settings/IgnoreList.java index 2ff0d5a1..1b34ea03 100644 --- a/client/src/main/java/ctbrec/ui/settings/IgnoreList.java +++ b/client/src/main/java/ctbrec/ui/settings/IgnoreList.java @@ -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 ignoreListView; - private List sites; + private final ObjectMapper mapper = ObjectMapperFactory.getMapper(); - public IgnoreList(List sites) { - this.sites = sites; + public IgnoreList() { createGui(); loadIgnoredModels(); } @@ -83,16 +71,14 @@ public class IgnoreList extends GridPane { private void removeSelectedModels() { List 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> 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> adapter = moshi.adapter(modelListType); try { - byte[] fileContent = Files.readAllBytes(file.toPath()); - List ignoredModels = adapter.fromJson(new String(fileContent, StandardCharsets.UTF_8)); + String fileContent = Files.readString(file.toPath()); + List 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?"; diff --git a/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java b/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java index 7776b1ae..4cfd3670 100644 --- a/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java @@ -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 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) 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); diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 4957abab..8f211f70 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -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 siteCategories = new ArrayList<>(); for (Site site : sites) { ofNullable(SiteUiFactory.getUi(site)).map(SiteUI::getConfigUI).map(ConfigUI::createConfigPanel) diff --git a/client/src/main/java/ctbrec/ui/tabs/RecentlyWatchedTab.java b/client/src/main/java/ctbrec/ui/tabs/RecentlyWatchedTab.java index cfcb1769..aae42a70 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecentlyWatchedTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecentlyWatchedTab.java @@ -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 filteredModels = FXCollections.observableArrayList(); - private ObservableList observableModels = FXCollections.observableArrayList(); - private TableView table = new TableView<>(); + private final ObservableList filteredModels = FXCollections.observableArrayList(); + private final ObservableList observableModels = FXCollections.observableArrayList(); + private final TableView 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 sites; public RecentlyWatchedTab(Recorder recorder, List 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 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> 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> adapter = moshi.adapter(type); + log.debug("Loading recently watched models from {}", recentlyWatched.getAbsolutePath()); try { - List fromJson = adapter.fromJson(Files.readString(recentlyWatched.toPath(), UTF_8)); - observableModels.addAll(fromJson); + List fromJson = mapper.readValue(Files.readString(recentlyWatched.toPath(), UTF_8), new TypeReference>() { + }); + 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); } } } diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java index e66fe308..61ddac2e 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java @@ -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); } diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java index f0d022fa..73f56497 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java @@ -273,6 +273,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect try { List 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); diff --git a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java index 790c82a1..0c8def4b 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/ModelImportExport.java @@ -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 includes, File targetFile) { } @@ -41,98 +47,98 @@ public class ModelImportExport { } public static void exportTo(List models, Set 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> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class)); - JsonAdapter> modelListAdapter = moshi.adapter(Types.newParameterizedType(List.class, Model.class)); - JsonAdapter> 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 entry : config.getSettings().modelPortraits.entrySet()) { - String modelUrl = entry.getKey(); - String portraitId = entry.getValue(); - Optional 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 entry : config.getSettings().modelPortraits.entrySet()) { + String modelUrl = entry.getKey(); + String portraitId = entry.getValue(); + Optional 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 importFrom(File target, List 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 modelAdapter = moshi.adapter(Model.class); - JsonAdapter> notesAdapter = moshi.adapter(Types.newParameterizedType(Map.class, String.class, String.class)); - JsonAdapter> modelGroupAdapter = moshi.adapter(Types.newParameterizedType(Set.class, ModelGroup.class)); - - List 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 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> 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> notesAdapter, Config config) throws IOException { - var notes = notesAdapter.fromJson(reader); - if (notes != null) { - config.getSettings().modelNotes.putAll(notes); - } - } - private static List readModels(JsonAdapter modelAdapter, JsonReader reader) throws IOException { - List 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(); + 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 readModels(JSONArray models) { + List 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(); - } - } - } } diff --git a/common/pom.xml b/common/pom.xml index 5aaad1ce..6406c6ee 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -29,14 +29,22 @@ com.squareup.okhttp3 okhttp - - com.squareup.moshi - moshi - org.json json + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.mapstruct + mapstruct + com.iheartradio.m3u8 open-m3u8 @@ -96,6 +104,32 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 188a0201..19297094 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -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 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 data) { // noop default implementation, can be overriden by concrete models } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { + public void writeSiteSpecificData(Map 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; } diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 9cfe6a02..1ea7ed41 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -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 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 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 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 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 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 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); } diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index b709d962..3ef8312f 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -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, Serializable { @@ -116,9 +115,9 @@ public interface Model extends Comparable, Serializable { Site getSite(); - void writeSiteSpecificData(JsonWriter writer) throws IOException; + void writeSiteSpecificData(Map data); - void readSiteSpecificData(JsonReader reader) throws IOException; + void readSiteSpecificData(Map data); boolean isSuspended(); diff --git a/common/src/main/java/ctbrec/Recording.java b/common/src/main/java/ctbrec/Recording.java index 6db923c2..c26bb083 100644 --- a/common/src/main/java/ctbrec/Recording.java +++ b/common/src/main/java/ctbrec/Recording.java @@ -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()) { diff --git a/common/src/main/java/ctbrec/RecordingSizeMonitor.java b/common/src/main/java/ctbrec/RecordingSizeMonitor.java index 62931a88..0e1938de 100644 --- a/common/src/main/java/ctbrec/RecordingSizeMonitor.java +++ b/common/src/main/java/ctbrec/RecordingSizeMonitor.java @@ -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 recordingByKey; protected final Map> keysByRecording; protected final Set 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 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 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(); } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 28083645..96cbc9f0 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -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 models = new ArrayList<>(); + public List models = new ArrayList<>(); public Set modelGroups = new HashSet<>(); public Map modelNotes = new HashMap<>(); public Map modelPortraits = new HashMap<>(); @Deprecated - public List modelsIgnored = new ArrayList<>(); + public List 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 postProcessors = new ArrayList<>(); + public List 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 recordLater = new ArrayList<>(); + public List recordLater = new ArrayList<>(); public boolean recordSingleFile = false; public long recordUntilDefaultDurationInMinutes = 24 * 60L; public boolean removeRecordingAfterPostProcessing = false; diff --git a/common/src/main/java/ctbrec/io/CookieContainerJsonAdapter.java b/common/src/main/java/ctbrec/io/CookieContainerJsonAdapter.java deleted file mode 100644 index 9498e85d..00000000 --- a/common/src/main/java/ctbrec/io/CookieContainerJsonAdapter.java +++ /dev/null @@ -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 { - - 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 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> 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(); - } - -} diff --git a/common/src/main/java/ctbrec/io/CookieJsonAdapter.java b/common/src/main/java/ctbrec/io/CookieJsonAdapter.java deleted file mode 100644 index fa549814..00000000 --- a/common/src/main/java/ctbrec/io/CookieJsonAdapter.java +++ /dev/null @@ -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 { - - @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(); - } -} diff --git a/common/src/main/java/ctbrec/io/FileJsonAdapter.java b/common/src/main/java/ctbrec/io/FileJsonAdapter.java deleted file mode 100644 index e3d38733..00000000 --- a/common/src/main/java/ctbrec/io/FileJsonAdapter.java +++ /dev/null @@ -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 { - - @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(); - } - } -} diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index bbfc4bd2..d76fb34d 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -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 adapter = moshi.adapter(CookieContainer.class).indent(" "); - String json = adapter.toJson(cookies); - + List containers = new ArrayList<>(); + cookieJar.getCookies().forEach((domain, cookieList) -> { + CookieContainer cookies = new CookieContainer(); + cookies.setDomain(domain); + List 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> cookies = cookieJar.getCookies(); - Moshi moshi = new Moshi.Builder() - .add(CookieContainer.class, new CookieContainerJsonAdapter()) - .build(); - JsonAdapter adapter = moshi.adapter(CookieContainer.class).indent(" "); - CookieContainer fromJson = adapter.fromJson(json); - Set>> entries = fromJson.entrySet(); - for (Entry> entry : entries) { - List filteredCookies = entry.getValue().stream() - .filter(c -> !Objects.equals("deleted", c.value())) + List fromJson = ObjectMapperFactory.getMapper().readValue(json, new TypeReference<>() { + }); + for (CookieContainer container : fromJson) { + List 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> { - + @Data + public static class CookieContainer { + private String domain; + private List 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(); } diff --git a/common/src/main/java/ctbrec/io/InstantJsonAdapter.java b/common/src/main/java/ctbrec/io/InstantJsonAdapter.java deleted file mode 100644 index e3d056c3..00000000 --- a/common/src/main/java/ctbrec/io/InstantJsonAdapter.java +++ /dev/null @@ -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 { - @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()); - } -} diff --git a/common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java b/common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java deleted file mode 100644 index 4d32e6ac..00000000 --- a/common/src/main/java/ctbrec/io/LocalTimeJsonAdapter.java +++ /dev/null @@ -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 { - @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()); - } -} diff --git a/common/src/main/java/ctbrec/io/ModelJsonAdapter.java b/common/src/main/java/ctbrec/io/ModelJsonAdapter.java deleted file mode 100644 index 4447f746..00000000 --- a/common/src/main/java/ctbrec/io/ModelJsonAdapter.java +++ /dev/null @@ -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 { - - private static final Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class); - - private List sites; - - public ModelJsonAdapter() { - } - - public ModelJsonAdapter(List 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); - } - } - -} diff --git a/common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java b/common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java deleted file mode 100644 index 298dbbd7..00000000 --- a/common/src/main/java/ctbrec/io/PostProcessorJsonAdapter.java +++ /dev/null @@ -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 { - - @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 entry : pp.getConfig().entrySet()) { - writer.name(entry.getKey()).value(entry.getValue()); - } - writer.endObject(); - writer.endObject(); - } - -} diff --git a/common/src/main/java/ctbrec/io/UuidJSonAdapter.java b/common/src/main/java/ctbrec/io/UuidJSonAdapter.java deleted file mode 100644 index b3a481a7..00000000 --- a/common/src/main/java/ctbrec/io/UuidJSonAdapter.java +++ /dev/null @@ -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 { - - @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()); - } - -} diff --git a/common/src/main/java/ctbrec/io/json/ObjectMapperFactory.java b/common/src/main/java/ctbrec/io/json/ObjectMapperFactory.java new file mode 100644 index 00000000..27af82f7 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/ObjectMapperFactory.java @@ -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; + } +} diff --git a/common/src/main/java/ctbrec/io/json/dto/CookieDto.java b/common/src/main/java/ctbrec/io/json/dto/CookieDto.java new file mode 100644 index 00000000..7d2ed6a5 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/dto/CookieDto.java @@ -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; +} diff --git a/common/src/main/java/ctbrec/io/json/dto/ModelDto.java b/common/src/main/java/ctbrec/io/json/dto/ModelDto.java new file mode 100644 index 00000000..29f5d3e2 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/dto/ModelDto.java @@ -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 siteSpecific = new HashMap<>(); + +} diff --git a/common/src/main/java/ctbrec/io/json/dto/PostProcessorDto.java b/common/src/main/java/ctbrec/io/json/dto/PostProcessorDto.java new file mode 100644 index 00000000..91a211d3 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/dto/PostProcessorDto.java @@ -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 config; +} diff --git a/common/src/main/java/ctbrec/io/json/dto/RecordingDto.java b/common/src/main/java/ctbrec/io/json/dto/RecordingDto.java new file mode 100644 index 00000000..0c4f21ea --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/dto/RecordingDto.java @@ -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 associatedFiles = new HashSet<>(); + private File absoluteFile = null; + private File postProcessedFile = null; + private int selectedResolution = -1; + private long lastSizeUpdate = 0; +} diff --git a/common/src/main/java/ctbrec/io/json/dto/converter/InstantToMillisConverter.java b/common/src/main/java/ctbrec/io/json/dto/converter/InstantToMillisConverter.java new file mode 100644 index 00000000..1e20acd4 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/dto/converter/InstantToMillisConverter.java @@ -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 { + + @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); + } +} diff --git a/common/src/main/java/ctbrec/io/json/dto/converter/MillisToInstantConverter.java b/common/src/main/java/ctbrec/io/json/dto/converter/MillisToInstantConverter.java new file mode 100644 index 00000000..9100dd96 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/dto/converter/MillisToInstantConverter.java @@ -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 { + @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); + } +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/CookieMapper.java b/common/src/main/java/ctbrec/io/json/mapper/CookieMapper.java new file mode 100644 index 00000000..f9122a6d --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/CookieMapper.java @@ -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(); + } +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/MappingException.java b/common/src/main/java/ctbrec/io/json/mapper/MappingException.java new file mode 100644 index 00000000..5dcb5deb --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/MappingException.java @@ -0,0 +1,7 @@ +package ctbrec.io.json.mapper; + +public class MappingException extends RuntimeException { + public MappingException(Exception cause) { + super(cause); + } +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/ModelFactory.java b/common/src/main/java/ctbrec/io/json/mapper/ModelFactory.java new file mode 100644 index 00000000..ce1465a2 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/ModelFactory.java @@ -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 modelClass = (Class) Class.forName(dto.getType()); + return modelClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new MappingException(e); + } + } +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/ModelMapper.java b/common/src/main/java/ctbrec/io/json/mapper/ModelMapper.java new file mode 100644 index 00000000..0763be47 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/ModelMapper.java @@ -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()); + } +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/PostProcessorFactory.java b/common/src/main/java/ctbrec/io/json/mapper/PostProcessorFactory.java new file mode 100644 index 00000000..433b5929 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/PostProcessorFactory.java @@ -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 ppClass = (Class) Class.forName(dto.getType()); + return ppClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new MappingException(e); + } + } +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/PostProcessorMapper.java b/common/src/main/java/ctbrec/io/json/mapper/PostProcessorMapper.java new file mode 100644 index 00000000..a20cfc0f --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/PostProcessorMapper.java @@ -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; +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/RecordingMapper.java b/common/src/main/java/ctbrec/io/json/mapper/RecordingMapper.java new file mode 100644 index 00000000..816bf2f3 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/RecordingMapper.java @@ -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); +} diff --git a/common/src/main/java/ctbrec/io/json/mapper/UriMapper.java b/common/src/main/java/ctbrec/io/json/mapper/UriMapper.java new file mode 100644 index 00000000..6c1b1e33 --- /dev/null +++ b/common/src/main/java/ctbrec/io/json/mapper/UriMapper.java @@ -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); + } +} diff --git a/common/src/main/java/ctbrec/recorder/RecordingManager.java b/common/src/main/java/ctbrec/recorder/RecordingManager.java index da7d5aea..b710ff1b 100644 --- a/common/src/main/java/ctbrec/recorder/RecordingManager.java +++ b/common/src/main/java/ctbrec/recorder/RecordingManager.java @@ -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 adapter; + private final List sites; private final List recordings = new ArrayList<>(); private final ReentrantLock recordingsLock = new ReentrantLock(); @@ -39,12 +38,7 @@ public class RecordingManager { public RecordingManager(Config config, List 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 getAll() { diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 15cf13af..7249904d 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -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 modelListResponseAdapter = moshi.adapter(ModelListResponse.class); - private final JsonAdapter recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class); - private final JsonAdapter modelRequestAdapter = moshi.adapter(ModelRequest.class); - private final JsonAdapter modelGroupRequestAdapter = moshi.adapter(ModelGroupRequest.class); - private final JsonAdapter modelGroupListResponseAdapter = moshi.adapter(ModelGroupListResponse.class); - private final JsonAdapter recordingRequestAdapter = moshi.adapter(RecordingRequest.class); - private final JsonAdapter simpleResponseAdapter = moshi.adapter(SimpleResponse.class); + + private final ObjectMapper mapper = ObjectMapperFactory.getMapper(); private List models = Collections.emptyList(); private List onlineModels = Collections.emptyList(); private List recordings = Collections.emptyList(); - private final ReentrantLock modelGroupLock = new ReentrantLock(); private final Set modelGroups = new HashSet<>(); + private final ReentrantLock modelGroupLock = new ReentrantLock(); private final List 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 newRecordings = resp.recordings; + List newRecordings = resp.recordings.stream().map(Mappers.getMapper(RecordingMapper.class)::toRecording).collect(Collectors.toList()); // fire changed events - for (Iterator 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 models; + String status; + String msg; + List models; } + @Data private static class ModelGroupListResponse { - public String status; - public String msg; - public List groups; + String status; + String msg; + List 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 recordings; + String status; + String msg; + List 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); diff --git a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java index 00354c1d..d725496d 100644 --- a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java @@ -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 models = Collections.synchronizedList(new ArrayList<>()); + private List 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 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 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 postProcessors = config.getSettings().postProcessors; + List 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 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"); } diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java b/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java index 7835285c..fd957195 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java @@ -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; } diff --git a/common/src/main/java/ctbrec/sites/SiteUtil.java b/common/src/main/java/ctbrec/sites/SiteUtil.java new file mode 100644 index 00000000..e8e3e4b1 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/SiteUtil.java @@ -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 getSiteForModel(List sites, Model model) { + for (Site site : sites) { + if (site.isSiteForModel(model)) { + return Optional.of(site); + } + } + return Optional.empty(); + } +} diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index e01d3920..7199b1ae 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -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 adapter = moshi.adapter(StreamInfo.class); - streamInfo = adapter.fromJson(content); + streamInfo = mapper.readValue(content, StreamInfo.class); return streamInfo; } else { int code = response.code(); diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java index 16b5a20b..0b461ca0 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java @@ -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 data) { + id = data.get("id"); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { - writer.name("id").value(id); + public void writeSiteSpecificData(Map data) { + data.put("id", id); } } diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index 6a66e0a0..7e064609 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -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 data) { + id = data.get("id"); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { - writer.name("id").value(id); + public void writeSiteSpecificData(Map data) { + data.put("id", id); } @Override diff --git a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java index 6406f9d9..e7781894 100644 --- a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java +++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java @@ -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 data) { + id = data.get("id"); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { - writer.name("id").value(id); + public void writeSiteSpecificData(Map data) { + data.put("id", id); } public void setStreamUrl(String streamUrl) { diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java index 0cbae883..e8698734 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminModel.java @@ -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 data) { + id = data.get("id"); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { + public void writeSiteSpecificData(Map 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) { diff --git a/common/src/main/java/ctbrec/sites/manyvids/MVLiveModel.java b/common/src/main/java/ctbrec/sites/manyvids/MVLiveModel.java index 81055eee..4d0e93a9 100644 --- a/common/src/main/java/ctbrec/sites/manyvids/MVLiveModel.java +++ b/common/src/main/java/ctbrec/sites/manyvids/MVLiveModel.java @@ -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 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 data) { + id = data.get("id"); } public void setId(String id) { diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index dbf17437..70448cf3 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -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 sessionStates = CacheBuilder.newBuilder().maximumSize(4000).build(); @@ -61,7 +58,6 @@ public class MyFreeCamsClient { private final Queue 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 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 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 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 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); diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 723f5896..a0d278cb 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -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 data) { + uid = Integer.parseInt(data.get("uid")); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { - writer.name("uid").value(uid); + public void writeSiteSpecificData(Map data) { + data.put("uid", Integer.toString(uid)); } @Override diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index 5e9a0697..1d8d1305 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -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 data) { + id = Long.parseLong(data.get("id")); } @Override - public void writeSiteSpecificData(JsonWriter writer) throws IOException { + public void writeSiteSpecificData(Map 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 diff --git a/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java b/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java new file mode 100644 index 00000000..8861cf9c --- /dev/null +++ b/common/src/test/java/ctbrec/io/json/mapper/ModelMapperTest.java @@ -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()); + } +} diff --git a/common/src/test/java/ctbrec/io/json/mapper/RecordingMapperTest.java b/common/src/test/java/ctbrec/io/json/mapper/RecordingMapperTest.java new file mode 100644 index 00000000..3a5c6b74 --- /dev/null +++ b/common/src/test/java/ctbrec/io/json/mapper/RecordingMapperTest.java @@ -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()); + + } +} diff --git a/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java b/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java index 163f7f59..5efbc850 100644 --- a/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java +++ b/common/src/test/java/ctbrec/recorder/RecordingPreconditionsTest.java @@ -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 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 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 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 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 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 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); diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java index 6a125889..4597bba7 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java @@ -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))); diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPpTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPpTest.java index 6c708791..7186b636 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPpTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPpTest.java @@ -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; } diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/DeleteTooShortTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/DeleteTooShortTest.java index 00e05a83..82c677cf 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/DeleteTooShortTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/DeleteTooShortTest.java @@ -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()); diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/RemoveKeepFileTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/RemoveKeepFileTest.java index 6463f22d..d9fdaf55 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/RemoveKeepFileTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/RemoveKeepFileTest.java @@ -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 { diff --git a/master/pom.xml b/master/pom.xml index 3071b300..1b4bf92a 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -20,6 +20,9 @@ 17 20.0.1 5.7.2 + 2.15.1 + 1.5.3.Final + 1.18.24 @@ -67,11 +70,6 @@ okhttp 4.9.3 - - com.squareup.moshi - moshi - 1.13.0 - org.json json @@ -123,6 +121,21 @@ commons-io 2.11.0 + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + org.junit.jupiter junit-jupiter-api @@ -159,7 +172,7 @@ org.projectlombok lombok - 1.18.24 + ${lombok.version} true @@ -186,6 +199,12 @@ lombok true + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + true + diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 4556a0db..ddd0618a 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -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; diff --git a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java index 27a231a2..0ae1c1a7 100644 --- a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java @@ -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 sites; - private List sites; public RecorderServlet(Recorder recorder, List 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 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 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 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 modelAdapter = new ModelJsonAdapter(); - List models = recorder.getModels(); - for (Iterator 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 recordings = recorder.getRecordings(); + for (Iterator 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 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 recAdapter = moshi.adapter(Recording.class); - List recordings = recorder.getRecordings(); - for (Iterator 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()); } } diff --git a/server/src/main/java/ctbrec/recorder/server/Request.java b/server/src/main/java/ctbrec/recorder/server/Request.java new file mode 100644 index 00000000..5987aa7b --- /dev/null +++ b/server/src/main/java/ctbrec/recorder/server/Request.java @@ -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; +} diff --git a/server/src/main/java/ctbrec/recorder/server/io/json/dto/RequestDto.java b/server/src/main/java/ctbrec/recorder/server/io/json/dto/RequestDto.java new file mode 100644 index 00000000..c7b09ee6 --- /dev/null +++ b/server/src/main/java/ctbrec/recorder/server/io/json/dto/RequestDto.java @@ -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; +} diff --git a/server/src/main/java/ctbrec/recorder/server/io/json/mapper/RequestMapper.java b/server/src/main/java/ctbrec/recorder/server/io/json/mapper/RequestMapper.java new file mode 100644 index 00000000..0f854bec --- /dev/null +++ b/server/src/main/java/ctbrec/recorder/server/io/json/mapper/RequestMapper.java @@ -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); +}