tvheadend

This commit is contained in:
Patrick Haßel 2024-11-19 12:14:01 +01:00
parent 668c590306
commit 3e487c10b5
20 changed files with 910 additions and 30 deletions

View File

@ -1,4 +1,4 @@
logging.level.de.ph87=DEBUG logging.level.de.ph87.home.tvheadend.TvheadendService=DEBUG
#- #-
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
spring.datasource.driverClassName=org.h2.Driver spring.datasource.driverClassName=org.h2.Driver

View File

@ -40,6 +40,11 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>

View File

@ -0,0 +1,26 @@
package de.ph87.home.common;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class EpochSecondsToZonedDateTime extends JsonDeserializer<ZonedDateTime> {
@Override
public ZonedDateTime deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
final long epochSeconds = jsonParser.getLongValue();
if (epochSeconds < 0) {
throw new RuntimeException();
}
if (epochSeconds == 0) {
return null;
}
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault());
}
}

View File

@ -0,0 +1,17 @@
package de.ph87.home.common;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.Duration;
public class SecondsToDuration extends JsonDeserializer<Duration> {
@Override
public Duration deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
return Duration.ofSeconds(jsonParser.getLongValue());
}
}

View File

@ -15,7 +15,7 @@ public class DeviceEvent {
private final PropertyDto<?> propertyDto; private final PropertyDto<?> propertyDto;
public boolean isValueDifferent() { public boolean isValueDifferent() {
return propertyDto.isValueDifferent(); return propertyDto.isValueChanged();
} }
} }

View File

@ -38,7 +38,7 @@ public class DeviceService {
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state); log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
final Device device = byUuidOrSlug(uuidOrSlug); final Device device = byUuidOrSlug(uuidOrSlug);
log.debug("setState: device={}", device); log.debug("setState: device={}", device);
propertyService.write(device.getStateProperty(), state); propertyService.write(device.getStateProperty(), state, Boolean.class);
} }
@NonNull @NonNull

View File

@ -21,6 +21,7 @@ public class DummyService {
register("wohnzimmer_verstaerker"); register("wohnzimmer_verstaerker");
register("wohnzimmer_fensterdeko"); register("wohnzimmer_fensterdeko");
register("wohnzimmer_haengelampe"); register("wohnzimmer_haengelampe");
register("receiver");
} }
private void register(final String id) { private void register(final String id) {

View File

@ -33,23 +33,43 @@ public class Property<T> {
@Nullable @Nullable
private State<T> state = null; private State<T> state = null;
private boolean valueDifferent = false; private boolean valueChanged = false;
public void setState(@Nullable final State<T> state) { public void setState(@Nullable final State<T> state) {
this.lastState = this.state; this.lastState = this.state;
this.state = state; this.state = state;
this.valueDifferent = (lastState == null) == (state == null) && (lastState == null || Objects.equals(lastState.getValue(), state.getValue())); this.valueChanged = (lastState == null) == (state == null) && (lastState == null || Objects.equals(lastState.getValue(), state.getValue()));
this.onStateSet.accept(this); this.onStateSet.accept(this);
} }
public void write(@NonNull final Object value) throws PropertyNotWritable, PropertyTypeMismatch { @Nullable
if (!type.isInstance(value)) { public T getStateValue() {
throw new PropertyTypeMismatch(this, value); if (state == null) {
return null;
} }
return state.getValue();
}
@Nullable
public <R extends T> R getStateValueAs(@NonNull final Class<R> type) throws PropertyTypeMismatch {
if (this.type != type) {
throw new PropertyTypeMismatch(this, type);
}
//noinspection unchecked
return (R) getStateValue();
}
@NonNull
public <R extends T> R getStateValueAs(@NonNull final Class<R> type, @NonNull final R fallbackIfNull) throws PropertyTypeMismatch {
final R value = getStateValueAs(type);
return Objects.requireNonNullElse(value, fallbackIfNull);
}
public void write(@NonNull final T value) throws PropertyNotWritable {
if (write == null) { if (write == null) {
throw new PropertyNotWritable(this); throw new PropertyNotWritable(this);
} }
write.accept(this, type.cast(value)); write.accept(this, value);
} }
} }

View File

@ -21,14 +21,14 @@ public class PropertyDto<T> {
@Nullable @Nullable
private final State<T> state; private final State<T> state;
private final boolean valueDifferent; private final boolean valueChanged;
public PropertyDto(@NonNull final Property<T> property) { public PropertyDto(@NonNull final Property<T> property) {
this.id = property.getId(); this.id = property.getId();
this.type = property.getType(); this.type = property.getType();
this.state = property.getState(); this.state = property.getState();
this.lastState = property.getLastState(); this.lastState = property.getLastState();
this.valueDifferent = property.isValueDifferent(); this.valueChanged = property.isValueChanged();
} }
} }

