Implement proper ordering

This commit is contained in:
2025-11-16 14:28:33 +05:00
parent 3021adfa7d
commit 902c223e69
14 changed files with 302 additions and 73 deletions

View File

@ -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);
}
} }

View File

@ -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) {
}

View File

@ -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;
} }

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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) {
} }
} }

View File

@ -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) {

View File

@ -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) {
} }
} }

View File

@ -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"));
}
}

View File

@ -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);
}
}

View File

@ -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("[.][^.]+$", "");
}
} }

View File

@ -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()));
}
}

View File

@ -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:

View File

@ -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")
); );