package ctbrec; import static ctbrec.Recording.State.*; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.event.EventBusHolder; import ctbrec.event.RecordingStateChangedEvent; import ctbrec.io.IoUtils; import ctbrec.recorder.download.Download; import ctbrec.recorder.download.VideoLengthDetector; public class Recording implements Serializable, Callable { private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class); private String id; private Model model; private transient Download download; private Instant startDate; private String path; private State status = State.UNKNOWN; private int progress = -1; private long sizeInByte = -1; private String metaDataFile; private boolean singleFile = false; private boolean pinned = false; private String note; private Set associatedFiles = new HashSet<>(); private File absoluteFile = null; private File postProcessedFile = null; private int selectedResolution = -1; public enum State { RECORDING("recording"), GENERATING_PLAYLIST("generating playlist"), POST_PROCESSING("post-processing"), FINISHED("finished"), DOWNLOADING("downloading"), DELETING("deleting"), DELETED("deleted"), UNKNOWN("unknown"), WAITING("waiting"), FAILED("failed"); private final String desc; State(String desc) { this.desc = desc; } @Override public String toString() { return desc; } } @Override public Recording call() throws Exception { download.call(); if (selectedResolution == -1) { selectedResolution = download.getSelectedResolution(); } return this; } public String getId() { return id; } public void setId(String id) { this.id = id; } public Instant getStartDate() { return startDate; } public void setStartDate(Instant startDate) { this.startDate = startDate; } public State getStatus() { return status; } public void setStatus(State status) { this.status = status; } public void setStatusWithEvent(State status) { setStatus(status); fireStatusEvent(status); } public int getProgress() { return this.progress; } public void setProgress(int progress) { this.progress = progress; } public void setPath(String path) { this.path = path; } public File getAbsoluteFile() { if (absoluteFile == null) { String recordingsDir = Config.getInstance().getSettings().recordingsDir; File recordingsFile = new File(recordingsDir, path); absoluteFile = recordingsFile; } return absoluteFile; } public void setAbsoluteFile(File absoluteFile) { this.absoluteFile = absoluteFile; } public File getPostProcessedFile() { if (postProcessedFile == null) { setPostProcessedFile(getAbsoluteFile()); } return postProcessedFile; } public void setPostProcessedFile(File postProcessedFile) { this.postProcessedFile = postProcessedFile; } public long getSizeInByte() { return sizeInByte; } public void setSizeInByte(long sizeInByte) { this.sizeInByte = sizeInByte; } public void postprocess() { getDownload().postprocess(this); } private void fireStatusEvent(State status) { RecordingStateChangedEvent evt = new RecordingStateChangedEvent(getDownload().getTarget(), status, getModel(), getStartDate()); EventBusHolder.BUS.post(evt); } public Model getModel() { return model; } public void setModel(Model model) { this.model = model; } public Download getDownload() { return download; } public void setDownload(Download download) { this.download = download; } public boolean isSingleFile() { return singleFile; } public void setSingleFile(boolean singleFile) { this.singleFile = singleFile; } public String getMetaDataFile() { return metaDataFile; } public void setMetaDataFile(String metaDataFile) { this.metaDataFile = metaDataFile; } public boolean isPinned() { return pinned; } public void setPinned(boolean pinned) { this.pinned = pinned; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } public int getSelectedResolution() { return selectedResolution; } public Duration getLength() { File ppFile = getPostProcessedFile(); if (ppFile.isDirectory()) { File playlist = new File(ppFile, "playlist.m3u8"); return VideoLengthDetector.getLength(playlist); } else { return VideoLengthDetector.getLength(ppFile); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof Recording other)) return false; if (getId() == null) { return other.getId() == null; } else return getId().equals(other.getId()); } @Override public String toString() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT); LocalDateTime localStartDate = LocalDateTime.ofInstant(getStartDate(), ZoneId.systemDefault()); return getModel().getSanitizedNamed() + '_' + formatter.format(localStartDate); } private long getSize() { try { Set files = getAllRecordingFiles(); long sum = 0; for (File file : files) { if (file.isDirectory()) { sum += IoUtils.getDirectorySize(file); } else { if (!file.exists()) { if (file.getName().endsWith(".m3u8")) { sum += IoUtils.getDirectorySize(file.getParentFile()); } } else { sum += file.length(); } } } return sum; } catch (IOException e) { LOG.error("Couldn't determine recording size", e); return -1; } } private Set getAllRecordingFiles() throws IOException { Set files = new HashSet<>(); if (absoluteFile != null) { files.add(absoluteFile.getCanonicalFile()); } if (postProcessedFile != null) { files.add(postProcessedFile.getCanonicalFile()); } for (String associatedFile : associatedFiles) { files.add(new File(associatedFile).getCanonicalFile()); } return files; } public void refresh() { sizeInByte = getSize(); } public boolean canBePostProcessed() { return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED; } public void setAssociatedFiles(Set associatedFiles) { this.associatedFiles = associatedFiles; } public Set getAssociatedFiles() { return associatedFiles; } public Optional getContactSheet() { return getAssociatedFiles().stream() .filter(filePath -> filePath.endsWith(".jpg")) .findFirst() .map(File::new); } }