diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 641c375f..6942d9a0 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -19,6 +19,9 @@ import ctbrec.io.BandwidthMeter; import ctbrec.io.ByteUnitFormatter; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; +import ctbrec.notes.LocalModelNotesService; +import ctbrec.notes.ModelNotesService; +import ctbrec.notes.RemoteModelNotesService; import ctbrec.recorder.NextGenLocalRecorder; import ctbrec.recorder.OnlineMonitor; import ctbrec.recorder.Recorder; @@ -98,6 +101,7 @@ public class CamrecApplication extends Application { private final List sites = new ArrayList<>(); public static HttpClient httpClient; public static PortraitStore portraitStore; + public static ModelNotesService modelNotesService; public static String title; private Stage primaryStage; private RecordingsTab recordingsTab; @@ -127,6 +131,7 @@ public class CamrecApplication extends Application { initSites(); startOnlineMonitor(); createPortraitStore(); + createModelNotesService(); createGui(primaryStage); checkForUpdates(); registerClipboardListener(); @@ -141,6 +146,14 @@ public class CamrecApplication extends Application { } } + private void createModelNotesService() { + if (config.getSettings().localRecording) { + modelNotesService = new LocalModelNotesService(config); + } else { + modelNotesService = new RemoteModelNotesService(httpClient, config); + } + } + private void registerTrayIconListener() { EventBusHolder.BUS.register(new Object() { @Subscribe @@ -244,7 +257,7 @@ public class CamrecApplication extends Application { var modelsTab = new RecordedTab(recorder, sites); tabPane.getTabs().add(modelsTab); - recordingsTab = new RecordingsTab("Recordings", recorder, config); + recordingsTab = new RecordingsTab("Recordings", recorder, config, modelNotesService); tabPane.getTabs().add(recordingsTab); if (config.getSettings().recentlyWatched) { tabPane.getTabs().add(new RecentlyWatchedTab(recorder, sites)); diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index 371a1f76..5c455ebc 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -188,7 +188,7 @@ public class Player { } private void expandPlaceHolders(String[] cmdline) { - ModelVariableExpander expander = new ModelVariableExpander(model, Config.getInstance(), null, null); + ModelVariableExpander expander = new ModelVariableExpander(model, CamrecApplication.modelNotesService, null, null); for (int i = 0; i < cmdline.length; i++) { var param = cmdline[i]; param = expander.expand(param); diff --git a/client/src/main/java/ctbrec/ui/action/EditNotesAction.java b/client/src/main/java/ctbrec/ui/action/EditNotesAction.java index 8fcfcee9..8202f17e 100644 --- a/client/src/main/java/ctbrec/ui/action/EditNotesAction.java +++ b/client/src/main/java/ctbrec/ui/action/EditNotesAction.java @@ -1,23 +1,23 @@ package ctbrec.ui.action; +import ctbrec.Model; +import ctbrec.notes.ModelNotesService; +import ctbrec.ui.CamrecApplication; +import ctbrec.ui.controls.Dialogs; +import javafx.scene.Cursor; +import javafx.scene.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.Model; -import ctbrec.ui.controls.Dialogs; -import javafx.scene.Cursor; -import javafx.scene.Node; - public class EditNotesAction { private static final Logger LOG = LoggerFactory.getLogger(EditNotesAction.class); - private Node source; - private Model model; - private Runnable callback; + private final Node source; + private final Model model; + private final Runnable callback; public EditNotesAction(Node source, Model selectedModel, Runnable callback) { this.source = source; @@ -27,26 +27,30 @@ public class EditNotesAction { public void execute() { source.setCursor(Cursor.WAIT); - String notes = Config.getInstance().getSettings().modelNotes.getOrDefault(model.getUrl(), ""); - Optional newNotes = Dialogs.showTextInput(source.getScene(), "Model Notes", "Notes for " + model.getName(), notes); - newNotes.ifPresent(n -> { - if (!n.trim().isEmpty()) { - Config.getInstance().getSettings().modelNotes.put(model.getUrl(), n); - } else { - Config.getInstance().getSettings().modelNotes.remove(model.getUrl()); - } - try { - Config.getInstance().save(); - } catch (IOException e) { - LOG.warn("Couldn't save config", e); - } - }); - if (callback != null) { - try { - callback.run(); - } catch (Exception e) { - LOG.error("Error while executing callback", e); + ModelNotesService notesService = CamrecApplication.modelNotesService; + try { + String notes = notesService.loadModelNotes(model.getUrl()).orElse(""); + Optional newNotes = Dialogs.showTextInput(source.getScene(), "Model Notes", "Notes for " + model.getName(), notes); + newNotes.ifPresent(n -> { + try { + if (!n.trim().isEmpty()) { + notesService.writeModelNotes(model.getUrl(), n); + } else { + notesService.removeModelNotes(model.getUrl()); + } + } catch (IOException e) { + LOG.warn("Couldn't save config", e); + } + }); + if (callback != null) { + try { + callback.run(); + } catch (Exception e) { + LOG.error("Error while executing callback", e); + } } + } catch (Exception e) { + Dialogs.showError(source.getScene(), "Model Notes", "Could not change model notes", e); } source.setCursor(Cursor.DEFAULT); } diff --git a/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java b/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java index 965c745e..80d534b2 100644 --- a/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java +++ b/client/src/main/java/ctbrec/ui/settings/VariablePlayGroundDialogFactory.java @@ -6,6 +6,7 @@ import ctbrec.StringUtil; import ctbrec.UnknownModel; import ctbrec.recorder.Recorder; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.ui.CamrecApplication; import ctbrec.ui.controls.Dialogs; import ctbrec.variableexpansion.ConfigVariableExpander; import ctbrec.variableexpansion.ModelVariableExpander; @@ -77,7 +78,7 @@ public class VariablePlayGroundDialogFactory { } }; - ModelVariableExpander modelVariableExpander = new ModelVariableExpander(unknownModel, config, recorder, errorHandler); + ModelVariableExpander modelVariableExpander = new ModelVariableExpander(unknownModel, CamrecApplication.modelNotesService, recorder, errorHandler); RecordingVariableExpander recordingVariableExpander = new RecordingVariableExpander(recording, errorHandler); ConfigVariableExpander configVariableExpander = new ConfigVariableExpander(config, errorHandler); diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java index b3061c09..e66fe308 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecordingsTab.java @@ -5,6 +5,7 @@ import ctbrec.Recording.State; import ctbrec.event.EventBusHolder; import ctbrec.event.RecordingStateChangedEvent; import ctbrec.io.UrlUtil; +import ctbrec.notes.ModelNotesService; import ctbrec.recorder.ProgressListener; import ctbrec.recorder.Recorder; import ctbrec.recorder.RecordingPinnedException; @@ -74,6 +75,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown private ScheduledService> updateService; private final Config config; + private final ModelNotesService modelNotesService; private final Recorder recorder; private long spaceTotal = -1; private long spaceFree = -1; @@ -89,10 +91,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown Label spaceUsedValue; Lock recordingsLock = new ReentrantLock(); - public RecordingsTab(String title, Recorder recorder, Config config) { + public RecordingsTab(String title, Recorder recorder, Config config, ModelNotesService modelNotesService) { super(title); this.recorder = recorder; this.config = config; + this.modelNotesService = modelNotesService; createGui(); setClosable(false); initializeUpdateService(); @@ -156,7 +159,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown TableColumn modelNotes = new TableColumn<>("Model Notes"); modelNotes.setId("modelNotes"); modelNotes.setPrefWidth(400); - modelNotes.setCellValueFactory(cdf -> new SimpleStringProperty(config.getModelNotes(cdf.getValue().getModel()))); + modelNotes.setCellValueFactory(cdf -> { + String modelNts; + try { + modelNts = modelNotesService.loadModelNotes(cdf.getValue().getModel().getUrl()).orElse(""); + } catch (IOException e) { + LOG.warn("Could not load model notes", e); + modelNts = ""; + } + return new SimpleStringProperty(modelNts); + }); table.getColumns().addAll(name, date, status, progress, size, resolution, notes, modelNotes); table.setItems(observableRecordings); @@ -261,7 +273,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown Recording recording = table.getSelectionModel().getSelectedItem(); if (recording != null) { var state = recording.getStatus(); - if(state == FINISHED || state == RECORDING) { + if (state == FINISHED || state == RECORDING) { play(recording); } } @@ -448,10 +460,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener, Shutdown var tmp = new CustomMouseBehaviorContextMenu(); ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) // - .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // - .removeModelAfterIgnore(true) // - .afterwards(table::refresh) // - .contributeToMenu(List.of(recordings.get(0).getModel()), tmp); + .withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) // + .removeModelAfterIgnore(true) // + .afterwards(table::refresh) // + .contributeToMenu(List.of(recordings.get(0).getModel()), tmp); var modelSubMenu = new Menu("Model"); modelSubMenu.getItems().addAll(tmp.getItems()); contextMenu.getItems().add(modelSubMenu); 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 968c48c3..f0d022fa 100644 --- a/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/recorded/AbstractRecordedModelsTab.java @@ -79,7 +79,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect protected StatePersistingTableView table; protected List> columns = new ArrayList<>(); protected LoadingCache portraitCache = CacheBuilder.newBuilder() - .expireAfterAccess(1, TimeUnit.DAYS) + .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000) .build(CacheLoader.from(this::loadModelPortrait)); @@ -346,7 +346,12 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect @Override public String get() { - String modelNotes = Config.getInstance().getModelNotes(m); + String modelNotes; + try { + modelNotes = CamrecApplication.modelNotesService.loadModelNotes(m.getUrl()).orElse(""); + } catch (IOException e) { + throw new RuntimeException(e); + } return modelNotes; } }; diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 19f48aab..9cfe6a02 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -345,9 +345,9 @@ public class Config { } } - public String getModelNotes(Model m) { - return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), ""); - } + // public String getModelNotes(Model m) { + // return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), ""); + // } public void disableSaving() { savingDisabled = true; diff --git a/common/src/main/java/ctbrec/RemoteService.java b/common/src/main/java/ctbrec/RemoteService.java new file mode 100644 index 00000000..e56049e6 --- /dev/null +++ b/common/src/main/java/ctbrec/RemoteService.java @@ -0,0 +1,23 @@ +package ctbrec; + +import okhttp3.Request; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class RemoteService { + + protected void addHmacIfNeeded(byte[] msg, Request.Builder builder) throws InvalidKeyException, NoSuchAlgorithmException { + if (Config.getInstance().getSettings().requireAuthentication) { + byte[] key = Config.getInstance().getSettings().key; + String hmac = Hmac.calculate(msg, key); + builder.addHeader("CTBREC-HMAC", hmac); + } + } + + protected void addHmacIfNeeded(String msg, Request.Builder builder) throws InvalidKeyException, NoSuchAlgorithmException { + addHmacIfNeeded(msg.getBytes(UTF_8), builder); + } +} diff --git a/common/src/main/java/ctbrec/image/RemotePortraitStore.java b/common/src/main/java/ctbrec/image/RemotePortraitStore.java index 3c2f592d..eb605c02 100644 --- a/common/src/main/java/ctbrec/image/RemotePortraitStore.java +++ b/common/src/main/java/ctbrec/image/RemotePortraitStore.java @@ -3,6 +3,7 @@ package ctbrec.image; import ctbrec.Config; import ctbrec.GlobalThreadPool; import ctbrec.Hmac; +import ctbrec.RemoteService; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import lombok.extern.slf4j.Slf4j; @@ -24,7 +25,7 @@ import static ctbrec.io.HttpConstants.MIMETYPE_IMAGE_JPG; import static java.nio.charset.StandardCharsets.UTF_8; @Slf4j -public class RemotePortraitStore implements PortraitStore { +public class RemotePortraitStore extends RemoteService implements PortraitStore { private final HttpClient httpClient; private final Config config; @@ -99,14 +100,6 @@ public class RemotePortraitStore implements PortraitStore { } } - private void addHmacIfNeeded(byte[] msg, Request.Builder builder) throws InvalidKeyException, NoSuchAlgorithmException { - if (Config.getInstance().getSettings().requireAuthentication) { - byte[] key = Config.getInstance().getSettings().key; - String hmac = Hmac.calculate(msg, key); - builder.addHeader("CTBREC-HMAC", hmac); - } - } - @Override public void writePortrait(String modelUrl, byte[] data) throws IOException { RequestBody body = RequestBody.create(data, MediaType.parse(MIMETYPE_IMAGE_JPG)); diff --git a/common/src/main/java/ctbrec/notes/LocalModelNotesService.java b/common/src/main/java/ctbrec/notes/LocalModelNotesService.java new file mode 100644 index 00000000..bc05e842 --- /dev/null +++ b/common/src/main/java/ctbrec/notes/LocalModelNotesService.java @@ -0,0 +1,47 @@ +package ctbrec.notes; + +import ctbrec.Config; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@RequiredArgsConstructor +public class LocalModelNotesService implements ModelNotesService { + + private final Config config; + + @Override + public Optional loadModelNotes(String modelUrl) { + return Optional.ofNullable(config.getSettings().modelNotes.get(modelUrl)); + } + + @Override + public Map loadAllModelNotes() throws IOException { + return Collections.unmodifiableMap(config.getSettings().modelNotes); + } + + @Override + public void writeModelNotes(String modelUrl, String notes) { + config.getSettings().modelNotes.put(modelUrl, notes); + save(); + } + + @Override + public void removeModelNotes(String modelUrl) { + config.getSettings().modelNotes.remove(modelUrl); + save(); + } + + private void save() { + try { + config.save(); + } catch (IOException e) { + log.warn("Could not save the settings", e); + } + } +} diff --git a/common/src/main/java/ctbrec/notes/ModelNotesService.java b/common/src/main/java/ctbrec/notes/ModelNotesService.java new file mode 100644 index 00000000..cf171ab3 --- /dev/null +++ b/common/src/main/java/ctbrec/notes/ModelNotesService.java @@ -0,0 +1,16 @@ +package ctbrec.notes; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +public interface ModelNotesService { + + Optional loadModelNotes(String modelUrl) throws IOException; + + Map loadAllModelNotes() throws IOException; + + void writeModelNotes(String modelUrl, String notes) throws IOException; + + void removeModelNotes(String modelUrl) throws IOException; +} diff --git a/common/src/main/java/ctbrec/notes/RemoteModelNotesService.java b/common/src/main/java/ctbrec/notes/RemoteModelNotesService.java new file mode 100644 index 00000000..02d26b82 --- /dev/null +++ b/common/src/main/java/ctbrec/notes/RemoteModelNotesService.java @@ -0,0 +1,154 @@ +package ctbrec.notes; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import ctbrec.Config; +import ctbrec.GlobalThreadPool; +import ctbrec.RemoteService; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import lombok.extern.slf4j.Slf4j; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@Slf4j +public class RemoteModelNotesService extends RemoteService implements ModelNotesService { + + private final HttpClient httpClient; + private final Config config; + + private final LoadingCache cache = CacheBuilder.newBuilder() + .expireAfterWrite(3, TimeUnit.SECONDS) + .maximumSize(10000) + .build(CacheLoader.from(this::updateCache)); + + public RemoteModelNotesService(HttpClient httpClient, Config config) { + this.httpClient = httpClient; + this.config = config; + transferOldNotesToServer(config.getSettings().modelNotes); + } + + private void transferOldNotesToServer(Map modelNotes) { + LocalModelNotesService localModelNotesStore = new LocalModelNotesService(config); + GlobalThreadPool.submit(() -> { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + List successfullyTransfered = new ArrayList<>(); + for (Map.Entry entry : modelNotes.entrySet()) { + try { + log.info("Uploading model notes to server {} - {}", entry.getKey(), entry.getValue()); + RemoteModelNotesService.this.writeModelNotes(entry.getKey(), entry.getValue()); + successfullyTransfered.add(entry.getKey()); + } catch (Exception e) { + log.warn("Could not transfer model notes from local to remote store: {} {} - {}", entry.getKey(), entry.getValue(), e.getLocalizedMessage()); + } + } + for (String s : successfullyTransfered) { + localModelNotesStore.removeModelNotes(s); + } + }); + } + + private String updateCache(String modelUrl) { + try { + var modelNotes = loadAllModelNotes(); + for (Map.Entry entry : modelNotes.entrySet()) { + cache.put(entry.getKey(), entry.getValue()); + } + return modelNotes.get(modelUrl); + } catch (Exception e) { + throw new CacheLoader.InvalidCacheLoadException("Loading of model notes from server failed"); + } + } + + @Override + public Map loadAllModelNotes() throws IOException { + Request.Builder builder = new Request.Builder().url(config.getServerUrl() + "/models/notes"); + try { + addHmacIfNeeded(new byte[0], builder); + log.trace("Loading all model notes from server"); + try (Response resp = httpClient.execute(builder.build())) { + if (resp.isSuccessful()) { + Map result = new HashMap<>(); + JSONObject json = new JSONObject(resp.body().string()); + JSONArray names = json.names(); + for (int i = 0; i < names.length(); i++) { + String name = names.getString(i); + result.put(name, json.getString(name)); + } + return Collections.unmodifiableMap(result); + } else { + throw new HttpException(resp.code(), resp.message()); + } + } + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IOException("Could not load model notes from server", e); + } + } + + @Override + public Optional loadModelNotes(String modelUrl) throws IOException { + try { + log.trace("Loading model notes for {}", modelUrl); + return Optional.of(cache.get(modelUrl)); + } catch (ExecutionException e) { + throw new IOException(e); + } + } + + @Override + public void writeModelNotes(String modelUrl, String notes) throws IOException { + Request.Builder builder = new Request.Builder() + .url(config.getServerUrl() + "/models/notes/" + URLEncoder.encode(modelUrl, UTF_8)) + .post(RequestBody.create(notes, MediaType.parse("text/plain"))); + try { + addHmacIfNeeded(notes, builder); + try (Response resp = httpClient.execute(builder.build())) { + if (resp.isSuccessful()) { + cache.put(modelUrl, notes); + } else { + throw new HttpException(resp.code(), resp.message()); + } + } + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IOException("Could not write model notes to server", e); + } + } + + @Override + public void removeModelNotes(String modelUrl) throws IOException { + Request.Builder builder = new Request.Builder() + .url(config.getServerUrl() + "/models/notes/" + URLEncoder.encode(modelUrl, UTF_8)) + .delete(); + try { + addHmacIfNeeded(new byte[0], builder); + try (Response resp = httpClient.execute(builder.build())) { + if (resp.isSuccessful()) { + cache.invalidate(modelUrl); + } else { + throw new HttpException(resp.code(), resp.message()); + } + } + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IOException("Could not delete model notes from server", e); + } + } +} diff --git a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java index 5badb613..c109756f 100644 --- a/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java @@ -5,6 +5,7 @@ import ctbrec.*; import ctbrec.Recording.State; import ctbrec.event.*; import ctbrec.io.HttpClient; +import ctbrec.notes.LocalModelNotesService; import ctbrec.recorder.download.Download; import ctbrec.recorder.postprocessing.PostProcessingContext; import ctbrec.recorder.postprocessing.PostProcessor; @@ -228,6 +229,7 @@ public class NextGenLocalRecorder implements Recorder { ctx.setRecorder(this); ctx.setRecording(recording); ctx.setRecordingManager(recordingManager); + ctx.setModelNotesService(new LocalModelNotesService(config)); return ctx; } diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java index 18b1edcc..3f30dc09 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessor.java @@ -2,6 +2,7 @@ package ctbrec.recorder.postprocessing; import ctbrec.Config; import ctbrec.Recording; +import ctbrec.notes.ModelNotesService; import ctbrec.variableexpansion.ConfigVariableExpander; import ctbrec.variableexpansion.ModelVariableExpander; import ctbrec.variableexpansion.RecordingVariableExpander; @@ -18,8 +19,9 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost public String fillInPlaceHolders(String input, PostProcessingContext ctx, AntlrSyntacErrorAdapter errorListener) { Recording rec = ctx.getRecording(); Config config = ctx.getConfig(); + ModelNotesService modelNotesService = ctx.getModelNotesService(); - ModelVariableExpander modelExpander = new ModelVariableExpander(rec.getModel(), config, ctx.getRecorder(), errorListener); + ModelVariableExpander modelExpander = new ModelVariableExpander(rec.getModel(), modelNotesService, ctx.getRecorder(), errorListener); RecordingVariableExpander recordingExpander = new RecordingVariableExpander(rec, errorListener); ConfigVariableExpander configExpander = new ConfigVariableExpander(config, errorListener); modelExpander.getPlaceholderValueSuppliers().putAll(recordingExpander.getPlaceholderValueSuppliers()); diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessingContext.java b/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessingContext.java index 057c948f..4aa93a4e 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessingContext.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/PostProcessingContext.java @@ -2,45 +2,17 @@ package ctbrec.recorder.postprocessing; import ctbrec.Config; import ctbrec.Recording; +import ctbrec.notes.ModelNotesService; import ctbrec.recorder.Recorder; import ctbrec.recorder.RecordingManager; +import lombok.Data; +@Data public class PostProcessingContext { private Recorder recorder; private Recording recording; private RecordingManager recordingManager; private Config config; - - public Recorder getRecorder() { - return recorder; - } - - public void setRecorder(Recorder recorder) { - this.recorder = recorder; - } - - public Recording getRecording() { - return recording; - } - - public void setRecording(Recording recording) { - this.recording = recording; - } - - public RecordingManager getRecordingManager() { - return recordingManager; - } - - public void setRecordingManager(RecordingManager recordingManager) { - this.recordingManager = recordingManager; - } - - public Config getConfig() { - return config; - } - - public void setConfig(Config config) { - this.config = config; - } + private ModelNotesService modelNotesService; } diff --git a/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java index 0165ed46..c7c564c0 100644 --- a/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java +++ b/common/src/main/java/ctbrec/variableexpansion/ModelVariableExpander.java @@ -1,26 +1,28 @@ package ctbrec.variableexpansion; -import ctbrec.Config; import ctbrec.Model; import ctbrec.ModelGroup; import ctbrec.StringUtil; +import ctbrec.notes.ModelNotesService; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.variableexpansion.functions.AntlrSyntacErrorAdapter; +import lombok.extern.slf4j.Slf4j; import java.util.Optional; import java.util.UUID; import static java.util.Optional.ofNullable; +@Slf4j public class ModelVariableExpander extends AbstractVariableExpander { - public ModelVariableExpander(Model model, Config config, Recorder recorder, AntlrSyntacErrorAdapter errorListener) { + public ModelVariableExpander(Model model, ModelNotesService modelNotesService, Recorder recorder, AntlrSyntacErrorAdapter errorListener) { super(errorListener); Optional modelGroup = Optional.ofNullable(recorder).flatMap(r -> r.getModelGroup(model)); placeholderValueSuppliers.put("modelName", ofNullable(model.getName())); placeholderValueSuppliers.put("modelDisplayName", ofNullable(model.getDisplayName())); - placeholderValueSuppliers.put("modelNotes", getSanitizedModelNotes(config, model)); + placeholderValueSuppliers.put("modelNotes", getSanitizedModelNotes(modelNotesService, model)); placeholderValueSuppliers.put("siteName", ofNullable(model).map(Model::getSite).map(Site::getName)); placeholderValueSuppliers.put("modelGroupName", modelGroup.map(ModelGroup::getName)); placeholderValueSuppliers.put("modelGroupId", modelGroup.map(ModelGroup::getId).map(UUID::toString)); @@ -30,7 +32,12 @@ public class ModelVariableExpander extends AbstractVariableExpander { return fillInPlaceHolders(input, placeholderValueSuppliers); } - private Optional getSanitizedModelNotes(Config config, Model m) { - return ofNullable(config.getModelNotes(m)).map(StringUtil::sanitize); + private Optional getSanitizedModelNotes(ModelNotesService modelNotesService, Model m) { + try { + return modelNotesService.loadModelNotes(m.getUrl()).map(StringUtil::sanitize); + } catch (Exception e) { + log.warn("Could not load model notes", e); + return Optional.empty(); + } } } diff --git a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java index c442c9e1..11e6babe 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPlaceholderAwarePostProcessorTest.java @@ -2,6 +2,7 @@ package ctbrec.recorder.postprocessing; import ctbrec.Config; import ctbrec.Recording; +import ctbrec.notes.ModelNotesService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,6 +15,7 @@ import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { @@ -21,6 +23,7 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { Recording rec; Config config; Move placeHolderAwarePp; + ModelNotesService modelNotesService; @Override @BeforeEach @@ -34,6 +37,8 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { rec.setSingleFile(true); config = mockConfig(); placeHolderAwarePp = new Move(); + modelNotesService = mock(ModelNotesService.class); + when(modelNotesService.loadModelNotes(any())).thenReturn(null); } @Test @@ -157,7 +162,6 @@ class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest { @Test void testMissingValueForPlaceholder() { String input = "asdf_${modelNotes}_asdf"; - when(config.getModelNotes(any())).thenReturn(null); assertEquals("asdf_${modelNotes}_asdf", 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 7b858f5f..dd3d38bf 100644 --- a/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPpTest.java +++ b/common/src/test/java/ctbrec/recorder/postprocessing/AbstractPpTest.java @@ -4,6 +4,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording; import ctbrec.Settings; +import ctbrec.notes.ModelNotesService; import ctbrec.recorder.Recorder; import ctbrec.recorder.RecordingManager; import ctbrec.sites.Site; @@ -18,6 +19,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.Optional; import static java.nio.file.StandardOpenOption.*; import static org.mockito.ArgumentMatchers.any; @@ -34,6 +36,7 @@ public abstract class AbstractPpTest { RecordingManager recordingManager; MockedStatic configStatic; + ModelNotesService modelNotesService; @BeforeEach public void setup() throws IOException { @@ -48,6 +51,8 @@ public abstract class AbstractPpTest { Files.writeString(postProcessed.toPath(), "foobar", CREATE_NEW, WRITE, TRUNCATE_EXISTING); Files.createDirectories(originalDir.toPath()); FileUtils.touch(new File(originalDir, "playlist.m3u8")); + modelNotesService = mock(ModelNotesService.class); + when(modelNotesService.loadModelNotes(any())).thenReturn(Optional.of("tag, foo, bar")); } @AfterEach @@ -63,7 +68,6 @@ public abstract class AbstractPpTest { Config config = mock(Config.class); Settings settings = mockSettings(); when(config.getSettings()).thenReturn(settings); - when(config.getModelNotes(any())).thenReturn("tag, foo, bar"); when(config.getConfigDir()).thenReturn(new File(baseDir.toFile(), "config")); configStatic = mockStatic(Config.class); configStatic.when(Config::getInstance).thenReturn(config); diff --git a/common/src/test/java/ctbrec/variableexpansion/ModelVariableExpanderTest.java b/common/src/test/java/ctbrec/variableexpansion/ModelVariableExpanderTest.java index cd10276c..bc7104a1 100644 --- a/common/src/test/java/ctbrec/variableexpansion/ModelVariableExpanderTest.java +++ b/common/src/test/java/ctbrec/variableexpansion/ModelVariableExpanderTest.java @@ -4,6 +4,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.Settings; import ctbrec.UnknownModel; +import ctbrec.notes.ModelNotesService; import ctbrec.recorder.Recorder; import ctbrec.sites.chaturbate.Chaturbate; import org.junit.jupiter.api.AfterEach; @@ -11,6 +12,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import java.io.IOException; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -21,8 +25,10 @@ class ModelVariableExpanderTest { Config config; MockedStatic configStatic; + ModelNotesService modelNotesService; + @BeforeEach - void setup() { + void setup() throws IOException { UnknownModel unknownModel = new UnknownModel(); Chaturbate chaturbate = new Chaturbate(); unknownModel.setName("Pussy_Galore"); @@ -30,6 +36,8 @@ class ModelVariableExpanderTest { unknownModel.setSite(chaturbate); this.model = unknownModel; this.config = mockConfig(); + this.modelNotesService = mock(ModelNotesService.class); + when(modelNotesService.loadModelNotes(any())).thenReturn(Optional.of("tag, foo, bar")); } @AfterEach @@ -43,7 +51,7 @@ class ModelVariableExpanderTest { @Test void testMultipleVariablesAsParameter() { Recorder recorder = mock(Recorder.class); - ModelVariableExpander modelVariableExpander = new ModelVariableExpander(model, config, recorder, null); + ModelVariableExpander modelVariableExpander = new ModelVariableExpander(model, modelNotesService, recorder, null); assertEquals("pussy_galore asdf pussy_galore", modelVariableExpander.expand("$lower(${modelName} ASDF ${modelName})")); } @@ -51,7 +59,6 @@ class ModelVariableExpanderTest { Config config = mock(Config.class); Settings settings = mockSettings(); when(config.getSettings()).thenReturn(settings); - when(config.getModelNotes(any())).thenReturn("tag, foo, bar"); configStatic = mockStatic(Config.class); configStatic.when(Config::getInstance).thenReturn(config); return config; diff --git a/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java b/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java index 3c2d157d..12271f3e 100644 --- a/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java @@ -14,10 +14,12 @@ import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import static java.nio.charset.StandardCharsets.UTF_8; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; public abstract class AbstractCtbrecServlet extends HttpServlet { - + public static final String INTERNAL_SERVER_ERROR = "Internal Server Error"; + protected static final String HMAC_ERROR_DOCUMENT = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"; private static final Logger LOG = LoggerFactory.getLogger(AbstractCtbrecServlet.class); boolean checkAuthentication(HttpServletRequest req, String body) throws InvalidKeyException, NoSuchAlgorithmException { @@ -93,6 +95,7 @@ public abstract class AbstractCtbrecServlet extends HttpServlet { void sendResponse(HttpServletResponse resp, int httpStatus, String message) { try { resp.setStatus(httpStatus); + resp.setCharacterEncoding(UTF_8.toString()); resp.getWriter().print(message); } catch (IOException e) { LOG.error("Couldn't write response", e); diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 80e566d5..bbee58f3 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -237,6 +237,10 @@ public class HttpServer { holder.getRegistration().setMultipartConfig(multipartConfig); defaultContext.addServlet(holder, ImageServlet.BASE_URL + "/*"); + ModelServlet modelServlet = new ModelServlet(config); + holder = new ServletHolder(modelServlet); + defaultContext.addServlet(holder, ModelServlet.BASE_URL + "/*"); + if (this.config.getSettings().webinterface) { startWebInterface(defaultContext, basicAuthContext); } diff --git a/server/src/main/java/ctbrec/recorder/server/ImageServlet.java b/server/src/main/java/ctbrec/recorder/server/ImageServlet.java index bfac2fc3..17b18f00 100644 --- a/server/src/main/java/ctbrec/recorder/server/ImageServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/ImageServlet.java @@ -22,8 +22,6 @@ import static javax.servlet.http.HttpServletResponse.*; public class ImageServlet extends AbstractCtbrecServlet { public static final String BASE_URL = "/image"; - public static final String INTERNAL_SERVER_ERROR = "Internal Server Error"; - private static final String HMAC_ERROR_DOCUMENT = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"; private static final Pattern URL_PATTERN_PORTRAIT_BY_ID = Pattern.compile(BASE_URL + "/portrait/([0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12})"); private static final Pattern URL_PATTERN_PORTRAIT_BY_URL = Pattern.compile(BASE_URL + "/portrait/url/(.*)"); private final PortraitStore portraitStore; @@ -31,7 +29,7 @@ public class ImageServlet extends AbstractCtbrecServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { - String requestURI = req.getRequestURI(); + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); try { boolean authenticated = checkAuthentication(req, ""); if (!authenticated) { @@ -55,7 +53,6 @@ public class ImageServlet extends AbstractCtbrecServlet { } private void servePortrait(HttpServletResponse resp, String portraitId) throws IOException { - log.debug("serving portrait {}", portraitId); Optional imageData = portraitStore.loadModelPortraitById(portraitId); if (imageData.isPresent()) { resp.setStatus(SC_OK); @@ -71,7 +68,7 @@ public class ImageServlet extends AbstractCtbrecServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { - String requestURI = req.getRequestURI(); + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); try { byte[] data = bodyAsByteArray(req); boolean authenticated = checkAuthentication(req, data); @@ -93,7 +90,7 @@ public class ImageServlet extends AbstractCtbrecServlet { @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) { - String requestURI = req.getRequestURI(); + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); try { boolean authenticated = checkAuthentication(req, ""); if (!authenticated) { diff --git a/server/src/main/java/ctbrec/recorder/server/ModelServlet.java b/server/src/main/java/ctbrec/recorder/server/ModelServlet.java new file mode 100644 index 00000000..2d157f73 --- /dev/null +++ b/server/src/main/java/ctbrec/recorder/server/ModelServlet.java @@ -0,0 +1,101 @@ +package ctbrec.recorder.server; + +import ctbrec.Config; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URLDecoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.*; + +@Slf4j +@RequiredArgsConstructor +public class ModelServlet extends AbstractCtbrecServlet { + + public static final String BASE_URL = "/models"; + + private static final Pattern URL_PATTERN_ALL_MODEL_NOTES = Pattern.compile(BASE_URL + "/notes/?"); + private static final Pattern URL_PATTERN_MODEL_NOTES = Pattern.compile(BASE_URL + "/notes/(.+?)"); + private final Config config; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); + try { + boolean authenticated = checkAuthentication(req, ""); + if (!authenticated) { + sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); + return; + } + + Matcher m; + if ((m = URL_PATTERN_MODEL_NOTES.matcher(requestURI)).matches()) { + String modelUrl = URLDecoder.decode(m.group(1), UTF_8); + String notes = config.getSettings().modelNotes.getOrDefault(modelUrl, ""); + log.debug("Model Notes Request {} - {}", modelUrl, notes); + resp.setContentType("text/plain"); + sendResponse(resp, SC_OK, notes); + } else if ((URL_PATTERN_ALL_MODEL_NOTES.matcher(requestURI)).matches()) { + JSONObject notes = new JSONObject(); + config.getSettings().modelNotes.forEach(notes::put); + resp.setContentType("application/json"); + sendResponse(resp, SC_OK, notes.toString()); + } + } catch (Exception e) { + log.error(INTERNAL_SERVER_ERROR, e); + sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); + try { + var body = body(req); + log.info("Notes: [{}]", body); + boolean authenticated = checkAuthentication(req, body); + if (!authenticated) { + sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); + return; + } + + Matcher m; + if ((m = URL_PATTERN_MODEL_NOTES.matcher(requestURI)).matches()) { + String modelUrl = URLDecoder.decode(m.group(1), UTF_8); + config.getSettings().modelNotes.put(modelUrl, body); + config.save(); + } + } catch (Exception e) { + log.error(INTERNAL_SERVER_ERROR, e); + sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); + } + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) { + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); + try { + boolean authenticated = checkAuthentication(req, ""); + if (!authenticated) { + sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); + return; + } + + Matcher m; + if ((m = URL_PATTERN_MODEL_NOTES.matcher(requestURI)).matches()) { + String modelUrl = URLDecoder.decode(m.group(1), UTF_8); + config.getSettings().modelNotes.remove(modelUrl); + config.save(); + } + } catch (Exception e) { + log.error(INTERNAL_SERVER_ERROR, e); + sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); + } + } +}