Compare commits
2 Commits
main
...
95ca3d1a65
| Author | SHA1 | Date | |
|---|---|---|---|
|
95ca3d1a65
|
|||
|
973e588947
|
@ -5,10 +5,23 @@ RUN apt-get update && \
|
|||||||
curl \
|
curl \
|
||||||
vim \
|
vim \
|
||||||
git \
|
git \
|
||||||
|
unzip \
|
||||||
|
python3.11 \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
ffmpeg && \
|
ffmpeg && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN ln -sf /usr/bin/python3.11 /usr/bin/python3
|
||||||
|
|
||||||
|
RUN curl -fsSL https://bun.sh/install > install.sh && \
|
||||||
|
chmod +x install.sh && \
|
||||||
|
./install.sh && \
|
||||||
|
rm install.sh
|
||||||
|
|
||||||
|
RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp && \
|
||||||
|
chmod a+rx /usr/local/bin/yt-dlp
|
||||||
|
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN groupadd --gid 1000 spring-app && \
|
RUN groupadd --gid 1000 spring-app && \
|
||||||
useradd --uid 1000 --gid spring-app --shell /bin/bash --create-home spring-app
|
useradd --uid 1000 --gid spring-app --shell /bin/bash --create-home spring-app
|
||||||
|
|||||||
26
pom.xml
26
pom.xml
@ -35,6 +35,8 @@
|
|||||||
<apache-tika.version>3.2.3</apache-tika.version>
|
<apache-tika.version>3.2.3</apache-tika.version>
|
||||||
<springdoc-openapi.version>2.8.5</springdoc-openapi.version>
|
<springdoc-openapi.version>2.8.5</springdoc-openapi.version>
|
||||||
<jaffree.version>2024.08.29</jaffree.version>
|
<jaffree.version>2024.08.29</jaffree.version>
|
||||||
|
<yt-dlp-java.version>2.0.6</yt-dlp-java.version>
|
||||||
|
<record-builder.version>51</record-builder.version>
|
||||||
</properties>
|
</properties>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
@ -124,6 +126,17 @@
|
|||||||
<artifactId>jaffree</artifactId>
|
<artifactId>jaffree</artifactId>
|
||||||
<version>${jaffree.version}</version>
|
<version>${jaffree.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.bivashy</groupId>
|
||||||
|
<artifactId>yt-dlp-java</artifactId>
|
||||||
|
<version>${yt-dlp-java.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.soabase.record-builder</groupId>
|
||||||
|
<artifactId>record-builder-core</artifactId>
|
||||||
|
<version>${record-builder.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
@ -160,6 +173,19 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>io.soabase.record-builder</groupId>
|
||||||
|
<artifactId>record-builder-processor</artifactId>
|
||||||
|
<version>${record-builder.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.bivashy.backend.composer.controller;
|
package com.bivashy.backend.composer.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -14,40 +13,42 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.auth.CustomUserDetails;
|
import com.bivashy.backend.composer.auth.CustomUserDetails;
|
||||||
import com.bivashy.backend.composer.dto.importing.TrackProgressDTO;
|
|
||||||
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.PlaylistTrackResponse;
|
||||||
import com.bivashy.backend.composer.dto.track.TrackBulkReorderRequest;
|
import com.bivashy.backend.composer.dto.track.TrackBulkReorderRequest;
|
||||||
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
||||||
import com.bivashy.backend.composer.model.SourceTypes;
|
import com.bivashy.backend.composer.dto.track.YoutubeTrackRequest;
|
||||||
|
import com.bivashy.backend.composer.dto.track.service.AddLocalTrackParamsBuilder;
|
||||||
|
import com.bivashy.backend.composer.exception.ImportTrackException;
|
||||||
import com.bivashy.backend.composer.service.TrackService;
|
import com.bivashy.backend.composer.service.TrackService;
|
||||||
import com.bivashy.backend.composer.service.importing.RedisProgressService;
|
import com.bivashy.backend.composer.util.SimpleBlob.MultipartBlob;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class TrackController {
|
public class TrackController {
|
||||||
private final TrackService trackService;
|
private final TrackService trackService;
|
||||||
private final RedisProgressService redisProgressService;
|
|
||||||
|
|
||||||
public TrackController(TrackService trackService, RedisProgressService redisProgressService) {
|
public TrackController(TrackService trackService) {
|
||||||
this.trackService = trackService;
|
this.trackService = trackService;
|
||||||
this.redisProgressService = redisProgressService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(path = "/playlist/{playlistId}/track/local", 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 CustomUserDetails user,
|
@AuthenticationPrincipal CustomUserDetails user,
|
||||||
@PathVariable Long playlistId,
|
@PathVariable long playlistId,
|
||||||
@ModelAttribute AddLocalTrackRequest request) throws IOException {
|
@ModelAttribute AddLocalTrackRequest request) throws ImportTrackException {
|
||||||
TrackResponse response = trackService.addLocalTrack(user, playlistId, request);
|
var params = AddLocalTrackParamsBuilder.builder()
|
||||||
redisProgressService.saveProgress(new TrackProgressDTO(playlistId,
|
.blob(new MultipartBlob(request.source()))
|
||||||
response.trackId(),
|
.build();
|
||||||
response.title(),
|
TrackResponse response = trackService.addLocalTrack(user, playlistId, params);
|
||||||
response.fileFormat(),
|
return ResponseEntity.ok(response);
|
||||||
SourceTypes.FILE,
|
}
|
||||||
100,
|
|
||||||
null,
|
@PostMapping(path = "/playlist/{playlistId}/track/youtube")
|
||||||
System.currentTimeMillis(),
|
public ResponseEntity<List<TrackResponse>> addYoutubeTrack(
|
||||||
user.getId()));
|
@AuthenticationPrincipal CustomUserDetails user,
|
||||||
|
@PathVariable long playlistId,
|
||||||
|
@RequestBody YoutubeTrackRequest request) throws ImportTrackException {
|
||||||
|
List<TrackResponse> response = trackService.addYoutubeTrack(user, playlistId, request);
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
package com.bivashy.backend.composer.controller.importing;
|
package com.bivashy.backend.composer.controller.importing;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.ServerSentEvent;
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.auth.CustomUserDetails;
|
import com.bivashy.backend.composer.auth.CustomUserDetails;
|
||||||
|
import com.bivashy.backend.composer.dto.importing.BaseTrackProgress;
|
||||||
import com.bivashy.backend.composer.dto.importing.ImportTrackKey;
|
import com.bivashy.backend.composer.dto.importing.ImportTrackKey;
|
||||||
import com.bivashy.backend.composer.dto.importing.TrackProgressDTO;
|
|
||||||
import com.bivashy.backend.composer.service.importing.RedisMessageSubscriber;
|
import com.bivashy.backend.composer.service.importing.RedisMessageSubscriber;
|
||||||
import com.bivashy.backend.composer.service.importing.RedisProgressService;
|
import com.bivashy.backend.composer.service.importing.RedisProgressService;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@ -15,10 +21,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Sinks;
|
import reactor.core.publisher.Sinks;
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class ProgressSSEController {
|
public class ProgressSSEController {
|
||||||
@ -65,11 +67,12 @@ public class ProgressSSEController {
|
|||||||
.build())
|
.build())
|
||||||
.doFirst(() -> {
|
.doFirst(() -> {
|
||||||
try {
|
try {
|
||||||
List<TrackProgressDTO> existingProgresses = redisProgressService.getPlaylistProgress(playlistId,
|
List<BaseTrackProgress> existingProgresses = redisProgressService.getPlaylistProgress(
|
||||||
|
playlistId,
|
||||||
userId);
|
userId);
|
||||||
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
for (TrackProgressDTO progress : existingProgresses) {
|
for (BaseTrackProgress progress : existingProgresses) {
|
||||||
sink.tryEmitNext(mapper.writeValueAsString(progress));
|
sink.tryEmitNext(mapper.writeValueAsString(progress));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.importing;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||||
|
@JsonSubTypes({
|
||||||
|
@JsonSubTypes.Type(value = PlaylistProgress.class, name = "PLAYLIST"),
|
||||||
|
@JsonSubTypes.Type(value = SingleTrackProgress.class, name = "TRACK"),
|
||||||
|
})
|
||||||
|
public abstract class BaseTrackProgress {
|
||||||
|
protected long playlistId;
|
||||||
|
protected long trackId;
|
||||||
|
protected long userId;
|
||||||
|
|
||||||
|
protected long timestamp;
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
public BaseTrackProgress(long playlistId, long trackId, long userId) {
|
||||||
|
this.playlistId = playlistId;
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.userId = userId;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getPlaylistId() {
|
||||||
|
return playlistId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTrackId() {
|
||||||
|
return trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setType(ProgressEntryType type) {
|
||||||
|
this.type = type.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.importing;
|
||||||
|
|
||||||
|
public class PlaylistProgress extends BaseTrackProgress {
|
||||||
|
private String ytdlnStdout;
|
||||||
|
private int overallProgress;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public PlaylistProgress(long playlistId, long trackId, long userId) {
|
||||||
|
super(playlistId, trackId, userId);
|
||||||
|
this.setType(ProgressEntryType.PLAYLIST);
|
||||||
|
this.status = "LOADING";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getYtdlnStdout() {
|
||||||
|
return ytdlnStdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setYtdlnStdout(String ytdlnStdout) {
|
||||||
|
this.ytdlnStdout = ytdlnStdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOverallProgress() {
|
||||||
|
return overallProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOverallProgress(int overallProgress) {
|
||||||
|
this.overallProgress = overallProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.importing;
|
||||||
|
|
||||||
|
public enum ProgressEntryType {
|
||||||
|
PLAYLIST,
|
||||||
|
TRACK,
|
||||||
|
EXTERNAL_TRACK
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.importing;
|
||||||
|
|
||||||
|
public class SingleTrackProgress extends BaseTrackProgress {
|
||||||
|
private String title;
|
||||||
|
private String format;
|
||||||
|
|
||||||
|
public SingleTrackProgress(long playlistId, long trackId, long userId, String title, String format) {
|
||||||
|
super(playlistId, trackId, userId);
|
||||||
|
this.setType(ProgressEntryType.TRACK);
|
||||||
|
this.title = title;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormat() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,116 +0,0 @@
|
|||||||
package com.bivashy.backend.composer.dto.importing;
|
|
||||||
|
|
||||||
public class TrackProgressDTO {
|
|
||||||
private long playlistId;
|
|
||||||
private long trackId;
|
|
||||||
private String trackTitle;
|
|
||||||
private String format;
|
|
||||||
private String sourceType;
|
|
||||||
private int progress;
|
|
||||||
private String metadata;
|
|
||||||
private Long timestamp;
|
|
||||||
private long userId;
|
|
||||||
|
|
||||||
public TrackProgressDTO() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackProgressDTO(long playlistId, long trackId, long userId) {
|
|
||||||
this.playlistId = playlistId;
|
|
||||||
this.trackId = trackId;
|
|
||||||
this.userId = userId;
|
|
||||||
this.timestamp = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackProgressDTO(long playlistId,
|
|
||||||
long trackId,
|
|
||||||
String trackTitle,
|
|
||||||
String format,
|
|
||||||
String sourceType,
|
|
||||||
int progress,
|
|
||||||
String metadata,
|
|
||||||
Long timestamp,
|
|
||||||
long userId) {
|
|
||||||
this.playlistId = playlistId;
|
|
||||||
this.trackId = trackId;
|
|
||||||
this.trackTitle = trackTitle;
|
|
||||||
this.format = format;
|
|
||||||
this.sourceType = sourceType;
|
|
||||||
this.progress = progress;
|
|
||||||
this.metadata = metadata;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getPlaylistId() {
|
|
||||||
return playlistId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPlaylistId(long playlistId) {
|
|
||||||
this.playlistId = playlistId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTrackId() {
|
|
||||||
return trackId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTrackId(long trackId) {
|
|
||||||
this.trackId = trackId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTrackTitle() {
|
|
||||||
return trackTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTrackTitle(String trackTitle) {
|
|
||||||
this.trackTitle = trackTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFormat() {
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFormat(String format) {
|
|
||||||
this.format = format;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSourceType() {
|
|
||||||
return sourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSourceType(String sourceType) {
|
|
||||||
this.sourceType = sourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getProgress() {
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgress(int progress) {
|
|
||||||
this.progress = progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMetadata() {
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMetadata(String metadata) {
|
|
||||||
this.metadata = metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(Long timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUserId(long userId) {
|
|
||||||
this.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.track;
|
||||||
|
|
||||||
|
public record YoutubeTrackRequest(String youtubeUrl) {
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.bivashy.backend.composer.dto.track.service;
|
||||||
|
|
||||||
|
import com.bivashy.backend.composer.util.SimpleBlob;
|
||||||
|
|
||||||
|
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||||
|
|
||||||
|
@RecordBuilder
|
||||||
|
public record AddLocalTrackParams(SimpleBlob blob, String ytdlpMetadata, boolean includeProgressHistory) {
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.bivashy.backend.composer.exception;
|
||||||
|
|
||||||
|
public class ImportTrackException extends Exception {
|
||||||
|
public ImportTrackException(String message, Exception cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportTrackException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,8 @@ import java.util.Map;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
public interface AudioBlobStorageService {
|
public interface AudioBlobStorageService {
|
||||||
|
String storeFolder();
|
||||||
|
|
||||||
String store(InputStream inputStream);
|
String store(InputStream inputStream);
|
||||||
|
|
||||||
String store(byte[] data);
|
String store(byte[] data);
|
||||||
@ -15,6 +17,8 @@ public interface AudioBlobStorageService {
|
|||||||
|
|
||||||
String store(byte[] data, Map<String, String> metadata);
|
String store(byte[] data, Map<String, String> metadata);
|
||||||
|
|
||||||
|
String store(String key, 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);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import software.amazon.awssdk.core.sync.RequestBody;
|
|||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||||
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
||||||
|
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
|
||||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -36,6 +37,18 @@ public class AudioS3StorageService implements AudioBlobStorageService {
|
|||||||
this.tika = tika;
|
this.tika = tika;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String storeFolder() {
|
||||||
|
String objectKey = newObjectName();
|
||||||
|
if (!objectKey.endsWith("/"))
|
||||||
|
objectKey = objectKey + "/";
|
||||||
|
s3Client.putObject(PutObjectRequest.builder()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(objectKey)
|
||||||
|
.build(), RequestBody.empty());
|
||||||
|
return objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String store(InputStream inputStream) {
|
public String store(InputStream inputStream) {
|
||||||
return store(inputStream, Map.of());
|
return store(inputStream, Map.of());
|
||||||
@ -61,7 +74,34 @@ public class AudioS3StorageService implements AudioBlobStorageService {
|
|||||||
.contentType(contentType)
|
.contentType(contentType)
|
||||||
.metadata(metadata)
|
.metadata(metadata)
|
||||||
.build(), RequestBody.fromBytes(data));
|
.build(), RequestBody.fromBytes(data));
|
||||||
return String.join("/", bucket, objectKey);
|
return objectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String store(String key, byte[] data, Map<String, String> metadata) {
|
||||||
|
try {
|
||||||
|
ResponseInputStream<GetObjectResponse> response = s3Client.getObject(GetObjectRequest.builder()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
s3Client.putObject(PutObjectRequest.builder()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.contentType(response.response().contentType())
|
||||||
|
.metadata(metadata)
|
||||||
|
.build(), RequestBody.fromBytes(data));
|
||||||
|
|
||||||
|
return key;
|
||||||
|
} catch (NoSuchKeyException e) {
|
||||||
|
System.out.println("no existing found");
|
||||||
|
s3Client.putObject(PutObjectRequest.builder()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.metadata(metadata)
|
||||||
|
.build(), RequestBody.fromBytes(data));
|
||||||
|
return key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -71,9 +111,6 @@ public class AudioS3StorageService implements AudioBlobStorageService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Blob read(String path) {
|
public Blob read(String path) {
|
||||||
if (path.startsWith(bucket + "/")) {
|
|
||||||
path = path.substring(bucket.length());
|
|
||||||
}
|
|
||||||
ResponseInputStream<GetObjectResponse> response = s3Client.getObject(GetObjectRequest.builder()
|
ResponseInputStream<GetObjectResponse> response = s3Client.getObject(GetObjectRequest.builder()
|
||||||
.bucket(bucket)
|
.bucket(bucket)
|
||||||
.key(path)
|
.key(path)
|
||||||
|
|||||||
@ -1,56 +1,100 @@
|
|||||||
package com.bivashy.backend.composer.service;
|
package com.bivashy.backend.composer.service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import com.bivashy.backend.composer.auth.CustomUserDetails;
|
import com.bivashy.backend.composer.auth.CustomUserDetails;
|
||||||
import com.bivashy.backend.composer.dto.track.AddLocalTrackRequest;
|
import com.bivashy.backend.composer.dto.importing.SingleTrackProgress;
|
||||||
import com.bivashy.backend.composer.dto.track.PlaylistTrackResponse;
|
import com.bivashy.backend.composer.dto.track.PlaylistTrackResponse;
|
||||||
import com.bivashy.backend.composer.dto.track.TrackBulkReorderRequest;
|
import com.bivashy.backend.composer.dto.track.TrackBulkReorderRequest;
|
||||||
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
import com.bivashy.backend.composer.dto.track.TrackResponse;
|
||||||
|
import com.bivashy.backend.composer.dto.track.YoutubeTrackRequest;
|
||||||
|
import com.bivashy.backend.composer.dto.track.service.AddLocalTrackParams;
|
||||||
|
import com.bivashy.backend.composer.dto.track.service.AddLocalTrackParamsBuilder;
|
||||||
|
import com.bivashy.backend.composer.exception.ImportTrackException;
|
||||||
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.TrackSource;
|
import com.bivashy.backend.composer.model.TrackSource;
|
||||||
import com.bivashy.backend.composer.repository.TrackRepository;
|
import com.bivashy.backend.composer.repository.TrackRepository;
|
||||||
import com.bivashy.backend.composer.service.MetadataParseService.Metadata;
|
import com.bivashy.backend.composer.service.MetadataParseService.Metadata;
|
||||||
|
import com.bivashy.backend.composer.service.importing.RedisProgressService;
|
||||||
|
import com.bivashy.backend.composer.util.SimpleBlob.PathBlob;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.jfposton.ytdlp.YtDlp;
|
||||||
|
import com.jfposton.ytdlp.YtDlpException;
|
||||||
|
import com.jfposton.ytdlp.YtDlpRequest;
|
||||||
|
import com.jfposton.ytdlp.mapper.VideoInfo;
|
||||||
|
|
||||||
|
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TrackService {
|
public class TrackService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TrackService.class);
|
||||||
|
public static final String DOWNLOADED_METADATA_FILE = "downloaded";
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private final TrackRepository trackRepository;
|
private final TrackRepository trackRepository;
|
||||||
private final TrackSourceService trackSourceService;
|
private final TrackSourceService trackSourceService;
|
||||||
private final TrackMetadataService trackMetadataService;
|
private final TrackMetadataService trackMetadataService;
|
||||||
private final TrackPlaylistService trackPlaylistService;
|
private final TrackPlaylistService trackPlaylistService;
|
||||||
private final MetadataParseService metadataParseService;
|
private final MetadataParseService metadataParseService;
|
||||||
|
private final RedisProgressService redisProgressService;
|
||||||
|
private final AudioS3StorageService s3StorageService;
|
||||||
|
|
||||||
public TrackService(TrackRepository trackRepository,
|
public TrackService(TrackRepository trackRepository, TrackSourceService trackSourceService,
|
||||||
TrackSourceService trackSourceService,
|
TrackMetadataService trackMetadataService, TrackPlaylistService trackPlaylistService,
|
||||||
TrackMetadataService trackMetadataService,
|
MetadataParseService metadataParseService, RedisProgressService redisProgressService,
|
||||||
TrackPlaylistService trackPlaylistService,
|
AudioS3StorageService s3StorageService) {
|
||||||
MetadataParseService metadataParseService) {
|
|
||||||
this.trackRepository = trackRepository;
|
this.trackRepository = trackRepository;
|
||||||
this.trackSourceService = trackSourceService;
|
this.trackSourceService = trackSourceService;
|
||||||
this.trackMetadataService = trackMetadataService;
|
this.trackMetadataService = trackMetadataService;
|
||||||
this.trackPlaylistService = trackPlaylistService;
|
this.trackPlaylistService = trackPlaylistService;
|
||||||
this.metadataParseService = metadataParseService;
|
this.metadataParseService = metadataParseService;
|
||||||
|
this.redisProgressService = redisProgressService;
|
||||||
|
this.s3StorageService = s3StorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackResponse addLocalTrack(CustomUserDetails user, Long playlistId, AddLocalTrackRequest request)
|
public TrackResponse addLocalTrack(CustomUserDetails user,
|
||||||
throws IOException {
|
long playlistId,
|
||||||
Optional<Metadata> metadata = metadataParseService.extractMetadata(request.source().getInputStream());
|
AddLocalTrackParams params)
|
||||||
|
throws ImportTrackException {
|
||||||
|
var request = params.blob();
|
||||||
|
Optional<Metadata> metadata = Optional.empty();
|
||||||
|
try (var inputStream = request.inputStream()) {
|
||||||
|
metadata = metadataParseService.extractMetadata(inputStream);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportTrackException("cannot extract metadata from " + request.fileName());
|
||||||
|
}
|
||||||
String ffprobeJson = metadata.map(Metadata::rawJson).orElse("{}");
|
String ffprobeJson = metadata.map(Metadata::rawJson).orElse("{}");
|
||||||
|
|
||||||
TrackSource trackSource = trackSourceService.createTrackSource(
|
TrackSource trackSource;
|
||||||
request.source().getBytes(), ffprobeJson, SourceTypes.FILE);
|
try {
|
||||||
|
trackSource = trackSourceService.createLocalTrackSource(
|
||||||
|
request.body(), ffprobeJson, params.ytdlpMetadata(), SourceTypes.FILE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportTrackException("cannot read blob body", e);
|
||||||
|
}
|
||||||
|
|
||||||
Track track = trackRepository.save(new Track(trackSource));
|
Track track = trackRepository.save(new Track(trackSource));
|
||||||
|
|
||||||
String fileName = fileNameWithoutExtension(request.source().getOriginalFilename());
|
String fileName = fileNameWithoutExtension(request.fileName());
|
||||||
String title = metadata.map(Metadata::title).orElse(fileName);
|
String title = metadata.map(Metadata::title).orElse(fileName);
|
||||||
String artist = metadata.map(Metadata::artist).orElse(null);
|
String artist = metadata.map(Metadata::artist).orElse(null);
|
||||||
int durationSeconds = metadata.map(Metadata::durationSeconds).map(Float::intValue).orElse(0);
|
int durationSeconds = metadata.map(Metadata::durationSeconds).map(Float::intValue).orElse(0);
|
||||||
@ -66,6 +110,12 @@ public class TrackService {
|
|||||||
if (metadata.isPresent()) {
|
if (metadata.isPresent()) {
|
||||||
fileFormat = metadata.map(m -> m.formatName()).get();
|
fileFormat = metadata.map(m -> m.formatName()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.includeProgressHistory()) {
|
||||||
|
redisProgressService
|
||||||
|
.saveProgress(new SingleTrackProgress(playlistId, track.getId(), user.getId(), title, fileFormat));
|
||||||
|
}
|
||||||
|
|
||||||
return new TrackResponse(
|
return new TrackResponse(
|
||||||
track.getId(),
|
track.getId(),
|
||||||
title,
|
title,
|
||||||
@ -76,6 +126,173 @@ public class TrackService {
|
|||||||
fileName);
|
fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<TrackResponse> addYoutubeTrack(CustomUserDetails user, long playlistId,
|
||||||
|
YoutubeTrackRequest request) throws ImportTrackException {
|
||||||
|
List<VideoInfo> videoInfos = Collections.emptyList();
|
||||||
|
try {
|
||||||
|
videoInfos = YtDlp.getVideoInfo(request.youtubeUrl());
|
||||||
|
} catch (YtDlpException e) {
|
||||||
|
throw new ImportTrackException("cannot `yt-dlp --dump-json` from " + request.youtubeUrl(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("videoinfos count {}", videoInfos.size());
|
||||||
|
|
||||||
|
if (videoInfos.size() == 0) {
|
||||||
|
throw new ImportTrackException("cannot find videoInfos");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoInfos.size() == 1) {
|
||||||
|
try {
|
||||||
|
VideoInfo videoInfo = videoInfos.get(0);
|
||||||
|
|
||||||
|
Path temporaryFolder = Files.createTempDirectory("yt-dlp-tmp");
|
||||||
|
var ytDlpRequest = new YtDlpRequest(request.youtubeUrl(), temporaryFolder.toAbsolutePath().toString());
|
||||||
|
ytDlpRequest.setOption("output", "%(id)s");
|
||||||
|
var response = YtDlp.execute(ytDlpRequest);
|
||||||
|
// TODO: write to RedisProgressService
|
||||||
|
|
||||||
|
TrackResponse result = null;
|
||||||
|
try (Stream<Path> pathStream = Files.walk(temporaryFolder)) {
|
||||||
|
List<Path> downloadedFiles = Files.walk(temporaryFolder).toList();
|
||||||
|
|
||||||
|
if (downloadedFiles.isEmpty())
|
||||||
|
throw new ImportTrackException("yt-dlp didn't downloaded anything for " + request.youtubeUrl());
|
||||||
|
|
||||||
|
for (Path downloadedFile : downloadedFiles) {
|
||||||
|
var params = AddLocalTrackParamsBuilder.builder()
|
||||||
|
.blob(new PathBlob(downloadedFile))
|
||||||
|
.ytdlpMetadata(OBJECT_MAPPER.writeValueAsString(videoInfo))
|
||||||
|
.includeProgressHistory(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
result = addLocalTrack(user,
|
||||||
|
playlistId,
|
||||||
|
params);
|
||||||
|
Files.delete(downloadedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Files.delete(temporaryFolder);
|
||||||
|
return List.of(result);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportTrackException("i/o during single youtube video downloading", e);
|
||||||
|
} catch (YtDlpException e) {
|
||||||
|
throw new ImportTrackException("cannot download youtube video " + request.youtubeUrl(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackSource trackSource = trackSourceService.createYoutubeTrackSource(SourceTypes.PLAYLIST);
|
||||||
|
return refreshYoutubePlaylist(playlistId, trackSource, videoInfos, request.youtubeUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TrackResponse> refreshYoutubePlaylist(long playlistId, TrackSource trackSource,
|
||||||
|
List<VideoInfo> videoInfos,
|
||||||
|
String youtubeUrl) throws ImportTrackException {
|
||||||
|
List<TrackResponse> result = new ArrayList<>();
|
||||||
|
logger.info(trackSource.getSourceUrl());
|
||||||
|
try {
|
||||||
|
Path temporaryFolder = Files.createTempDirectory("yt-dlp-tmp");
|
||||||
|
logger.info("temporaryFolder created {}", temporaryFolder.toString());
|
||||||
|
|
||||||
|
String downloadedMetadataKey = trackSource.getSourceUrl() + DOWNLOADED_METADATA_FILE;
|
||||||
|
try {
|
||||||
|
var rawBody = s3StorageService
|
||||||
|
.readRaw(downloadedMetadataKey);
|
||||||
|
Files.write(temporaryFolder.resolve(DOWNLOADED_METADATA_FILE), rawBody);
|
||||||
|
} catch (NoSuchKeyException e) {
|
||||||
|
logger.warn(".downloaded metadata file was not found, ignoring");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ytDlpRequest = new YtDlpRequest(youtubeUrl, temporaryFolder.toAbsolutePath().toString());
|
||||||
|
ytDlpRequest.setOption("output", "%(id)s");
|
||||||
|
ytDlpRequest.setOption("download-archive", DOWNLOADED_METADATA_FILE);
|
||||||
|
ytDlpRequest.setOption("extract-audio");
|
||||||
|
ytDlpRequest.setOption("audio-quality", 0);
|
||||||
|
ytDlpRequest.setOption("audio-format", "best");
|
||||||
|
ytDlpRequest.setOption("no-overwrites");
|
||||||
|
var response = YtDlp.execute(ytDlpRequest);
|
||||||
|
logger.info("yt dlp response {}", response);
|
||||||
|
|
||||||
|
// TODO: write to RedisProgressService
|
||||||
|
|
||||||
|
try (Stream<Path> pathStream = Files.walk(temporaryFolder)) {
|
||||||
|
List<Path> downloadedFiles = Files.walk(temporaryFolder).toList();
|
||||||
|
logger.info("downloaded file count {}", downloadedFiles.size());
|
||||||
|
|
||||||
|
for (Path path : downloadedFiles) {
|
||||||
|
if (Files.isDirectory(path))
|
||||||
|
continue;
|
||||||
|
boolean isMetadataFile = path.getFileName().toString().equals(DOWNLOADED_METADATA_FILE);
|
||||||
|
var body = Files.readAllBytes(path);
|
||||||
|
|
||||||
|
if (isMetadataFile) {
|
||||||
|
s3StorageService.store(downloadedMetadataKey, body, Map.of());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String fileName = fileNameWithoutExtension(path.getFileName().toString());
|
||||||
|
VideoInfo videoInfo = videoInfos.stream()
|
||||||
|
.filter(v -> v.getId().equals(fileName))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
String audioKey = trackSource.getSourceUrl() + UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
logger.info("downloaded file {} and info {}, key {}", fileName, videoInfo.getTitle(), audioKey);
|
||||||
|
|
||||||
|
Optional<Metadata> metadata = Optional.empty();
|
||||||
|
|
||||||
|
try (var inputStream = Files.newInputStream(path)) {
|
||||||
|
metadata = metadataParseService.extractMetadata(inputStream);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportTrackException("cannot extract metadata from " + path.toString());
|
||||||
|
}
|
||||||
|
String ffprobeJson = metadata.map(Metadata::rawJson).orElse("{}");
|
||||||
|
|
||||||
|
TrackSource playlistEntrySource;
|
||||||
|
try {
|
||||||
|
playlistEntrySource = trackSourceService.createTrackSourceWithKey(audioKey, body, ffprobeJson,
|
||||||
|
OBJECT_MAPPER.writeValueAsString(videoInfo), SourceTypes.PLAYLIST);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportTrackException("cannot read blob body", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Track track = trackRepository.save(new Track(playlistEntrySource));
|
||||||
|
|
||||||
|
String title = videoInfo.getTitle();
|
||||||
|
String artist = metadata.map(Metadata::artist).orElse(null);
|
||||||
|
int durationSeconds = metadata.map(Metadata::durationSeconds).map(Float::intValue).orElse(0);
|
||||||
|
// TODO: thumbnail
|
||||||
|
// TODO: Recognize music if the duration is less than five minutes
|
||||||
|
// (configurable), and if not, it is a playlist and should be marked as is
|
||||||
|
trackMetadataService.createTrackMetadata(
|
||||||
|
track, title, fileName, audioKey, artist, null, durationSeconds);
|
||||||
|
|
||||||
|
trackPlaylistService.insertTrackAtEnd(playlistId, track.getId());
|
||||||
|
|
||||||
|
String fileFormat = "unknown";
|
||||||
|
if (metadata.isPresent()) {
|
||||||
|
fileFormat = metadata.map(m -> m.formatName()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackResponse = new TrackResponse(
|
||||||
|
track.getId(),
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
audioKey,
|
||||||
|
fileFormat,
|
||||||
|
durationSeconds,
|
||||||
|
fileName);
|
||||||
|
result.add(trackResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ImportTrackException("i/o during playlist youtube video downloading", e);
|
||||||
|
} catch (YtDlpException e) {
|
||||||
|
throw new ImportTrackException("cannot download youtube video " + youtubeUrl, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<PlaylistTrackResponse> getPlaylistTracks(CustomUserDetails user, Long playlistId) {
|
public List<PlaylistTrackResponse> getPlaylistTracks(CustomUserDetails user, Long playlistId) {
|
||||||
return trackPlaylistService.getPlaylistTracks(playlistId).stream()
|
return trackPlaylistService.getPlaylistTracks(playlistId).stream()
|
||||||
.map(pt -> {
|
.map(pt -> {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.bivashy.backend.composer.service;
|
package com.bivashy.backend.composer.service;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -12,6 +13,9 @@ import com.bivashy.backend.composer.repository.TrackSourceRepository;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TrackSourceService {
|
public class TrackSourceService {
|
||||||
|
public static final String FFPROBE_METADATA_KEY = "ffprobe";
|
||||||
|
public static final String YTDLP_METADATA_KEY = "ytdlp";
|
||||||
|
|
||||||
private final TrackSourceRepository trackSourceRepository;
|
private final TrackSourceRepository trackSourceRepository;
|
||||||
private final SourceTypeRepository sourceTypeRepository;
|
private final SourceTypeRepository sourceTypeRepository;
|
||||||
private final AudioBlobStorageService s3Service;
|
private final AudioBlobStorageService s3Service;
|
||||||
@ -24,10 +28,38 @@ public class TrackSourceService {
|
|||||||
this.s3Service = s3Service;
|
this.s3Service = s3Service;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackSource createTrackSource(byte[] audioBytes, String ffprobeJson, String sourceType) {
|
public TrackSource createLocalTrackSource(byte[] audioBytes,
|
||||||
String audioPath = s3Service.store(audioBytes, Map.of("ffprobe", ffprobeJson));
|
String ffprobeJson,
|
||||||
|
String ytdlpMetadata,
|
||||||
|
String sourceType) {
|
||||||
|
Map<String, String> metadata = new HashMap<>(Map.of(YTDLP_METADATA_KEY, ffprobeJson));
|
||||||
|
if (ytdlpMetadata != null) {
|
||||||
|
// TODO: Add tag or smth?
|
||||||
|
}
|
||||||
|
String audioPath = s3Service.store(audioBytes, metadata);
|
||||||
|
|
||||||
SourceType type = sourceTypeRepository.findByName(sourceType)
|
SourceType type = sourceTypeRepository.findByName(sourceType)
|
||||||
.orElseThrow(() -> new IllegalStateException("Source type not found: " + sourceType));
|
.orElseThrow(() -> new IllegalStateException("Source type not found: " + sourceType));
|
||||||
return trackSourceRepository.save(new TrackSource(audioPath, type, LocalDateTime.now()));
|
return trackSourceRepository.save(new TrackSource(audioPath, type, LocalDateTime.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TrackSource createTrackSourceWithKey(String key, byte[] audioBytes, String ffprobeJson,
|
||||||
|
String ytdlpMetadata, String sourceType) {
|
||||||
|
Map<String, String> metadata = new HashMap<>(Map.of(YTDLP_METADATA_KEY, ffprobeJson));
|
||||||
|
if (ytdlpMetadata != null) {
|
||||||
|
// TODO: Add tag or smth?
|
||||||
|
}
|
||||||
|
String audioPath = s3Service.store(key, audioBytes, metadata);
|
||||||
|
|
||||||
|
SourceType type = sourceTypeRepository.findByName(sourceType)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Source type not found: " + sourceType));
|
||||||
|
return trackSourceRepository.save(new TrackSource(audioPath, type, LocalDateTime.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackSource createYoutubeTrackSource(String sourceType) {
|
||||||
|
String folderPath = s3Service.storeFolder();
|
||||||
|
SourceType type = sourceTypeRepository.findByName(sourceType)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Source type not found: " + sourceType));
|
||||||
|
return trackSourceRepository.save(new TrackSource(folderPath, type, LocalDateTime.now()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.bivashy.backend.composer.service.importing;
|
package com.bivashy.backend.composer.service.importing;
|
||||||
|
|
||||||
|
import com.bivashy.backend.composer.dto.importing.BaseTrackProgress;
|
||||||
import com.bivashy.backend.composer.dto.importing.ImportTrackKey;
|
import com.bivashy.backend.composer.dto.importing.ImportTrackKey;
|
||||||
import com.bivashy.backend.composer.dto.importing.TrackProgressDTO;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
@ -22,7 +22,7 @@ public class RedisProgressService {
|
|||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveProgress(TrackProgressDTO progress) {
|
public void saveProgress(BaseTrackProgress progress) {
|
||||||
try {
|
try {
|
||||||
String key = ImportTrackKey.progressKey(progress.getPlaylistId(), progress.getUserId());
|
String key = ImportTrackKey.progressKey(progress.getPlaylistId(), progress.getUserId());
|
||||||
String trackKey = ImportTrackKey.trackKey(
|
String trackKey = ImportTrackKey.trackKey(
|
||||||
@ -44,16 +44,16 @@ public class RedisProgressService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TrackProgressDTO> getPlaylistProgress(long playlistId, long userId) {
|
public List<BaseTrackProgress> getPlaylistProgress(long playlistId, long userId) {
|
||||||
try {
|
try {
|
||||||
String key = ImportTrackKey.progressKey(playlistId, userId);
|
String key = ImportTrackKey.progressKey(playlistId, userId);
|
||||||
Map<Object, Object> progressMap = redisTemplate.opsForHash().entries(key);
|
Map<Object, Object> progressMap = redisTemplate.opsForHash().entries(key);
|
||||||
|
|
||||||
List<TrackProgressDTO> progressList = new ArrayList<>();
|
List<BaseTrackProgress> progressList = new ArrayList<>();
|
||||||
for (Object value : progressMap.values()) {
|
for (Object value : progressMap.values()) {
|
||||||
TrackProgressDTO progress = objectMapper.readValue(
|
BaseTrackProgress progress = objectMapper.readValue(
|
||||||
(String) value,
|
(String) value,
|
||||||
TrackProgressDTO.class);
|
BaseTrackProgress.class);
|
||||||
progressList.add(progress);
|
progressList.add(progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,13 +65,13 @@ public class RedisProgressService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackProgressDTO getTrackProgress(long playlistId, long trackId, long userId) {
|
public BaseTrackProgress getTrackProgress(long playlistId, long trackId, long userId) {
|
||||||
try {
|
try {
|
||||||
String key = ImportTrackKey.trackKey(playlistId, trackId, userId);
|
String key = ImportTrackKey.trackKey(playlistId, trackId, userId);
|
||||||
String progressJson = redisTemplate.opsForValue().get(key);
|
String progressJson = redisTemplate.opsForValue().get(key);
|
||||||
|
|
||||||
if (progressJson != null) {
|
if (progressJson != null) {
|
||||||
return objectMapper.readValue(progressJson, TrackProgressDTO.class);
|
return objectMapper.readValue(progressJson, BaseTrackProgress.class);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -79,7 +79,7 @@ public class RedisProgressService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void publishProgressUpdate(TrackProgressDTO progress) {
|
private void publishProgressUpdate(BaseTrackProgress progress) {
|
||||||
try {
|
try {
|
||||||
String channel = ImportTrackKey.redisChannelKey(progress.getPlaylistId(), progress.getUserId());
|
String channel = ImportTrackKey.redisChannelKey(progress.getPlaylistId(), progress.getUserId());
|
||||||
String message = objectMapper.writeValueAsString(progress);
|
String message = objectMapper.writeValueAsString(progress);
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.bivashy.backend.composer.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
public interface SimpleBlob {
|
||||||
|
InputStream inputStream() throws IOException;
|
||||||
|
|
||||||
|
byte[] body() throws IOException;
|
||||||
|
|
||||||
|
String fileName();
|
||||||
|
|
||||||
|
public static class MultipartBlob implements SimpleBlob {
|
||||||
|
private final MultipartFile multipartFile;
|
||||||
|
|
||||||
|
public MultipartBlob(MultipartFile multipartFile) {
|
||||||
|
this.multipartFile = multipartFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream inputStream() throws IOException {
|
||||||
|
return multipartFile.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] body() throws IOException {
|
||||||
|
return multipartFile.getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String fileName() {
|
||||||
|
return multipartFile.getOriginalFilename();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PathBlob implements SimpleBlob {
|
||||||
|
private final Path path;
|
||||||
|
|
||||||
|
public PathBlob(Path path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream inputStream() throws IOException {
|
||||||
|
return Files.newInputStream(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] body() throws IOException {
|
||||||
|
return Files.readAllBytes(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String fileName() {
|
||||||
|
return path.getFileName().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user