From 18626a5db81971bbd01f887b02c62b38dfcd8584 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 17:43:13 +0100 Subject: [PATCH 1/7] feat(markers): implement marker entity & service --- .../controllers/GlobalExceptionHandler.java | 7 +++ .../controllers/MarkerController.java | 54 +++++++++++++++++++ .../controllers/dto/MarkerCreateRequest.java | 7 +++ .../controllers/dto/MarkerResponse.java | 11 ++++ .../com/ddf/vodsystem/entities/Marker.java | 33 ++++++++++++ .../vodsystem/exceptions/NotStreaming.java | 7 +++ .../repositories/MarkerRepository.java | 15 ++++++ .../ddf/vodsystem/services/ClipService.java | 2 +- .../ddf/vodsystem/services/MarkerService.java | 52 ++++++++++++++++++ src/main/resources/db/schema.sql | 11 ++++ 10 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ddf/vodsystem/controllers/MarkerController.java create mode 100644 src/main/java/com/ddf/vodsystem/controllers/dto/MarkerCreateRequest.java create mode 100644 src/main/java/com/ddf/vodsystem/controllers/dto/MarkerResponse.java create mode 100644 src/main/java/com/ddf/vodsystem/entities/Marker.java create mode 100644 src/main/java/com/ddf/vodsystem/exceptions/NotStreaming.java create mode 100644 src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java create mode 100644 src/main/java/com/ddf/vodsystem/services/MarkerService.java diff --git a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java index d5ecca9..b1f9ba7 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java +++ b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java @@ -108,4 +108,11 @@ public class GlobalExceptionHandler { APIResponse response = new APIResponse<>(ERROR, ex.getMessage(), null); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + + @ExceptionHandler(NotStreaming.class) + public ResponseEntity> handleNotStreaming(NotStreaming ex) { + logger.error("NotStreaming: {}", ex.getMessage()); + APIResponse response = new APIResponse<>(ERROR, ex.getMessage(), null); + return ResponseEntity.status(HttpStatus.CONFLICT).body(response); + } } \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java b/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java new file mode 100644 index 0000000..147978d --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java @@ -0,0 +1,54 @@ +package com.ddf.vodsystem.controllers; + +import com.ddf.vodsystem.controllers.dto.MarkerCreateRequest; +import com.ddf.vodsystem.controllers.dto.MarkerResponse; +import com.ddf.vodsystem.dto.APIResponse; +import com.ddf.vodsystem.entities.Marker; +import com.ddf.vodsystem.services.MarkerService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/markers") +public class MarkerController { + private static final String SUCCESS = "success"; + + private final MarkerService markerService; + + public MarkerController(MarkerService markerService) { + this.markerService = markerService; + } + + @GetMapping("") + public ResponseEntity>> getMarkers() { + List markers = markerService.getUserMarkers().stream() + .map(this::convertToDTO) + .toList(); + + return ResponseEntity.ok( + new APIResponse<>(SUCCESS, "Markers retrieved successfully", markers) + ); + } + + @PostMapping("") + public ResponseEntity> createMarker(@Valid @RequestBody MarkerCreateRequest request) { + Marker marker = markerService.create(request.message()); + + return ResponseEntity.ok( + new APIResponse<>(SUCCESS, "Marker created successfully", convertToDTO(marker)) + ); + } + + private MarkerResponse convertToDTO(Marker marker) { + return new MarkerResponse( + marker.getId(), + marker.getUser().getId(), + marker.getStream().getId(), + marker.getMessage(), + marker.getTimestamp() + ); + } +} diff --git a/src/main/java/com/ddf/vodsystem/controllers/dto/MarkerCreateRequest.java b/src/main/java/com/ddf/vodsystem/controllers/dto/MarkerCreateRequest.java new file mode 100644 index 0000000..bcefad8 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/controllers/dto/MarkerCreateRequest.java @@ -0,0 +1,7 @@ +package com.ddf.vodsystem.controllers.dto; + +import jakarta.validation.constraints.NotBlank; + +public record MarkerCreateRequest( + @NotBlank String message +) {} diff --git a/src/main/java/com/ddf/vodsystem/controllers/dto/MarkerResponse.java b/src/main/java/com/ddf/vodsystem/controllers/dto/MarkerResponse.java new file mode 100644 index 0000000..54d2510 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/controllers/dto/MarkerResponse.java @@ -0,0 +1,11 @@ +package com.ddf.vodsystem.controllers.dto; + +import java.time.Instant; + +public record MarkerResponse( + Long id, + Long userId, + Long streamId, + String message, + Instant timestamp +) {} diff --git a/src/main/java/com/ddf/vodsystem/entities/Marker.java b/src/main/java/com/ddf/vodsystem/entities/Marker.java new file mode 100644 index 0000000..5ff9ef7 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/entities/Marker.java @@ -0,0 +1,33 @@ +package com.ddf.vodsystem.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Data; + +import java.time.Instant; + +@Entity +@Table(name = "markers") +@Data +public class Marker { + @Id + @Column(name = "id", nullable = false, unique = true) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnore + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JsonIgnore + @JoinColumn(name = "stream_id", nullable = false) + private Stream stream; + + @Column(name = "message", length = 255) + private String message; + + @Column(name = "timestamp") + private Instant timestamp; +} diff --git a/src/main/java/com/ddf/vodsystem/exceptions/NotStreaming.java b/src/main/java/com/ddf/vodsystem/exceptions/NotStreaming.java new file mode 100644 index 0000000..15cc395 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/exceptions/NotStreaming.java @@ -0,0 +1,7 @@ +package com.ddf.vodsystem.exceptions; + +public class NotStreaming extends RuntimeException { + public NotStreaming(String message) { + super(message); + } +} diff --git a/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java b/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java new file mode 100644 index 0000000..581d286 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java @@ -0,0 +1,15 @@ +package com.ddf.vodsystem.repositories; + +import com.ddf.vodsystem.entities.Marker; +import com.ddf.vodsystem.entities.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MarkerRepository extends JpaRepository { + @Query("SELECT m FROM Marker m WHERE m.user = ?1") + List findByUser(User user); +} diff --git a/src/main/java/com/ddf/vodsystem/services/ClipService.java b/src/main/java/com/ddf/vodsystem/services/ClipService.java index 6000245..e0dfb43 100644 --- a/src/main/java/com/ddf/vodsystem/services/ClipService.java +++ b/src/main/java/com/ddf/vodsystem/services/ClipService.java @@ -174,7 +174,7 @@ public class ClipService { Path file = directoryService.resolvePath(path); if (!Files.exists(file)) { - throw new JobNotFound("Thumbnail file not found"); + throw new ClipNotFound("Thumbnail file not found"); } return new FileSystemResource(file); diff --git a/src/main/java/com/ddf/vodsystem/services/MarkerService.java b/src/main/java/com/ddf/vodsystem/services/MarkerService.java new file mode 100644 index 0000000..bb559b0 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/services/MarkerService.java @@ -0,0 +1,52 @@ +package com.ddf.vodsystem.services; + +import com.ddf.vodsystem.entities.Marker; +import com.ddf.vodsystem.entities.Stream; +import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.NotAuthenticated; +import com.ddf.vodsystem.exceptions.NotStreaming; +import com.ddf.vodsystem.repositories.MarkerRepository; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@Service +public class MarkerService { + + private final UserService userService; + private final StreamService streamService; + private final MarkerRepository markerRepository; + + public MarkerService(UserService userService, StreamService streamService, MarkerRepository markerRepository) { + this.userService = userService; + this.streamService = streamService; + this.markerRepository = markerRepository; + } + + public List getUserMarkers() { + Optional user = userService.getLoggedInUser(); + + if (user.isEmpty()) { + throw new NotAuthenticated("Must be logged in to get markers"); + } + + return markerRepository.findByUser(user.get()); + } + + public Marker create(String message) { + Optional stream = streamService.getActiveStream(); + + if (stream.isEmpty()) { + throw new NotStreaming("User must be streaming to mark"); + } + + Marker marker = new Marker(); + marker.setStream(stream.get()); + marker.setUser(stream.get().getUser()); + marker.setMessage(message); + marker.setTimestamp(Instant.now()); + return markerRepository.saveAndFlush(marker); + } +} diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index efbb646..4f33216 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -54,4 +54,15 @@ CREATE TABLE IF NOT EXISTS vods ( video_path VARCHAR(255) NOT NULL, thumbnail_path VARCHAR(255) NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS markers ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + stream_id BIGINT NOT NULL, + message VARCHAR(255), + timestamp TIMESTAMPTZ DEFAULT NOW(), + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (stream_id) REFERENCES streams(id) ON DELETE CASCADE ); \ No newline at end of file -- 2.49.1 From 62dc0b0491238ba2a7a770bf4bb883daa2d21a40 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 17:50:29 +0100 Subject: [PATCH 2/7] docs(bruno): marker endpoints --- bruno/VoD-System/Markers/Create Marker.bru | 22 ++++++++++++++++++++ bruno/VoD-System/Markers/Get all markers.bru | 11 ++++++++++ bruno/VoD-System/Markers/folder.bru | 8 +++++++ 3 files changed, 41 insertions(+) create mode 100644 bruno/VoD-System/Markers/Create Marker.bru create mode 100644 bruno/VoD-System/Markers/Get all markers.bru create mode 100644 bruno/VoD-System/Markers/folder.bru diff --git a/bruno/VoD-System/Markers/Create Marker.bru b/bruno/VoD-System/Markers/Create Marker.bru new file mode 100644 index 0000000..633827b --- /dev/null +++ b/bruno/VoD-System/Markers/Create Marker.bru @@ -0,0 +1,22 @@ +meta { + name: Create Marker + type: http + seq: 2 +} + +post { + url: {{base_url}}/markers + body: json + auth: inherit +} + +body:json { + { + "message": "Nice play" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/VoD-System/Markers/Get all markers.bru b/bruno/VoD-System/Markers/Get all markers.bru new file mode 100644 index 0000000..c11a1a4 --- /dev/null +++ b/bruno/VoD-System/Markers/Get all markers.bru @@ -0,0 +1,11 @@ +meta { + name: Get all markers + type: http + seq: 1 +} + +get { + url: {{base_url}}/markers + body: none + auth: inherit +} diff --git a/bruno/VoD-System/Markers/folder.bru b/bruno/VoD-System/Markers/folder.bru new file mode 100644 index 0000000..01e9479 --- /dev/null +++ b/bruno/VoD-System/Markers/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Markers + seq: 6 +} + +auth { + mode: inherit +} -- 2.49.1 From dc33d1df33099b10c8f09a2dea9b6fe18786f29b Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 18:35:22 +0100 Subject: [PATCH 3/7] feat(markers): get by id --- bruno/VoD-System/Markers/Create Marker.bru | 2 +- bruno/VoD-System/Markers/Get Marker.bru | 16 ++++++++++++++++ bruno/VoD-System/Markers/Get all markers.bru | 2 +- .../controllers/GlobalExceptionHandler.java | 7 +++++++ .../vodsystem/controllers/MarkerController.java | 9 +++++++++ .../ddf/vodsystem/exceptions/MarkerNotFound.java | 7 +++++++ .../ddf/vodsystem/services/MarkerService.java | 16 ++++++++++++++++ 7 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 bruno/VoD-System/Markers/Get Marker.bru create mode 100644 src/main/java/com/ddf/vodsystem/exceptions/MarkerNotFound.java diff --git a/bruno/VoD-System/Markers/Create Marker.bru b/bruno/VoD-System/Markers/Create Marker.bru index 633827b..426eee7 100644 --- a/bruno/VoD-System/Markers/Create Marker.bru +++ b/bruno/VoD-System/Markers/Create Marker.bru @@ -1,7 +1,7 @@ meta { name: Create Marker type: http - seq: 2 + seq: 3 } post { diff --git a/bruno/VoD-System/Markers/Get Marker.bru b/bruno/VoD-System/Markers/Get Marker.bru new file mode 100644 index 0000000..865af8c --- /dev/null +++ b/bruno/VoD-System/Markers/Get Marker.bru @@ -0,0 +1,16 @@ +meta { + name: Get Marker + type: http + seq: 1 +} + +get { + url: {{base_url}}/markers/5 + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno/VoD-System/Markers/Get all markers.bru b/bruno/VoD-System/Markers/Get all markers.bru index c11a1a4..d26e200 100644 --- a/bruno/VoD-System/Markers/Get all markers.bru +++ b/bruno/VoD-System/Markers/Get all markers.bru @@ -1,7 +1,7 @@ meta { name: Get all markers type: http - seq: 1 + seq: 2 } get { diff --git a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java index b1f9ba7..a42e2e2 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java +++ b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java @@ -115,4 +115,11 @@ public class GlobalExceptionHandler { APIResponse response = new APIResponse<>(ERROR, ex.getMessage(), null); return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } + + @ExceptionHandler(MarkerNotFound.class) + public ResponseEntity> handleMarkerNotFound(MarkerNotFound ex) { + logger.error("MarkerNotFound: {}", ex.getMessage()); + APIResponse response = new APIResponse<>(ERROR, ex.getMessage(), null); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } } \ No newline at end of file diff --git a/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java b/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java index 147978d..b10d276 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java @@ -33,6 +33,15 @@ public class MarkerController { ); } + @GetMapping("/{id}") + public ResponseEntity> getMarkerById(@PathVariable Long id) { + Marker marker = markerService.getMarkerById(id); + + return ResponseEntity.ok( + new APIResponse<>(SUCCESS, "Marker retrieved successfully", convertToDTO(marker)) + ); + } + @PostMapping("") public ResponseEntity> createMarker(@Valid @RequestBody MarkerCreateRequest request) { Marker marker = markerService.create(request.message()); diff --git a/src/main/java/com/ddf/vodsystem/exceptions/MarkerNotFound.java b/src/main/java/com/ddf/vodsystem/exceptions/MarkerNotFound.java new file mode 100644 index 0000000..197c7d0 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/exceptions/MarkerNotFound.java @@ -0,0 +1,7 @@ +package com.ddf.vodsystem.exceptions; + +public class MarkerNotFound extends RuntimeException { + public MarkerNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/com/ddf/vodsystem/services/MarkerService.java b/src/main/java/com/ddf/vodsystem/services/MarkerService.java index bb559b0..c23c27a 100644 --- a/src/main/java/com/ddf/vodsystem/services/MarkerService.java +++ b/src/main/java/com/ddf/vodsystem/services/MarkerService.java @@ -3,6 +3,7 @@ package com.ddf.vodsystem.services; import com.ddf.vodsystem.entities.Marker; import com.ddf.vodsystem.entities.Stream; import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.MarkerNotFound; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.exceptions.NotStreaming; import com.ddf.vodsystem.repositories.MarkerRepository; @@ -25,6 +26,21 @@ public class MarkerService { this.markerRepository = markerRepository; } + public Marker getMarkerById(Long id) { + Optional marker = markerRepository.findById(id); + + if (marker.isEmpty()) { + throw new MarkerNotFound("Marker does not exist"); + } + + Optional user = userService.getLoggedInUser(); + if (user.isEmpty() || !user.get().equals(marker.get().getUser())) { + throw new NotAuthenticated("User not authenticated for this marker"); + } + + return marker.get(); + } + public List getUserMarkers() { Optional user = userService.getLoggedInUser(); -- 2.49.1 From 2cac1770f9897d5993454b7af71e412fc92961aa Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 18:53:23 +0100 Subject: [PATCH 4/7] feat(markers): auto-delete after 8 hours --- .env.example | 4 +++- docker-compose.dev.yml | 2 ++ docker-compose.yml | 2 ++ nginx/default.conf.template | 2 +- nginx/docker-entrypoint.sh | 2 +- .../vodsystem/repositories/MarkerRepository.java | 4 ++++ .../com/ddf/vodsystem/services/MarkerService.java | 14 ++++++++++++++ src/main/resources/application.properties | 1 + 8 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 6083238..a4e6dd5 100644 --- a/.env.example +++ b/.env.example @@ -15,4 +15,6 @@ POSTGRES_PASSWORD=mypassword POSTGRES_DB=vodSystem FRONTEND_URL= -BACKEND_URL= \ No newline at end of file +BACKEND_URL= + +STREAM_RING_LENGTH_SECOND=28800 \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6cc3fbf..61149a9 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -22,6 +22,8 @@ services: container_name: vod_rtmp extra_hosts: - "host.docker.internal:host-gateway" + env_file: + - .env environment: - WEB_SERVER_URL=http://host.docker.internal:8080 ports: diff --git a/docker-compose.yml b/docker-compose.yml index 201b8dd..cc38d19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,8 @@ services: context: ./nginx dockerfile: Dockerfile container_name: vod_rtmp + env_file: + - .env environment: - WEB_SERVER_URL=http://backend:8080 ports: diff --git a/nginx/default.conf.template b/nginx/default.conf.template index 1cc95aa..05fa14b 100644 --- a/nginx/default.conf.template +++ b/nginx/default.conf.template @@ -20,7 +20,7 @@ rtmp { hls_path /stream; hls_fragment_naming system; hls_fragment 3; - hls_playlist_length 28800; + hls_playlist_length ${STREAM_RING_LENGTH_SECOND}; on_publish ${WEB_SERVER_URL}/api/v1/stream/start; on_publish_done ${WEB_SERVER_URL}/api/v1/stream/stop; diff --git a/nginx/docker-entrypoint.sh b/nginx/docker-entrypoint.sh index 3d1c9c0..0859fb6 100644 --- a/nginx/docker-entrypoint.sh +++ b/nginx/docker-entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/sh # must only run on certain env files, otherwise it replaces too much -envsubst '${WEB_SERVER_URL}' \ +envsubst '${WEB_SERVER_URL} ${STREAM_RING_LENGTH_SECOND}' \ < /etc/nginx/templates/default.conf.template \ > /etc/nginx/nginx.conf diff --git a/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java b/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java index 581d286..d5b7d82 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java @@ -6,10 +6,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.time.Instant; import java.util.List; @Repository public interface MarkerRepository extends JpaRepository { @Query("SELECT m FROM Marker m WHERE m.user = ?1") List findByUser(User user); + + @Query("SELECT m FROM Marker m WHERE m.timestamp <= ?1") + List findAllBefore(Instant time); } diff --git a/src/main/java/com/ddf/vodsystem/services/MarkerService.java b/src/main/java/com/ddf/vodsystem/services/MarkerService.java index c23c27a..b747f6a 100644 --- a/src/main/java/com/ddf/vodsystem/services/MarkerService.java +++ b/src/main/java/com/ddf/vodsystem/services/MarkerService.java @@ -7,6 +7,9 @@ import com.ddf.vodsystem.exceptions.MarkerNotFound; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.exceptions.NotStreaming; import com.ddf.vodsystem.repositories.MarkerRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.time.Instant; @@ -20,6 +23,9 @@ public class MarkerService { private final StreamService streamService; private final MarkerRepository markerRepository; + @Value("${stream.max-length}") + private Long maxStreamLength; + public MarkerService(UserService userService, StreamService streamService, MarkerRepository markerRepository) { this.userService = userService; this.streamService = streamService; @@ -65,4 +71,12 @@ public class MarkerService { marker.setTimestamp(Instant.now()); return markerRepository.saveAndFlush(marker); } + + @Scheduled(fixedDelay = 360_000) + @Transactional + public void deleteOldMarkers() { + Instant cutoff = Instant.now().minusSeconds(maxStreamLength); + List stale = markerRepository.findAllBefore(cutoff); + markerRepository.deleteAllInBatch(stale); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9701c07..3fd439f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,6 +3,7 @@ spring.application.name=vodSystem # VODs spring.servlet.multipart.max-file-size=2GB spring.servlet.multipart.max-request-size=2GB +stream.max-length=${STREAM_RING_LENGTH_SECOND} # Database spring.datasource.url=${POSTGRES_URL} -- 2.49.1 From ba378680c67893347c231a4dbd2c1d5d9822ad21 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 19:09:07 +0100 Subject: [PATCH 5/7] feat(markers): add save endpoint with markers --- .../vodsystem/controllers/MediaController.java | 16 ++++++++++++++++ .../ddf/vodsystem/services/MediaService.java | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ddf/vodsystem/controllers/MediaController.java b/src/main/java/com/ddf/vodsystem/controllers/MediaController.java index 0d5185f..77ff18e 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/MediaController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/MediaController.java @@ -1,6 +1,7 @@ package com.ddf.vodsystem.controllers; import com.ddf.vodsystem.controllers.dto.ClipSectionRequest; +import com.ddf.vodsystem.controllers.dto.SaveSectionByMarkerRequest; import com.ddf.vodsystem.controllers.dto.SaveSectionRequest; import com.ddf.vodsystem.controllers.dto.UUIDResponse; import com.ddf.vodsystem.dto.Job; @@ -54,6 +55,21 @@ public class MediaController { )); } + @PostMapping("/save/markers") + public ResponseEntity> saveByMarkers( + @Valid @RequestBody SaveSectionByMarkerRequest saveSectionByMarkerRequest) throws IOException { + Job job = mediaService.saveSection( + saveSectionByMarkerRequest.startMarkerId(), + saveSectionByMarkerRequest.endMarkerId(), + saveSectionByMarkerRequest.title(), + saveSectionByMarkerRequest.description()); + return ResponseEntity.ok(new APIResponse<>( + SUCCESS, + "Section saving successfully started", + new UUIDResponse(job.getUuid()) + )); + } + @PostMapping("/clip") public ResponseEntity> clip( @RequestBody ClipSectionRequest clipSectionRequest) throws IOException { diff --git a/src/main/java/com/ddf/vodsystem/services/MediaService.java b/src/main/java/com/ddf/vodsystem/services/MediaService.java index e466230..db2a6bc 100644 --- a/src/main/java/com/ddf/vodsystem/services/MediaService.java +++ b/src/main/java/com/ddf/vodsystem/services/MediaService.java @@ -3,6 +3,7 @@ package com.ddf.vodsystem.services; import com.ddf.vodsystem.dto.ClipOptions; import com.ddf.vodsystem.dto.Job; import com.ddf.vodsystem.dto.JobState; +import com.ddf.vodsystem.entities.Marker; import com.ddf.vodsystem.entities.User; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.services.media.CompressionService; @@ -32,12 +33,13 @@ public class MediaService { private static final int HLS_SEGMENT_LENGTH = 3; private final StreamActionsService streamActionsService; private final VodService vodService; + private final MarkerService markerService; public MediaService(JobRegistryService jobRegistryService, DirectoryService directoryService, CompressionService compressionService, UserService userService, - ClipService clipService, StreamActionsService streamActionsService, VodService vodService) { + ClipService clipService, StreamActionsService streamActionsService, VodService vodService, MarkerService markerService) { this.jobRegistryService = jobRegistryService; this.directoryService = directoryService; this.compressionService = compressionService; @@ -45,6 +47,7 @@ public class MediaService { this.clipService = clipService; this.streamActionsService = streamActionsService; this.vodService = vodService; + this.markerService = markerService; } /** @@ -149,6 +152,18 @@ public class MediaService { ); } + public Job saveSection( + Long startMarkerId, + Long endMarkerId, + String title, + String description + ) throws IOException { + Marker start = markerService.getMarkerById(startMarkerId); + Marker end = markerService.getMarkerById(endMarkerId); + + return saveSection(start.getTimestamp(), end.getTimestamp(), title, description); + } + /** * Saves the last {@code duration} seconds of the currently authenticated user's live stream as a clip. *

