forked from j62/ctbrec
1
0
Fork 0

Save model notes on server, when running in client/server mode

This commit is contained in:
0xb00bface 2023-04-25 19:16:37 +02:00
parent 63ffe78c36
commit 86e8f00aaf
23 changed files with 476 additions and 105 deletions

View File

@ -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<Site> 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));

View File

@ -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);

View File

@ -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<String> 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<String> 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);
}

View File

@ -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);

View File

@ -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<List<JavaFxRecording>> 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<JavaFxRecording, String> 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);

View File

@ -79,7 +79,7 @@ public abstract class AbstractRecordedModelsTab extends Tab implements TabSelect
protected StatePersistingTableView<JavaFxModel> table;
protected List<TableColumn<JavaFxModel, ?>> columns = new ArrayList<>();
protected LoadingCache<Model, Image> 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;
}
};

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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));

View File

@ -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<String> loadModelNotes(String modelUrl) {
return Optional.ofNullable(config.getSettings().modelNotes.get(modelUrl));
}
@Override
public Map<String, String> 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);
}
}
}

View File

@ -0,0 +1,16 @@
package ctbrec.notes;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
public interface ModelNotesService {
Optional<String> loadModelNotes(String modelUrl) throws IOException;
Map<String, String> loadAllModelNotes() throws IOException;
void writeModelNotes(String modelUrl, String notes) throws IOException;
void removeModelNotes(String modelUrl) throws IOException;
}

View File

@ -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<String, String> 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<String, String> modelNotes) {
LocalModelNotesService localModelNotesStore = new LocalModelNotesService(config);
GlobalThreadPool.submit(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
List<String> successfullyTransfered = new ArrayList<>();
for (Map.Entry<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String> 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);
}
}
}

View File

@ -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;
}

View File

@ -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());

View File

@ -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;
}

View File

@ -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> 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<Object> getSanitizedModelNotes(Config config, Model m) {
return ofNullable(config.getModelNotes(m)).map(StringUtil::sanitize);
private Optional<Object> 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();
}
}
}

View File

@ -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)));
}

View File

@ -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<Config> 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);

View File

@ -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<Config> 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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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<byte[]> 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) {

View File

@ -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);
}
}
}