Implement proper ordering
This commit is contained in:
@ -1,17 +1,19 @@
|
|||||||
package com.bivashy.backend.composer.controller;
|
package com.bivashy.backend.composer.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.dto.track.AddLocalTrackRequest;
|
import com.bivashy.backend.composer.dto.track.AddLocalTrackRequest;
|
||||||
|
import com.bivashy.backend.composer.dto.track.PlaylistTrackResponse;
|
||||||
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
||||||
import com.bivashy.backend.composer.model.User;
|
import com.bivashy.backend.composer.model.User;
|
||||||
import com.bivashy.backend.composer.service.TrackService;
|
import com.bivashy.backend.composer.service.TrackService;
|
||||||
@ -24,7 +26,7 @@ public class TrackController {
|
|||||||
this.trackService = trackService;
|
this.trackService = trackService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/playlist/{playlistId}/track", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(path = "/playlist/{playlistId}/track/local", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<TrackResponse> addLocalTrack(
|
public ResponseEntity<TrackResponse> addLocalTrack(
|
||||||
@AuthenticationPrincipal User user,
|
@AuthenticationPrincipal User user,
|
||||||
@PathVariable Long playlistId,
|
@PathVariable Long playlistId,
|
||||||
@ -32,4 +34,12 @@ public class TrackController {
|
|||||||
TrackResponse response = trackService.addLocalTrack(user, playlistId, request);
|
TrackResponse response = trackService.addLocalTrack(user, playlistId, request);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/playlist/{playlistId}/tracks")
|
||||||
|
public ResponseEntity<List<PlaylistTrackResponse>> getPlaylistTracks(
|
||||||
|
@AuthenticationPrincipal User user,
|
||||||
|
@PathVariable Long playlistId) {
|
||||||
|
List<PlaylistTrackResponse> tracks = trackService.getPlaylistTracks(user, playlistId);
|
||||||
|
return ResponseEntity.ok(tracks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.track;
|
||||||
|
|
||||||
|
public record PlaylistTrackResponse(
|
||||||
|
Long trackId,
|
||||||
|
String title,
|
||||||
|
String artist,
|
||||||
|
String audioPath,
|
||||||
|
Integer durationSeconds,
|
||||||
|
String fileName) {
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.bivashy.backend.composer.model;
|
package com.bivashy.backend.composer.model;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.model.key.PlaylistTrackId;
|
import com.bivashy.backend.composer.model.key.PlaylistTrackId;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
@ -24,8 +26,8 @@ public class TrackPlaylist {
|
|||||||
@Column(name = "track_id", nullable = false)
|
@Column(name = "track_id", nullable = false)
|
||||||
private Long trackId;
|
private Long trackId;
|
||||||
|
|
||||||
@Column(name = "order_index", nullable = false)
|
@Column(name = "order_index", nullable = false, precision = 1000, scale = 500)
|
||||||
private Long order;
|
private BigDecimal order;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "playlist_id", insertable = false, updatable = false)
|
@JoinColumn(name = "playlist_id", insertable = false, updatable = false)
|
||||||
@ -38,7 +40,7 @@ public class TrackPlaylist {
|
|||||||
TrackPlaylist() {
|
TrackPlaylist() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackPlaylist(Long playlistId, Long trackId, Long order) {
|
public TrackPlaylist(Long playlistId, Long trackId, BigDecimal order) {
|
||||||
this.playlistId = playlistId;
|
this.playlistId = playlistId;
|
||||||
this.trackId = trackId;
|
this.trackId = trackId;
|
||||||
this.order = order;
|
this.order = order;
|
||||||
@ -52,10 +54,14 @@ public class TrackPlaylist {
|
|||||||
return trackId;
|
return trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getOrder() {
|
public BigDecimal getOrder() {
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOrder(BigDecimal order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
|
||||||
public Playlist getPlaylist() {
|
public Playlist getPlaylist() {
|
||||||
return playlist;
|
return playlist;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package com.bivashy.backend.composer.repository;
|
package com.bivashy.backend.composer.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@ -7,4 +9,5 @@ import com.bivashy.backend.composer.model.TrackMetadata;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface TrackMetadataRepository extends JpaRepository<TrackMetadata, Long> {
|
public interface TrackMetadataRepository extends JpaRepository<TrackMetadata, Long> {
|
||||||
|
Optional<TrackMetadata> findByTrackId(Long trackId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,23 @@
|
|||||||
package com.bivashy.backend.composer.repository;
|
package com.bivashy.backend.composer.repository;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.model.TrackPlaylist;
|
import com.bivashy.backend.composer.model.TrackPlaylist;
|
||||||
import com.bivashy.backend.composer.model.key.PlaylistTrackId;
|
import com.bivashy.backend.composer.model.key.PlaylistTrackId;
|
||||||
|
|
||||||
public interface TrackPlaylistRepository extends JpaRepository<TrackPlaylist, PlaylistTrackId> {
|
public interface TrackPlaylistRepository extends JpaRepository<TrackPlaylist, PlaylistTrackId> {
|
||||||
|
List<TrackPlaylist> findByPlaylistIdOrderByOrderAsc(Long playlistId);
|
||||||
|
|
||||||
|
@Query("SELECT COALESCE(MAX(tp.order), 0) FROM TrackPlaylist tp WHERE tp.playlistId = :playlistId")
|
||||||
|
BigDecimal findMaxOrderByPlaylistId(@Param("playlistId") Long playlistId);
|
||||||
|
|
||||||
|
@Query("SELECT MIN(tp.order) FROM TrackPlaylist tp WHERE tp.playlistId = :playlistId AND tp.order > :order")
|
||||||
|
BigDecimal findNextOrderByPlaylistIdAndOrderGreaterThan(@Param("playlistId") Long playlistId,
|
||||||
|
@Param("order") BigDecimal order);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.bivashy.backend.composer.service;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
@ -10,10 +11,14 @@ public interface AudioBlobStorageService {
|
|||||||
|
|
||||||
String store(byte[] data);
|
String store(byte[] data);
|
||||||
|
|
||||||
|
String store(InputStream inputStream, Map<String, String> metadata);
|
||||||
|
|
||||||
|
String store(byte[] data, Map<String, String> metadata);
|
||||||
|
|
||||||
byte[] readRaw(String path) throws IOException;
|
byte[] readRaw(String path) throws IOException;
|
||||||
|
|
||||||
Blob read(String path);
|
Blob read(String path);
|
||||||
|
|
||||||
public record Blob(InputStream stream, MediaType contentType) {
|
public record Blob(InputStream stream, MediaType contentType, Map<String, String> metadata) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.bivashy.backend.composer.service;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.tika.Tika;
|
import org.apache.tika.Tika;
|
||||||
@ -37,17 +38,28 @@ public class AudioS3StorageService implements AudioBlobStorageService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String store(InputStream inputStream) {
|
public String store(InputStream inputStream) {
|
||||||
return store(new ByteArrayInputStream(DEFAULT_BUFFER).readAllBytes());
|
return store(inputStream, Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String store(byte[] data) {
|
public String store(byte[] data) {
|
||||||
|
return store(data, Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String store(InputStream inputStream, Map<String, String> metadata) {
|
||||||
|
return store(new ByteArrayInputStream(DEFAULT_BUFFER).readAllBytes(), metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String store(byte[] data, Map<String, String> metadata) {
|
||||||
String objectKey = newObjectName();
|
String objectKey = newObjectName();
|
||||||
String contentType = detectContentType(data);
|
String contentType = detectContentType(data);
|
||||||
s3Client.putObject(PutObjectRequest.builder()
|
s3Client.putObject(PutObjectRequest.builder()
|
||||||
.bucket(bucket)
|
.bucket(bucket)
|
||||||
.key(objectKey)
|
.key(objectKey)
|
||||||
.contentType(contentType)
|
.contentType(contentType)
|
||||||
|
.metadata(metadata)
|
||||||
.build(), RequestBody.fromBytes(data));
|
.build(), RequestBody.fromBytes(data));
|
||||||
return String.join("/", bucket, objectKey);
|
return String.join("/", bucket, objectKey);
|
||||||
}
|
}
|
||||||
@ -73,7 +85,7 @@ public class AudioS3StorageService implements AudioBlobStorageService {
|
|||||||
} catch (InvalidMediaTypeException e) {
|
} catch (InvalidMediaTypeException e) {
|
||||||
logger.error("invalid media type", e);
|
logger.error("invalid media type", e);
|
||||||
}
|
}
|
||||||
return new Blob(response, mediaType);
|
return new Blob(response, mediaType, response.response().metadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String detectContentType(byte[] data) {
|
private String detectContentType(byte[] data) {
|
||||||
|
|||||||
@ -2,32 +2,68 @@ package com.bivashy.backend.composer.service;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.github.kokorin.jaffree.ffprobe.FFprobe;
|
import com.github.kokorin.jaffree.ffprobe.FFprobe;
|
||||||
import com.github.kokorin.jaffree.ffprobe.FFprobeResult;
|
import com.github.kokorin.jaffree.ffprobe.FFprobeResult;
|
||||||
import com.github.kokorin.jaffree.ffprobe.Format;
|
|
||||||
import com.github.kokorin.jaffree.ffprobe.PipeInput;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class MetadataParseService {
|
public class MetadataParseService {
|
||||||
|
private final ObjectMapper ffprobeObjectMapper;
|
||||||
|
|
||||||
|
public MetadataParseService() {
|
||||||
|
var result = new ObjectMapper();
|
||||||
|
result.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||||
|
result.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
|
this.ffprobeObjectMapper = result;
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<Metadata> extractMetadata(InputStream input) throws IOException {
|
public Optional<Metadata> extractMetadata(InputStream input) throws IOException {
|
||||||
|
Path tempFile = Files.createTempFile("metadata-file", "");
|
||||||
|
Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
FFprobeResult result = FFprobe.atPath()
|
FFprobeResult result = FFprobe.atPath()
|
||||||
.setShowFormat(true)
|
.setShowFormat(true)
|
||||||
.setInput(PipeInput.pumpFrom(input))
|
.setShowStreams(true)
|
||||||
|
.setInput(tempFile)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
Format format = result.getFormat();
|
Files.deleteIfExists(tempFile);
|
||||||
|
|
||||||
if (format == null) {
|
var format = Optional.ofNullable(result.getFormat());
|
||||||
return Optional.empty();
|
|
||||||
|
Optional<Float> formatDuration = format.map(f -> f.getDuration());
|
||||||
|
List<Optional<Float>> streamDuration = Optional.ofNullable(result.getStreams())
|
||||||
|
.map(streams -> streams.stream()
|
||||||
|
.map(s -> s.getDuration())
|
||||||
|
.map(Optional::ofNullable).toList())
|
||||||
|
.orElse(List.of());
|
||||||
|
|
||||||
|
var foundDuration = Stream.of(Collections.singletonList(formatDuration), streamDuration)
|
||||||
|
.flatMap(Collection::stream)
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
var jsonResult = ffprobeObjectMapper.writeValueAsString(result);
|
||||||
|
|
||||||
|
var title = format.map(f -> f.getTag("title")).orElse(null);
|
||||||
|
var artist = format.map(f -> f.getTag("artist")).orElse(null);
|
||||||
|
|
||||||
|
return Optional.of(new Metadata(title, artist, foundDuration.orElse(0f), jsonResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.of(new Metadata(format.getTag("title"), format.getTag("artist"), format.getDuration()));
|
public static record Metadata(String title, String artist, Float durationSeconds, String rawJson) {
|
||||||
}
|
|
||||||
|
|
||||||
public static record Metadata(String title, String artist, Float durationSeconds) {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.bivashy.backend.composer.service;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import com.bivashy.backend.composer.model.Track;
|
||||||
|
import com.bivashy.backend.composer.model.TrackMetadata;
|
||||||
|
import com.bivashy.backend.composer.repository.TrackMetadataRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TrackMetadataService {
|
||||||
|
private final TrackMetadataRepository trackMetadataRepository;
|
||||||
|
|
||||||
|
public TrackMetadataService(TrackMetadataRepository trackMetadataRepository) {
|
||||||
|
this.trackMetadataRepository = trackMetadataRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackMetadata createTrackMetadata(Track track, String title, String fileName, String audioPath,
|
||||||
|
String artist, String thumbnailPath, int durationSeconds) {
|
||||||
|
return trackMetadataRepository.save(
|
||||||
|
new TrackMetadata(track, title, fileName, audioPath, artist, thumbnailPath, durationSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackMetadata getTrackMetadata(Long trackId) {
|
||||||
|
return trackMetadataRepository.findByTrackId(trackId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Metadata not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package com.bivashy.backend.composer.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.bivashy.backend.composer.model.TrackPlaylist;
|
||||||
|
import com.bivashy.backend.composer.model.key.PlaylistTrackId;
|
||||||
|
import com.bivashy.backend.composer.repository.TrackPlaylistRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TrackPlaylistService {
|
||||||
|
private final TrackPlaylistRepository repository;
|
||||||
|
|
||||||
|
public TrackPlaylistService(TrackPlaylistRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TrackPlaylist> getPlaylistTracks(Long playlistId) {
|
||||||
|
return repository.findByPlaylistIdOrderByOrderAsc(playlistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void insertTrackAtEnd(Long playlistId, Long trackId) {
|
||||||
|
addTrack(playlistId, trackId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal findIntermediateOrder(BigDecimal prev, BigDecimal next) {
|
||||||
|
if (prev == null && next == null) {
|
||||||
|
return BigDecimal.ONE;
|
||||||
|
} else if (prev == null) {
|
||||||
|
return next.divide(BigDecimal.valueOf(2), 1000, RoundingMode.HALF_UP);
|
||||||
|
} else if (next == null) {
|
||||||
|
return prev.add(BigDecimal.ONE);
|
||||||
|
} else {
|
||||||
|
return prev.add(next).divide(BigDecimal.valueOf(2), 1000, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTrack(Long playlistId, Long trackId, BigDecimal afterOrder) {
|
||||||
|
BigDecimal newOrder;
|
||||||
|
if (afterOrder == null) {
|
||||||
|
// Insert at the end
|
||||||
|
BigDecimal maxOrder = repository.findMaxOrderByPlaylistId(playlistId);
|
||||||
|
newOrder = findIntermediateOrder(maxOrder, null);
|
||||||
|
} else {
|
||||||
|
// Insert after a specific track
|
||||||
|
BigDecimal nextOrder = repository.findNextOrderByPlaylistIdAndOrderGreaterThan(playlistId, afterOrder);
|
||||||
|
newOrder = findIntermediateOrder(afterOrder, nextOrder);
|
||||||
|
}
|
||||||
|
TrackPlaylist trackPlaylist = new TrackPlaylist(playlistId, trackId, newOrder);
|
||||||
|
repository.save(trackPlaylist);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reorderTrack(Long playlistId, Long trackId, BigDecimal afterOrder) {
|
||||||
|
TrackPlaylist trackPlaylist = repository.findById(new PlaylistTrackId(playlistId, trackId))
|
||||||
|
.orElseThrow(() -> new RuntimeException("TrackPlaylist not found"));
|
||||||
|
|
||||||
|
BigDecimal nextOrder = repository.findNextOrderByPlaylistIdAndOrderGreaterThan(playlistId, afterOrder);
|
||||||
|
BigDecimal newOrder = findIntermediateOrder(afterOrder, nextOrder);
|
||||||
|
|
||||||
|
trackPlaylist.setOrder(newOrder);
|
||||||
|
repository.save(trackPlaylist);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,88 +1,88 @@
|
|||||||
package com.bivashy.backend.composer.service;
|
package com.bivashy.backend.composer.service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.dto.track.AddLocalTrackRequest;
|
import com.bivashy.backend.composer.dto.track.AddLocalTrackRequest;
|
||||||
|
import com.bivashy.backend.composer.dto.track.PlaylistTrackResponse;
|
||||||
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
||||||
import com.bivashy.backend.composer.model.SourceType;
|
|
||||||
import com.bivashy.backend.composer.model.SourceTypes;
|
import com.bivashy.backend.composer.model.SourceTypes;
|
||||||
import com.bivashy.backend.composer.model.Track;
|
import com.bivashy.backend.composer.model.Track;
|
||||||
import com.bivashy.backend.composer.model.TrackMetadata;
|
import com.bivashy.backend.composer.model.TrackMetadata;
|
||||||
import com.bivashy.backend.composer.model.TrackPlaylist;
|
|
||||||
import com.bivashy.backend.composer.model.TrackSource;
|
import com.bivashy.backend.composer.model.TrackSource;
|
||||||
import com.bivashy.backend.composer.model.User;
|
import com.bivashy.backend.composer.model.User;
|
||||||
import com.bivashy.backend.composer.repository.SourceTypeRepository;
|
|
||||||
import com.bivashy.backend.composer.repository.TrackMetadataRepository;
|
|
||||||
import com.bivashy.backend.composer.repository.TrackPlaylistRepository;
|
|
||||||
import com.bivashy.backend.composer.repository.TrackRepository;
|
import com.bivashy.backend.composer.repository.TrackRepository;
|
||||||
import com.bivashy.backend.composer.repository.TrackSourceRepository;
|
|
||||||
import com.bivashy.backend.composer.service.MetadataParseService.Metadata;
|
import com.bivashy.backend.composer.service.MetadataParseService.Metadata;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TrackService {
|
public class TrackService {
|
||||||
private final AudioBlobStorageService s3Service;
|
|
||||||
private final TrackSourceRepository trackSourceRepository;
|
|
||||||
private final TrackRepository trackRepository;
|
private final TrackRepository trackRepository;
|
||||||
private final TrackMetadataRepository trackMetadataRepository;
|
private final TrackSourceService trackSourceService;
|
||||||
private final TrackPlaylistRepository trackPlaylistRepository;
|
private final TrackMetadataService trackMetadataService;
|
||||||
private final SourceTypeRepository sourceTypeRepository;
|
private final TrackPlaylistService trackPlaylistService;
|
||||||
private final MetadataParseService metadataParseService;
|
private final MetadataParseService metadataParseService;
|
||||||
|
|
||||||
public TrackService(AudioBlobStorageService s3Service, TrackSourceRepository trackSourceRepository,
|
public TrackService(TrackRepository trackRepository,
|
||||||
TrackRepository trackRepository, TrackMetadataRepository trackMetadataRepository,
|
TrackSourceService trackSourceService,
|
||||||
TrackPlaylistRepository trackPlaylistRepository,
|
TrackMetadataService trackMetadataService,
|
||||||
SourceTypeRepository sourceTypeRepository, MetadataParseService metadataParseService) {
|
TrackPlaylistService trackPlaylistService,
|
||||||
this.s3Service = s3Service;
|
MetadataParseService metadataParseService) {
|
||||||
this.trackSourceRepository = trackSourceRepository;
|
|
||||||
this.trackRepository = trackRepository;
|
this.trackRepository = trackRepository;
|
||||||
this.trackMetadataRepository = trackMetadataRepository;
|
this.trackSourceService = trackSourceService;
|
||||||
this.trackPlaylistRepository = trackPlaylistRepository;
|
this.trackMetadataService = trackMetadataService;
|
||||||
this.sourceTypeRepository = sourceTypeRepository;
|
this.trackPlaylistService = trackPlaylistService;
|
||||||
this.metadataParseService = metadataParseService;
|
this.metadataParseService = metadataParseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackResponse addLocalTrack(User user, Long playlistId, AddLocalTrackRequest request) throws IOException {
|
public TrackResponse addLocalTrack(User user, Long playlistId, AddLocalTrackRequest request) throws IOException {
|
||||||
String audioPath = s3Service.store(request.source().getBytes());
|
|
||||||
|
|
||||||
Optional<Metadata> metadata = metadataParseService.extractMetadata(request.source().getInputStream());
|
Optional<Metadata> metadata = metadataParseService.extractMetadata(request.source().getInputStream());
|
||||||
|
String ffprobeJson = metadata.map(Metadata::rawJson).orElse("{}");
|
||||||
|
|
||||||
Optional<SourceType> possibleSourceType = sourceTypeRepository.findByName(SourceTypes.FILE);
|
TrackSource trackSource = trackSourceService.createTrackSource(
|
||||||
if (possibleSourceType.isEmpty()) {
|
request.source().getBytes(), ffprobeJson, SourceTypes.FILE);
|
||||||
throw new IllegalStateException("cannot find source type " + SourceTypes.FILE);
|
|
||||||
}
|
|
||||||
SourceType sourceType = possibleSourceType.get();
|
|
||||||
TrackSource trackSource = new TrackSource(audioPath, sourceType, LocalDateTime.now());
|
|
||||||
trackSource = trackSourceRepository.save(trackSource);
|
|
||||||
|
|
||||||
Track track = new Track(trackSource);
|
Track track = trackRepository.save(new Track(trackSource));
|
||||||
track = trackRepository.save(track);
|
|
||||||
|
|
||||||
String fileName = request.source().getOriginalFilename();
|
String fileName = fileNameWithoutExtension(request.source().getOriginalFilename());
|
||||||
String title = metadata.map(m -> m.title()).orElse(fileName);
|
String title = metadata.map(Metadata::title).orElse(fileName);
|
||||||
String artist = metadata.map(m -> m.artist()).orElse(null);
|
String artist = metadata.map(Metadata::artist).orElse(null);
|
||||||
String thumbnailPath = null; // TODO:?
|
int durationSeconds = metadata.map(Metadata::durationSeconds).map(Float::intValue).orElse(0);
|
||||||
int durationSeconds = metadata.map(m -> m.durationSeconds()).map(Float::intValue).orElse(0);
|
trackMetadataService.createTrackMetadata(
|
||||||
|
track, title, fileName, trackSource.getSourceUrl(), artist, null, durationSeconds);
|
||||||
TrackMetadata trackMetadata = new TrackMetadata(track, title, fileName, audioPath,
|
|
||||||
artist, thumbnailPath,
|
|
||||||
durationSeconds);
|
|
||||||
trackMetadata = trackMetadataRepository.save(trackMetadata);
|
|
||||||
|
|
||||||
// TODO: use linked list instead of int order?
|
|
||||||
TrackPlaylist playlistTrack = new TrackPlaylist(playlistId, track.getId(), /* order */ 0L);
|
|
||||||
playlistTrack = trackPlaylistRepository.save(playlistTrack);
|
|
||||||
|
|
||||||
|
trackPlaylistService.insertTrackAtEnd(playlistId, track.getId());
|
||||||
return new TrackResponse(
|
return new TrackResponse(
|
||||||
track.getId(),
|
track.getId(),
|
||||||
trackMetadata.getTitle(),
|
title,
|
||||||
trackMetadata.getArtist(),
|
artist,
|
||||||
trackMetadata.getAudioPath(),
|
trackSource.getSourceUrl(),
|
||||||
trackMetadata.getDurationSeconds(),
|
durationSeconds,
|
||||||
trackMetadata.getFileName());
|
fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PlaylistTrackResponse> getPlaylistTracks(User user, Long playlistId) {
|
||||||
|
return trackPlaylistService.getPlaylistTracks(playlistId).stream()
|
||||||
|
.map(pt -> {
|
||||||
|
Track track = trackRepository.findById(pt.getTrackId())
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Track not found"));
|
||||||
|
TrackMetadata metadata = trackMetadataService.getTrackMetadata(track.getId());
|
||||||
|
return new PlaylistTrackResponse(
|
||||||
|
track.getId(),
|
||||||
|
metadata.getTitle(),
|
||||||
|
metadata.getArtist(),
|
||||||
|
metadata.getAudioPath(),
|
||||||
|
metadata.getDurationSeconds(),
|
||||||
|
metadata.getFileName());
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fileNameWithoutExtension(String fileName) {
|
||||||
|
return fileName.replaceFirst("[.][^.]+$", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.bivashy.backend.composer.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.bivashy.backend.composer.model.SourceType;
|
||||||
|
import com.bivashy.backend.composer.model.TrackSource;
|
||||||
|
import com.bivashy.backend.composer.repository.SourceTypeRepository;
|
||||||
|
import com.bivashy.backend.composer.repository.TrackSourceRepository;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TrackSourceService {
|
||||||
|
private final TrackSourceRepository trackSourceRepository;
|
||||||
|
private final SourceTypeRepository sourceTypeRepository;
|
||||||
|
private final AudioBlobStorageService s3Service;
|
||||||
|
|
||||||
|
public TrackSourceService(TrackSourceRepository trackSourceRepository,
|
||||||
|
SourceTypeRepository sourceTypeRepository,
|
||||||
|
AudioBlobStorageService s3Service) {
|
||||||
|
this.trackSourceRepository = trackSourceRepository;
|
||||||
|
this.sourceTypeRepository = sourceTypeRepository;
|
||||||
|
this.s3Service = s3Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackSource createTrackSource(byte[] audioBytes, String ffprobeJson, String sourceType) {
|
||||||
|
String audioPath = s3Service.store(audioBytes, Map.of("ffprobe", ffprobeJson));
|
||||||
|
SourceType type = sourceTypeRepository.findByName(sourceType)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Source type not found: " + sourceType));
|
||||||
|
return trackSourceRepository.save(new TrackSource(audioPath, type, LocalDateTime.now()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,10 @@ spring:
|
|||||||
access-key: ${S3_ACCESS_KEY}
|
access-key: ${S3_ACCESS_KEY}
|
||||||
secret-key: ${S3_SECRET_KEY}
|
secret-key: ${S3_SECRET_KEY}
|
||||||
bucket: ${S3_BUCKET}
|
bucket: ${S3_BUCKET}
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 8096MB
|
||||||
|
max-request-size: 8096MB
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
@ -64,11 +64,11 @@ CREATE TABLE IF NOT EXISTS "playlist" (
|
|||||||
CREATE TABLE IF NOT EXISTS "playlist_track" (
|
CREATE TABLE IF NOT EXISTS "playlist_track" (
|
||||||
"playlist_id" bigint NOT NULL,
|
"playlist_id" bigint NOT NULL,
|
||||||
"track_id" bigint NOT NULL,
|
"track_id" bigint NOT NULL,
|
||||||
"order_index" bigint NOT NULL,
|
"order_index" numeric NOT NULL,
|
||||||
CONSTRAINT "pk_playlist_track" PRIMARY KEY ("playlist_id", "track_id"),
|
CONSTRAINT "pk_playlist_track_new" PRIMARY KEY ("playlist_id", "track_id"),
|
||||||
CONSTRAINT "fk_playlist_track_playlist_id"
|
CONSTRAINT "fk_playlist_track_playlist_id_new"
|
||||||
FOREIGN KEY ("playlist_id") REFERENCES "playlist" ("id"),
|
FOREIGN KEY ("playlist_id") REFERENCES "playlist" ("id"),
|
||||||
CONSTRAINT "fk_playlist_track_track_id"
|
CONSTRAINT "fk_playlist_track_track_id_new"
|
||||||
FOREIGN KEY ("track_id") REFERENCES "track" ("id")
|
FOREIGN KEY ("track_id") REFERENCES "track" ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user