h-lab/src/main/java/com/hlab/yanalyst/domain/upload/YoutubeResumableUploadClient.java

69 lines
3.7 KiB
Java

package com.hlab.yanalyst.domain.upload;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
@Component
public class YoutubeResumableUploadClient {
private static final String UPLOAD_URL = "https://www.googleapis.com/upload/youtube/v3/videos";
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
public YoutubeResumableUploadClient(@Qualifier("restTemplate") RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
public String upload(String accessToken, Path file, long size, String contentType, String title,
String description, String privacyStatus, Instant publishAt) throws Exception {
ObjectNode metadata = objectMapper.createObjectNode();
metadata.putObject("snippet").put("title", title).put("description", description).put("categoryId", "22");
ObjectNode status = metadata.putObject("status");
status.put("privacyStatus", publishAt == null ? privacyStatus : "private");
status.put("selfDeclaredMadeForKids", false);
if (publishAt != null) status.put("publishAt", publishAt.toString());
URI initiateUri = UriComponentsBuilder.fromUriString(UPLOAD_URL)
.queryParam("uploadType", "resumable").queryParam("part", "snippet,status").build().toUri();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Upload-Content-Type", contentType);
headers.set("X-Upload-Content-Length", Long.toString(size));
ResponseEntity<String> initiated = restTemplate.exchange(initiateUri, HttpMethod.POST,
new HttpEntity<>(objectMapper.writeValueAsString(metadata), headers), String.class);
URI sessionUri = initiated.getHeaders().getLocation();
if (sessionUri == null) throw new IllegalStateException("YouTube 업로드 세션 주소를 받지 못했습니다.");
HttpRequest request = HttpRequest.newBuilder(sessionUri).timeout(Duration.ofHours(2))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", contentType)
.header("Content-Length", Long.toString(size))
.PUT(HttpRequest.BodyPublishers.ofFile(file)).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IllegalStateException("YouTube 업로드 실패 (HTTP " + response.statusCode() + "): "
+ response.body().substring(0, Math.min(500, response.body().length())));
}
JsonNode result = objectMapper.readTree(response.body());
String videoId = result.path("id").asText("");
if (videoId.isBlank()) throw new IllegalStateException("YouTube 영상 ID를 받지 못했습니다.");
return videoId;
}
}