feat(api): fully support range requests on backend
This commit is contained in:
@@ -6,10 +6,7 @@ import com.ddf.vodsystem.controllers.dto.ClipUpdateRequest;
|
||||
import com.ddf.vodsystem.entities.Clip;
|
||||
import com.ddf.vodsystem.services.ClipService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourceRegion;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -81,40 +78,23 @@ public class ClipController {
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/media")
|
||||
public ResponseEntity<?> downloadClip(@PathVariable Long id, @RequestHeader HttpHeaders headers) throws IOException {
|
||||
@GetMapping(value = "/{id}/media", produces = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Resource> downloadClip(@PathVariable Long id) throws IOException {
|
||||
Resource resource = clipService.downloadClip(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
List<HttpRange> ranges = headers.getRange();
|
||||
|
||||
if (!ranges.isEmpty()) {
|
||||
ResourceRegion region = createResourceRegion(resource, ranges.getFirst());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(region);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource)
|
||||
.orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.contentLength(resource.contentLength())
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
private ResourceRegion createResourceRegion(Resource resource, HttpRange range) throws IOException {
|
||||
long contentLength = resource.contentLength();
|
||||
long start = range.getRangeStart(contentLength);
|
||||
long end = range.getRangeEnd(contentLength);
|
||||
return new ResourceRegion(resource, start, end - start + 1);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/thumbnail")
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
|
||||
Resource resource = clipService.downloadThumbnail(id);
|
||||
|
||||
@@ -102,6 +102,13 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ClipMediaUnavailable.class)
|
||||
public ResponseEntity<APIResponse<Void>> handleClipMediaUnavailable(ClipMediaUnavailable ex) {
|
||||
logger.error("ClipMediaUnavailable (clip {}): {}", ex.getClipId(), ex.getMessage());
|
||||
APIResponse<Void> response = new APIResponse<>(ERROR, "Clip media is currently unavailable", null);
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(VodNotFound.class)
|
||||
public ResponseEntity<APIResponse<Void>> handleVodNotFound(VodNotFound ex) {
|
||||
logger.error("VodNotFound: {}", ex.getMessage());
|
||||
@@ -109,6 +116,13 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(VodMediaUnavailable.class)
|
||||
public ResponseEntity<APIResponse<Void>> handleVodMediaUnavailable(VodMediaUnavailable ex) {
|
||||
logger.error("VodMediaUnavailable (vod {}): {}", ex.getVodId(), ex.getMessage());
|
||||
APIResponse<Void> response = new APIResponse<>(ERROR, "Vod media is currently unavailable", null);
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotStreaming.class)
|
||||
public ResponseEntity<APIResponse<Void>> handleNotStreaming(NotStreaming ex) {
|
||||
logger.error("NotStreaming: {}", ex.getMessage());
|
||||
|
||||
@@ -6,10 +6,7 @@ import com.ddf.vodsystem.dto.APIResponse;
|
||||
import com.ddf.vodsystem.entities.Vod;
|
||||
import com.ddf.vodsystem.services.VodService;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourceRegion;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRange;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -58,40 +55,23 @@ public class VodController {
|
||||
"Vod with ID " + id + " has been deleted"));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/media")
|
||||
public ResponseEntity<?> downloadVod(@PathVariable Long id, @RequestHeader HttpHeaders headers) throws IOException {
|
||||
@GetMapping(value = "/{id}/media", produces = MediaType.ALL_VALUE)
|
||||
public ResponseEntity<Resource> downloadVod(@PathVariable Long id) throws IOException {
|
||||
Resource resource = vodService.downloadVod(id);
|
||||
|
||||
if (resource == null || !resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
List<HttpRange> ranges = headers.getRange();
|
||||
|
||||
if (!ranges.isEmpty()) {
|
||||
ResourceRegion region = createResourceRegion(resource, ranges.getFirst());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.body(region);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
String.format(FILENAME_HEADER, resource.getFilename()))
|
||||
.contentType(MediaTypeFactory.getMediaType(resource)
|
||||
.orElse(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.contentLength(resource.contentLength())
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
private ResourceRegion createResourceRegion(Resource resource, HttpRange range) throws IOException {
|
||||
long contentLength = resource.contentLength();
|
||||
long start = range.getRangeStart(contentLength);
|
||||
long end = range.getRangeEnd(contentLength);
|
||||
return new ResourceRegion(resource, start, end - start + 1);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/thumbnail")
|
||||
public ResponseEntity<Resource> downloadThumbnail(@PathVariable Long id) {
|
||||
Resource resource = vodService.downloadThumbnail(id);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ddf.vodsystem.exceptions;
|
||||
|
||||
public class ClipMediaUnavailable extends RuntimeException {
|
||||
private final Long clipId;
|
||||
|
||||
public ClipMediaUnavailable(Long clipId, String message) {
|
||||
this(clipId, message, null);
|
||||
}
|
||||
|
||||
public ClipMediaUnavailable(Long clipId, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.clipId = clipId;
|
||||
}
|
||||
|
||||
public Long getClipId() {
|
||||
return clipId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ddf.vodsystem.exceptions;
|
||||
|
||||
public class VodMediaUnavailable extends RuntimeException {
|
||||
private final Long vodId;
|
||||
|
||||
public VodMediaUnavailable(Long vodId, String message) {
|
||||
this(vodId, message, null);
|
||||
}
|
||||
|
||||
public VodMediaUnavailable(Long vodId, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.vodId = vodId;
|
||||
}
|
||||
|
||||
public Long getVodId() {
|
||||
return vodId;
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,10 @@ public class ClipService {
|
||||
throw new ClipNotFound("Clip file not found");
|
||||
}
|
||||
|
||||
if (!Files.isReadable(file)) {
|
||||
throw new ClipMediaUnavailable(id, "Clip file is not readable");
|
||||
}
|
||||
|
||||
return new FileSystemResource(file);
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,10 @@ public class VodService {
|
||||
throw new VodNotFound("Vod file not found for ID: " + id);
|
||||
}
|
||||
|
||||
if (!Files.isReadable(file)) {
|
||||
throw new VodMediaUnavailable(id, "Vod file is not readable");
|
||||
}
|
||||
|
||||
return new FileSystemResource(file);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user