package ctbrec; import static java.nio.charset.StandardCharsets.*; import static java.nio.file.StandardOpenOption.*; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import ctbrec.Settings.SplitStrategy; import ctbrec.io.FileJsonAdapter; import ctbrec.io.ModelJsonAdapter; import ctbrec.io.PostProcessorJsonAdapter; import ctbrec.recorder.postprocessing.DeleteTooShort; import ctbrec.recorder.postprocessing.PostProcessor; import ctbrec.recorder.postprocessing.RemoveKeepFile; import ctbrec.recorder.postprocessing.Script; import ctbrec.sites.Site; import ctbrec.sites.cam4.Cam4Model; public class Config { private static final Logger LOG = LoggerFactory.getLogger(Config.class); private static Config instance; private Settings settings; private String filename; private List sites; private File configDir; public static final String RECORDING_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss_SSS"; private Config(List sites) { this.sites = sites; if(System.getProperty("ctbrec.config.dir") != null) { configDir = new File(System.getProperty("ctbrec.config.dir")); } else { configDir = OS.getConfigDir(); } if(System.getProperty("ctbrec.config") != null) { filename = System.getProperty("ctbrec.config"); } else { filename = "settings.json"; } } 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()) .build(); JsonAdapter adapter = moshi.adapter(Settings.class).lenient(); File configFile = new File(configDir, filename); 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"); fileContent[0] = ' '; fileContent[1] = ' '; fileContent[2] = ' '; } String json = new String(fileContent, UTF_8).trim(); settings = adapter.fromJson(json); settings.httpTimeout = Math.max(settings.httpTimeout, 10_000); if (settings.recordingsDir.endsWith("/")) { settings.recordingsDir = settings.recordingsDir.substring(0, settings.recordingsDir.length() - 1); } } catch (Exception e) { settings = OS.getDefaultSettings(); for (Site site : sites) { site.setEnabled(!settings.disabledSites.contains(site.getName())); } makeBackup(configFile); throw e; } } else { LOG.error("Config file does not exist. Falling back to default values."); settings = OS.getDefaultSettings(); } for (Site site : sites) { site.setEnabled(!settings.disabledSites.contains(site.getName())); } migrateOldSettings(); } @SuppressWarnings("deprecation") private void migrateOldSettings() { // 3.8.0 from maxResolution only to resolution range if(settings.minimumResolution == settings.maximumResolution && settings.minimumResolution == 0) { settings.minimumResolution = 0; settings.maximumResolution = 8640; } // 3.10.0 if (StringUtil.isNotBlank(settings.postProcessing)) { Script script = new Script(); script.getConfig().put(Script.SCRIPT_EXECUTABLE, settings.postProcessing); script.getConfig().put(Script.SCRIPT_PARAMS, "${absoluteParentPath} ${absolutePath} ${modelName} ${siteName} ${epochSecond}"); settings.postProcessors.add(script); settings.postProcessing = null; } if (settings.minimumLengthInSeconds > 0) { DeleteTooShort deleteTooShort = new DeleteTooShort(); deleteTooShort.getConfig().put(DeleteTooShort.MIN_LEN_IN_SECS, Integer.toString(settings.minimumLengthInSeconds)); settings.postProcessors.add(deleteTooShort); settings.minimumLengthInSeconds = 0; } if (settings.removeRecordingAfterPostProcessing) { RemoveKeepFile removeKeepFile = new RemoveKeepFile(); settings.postProcessors.add(removeKeepFile); settings.removeRecordingAfterPostProcessing = false; } // 3.10.7 if (StringUtil.isNotBlank(settings.username)) { settings.chaturbateUsername = settings.username; settings.username = null; } if (StringUtil.isNotBlank(settings.password)) { settings.chaturbatePassword = settings.password; settings.password = null; } if (settings.splitRecordings > 0) { settings.splitStrategy = SplitStrategy.TIME; settings.splitRecordingsAfterSecs = settings.splitRecordings; settings.splitRecordings = 0; } // migrate old config from ctbrec-minimal browser File oldLocation = new File(OS.getConfigDir().getParentFile(), "ctbrec-minimal-browser"); if (oldLocation.exists()) { File newLocation = new File(getConfigDir(), oldLocation.getName()); try { if (!newLocation.exists()) { LOG.debug("Moving minimal browser config {} --> {}", oldLocation, newLocation); FileUtils.moveDirectory(oldLocation, newLocation); } else { LOG.debug("minimal browser settings have been migrated before"); } } catch (IOException e) { LOG.error("Couldn't migrate minimal browser config location", e); } } // 3.10.10 model notes due to Cam4 URL change for (Iterator> iterator = settings.modelNotes.entrySet().iterator(); iterator.hasNext();) { Entry note = iterator.next(); if (note.getKey().contains("cam4") && note.getKey().endsWith("/")) { Cam4Model model = new Cam4Model(); model.setUrl(note.getKey()); settings.modelNotes.put(model.getUrl(), note.getValue()); iterator.remove(); } } // 3.11.0 make Cam4 model names lower case settings.models.stream() .filter(Cam4Model.class::isInstance) .forEach(m -> m.setName(m.getName().toLowerCase())); settings.modelsIgnored.stream() .filter(Cam4Model.class::isInstance) .forEach(m -> m.setName(m.getName().toLowerCase())); // 4.1.2 reduce models ignore to store only the URL if (settings.modelsIgnored != null && !settings.modelsIgnored.isEmpty()) { settings.ignoredModels = settings.modelsIgnored.stream() .map(Model::getUrl) .collect(Collectors.toList()); settings.modelsIgnored = null; } } private void makeBackup(File source) { try { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); String timestamp = sdf.format(new Date()); String backup = source.getName() + '.' + timestamp; File target = new File(source.getParentFile(), backup); Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { LOG.error("Couldn't create backup of settings file", e); } } public static synchronized void init(List sites) throws IOException { if (instance == null) { instance = new Config(sites); instance.load(); } } public static synchronized Config getInstance() { if (instance == null) { throw new IllegalStateException("Config not initialized, call init() first"); } return instance; } public Settings getSettings() { return settings; } public synchronized void save() throws IOException { Moshi moshi = new Moshi.Builder() .add(Model.class, new ModelJsonAdapter()) .add(PostProcessor.class, new PostProcessorJsonAdapter()) .add(File.class, new FileJsonAdapter()) .build(); JsonAdapter adapter = moshi.adapter(Settings.class).indent(" "); String json = adapter.toJson(settings); File configFile = new File(configDir, filename); LOG.debug("Saving config to {}", configFile.getAbsolutePath()); Files.createDirectories(configDir.toPath()); Files.write(configFile.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING); } public static boolean isServerMode() { return Objects.equals(System.getProperty("ctbrec.server.mode"), "1"); } public static boolean isDevMode() { return Objects.equals(System.getenv("CTBREC_DEV"), "1"); } public File getConfigDir() { return configDir; } public File getFileForRecording(Model model, String suffix, Instant startTime) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(RECORDING_DATE_FORMAT); LocalDateTime startDateTime = LocalDateTime.ofInstant(startTime, ZoneId.systemDefault()); String formattedDate = dateTimeFormatter.format(startDateTime); File dirForRecording = getDirForRecording(model, formattedDate); File targetFile = new File(dirForRecording, model.getSanitizedNamed() + '_' + formattedDate + '.' + suffix); return targetFile; } private File getDirForRecording(Model model, String formattedDate) { switch (getSettings().recordingsDirStructure) { case ONE_PER_MODEL: return new File(getSettings().recordingsDir, model.getSanitizedNamed()); case ONE_PER_RECORDING: File modelDir = new File(getSettings().recordingsDir, model.getSanitizedNamed()); return new File(modelDir, formattedDate); case FLAT: default: return new File(getSettings().recordingsDir); } } public String getServerUrl() { String scheme = getSettings().transportLayerSecurity ? "https" : "http"; // int port = getSettings().transportLayerSecurity ? getSettings().httpSecurePort : getSettings().httpPort; int port = getSettings().httpPort; String baseUrl = scheme + "://" + getSettings().httpServer + ":" + port + getContextPath(); return baseUrl; } public String getContextPath() { String context = Optional.ofNullable(getSettings().servletContext).orElse(""); if (!context.startsWith("/") && !context.isEmpty()) { context = '/' + context; } if (context.endsWith("/")) { context = context.substring(0, context.length() - 1); } return context; } public String getModelNotes(Model m) { return Config.getInstance().getSettings().modelNotes.getOrDefault(m.getUrl(), ""); } }