-- 2.49.1 From 1a0778d1d6f32f9c7c5a03c7c3fe254cb1a4ad05 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 19:09:25 +0100 Subject: [PATCH 6/7] docs(bruno): save with marker endpoint --- .../Media/Save Stream Section By Markers.bru | 24 +++++++++++++++++++ .../dto/SaveSectionByMarkerRequest.java | 9 +++++++ 2 files changed, 33 insertions(+) create mode 100644 bruno/VoD-System/Media/Save Stream Section By Markers.bru create mode 100644 src/main/java/com/ddf/vodsystem/controllers/dto/SaveSectionByMarkerRequest.java diff --git a/bruno/VoD-System/Media/Save Stream Section By Markers.bru b/bruno/VoD-System/Media/Save Stream Section By Markers.bru new file mode 100644 index 0000000..9b5f6b8 --- /dev/null +++ b/bruno/VoD-System/Media/Save Stream Section By Markers.bru @@ -0,0 +1,24 @@ +meta { + name: Save Stream Section By Markers + type: http + seq: 4 +} + +post { + url: {{base_url}}/media/save/markers + body: json + auth: inherit +} + +body:json { + { + "startMarkerId": 1, + "endMarkerId": 2, + "title": "Hello", + "description": "my clip" + } +} + +script:post-response { + bru.setEnvVar("uuid", res.getBody().data.uuid); +} diff --git a/src/main/java/com/ddf/vodsystem/controllers/dto/SaveSectionByMarkerRequest.java b/src/main/java/com/ddf/vodsystem/controllers/dto/SaveSectionByMarkerRequest.java new file mode 100644 index 0000000..d9fb862 --- /dev/null +++ b/src/main/java/com/ddf/vodsystem/controllers/dto/SaveSectionByMarkerRequest.java @@ -0,0 +1,9 @@ +package com.ddf.vodsystem.controllers.dto; + +import jakarta.validation.constraints.NotNull; + +public record SaveSectionByMarkerRequest( + @NotNull Long startMarkerId, + @NotNull Long endMarkerId, + String title, + String description) {} -- 2.49.1 From 41fda538176e9ed72f768ad74d57b8e954e7007e Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 19:13:55 +0100 Subject: [PATCH 7/7] docs(docstring): other stream save method --- .../ddf/vodsystem/services/MediaService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/ddf/vodsystem/services/MediaService.java b/src/main/java/com/ddf/vodsystem/services/MediaService.java index db2a6bc..50c88cc 100644 --- a/src/main/java/com/ddf/vodsystem/services/MediaService.java +++ b/src/main/java/com/ddf/vodsystem/services/MediaService.java @@ -5,6 +5,7 @@ import com.ddf.vodsystem.dto.Job; import com.ddf.vodsystem.dto.JobState; import com.ddf.vodsystem.entities.Marker; import com.ddf.vodsystem.entities.User; +import com.ddf.vodsystem.exceptions.MarkerNotFound; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.services.media.CompressionService; import com.ddf.vodsystem.services.media.StreamActionsService; @@ -152,6 +153,25 @@ public class MediaService { ); } + /** + * Saves a section of the currently authenticated user's live stream between two markers. + *

+ * Resolves {@code startMarkerId} and {@code endMarkerId} to their marker timestamps and + * delegates to {@link #saveSection(Instant, Instant, String, String)}. + * + * @param startMarkerId id of the marker whose timestamp begins the section to save + * @param endMarkerId id of the marker whose timestamp ends the section to save + * @param title title for the saved VoD; defaults to the current timestamp if {@code null} + * @param description description for the saved VoD; defaults to an empty string if {@code null} + * @return the {@link Job} tracking the save; state transitions to + * {@link JobState#SUCCEEDED} or {@link JobState#FAILED} asynchronously + * @throws MarkerNotFound if either marker id does not exist + * @throws NotAuthenticated if no user is currently authenticated, or the caller does not + * own one of the referenced markers + * @throws IllegalArgumentException if the start marker's timestamp is not before the end + * marker's, or if no stream segments exist in the given range + * @throws IOException if reading the stream directory or its segments fails + */ public Job saveSection( Long startMarkerId, Long endMarkerId, -- 2.49.1