[Feature] Youtube importing and refreshing implementation (ytdlp) #1

Merged
bivashy merged 5 commits from feature/youtube-import-impl into main 2026-01-07 22:56:37 +00:00
18 changed files with 590 additions and 170 deletions
Showing only changes of commit b083e592f5 - Show all commits

View File

@ -14,10 +14,10 @@ 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.track.AddLocalTrackRequest; import com.bivashy.backend.composer.dto.track.AddLocalTrackRequest;
import com.bivashy.backend.composer.dto.track.AddYoutubeTrackRequest;
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.AddLocalTrackParamsBuilder; import com.bivashy.backend.composer.dto.track.service.AddLocalTrackParamsBuilder;
import com.bivashy.backend.composer.exception.ImportTrackException; import com.bivashy.backend.composer.exception.ImportTrackException;
import com.bivashy.backend.composer.service.TrackService; import com.bivashy.backend.composer.service.TrackService;
@ -47,7 +47,7 @@ public class TrackController {
public ResponseEntity<List<TrackResponse>> addYoutubeTrack( public ResponseEntity<List<TrackResponse>> addYoutubeTrack(
@AuthenticationPrincipal CustomUserDetails user, @AuthenticationPrincipal CustomUserDetails user,
@PathVariable long playlistId, @PathVariable long playlistId,
@RequestBody AddYoutubeTrackRequest request) throws ImportTrackException { @RequestBody YoutubeTrackRequest request) throws ImportTrackException {
List<TrackResponse> response = trackService.addYoutubeTrack(user, playlistId, request); List<TrackResponse> response = trackService.addYoutubeTrack(user, playlistId, request);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }

View File

@ -1,4 +0,0 @@
package com.bivashy.backend.composer.dto.track;
public record AddYoutubeTrackRequest(String youtubeUrl) {
}

View File

@ -0,0 +1,4 @@
package com.bivashy.backend.composer.dto.track;
public record YoutubeTrackRequest(String youtubeUrl) {
}

View File

@ -17,7 +17,7 @@ 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); String store(String key, byte[] data, Map<String, String> metadata);
byte[] readRaw(String path) throws IOException; byte[] readRaw(String path) throws IOException;

View File

@ -78,7 +78,7 @@ public class AudioS3StorageService implements AudioBlobStorageService {
} }
@Override @Override
public String store(String key, byte[] data) { public String store(String key, byte[] data, Map<String, String> metadata) {
try { try {
ResponseInputStream<GetObjectResponse> response = s3Client.getObject(GetObjectRequest.builder() ResponseInputStream<GetObjectResponse> response = s3Client.getObject(GetObjectRequest.builder()
.bucket(bucket) .bucket(bucket)
@ -89,7 +89,7 @@ public class AudioS3StorageService implements AudioBlobStorageService {
.bucket(bucket) .bucket(bucket)
.key(key) .key(key)
.contentType(response.response().contentType()) .contentType(response.response().contentType())
.metadata(response.response().metadata()) .metadata(metadata)
.build(), RequestBody.fromBytes(data)); .build(), RequestBody.fromBytes(data));
return key; return key;
@ -98,6 +98,7 @@ public class AudioS3StorageService implements AudioBlobStorageService {
s3Client.putObject(PutObjectRequest.builder() s3Client.putObject(PutObjectRequest.builder()
.bucket(bucket) .bucket(bucket)
.key(key) .key(key)
.metadata(metadata)
.build(), RequestBody.fromBytes(data)); .build(), RequestBody.fromBytes(data));
return key; return key;
} }

View File

@ -1,12 +1,12 @@
package com.bivashy.backend.composer.service; package com.bivashy.backend.composer.service;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; 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.UUID;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -20,10 +20,10 @@ 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.importing.SingleTrackProgress; import com.bivashy.backend.composer.dto.importing.SingleTrackProgress;
import com.bivashy.backend.composer.dto.track.AddYoutubeTrackRequest;
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.AddLocalTrackParams;
import com.bivashy.backend.composer.dto.track.service.AddLocalTrackParamsBuilder; import com.bivashy.backend.composer.dto.track.service.AddLocalTrackParamsBuilder;
import com.bivashy.backend.composer.exception.ImportTrackException; import com.bivashy.backend.composer.exception.ImportTrackException;
@ -128,7 +128,7 @@ public class TrackService {
@Transactional @Transactional
public List<TrackResponse> addYoutubeTrack(CustomUserDetails user, long playlistId, public List<TrackResponse> addYoutubeTrack(CustomUserDetails user, long playlistId,
AddYoutubeTrackRequest request) throws ImportTrackException { YoutubeTrackRequest request) throws ImportTrackException {
List<VideoInfo> videoInfos = Collections.emptyList(); List<VideoInfo> videoInfos = Collections.emptyList();
try { try {
videoInfos = YtDlp.getVideoInfo(request.youtubeUrl()); videoInfos = YtDlp.getVideoInfo(request.youtubeUrl());
@ -226,7 +226,7 @@ public class TrackService {
var body = Files.readAllBytes(path); var body = Files.readAllBytes(path);
if (isMetadataFile) { if (isMetadataFile) {
s3StorageService.store(downloadedMetadataKey, body); s3StorageService.store(downloadedMetadataKey, body, Map.of());
continue; continue;
} }
String fileName = fileNameWithoutExtension(path.getFileName().toString()); String fileName = fileNameWithoutExtension(path.getFileName().toString());
@ -239,7 +239,6 @@ public class TrackService {
logger.info("downloaded file {} and info {}, key {}", fileName, videoInfo.getTitle(), audioKey); logger.info("downloaded file {} and info {}, key {}", fileName, videoInfo.getTitle(), audioKey);
audioKey = s3StorageService.store(audioKey, body);
Optional<Metadata> metadata = Optional.empty(); Optional<Metadata> metadata = Optional.empty();
try (var inputStream = Files.newInputStream(path)) { try (var inputStream = Files.newInputStream(path)) {
@ -251,8 +250,8 @@ public class TrackService {
TrackSource playlistEntrySource; TrackSource playlistEntrySource;
try { try {
playlistEntrySource = trackSourceService.createLocalTrackSource( playlistEntrySource = trackSourceService.createTrackSourceWithKey(audioKey, body, ffprobeJson,
body, ffprobeJson, OBJECT_MAPPER.writeValueAsString(videoInfo), SourceTypes.PLAYLIST); OBJECT_MAPPER.writeValueAsString(videoInfo), SourceTypes.PLAYLIST);
} catch (IOException e) { } catch (IOException e) {
throw new ImportTrackException("cannot read blob body", e); throw new ImportTrackException("cannot read blob body", e);
} }
@ -266,7 +265,7 @@ public class TrackService {
// TODO: Recognize music if the duration is less than five minutes // 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 // (configurable), and if not, it is a playlist and should be marked as is
trackMetadataService.createTrackMetadata( trackMetadataService.createTrackMetadata(
track, title, fileName, trackSource.getSourceUrl(), artist, null, durationSeconds); track, title, fileName, audioKey, artist, null, durationSeconds);
trackPlaylistService.insertTrackAtEnd(playlistId, track.getId()); trackPlaylistService.insertTrackAtEnd(playlistId, track.getId());

View File

@ -43,6 +43,19 @@ public class TrackSourceService {
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) { public TrackSource createYoutubeTrackSource(String sourceType) {
String folderPath = s3Service.storeFolder(); String folderPath = s3Service.storeFolder();
SourceType type = sourceTypeRepository.findByName(sourceType) SourceType type = sourceTypeRepository.findByName(sourceType)