package ctbrec.ui.sites.myfreecams; import static java.nio.charset.StandardCharsets.*; import static java.nio.file.StandardOpenOption.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import javax.xml.bind.JAXBException; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Model; import ctbrec.Settings; import ctbrec.StringUtil; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.sites.mfc.SessionState; import ctbrec.sites.mfc.User; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.controls.SearchBox; import ctbrec.ui.tabs.TabSelectionListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.stage.FileChooser; import javafx.util.Duration; public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class); private ScrollPane scrollPane = new ScrollPane(); private TableView table = new TableView<>(); private ObservableList filteredModels = FXCollections.observableArrayList(); private ObservableList observableModels = FXCollections.observableArrayList(); private TableUpdateService updateService; private MyFreeCams mfc; private ReentrantLock lock = new ReentrantLock(); private SearchBox filterInput; private Label count = new Label("models"); private List> columns = new ArrayList<>(); private ContextMenu popup; private long lastJsonWrite = 0; public MyFreeCamsTableTab(MyFreeCams mfc) { this.mfc = mfc; setText("Tabular"); setClosable(false); createGui(); restoreState(); loadData(); initUpdateService(); filter(filterInput.getText()); } private void initUpdateService() { updateService = new TableUpdateService(mfc); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1))); updateService.setOnSucceeded(this::onSuccess); updateService.setOnFailed(event -> LOG.info("Couldn't update MyFreeCams model table", event.getSource().getException())); } private void onSuccess(WorkerStateEvent evt) { Collection sessionStates = updateService.getValue(); if (sessionStates == null) { return; } lock.lock(); try { for (SessionState updatedModel : sessionStates) { ModelTableRow row = new ModelTableRow(updatedModel); int index = observableModels.indexOf(row); if (index == -1) { observableModels.add(row); } else { observableModels.get(index).update(updatedModel); } } for (Iterator iterator = observableModels.iterator(); iterator.hasNext();) { ModelTableRow model = iterator.next(); boolean found = false; for (SessionState sessionState : sessionStates) { if(Objects.equals(sessionState.getUid(), model.uid)) { found = true; break; } } if(!found) { iterator.remove(); } } } finally { lock.unlock(); } filteredModels.clear(); filter(filterInput.getText()); table.sort(); long now = System.currentTimeMillis(); if( (now - lastJsonWrite) > TimeUnit.SECONDS.toMillis(30)) { lastJsonWrite = now; saveData(); } } private void createGui() { BorderPane layout = new BorderPane(); layout.setPadding(new Insets(5, 10, 10, 10)); filterInput = new SearchBox(false); filterInput.setPromptText("Filter"); filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { String filter = filterInput.getText(); Config.getInstance().getSettings().mfcModelsTableFilter = filter; lock.lock(); try { filter(filter); } finally { lock.unlock(); } }); filterInput.getStyleClass().remove("search-box-icon"); HBox.setHgrow(filterInput, Priority.ALWAYS); Button export = new Button("⬇"); export.setOnAction(this::export); export.setTooltip(new Tooltip("Export data")); Button columnSelection = new Button("⚙"); columnSelection.setOnAction(this::showColumnSelection); columnSelection.setTooltip(new Tooltip("Select columns")); HBox topBar = new HBox(5); topBar.getChildren().addAll(filterInput, count, export, columnSelection); count.prefHeightProperty().bind(filterInput.heightProperty()); count.setAlignment(Pos.CENTER); layout.setTop(topBar); BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0)); table.setItems(observableModels); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSortOrder().addListener(createSortOrderChangedListener()); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); if (popup != null) { popup.show(table, event.getScreenX(), event.getScreenY()); } event.consume(); }); table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { if (popup != null) { popup.hide(); } }); table.getColumns().addListener((ListChangeListener>)(e -> saveState())); int idx = 0; TableColumn uid = createTableColumn("UID", 65, idx++); uid.setId("uid"); uid.setCellValueFactory(cdf -> cdf.getValue().uidProperty()); addTableColumnIfEnabled(uid); TableColumn name = createTableColumn("Name", 200, idx++); name.setId("name"); name.setCellValueFactory(cdf -> cdf.getValue().nameProperty()); addTableColumnIfEnabled(name); TableColumn state = createTableColumn("State", 130, idx++); state.setId("state"); state.setCellValueFactory(cdf -> cdf.getValue().stateProperty()); addTableColumnIfEnabled(state); TableColumn hd = createTableColumn("HD", 130, idx++); hd.setId("hd"); hd.setCellValueFactory(cdf -> cdf.getValue().hdProperty()); addTableColumnIfEnabled(hd); TableColumn webrtc = createTableColumn("webrtc", 130, idx++); webrtc.setId("webrtc"); webrtc.setCellValueFactory(cdf -> cdf.getValue().webrtcProperty()); addTableColumnIfEnabled(webrtc); TableColumn flags = createTableColumn("Flags", 75, idx++); flags.setId("flags"); flags.setCellValueFactory(cdf -> cdf.getValue().flagsProperty()); addTableColumnIfEnabled(flags); TableColumn camscore = createTableColumn("Score", 75, idx++); camscore.setId("camscore"); camscore.setCellValueFactory(cdf -> cdf.getValue().camScoreProperty()); addTableColumnIfEnabled(camscore); // this is always 0, use https://api.myfreecams.com/missmfc and https://api.myfreecams.com/missmfc/online // TableColumn missMfc = createTableColumn("Miss MFC", 75, idx++); // missMfc.setId("missMfc"); // missMfc.setCellValueFactory(cdf -> { // Integer mmfc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getMissmfc()).orElse(-1); // return new SimpleIntegerProperty(mmfc); // }); // addTableColumnIfEnabled(missMfc); TableColumn newModel = createTableColumn("New", 60, idx++); newModel.setId("newModel"); newModel.setCellValueFactory(cdf -> cdf.getValue().newModelProperty()); addTableColumnIfEnabled(newModel); TableColumn ethnic = createTableColumn("Ethnicity", 130, idx++); ethnic.setId("ethnic"); ethnic.setCellValueFactory(cdf -> cdf.getValue().ethnicityProperty()); addTableColumnIfEnabled(ethnic); TableColumn country = createTableColumn("Country", 160, idx++); country.setId("country"); country.setCellValueFactory(cdf -> cdf.getValue().countryProperty()); addTableColumnIfEnabled(country); TableColumn continent = createTableColumn("Continent", 100, idx++); continent.setId("continent"); continent.setCellValueFactory(cdf -> cdf.getValue().continentProperty()); addTableColumnIfEnabled(continent); TableColumn occupation = createTableColumn("Occupation", 160, idx++); occupation.setId("occupation"); occupation.setCellValueFactory(cdf -> cdf.getValue().occupationProperty()); addTableColumnIfEnabled(occupation); TableColumn tags = createTableColumn("Tags", 300, idx++); tags.setId("tags"); tags.setCellValueFactory(cdf -> cdf.getValue().tagsProperty()); addTableColumnIfEnabled(tags); TableColumn blurp = createTableColumn("Blurp", 300, idx++); blurp.setId("blurp"); blurp.setCellValueFactory(cdf -> cdf.getValue().blurpProperty()); addTableColumnIfEnabled(blurp); TableColumn topic = createTableColumn("Topic", 600, idx); topic.setId("topic"); topic.setCellValueFactory(cdf -> cdf.getValue().topicProperty()); addTableColumnIfEnabled(topic); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); scrollPane.setContent(table); scrollPane.setStyle("-fx-background-color: -fx-background"); layout.setCenter(scrollPane); setContent(layout); } private ContextMenu createContextMenu() { ObservableList selectedStates = table.getSelectionModel().getSelectedItems(); if (selectedStates.isEmpty()) { return null; } List selectedModels = new ArrayList<>(); for (ModelTableRow sessionState : selectedStates) { if(sessionState.name.get() != null) { MyFreeCamsModel model = mfc.createModel(sessionState.name.get()); mfc.getClient().update(model); selectedModels.add(model); } } MenuItem copyUrl = new MenuItem("Copy URL"); copyUrl.setOnAction(e -> { Model selected = selectedModels.get(0); final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(selected.getUrl()); clipboard.setContent(content); }); MenuItem startRecording = new MenuItem("Start Recording"); startRecording.setOnAction(e -> startRecording(selectedModels)); MenuItem openInBrowser = new MenuItem("Open in Browser"); openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl())); MenuItem openInPlayer = new MenuItem("Open in Player"); openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0))); MenuItem follow = new MenuItem("Follow"); follow.setOnAction(e -> new FollowAction(getTabPane(), selectedModels).execute()); ContextMenu menu = new ContextMenu(); menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow); if (selectedModels.size() > 1) { copyUrl.setDisable(true); openInPlayer.setDisable(true); openInBrowser.setDisable(true); } if (Objects.equals(System.getenv("CTBREC_DEV"), "1")) { MenuItem debug = new MenuItem("debug"); debug.setOnAction(e -> new Thread(() -> { for (Model m : selectedModels) { try { List sources = m.getStreamSources(); for (StreamSource src : sources) { LOG.info("m:{} s:{} bandwidth:{} height:{}", m.getName(), src.mediaPlaylistUrl, src.bandwidth, src.height); } LOG.info("==============="); } catch (IOException | ExecutionException | ParseException | PlaylistException | JAXBException e1) { LOG.error("Couldn't get stream sources", e1); } } }).start()); menu.getItems().add(debug); } return menu; } private void startRecording(List selectedModels) { new StartRecordingAction(getTabPane(), selectedModels, mfc.getRecorder()).execute(); } private void openInPlayer(Model selectedModel) { new PlayAction(getTabPane(), selectedModel).execute(); } private void addTableColumnIfEnabled(TableColumn tc) { if(isColumnEnabled(tc)) { table.getColumns().add(tc); } } private void filter(String filter) { lock.lock(); try { if (StringUtil.isBlank(filter)) { observableModels.addAll(filteredModels); filteredModels.clear(); return; } String[] tokens = filter.split(" "); observableModels.addAll(filteredModels); filteredModels.clear(); for (int i = 0; i < table.getItems().size(); i++) { StringBuilder sb = new StringBuilder(); for (TableColumn tc : table.getColumns()) { Object cellData = tc.getCellData(i); if(cellData != null) { String content = cellData.toString(); sb.append(content).append(' '); } } String searchText = sb.toString(); boolean tokensMissing = false; for (String token : tokens) { if(!searchText.toLowerCase().contains(token.toLowerCase())) { tokensMissing = true; break; } } if(tokensMissing) { ModelTableRow sessionState = table.getItems().get(i); filteredModels.add(sessionState); } } observableModels.removeAll(filteredModels); } finally { lock.unlock(); int filtered = filteredModels.size(); int showing = observableModels.size(); int total = showing + filtered; count.setText(showing + "/" + total); } } private void export(ActionEvent evt) { FileChooser chooser = new FileChooser(); File file = chooser.showSaveDialog(getTabPane().getScene().getWindow()); if(file != null) { try(FileOutputStream fout = new FileOutputStream(file)) { PrintStream ps = new PrintStream(fout); List union = new ArrayList<>(); union.addAll(filteredModels); union.addAll(observableModels); ps.println("\"uid\",\"blurp\",\"camScore\",\"continent\",\"country\",\"ethnic\",\"name\",\"new\",\"occupation\",\"state\",\"tags\",\"topic\""); for (ModelTableRow row : union) { ps.print("\"" + row.uid + "\""); ps.print(','); ps.print(escape(row.blurp)); ps.print(','); ps.print(escape(row.camScore)); ps.print(','); ps.print(escape(row.continent)); ps.print(','); ps.print(escape(row.country)); ps.print(','); ps.print(escape(row.ethnic)); ps.print(','); ps.print(escape(row.name)); ps.print(','); ps.print(escape(row.newModel)); ps.print(','); ps.print(escape(row.occupation)); ps.print(','); ps.print(escape(row.state)); ps.print(','); ps.print(escape(row.tags)); ps.print(','); ps.print(escape(row.topic)); ps.println(); } } catch (Exception e) { LOG.debug("Couldn't write mfc models table data", e); } } } private String escape(Property prop) { String value = prop.getValue() != null ? prop.getValue().toString() : ""; return "\"" + value.replaceAll("\"", "\"\"") + "\""; } private void showColumnSelection(ActionEvent evt) { ContextMenu menu = new ContextMenu(); for (TableColumn tc : columns) { CheckMenuItem item = new CheckMenuItem(tc.getText()); item.setSelected(isColumnEnabled(tc)); menu.getItems().add(item); item.setOnAction(e -> { if(item.isSelected()) { Config.getInstance().getSettings().mfcDisabledModelsTableColumns.remove(tc.getText()); for (int i = table.getColumns().size()-1; i>=0; i--) { TableColumn other = table.getColumns().get(i); int idx = (int) tc.getUserData(); int otherIdx = (int) other.getUserData(); if(otherIdx < idx) { table.getColumns().add(i+1, tc); break; } } } else { Config.getInstance().getSettings().mfcDisabledModelsTableColumns.add(tc.getText()); table.getColumns().remove(tc); } }); } Button src = (Button) evt.getSource(); Point2D location = src.localToScreen(src.getTranslateX(), src.getTranslateY()); menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5); } private boolean isColumnEnabled(TableColumn tc) { return !Config.getInstance().getSettings().mfcDisabledModelsTableColumns.contains(tc.getText()); } private TableColumn createTableColumn(String text, int width, int idx) { TableColumn tc = new TableColumn<>(text); tc.setPrefWidth(width); tc.sortTypeProperty().addListener((obs, o, n) -> saveState()); tc.widthProperty().addListener((obs, o, n) -> saveState()); tc.setUserData(idx); columns.add(tc); return tc; } @Override public void selected() { if(updateService != null) { State s = updateService.getState(); if (s != State.SCHEDULED && s != State.RUNNING) { updateService.reset(); updateService.restart(); } } } @Override public void deselected() { if(updateService != null) { updateService.cancel(); } saveData(); observableModels.clear(); } private void saveData() { try { List union = new ArrayList<>(); union.addAll(filteredModels); union.addAll(observableModels); JSONArray data = new JSONArray(); for (ModelTableRow row : union) { JSONObject model = new JSONObject(); model.put("uid", row.uid); model.put("blurp", row.blurp.get()); model.put("camScore", row.camScore.get()); model.put("continent", row.continent.get()); model.put("country", row.country.get()); model.put("ethnic", row.ethnic.get()); model.put("name", row.name.get()); model.put("newModel", row.newModel.get()); model.put("occupation", row.occupation.get()); model.put("state", row.state.get()); model.put("tags", row.tags.get()); model.put("topic", row.topic.get()); data.put(model); } File file = new File(Config.getInstance().getConfigDir(), "mfc-models.json"); Files.write(file.toPath(), data.toString(2).getBytes(UTF_8), CREATE, WRITE); saveState(); } catch (Exception e) { LOG.debug("Couldn't write mfc models table data: {}", e.getMessage()); } } private void loadData() { try { File file = new File(Config.getInstance().getConfigDir(), "mfc-models.json"); if(!file.exists()) { return; } String json = new String(Files.readAllBytes(file.toPath()), "utf-8"); JSONArray data = new JSONArray(json); for (int i = 0; i < data.length(); i++) { try { ModelTableRow row = new ModelTableRow(); JSONObject model = data.getJSONObject(i); row.uid = model.getInt("uid"); row.blurp.set(model.optString("blurp")); row.camScore.set(model.optDouble("camScore")); row.continent.set(model.optString("continent")); row.country.set(model.optString("country")); row.ethnic.set(model.optString("ethnic")); row.name.set(model.optString("name")); row.newModel.set(model.optString("newModel")); row.occupation.set(model.optString("occupation")); row.state.set(model.optString("state")); row.tags.set(model.optString("tags")); row.topic.set(model.optString("topic")); observableModels.add(row); } catch (Exception e) { // ignore this error } } } catch (Exception e) { LOG.debug("Couldn't read mfc models table data: {}", e.getMessage()); } } private void saveState() { Settings settings = Config.getInstance().getSettings(); if (!table.getSortOrder().isEmpty()) { TableColumn col = table.getSortOrder().get(0); settings.mfcModelsTableSortColumn = col.getText(); settings.mfcModelsTableSortType = col.getSortType().toString(); } int size = table.getColumns().size(); double[] columnWidths = new double[size]; String[] columnIds = new String[size]; for (int i = 0; i < columnWidths.length; i++) { columnWidths[i] = table.getColumns().get(i).getWidth(); columnIds[i] = table.getColumns().get(i).getId(); } settings.mfcModelsTableColumnWidths = columnWidths; settings.mfcModelsTableColumnIds = columnIds; } private void restoreState() { restoreColumnOrder(); restoreColumnWidths(); restoreSorting(); filterInput.setText(Config.getInstance().getSettings().mfcModelsTableFilter); } private void restoreSorting() { String sortCol = Config.getInstance().getSettings().mfcModelsTableSortColumn; if (StringUtil.isNotBlank(sortCol)) { for (TableColumn col : table.getColumns()) { if (Objects.equals(sortCol, col.getText())) { col.setSortType(SortType.valueOf(Config.getInstance().getSettings().mfcModelsTableSortType)); table.getSortOrder().clear(); table.getSortOrder().add(col); break; } } } } private void restoreColumnOrder() { String[] columnIds = Config.getInstance().getSettings().mfcModelsTableColumnIds; ObservableList> tableColumns = table.getColumns(); for (int i = 0; i < columnIds.length; i++) { for (int j = 0; j < table.getColumns().size(); j++) { if(Objects.equals(columnIds[i], tableColumns.get(j).getId())) { TableColumn col = tableColumns.get(j); tableColumns.remove(j); // NOSONAR tableColumns.add(i, col); } } } } private void restoreColumnWidths() { double[] columnWidths = Config.getInstance().getSettings().mfcModelsTableColumnWidths; if (columnWidths != null && columnWidths.length == table.getColumns().size()) { for (int i = 0; i < columnWidths.length; i++) { table.getColumns().get(i).setPrefWidth(columnWidths[i]); } } } private ListChangeListener> createSortOrderChangedListener() { return c -> saveState(); } private static class ModelTableRow { private Integer uid; private StringProperty name = new SimpleStringProperty(); private StringProperty state = new SimpleStringProperty(); private DoubleProperty camScore = new SimpleDoubleProperty(); private StringProperty newModel = new SimpleStringProperty(); private StringProperty ethnic = new SimpleStringProperty(); private StringProperty country = new SimpleStringProperty(); private StringProperty continent = new SimpleStringProperty(); private StringProperty occupation = new SimpleStringProperty(); private StringProperty tags = new SimpleStringProperty(); private StringProperty blurp = new SimpleStringProperty(); private StringProperty topic = new SimpleStringProperty(); private BooleanProperty isHd = new SimpleBooleanProperty(); private BooleanProperty isWebrtc = new SimpleBooleanProperty(); private SimpleIntegerProperty uidProperty = new SimpleIntegerProperty(); private SimpleIntegerProperty flagsProperty = new SimpleIntegerProperty(); public ModelTableRow(SessionState st) { update(st); } private ModelTableRow() { } public void update(SessionState st) { uid = st.getUid(); uidProperty.set(uid); setProperty(name, Optional.ofNullable(st.getNm())); setProperty(state, Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString())); setProperty(camScore, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getCamscore)); Optional isNew = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getNewModel); if(isNew.isPresent()) { newModel.set(isNew.get() == 1 ? "new" : ""); } setProperty(ethnic, Optional.ofNullable(st.getU()).map(User::getEthnic)); setProperty(country, Optional.ofNullable(st.getU()).map(User::getCountry)); setProperty(continent, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getContinent)); setProperty(occupation, Optional.ofNullable(st.getU()).map(User::getOccupation)); int flags = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getFlags).orElse(0); //isHd.set((flags & 1024) == 1024); isWebrtc.set((flags & 524288) == 524288); isHd.set(Optional.ofNullable(st.getU()).map(User::getPhase).orElse("z").equalsIgnoreCase("a")); flagsProperty.setValue(flags); Set tagSet = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getTags).orElse(Collections.emptySet()); if(!tagSet.isEmpty()) { StringBuilder sb = new StringBuilder(); for (String t : tagSet) { sb.append(t).append(',').append(' '); } tags.set(sb.substring(0, sb.length()-2)); } setProperty(blurp, Optional.ofNullable(st.getU()).map(User::getBlurb)); String tpc = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getTopic).orElse("n/a"); try { tpc = URLDecoder.decode(tpc, "utf-8"); } catch (UnsupportedEncodingException e) { LOG.warn("Couldn't url decode topic", e); } topic.set(tpc); } private void setProperty(Property prop, Optional value) { if(value.isPresent() && !Objects.equals(value.get(), prop.getValue())) { prop.setValue(value.get()); } } public StringProperty nameProperty() { return name; } public StringProperty stateProperty() { return state; } public DoubleProperty camScoreProperty() { return camScore; } public StringProperty newModelProperty() { return newModel; } public StringProperty ethnicityProperty() { return ethnic; } public StringProperty countryProperty() { return country; } public StringProperty continentProperty() { return continent; } public StringProperty occupationProperty() { return occupation; } public StringProperty tagsProperty() { return tags; } public StringProperty blurpProperty() { return blurp; } public StringProperty topicProperty() { return topic; } public BooleanProperty hdProperty() { return isHd; } public BooleanProperty webrtcProperty() { return isWebrtc; } public IntegerProperty flagsProperty() { return flagsProperty; } public IntegerProperty uidProperty() { return uidProperty; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((uid == null) ? 0 : uid.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ModelTableRow other = (ModelTableRow) obj; if (uid == null) { if (other.uid != null) return false; } else if (!uid.equals(other.uid)) return false; return true; } } }