[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.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.TrackBulkReorderRequest;
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.exception.ImportTrackException;
import com.bivashy.backend.composer.service.TrackService;
@ -47,7 +47,7 @@ public class TrackController {
public ResponseEntity<List<TrackResponse>> addYoutubeTrack(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable long playlistId,
@RequestBody AddYoutubeTrackRequest request) throws ImportTrackException {
@RequestBody YoutubeTrackRequest request) throws ImportTrackException {
List<TrackResponse> response = trackService.addYoutubeTrack(user, playlistId, request);
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(String key, byte[] data);
String store(String key, byte[] data, Map<String, String> metadata);
byte[] readRaw(String path) throws IOException;

View File

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

View File

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

View File

@ -43,6 +43,19 @@ public class TrackSourceService {
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)