View File

@ -34,30 +34,22 @@ public class PropertyService {
@Nullable @Nullable
public <TYPE> State<TYPE> read(@NonNull final String id, @NonNull final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch { public <TYPE> State<TYPE> read(@NonNull final String id, @NonNull final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
log.debug("read: id={}", id); log.debug("read: id={}", id);
final Property<?> property = byIdAndType(id, type); return byIdAndType(id, type).getState();
if (property.getState() == null) {
return null;
}
if (type.isInstance(property.getState().getValue())) {
//noinspection unchecked
return (State<TYPE>) property.getState();
}
throw new PropertyTypeMismatch(property, type);
} }
public void write(@NonNull final String id, @NonNull final Object value) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable { public <TYPE> void write(@NonNull final String id, @NonNull final TYPE value, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value); log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value);
final Property<?> property = byIdAndType(id, value.getClass()); byIdAndType(id, type).write(value);
property.write(value);
} }
@NonNull @NonNull
private <TYPE> Property<?> byIdAndType(final String id, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch { public <TYPE> Property<TYPE> byIdAndType(final String id, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
final Property<?> property = findById(id).orElseThrow(() -> new PropertyNotFound(id)); final Property<?> property = findById(id).orElseThrow(() -> new PropertyNotFound(id));
if (type != property.getType()) { if (type != property.getType()) {
throw new PropertyTypeMismatch(property, type); throw new PropertyTypeMismatch(property, type);
} }
return property; //noinspection unchecked
return (Property<TYPE>) property;
} }
@NonNull @NonNull
@ -84,7 +76,7 @@ public class PropertyService {
private void onStateSet(@NonNull final Property<?> property) { private void onStateSet(@NonNull final Property<?> property) {
final PropertyDto<?> dto = toDto(property); final PropertyDto<?> dto = toDto(property);
log.debug("Property updated: {}", dto); log.debug("Property updated: {}", dto);
if (dto.isValueDifferent()) { if (dto.isValueChanged()) {
log.info("Property changed: {}", dto); log.info("Property changed: {}", dto);
} }
applicationEventPublisher.publishEvent(dto); applicationEventPublisher.publishEvent(dto);

View File

@ -11,8 +11,4 @@ public class PropertyTypeMismatch extends Exception {
super("Property type mismatch: id=%s, expected=%s, given=%s".formatted(property.getId(), property.getType().getSimpleName(), type.getSimpleName())); super("Property type mismatch: id=%s, expected=%s, given=%s".formatted(property.getId(), property.getType().getSimpleName(), type.getSimpleName()));
} }
public PropertyTypeMismatch(@NonNull final Property<?> property, @NonNull final Object value) {
super("Property type mismatch: id=%s, expected=%s, given=%s, value=%s".formatted(property.getId(), property.getType().getSimpleName(), value.getClass().getSimpleName(), value));
}
} }

View File

@ -0,0 +1,24 @@
package de.ph87.home.tvheadend;
import lombok.Data;
import lombok.NonNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Data
@Component
@ConfigurationProperties(prefix = "de.ph87.home.tvheadend")
public class TvheadendConfig {
@NonNull
private String receiver = "receiver";
@NonNull
private Duration BEFORE_RECORDING = Duration.ofMinutes(10);
@NonNull
private Duration AFTER_NEED = Duration.ofMinutes(10);
}

View File

@ -0,0 +1,148 @@
package de.ph87.home.tvheadend;
import de.ph87.home.property.*;
import de.ph87.home.tvheadend.api.TvheadendApiService;
import de.ph87.home.tvheadend.api.TvheadendStatus;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@EnableScheduling
@RequiredArgsConstructor
public class TvheadendService {
private final TvheadendApiService tvheadendApiService;
private final TvheadendConfig tvheadendConfig;
private final PropertyService propertyService;
@Nullable
private Boolean lastNeeded = null;
@Nullable
private ZonedDateTime lastNeededChanged = null;
@Scheduled(initialDelay = 0, fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void update() {
final TvheadendStatus status;
try {
status = tvheadendApiService.fetch();
} catch (IOException e) {
log.error("Failed to get tvheadend status: {}", e.getMessage());
return;
}
final Property<Boolean> receiver;
try {
receiver = propertyService.byIdAndType(tvheadendConfig.getReceiver(), Boolean.class);
} catch (PropertyNotFound | PropertyTypeMismatch e) {
log.warn("Failed to retrieve receiver Property: id={}, error={}", tvheadendConfig.getReceiver(), e.getMessage());
return;
}
try {
updateUnsafe(status, receiver);
} catch (PropertyNotWritable | PropertyNotFound | PropertyTypeMismatch e) {
log.error("Cannot update receiver: {}", e.getMessage());
}
}
private void updateUnsafe(@NonNull final TvheadendStatus status, @NonNull final Property<Boolean> receiver) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
final ZonedDateTime now = ZonedDateTime.now();
final String shouldBeMessage;
if (status.receiverNeeded) {
shouldBeMessage = "Receiver is needed.";
} else if (status.recordingNext != null) {
shouldBeMessage = "Receiver is needed in %s".formatted(durationToString(Duration.between(now, status.recordingNext.getStart_real().minus(tvheadendConfig.getBEFORE_RECORDING()))));
}else{
shouldBeMessage = "Receiver is NOT needed.";
}
final boolean justChanged = lastNeededChanged == null || !Objects.equals(lastNeeded, status.receiverNeeded);
if (justChanged) {
log.info(shouldBeMessage);
lastNeeded = status.receiverNeeded;
lastNeededChanged = now;
} else {
log.debug(shouldBeMessage);
}
final boolean isOn = receiver.getStateValueAs(Boolean.class, false);
if (status.receiverNeeded) {
if (!isOn) {
doSwitch(receiver, true);
} else if (justChanged) {
log.info("Receiver is already ON");
}
} else if (isOn) {
final Duration neededOffFor = Duration.between(lastNeededChanged, now);
final Duration rest = tvheadendConfig.getAFTER_NEED().minus(neededOffFor);
if (rest.toNanos() <= 0) {
doSwitch(receiver, false);
} else {
final String delayedMessage = "Receiver shutdown in %s".formatted(durationToString(rest));
if (justChanged) {
log.info(delayedMessage);
} else {
log.debug(delayedMessage);
}
}
} else if (justChanged) {
log.info("Receiver is already OFF");
}
}
@NonNull
private String durationToString(@NonNull final Duration duration) {
if (duration.toDays() > 1) {
return "%dd".formatted(duration.toDays());
} else if (duration.toHours() > 1) {
return "%dh".formatted(duration.toHours());
} else if (duration.toMinutes() > 1) {
return "%dm".formatted(duration.toMinutes());
} else if (duration.toSeconds() > 1) {
return "%ds".formatted(duration.toSeconds());
} else if (duration.toMillis() > 1) {
return "%dms".formatted(duration.toMillis());
}
return "now";
}
private static void doSwitch(@NonNull final Property<Boolean> receiver, final boolean state) throws PropertyNotWritable {
log.info("Switching receiver {}", state ? "ON" : "OFF");
receiver.write(state);
if (!state) {
log.info("Restarting tvheadend...");
try {
final Process process = Runtime.getRuntime().exec(new String[]{"sudo", "/bin/systemctl", "restart", "tvheadend"});
final String output = new String(process.getInputStream().readAllBytes()).replace("\n", "\\n").replace("\r", "\\r");
final int code = process.waitFor();
if (code != 0) {
log.error("return: {}", code);
log.error("output: {}", output);
throw new IOException("Process returned: %d".formatted(code));
} else {
log.debug("return: {}", code);
log.debug("output: {}", output);
}
log.info("tvheadend restarted");
} catch (IOException | InterruptedException e) {
log.error("Failed to restart tvheadend: {}", e.getMessage());
}
}
}
}

View File

@ -0,0 +1,43 @@
package de.ph87.home.tvheadend.api;
import de.ph87.home.tvheadend.TvheadendConfig;
import de.ph87.home.tvheadend.api.dto.DvrEntryGridUpcoming;
import de.ph87.home.tvheadend.api.dto.StatusConnections;
import de.ph87.home.tvheadend.api.dto.StatusInputs;
import de.ph87.home.tvheadend.api.dto.StatusSubscriptions;
import jakarta.annotation.Nullable;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Slf4j
@Getter
@Service
@RequiredArgsConstructor
public class TvheadendApiService {
private final TvheadendHttpHelper tvheadendHttpHelper;
private final TvheadendConfig tvheadendConfig;
@Nullable
private TvheadendStatus status = null;
@NonNull
public TvheadendStatus fetch() throws IOException {
status = null;
status = new TvheadendStatus(
tvheadendConfig.getBEFORE_RECORDING(),
tvheadendHttpHelper.get("status/inputs", StatusInputs.class).getEntries(),
tvheadendHttpHelper.get("status/subscriptions", StatusSubscriptions.class).getEntries(),
tvheadendHttpHelper.get("status/connections", StatusConnections.class).getEntries(),
tvheadendHttpHelper.get("dvr/entry/grid_upcoming", DvrEntryGridUpcoming.class).getEntries()
);
return status;
}
}

View File

@ -0,0 +1,75 @@
package de.ph87.home.tvheadend.api;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.utils.Base64;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings("unused")
public class TvheadendHttpHelper {
private static final String URL = "http://10.0.0.50:9981/api/%s";
private final ObjectMapper objectMapper;
@NonNull
public <T> T get(@NonNull final String path, @NonNull final Class<? extends T> clazz) throws IOException {
return parse(clazz, open(false, path));
}
public void post(@NonNull final String path, @NonNull final Object request) throws IOException {
final HttpURLConnection connection = openPost(path, request);
if (connection.getResponseCode() != 200) {
throw new IOException("Response code: " + connection.getResponseCode());
}
}
@NonNull
public <T> T post(@NonNull final String path, @NonNull final Object request, @NonNull final Class<? extends T> clazz) throws IOException {
return parse(clazz, openPost(path, request));
}
@NonNull
private static HttpURLConnection openPost(@NonNull final String path, @NonNull final Object request) throws IOException {
final HttpURLConnection connection = open(true, path);
connection.getOutputStream().write(request.toString().getBytes());
return connection;
}
@NonNull
private <T> T parse(@NonNull final Class<? extends T> clazz, @NonNull final HttpURLConnection connection) throws IOException {
final String response = new String(connection.getInputStream().readAllBytes());
log.debug(response);
try {
return objectMapper.readValue(response, clazz);
} catch (JsonMappingException e) {
log.error("Failed to deserialize json: {}", e.getMessage());
log.error(response);
throw e;
}
}
@NonNull
private static HttpURLConnection open(final boolean post, @NonNull final String path) throws IOException {
final String url = URL.formatted(path);
final String method = post ? "POST" : "GET";
log.debug("open {} {}", method, url);
final HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
connection.setRequestMethod(method);
connection.setDoOutput(post);
connection.setRequestProperty("Authorization", "Basic " + new String(Base64.encodeBase64("api:api".getBytes(StandardCharsets.UTF_8))));
return connection;
}
}

View File

@ -0,0 +1,59 @@
package de.ph87.home.tvheadend.api;
import de.ph87.home.tvheadend.api.dto.DvrEntryGridUpcoming;
import de.ph87.home.tvheadend.api.dto.StatusConnections;
import de.ph87.home.tvheadend.api.dto.StatusInputs;
import de.ph87.home.tvheadend.api.dto.StatusSubscriptions;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.ToString;
import java.time.Duration;
import java.util.Comparator;
import java.util.List;
@ToString
public class TvheadendStatus {
@NonNull
public final List<StatusConnections.Entry> connectionList;
@NonNull
public final List<StatusSubscriptions.Entry> subscriptionList;
@NonNull
public final List<StatusInputs.Entry> inputList;
@NonNull
public final List<DvrEntryGridUpcoming.Entry> recordingList;
@Nullable
public final DvrEntryGridUpcoming.Entry recordingNext;
public final boolean receiverNeededForConnected;
public final boolean receiverNeededForSubscribed;
public final boolean receiverNeededForRecording;
public final boolean receiverNeeded;
public TvheadendStatus(
@NonNull final Duration beforeRecording,
@NonNull final List<StatusInputs.Entry> inputList,
@NonNull final List<StatusSubscriptions.Entry> subscriptionList,
@NonNull final List<StatusConnections.Entry> connectionList,
@NonNull final List<DvrEntryGridUpcoming.Entry> recordingList
) {
this.connectionList = connectionList;
this.subscriptionList = subscriptionList;
this.inputList = inputList;
this.recordingList = recordingList;
this.recordingNext = recordingList.stream().min(Comparator.comparing(DvrEntryGridUpcoming.Entry::getStart_real)).orElse(null);
this.receiverNeededForConnected = !connectionList.isEmpty();
this.receiverNeededForSubscribed = !subscriptionList.isEmpty();
this.receiverNeededForRecording = recordingNext != null && recordingNext.shouldBeOnForRecording(beforeRecording);
this.receiverNeeded = this.receiverNeededForConnected || this.receiverNeededForSubscribed || this.receiverNeededForRecording;
}
}

View File

@ -0,0 +1,259 @@
package de.ph87.home.tvheadend.api.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.ph87.home.common.EpochSecondsToZonedDateTime;
import de.ph87.home.common.SecondsToDuration;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAmount;
import java.util.List;
import java.util.Map;
@Getter
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class DvrEntryGridUpcoming {
@JsonProperty
private long total;
@JsonProperty
private List<Entry> entries;
@Getter
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Entry {
@JsonProperty
private String uuid;
@JsonProperty
private boolean enabled;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime create;
@JsonProperty
private long watched;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime start;
@JsonProperty
@JsonDeserialize(using = SecondsToDuration.class)
private Duration start_extra;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime start_real;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime stop;
@JsonProperty
@JsonDeserialize(using = SecondsToDuration.class)
private Duration stop_extra;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime stop_real;
@JsonProperty
@JsonDeserialize(using = SecondsToDuration.class)
private Duration duration;
@JsonProperty
private String channel;
@JsonProperty
private String channelname;
@JsonProperty
private String image;
@JsonProperty
private String fanart_image;
@JsonProperty
private Map<String, String> title;
@JsonProperty
private String disp_title;
@JsonProperty
private String disp_subtitle;
@JsonProperty
private Map<String, String> summary;
@JsonProperty
private String disp_summary;
@JsonProperty
@ToString.Exclude
private Map<String, String> description;
@JsonProperty
@ToString.Exclude
private String disp_description;
@JsonProperty
private String disp_extratext;
@JsonProperty
private int pri;
@JsonProperty
private int retention;
@JsonProperty
private int removal;
@JsonProperty
private int playposition;
@JsonProperty
private int playcount;
@JsonProperty
private String config_name;
@JsonProperty
private String owner;
@JsonProperty
private String creator;
@JsonProperty
private String filename;
@JsonProperty
private int errorcode;
@JsonProperty
private int errors;
@JsonProperty
private int data_errors;
@JsonProperty
private long dvb_eid;
@JsonProperty
private boolean noresched;
@JsonProperty
private boolean norerecord;
@JsonProperty
private long fileremoved;
@JsonProperty
private String uri;
@JsonProperty
private String autorec;
@JsonProperty
private String autorec_caption;
@JsonProperty
private String timerec;
@JsonProperty
private String timerec_caption;
@JsonProperty
private String parent;
@JsonProperty
private String child;
@JsonProperty
private long content_type;
@JsonProperty
private int copyright_year;
@JsonProperty
private long broadcast;
@JsonProperty
private String episode_disp;
@JsonProperty
private String url;
@JsonProperty
private long filesize;
@JsonProperty
private String status;
@JsonProperty
private String sched_status;
@JsonProperty
private long duplicate;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime first_aired;
@JsonProperty
private String comment;
@JsonProperty
private List<String> category;
// @JsonProperty
// private List<Object> credits;
@JsonProperty
private List<String> keyword;
@JsonProperty
private List<Integer> genre;
@JsonProperty
private int age_rating;
@JsonProperty
private String rating_label_saved;
@JsonProperty
private String rating_icon_saved;
@JsonProperty
private String rating_country_saved;
@JsonProperty
private String rating_authority_saved;
@JsonProperty
private String rating_label_uuid;
@JsonProperty
private String rating_icon;
@JsonProperty
private String rating_label;
public boolean shouldBeOnForRecording(@NonNull final TemporalAmount tolerance) {
final ZonedDateTime now = ZonedDateTime.now();
return !start_real.minus(tolerance).isAfter(now) && !stop_real.plus(tolerance).isBefore(now);
}
}
}

View File

@ -0,0 +1,56 @@
package de.ph87.home.tvheadend.api.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.ph87.home.common.EpochSecondsToZonedDateTime;
import lombok.Getter;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.List;
@Getter
@ToString
public class StatusConnections {
@JsonProperty
private long totalCount;
@JsonProperty
private List<Entry> entries;
@Getter
@ToString
public static class Entry {
@JsonProperty
private long id;
@JsonProperty
private String server;
@JsonProperty
private int server_port;
@JsonProperty
private String peer;
@JsonProperty
private int peer_port;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime started;
@JsonProperty
private long streaming;
@JsonProperty
private String type;
@JsonProperty
private String user;
}
}

View File

@ -0,0 +1,82 @@
package de.ph87.home.tvheadend.api.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.ToString;
import java.util.List;
@Getter
@ToString
public class StatusInputs {
@JsonProperty
private long totalCount;
@JsonProperty
private List<Entry> entries;
@Getter
@ToString
public static class Entry {
@JsonProperty
private int ec_block;
@JsonProperty
private String uuid;
@JsonProperty
private int signal_scale;
@JsonProperty
private int weight;
@JsonProperty
private int cc;
@JsonProperty
private int ber;
@JsonProperty
private int tc_block;
@JsonProperty
private String input;
@JsonProperty
private int tc_bit;
@JsonProperty
private int unc;
@JsonProperty
private int te;
@JsonProperty
private int bps;
@JsonProperty
private int subs;
@JsonProperty
private int ec_bit;
@JsonProperty
private List<Integer> pids;
@JsonProperty
private String stream;
@JsonProperty
private int snr;
@JsonProperty
private int signal;
@JsonProperty
private int snr_scale;
}
}

View File

@ -0,0 +1,77 @@
package de.ph87.home.tvheadend.api.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.ph87.home.common.EpochSecondsToZonedDateTime;
import lombok.Getter;
import lombok.ToString;
import java.time.ZonedDateTime;
import java.util.List;
@Getter
@ToString
public class StatusSubscriptions {
@JsonProperty
private long totalCount;
@JsonProperty
private List<Entry> entries;
@Getter
@ToString
public static class Entry {
@JsonProperty
private long id;
@JsonProperty
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
private ZonedDateTime start;
@JsonProperty
private long errors;
@JsonProperty
private String state;
@JsonProperty
private String hostname;
@JsonProperty
private String username;
@JsonProperty
private String client;
@JsonProperty
private String title;
@JsonProperty
private String channel;
@JsonProperty
private String service;
@JsonProperty
private List<Integer> pids;
@JsonProperty
private String profile;
@JsonProperty
private long in;
@JsonProperty
private long out;
@JsonProperty
private long total_in;
@JsonProperty
private long total_out;
}
}