-
Notifications
You must be signed in to change notification settings - Fork 0
feat: video api #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: video api #17
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
fffd054
feat: μμ λͺ©λ‘ μ‘°ν api ꡬν (#15)
young0206 6048576
feat: μμ λ±λ‘ api ꡬν (#15)
young0206 86366ff
feat: μ νλΈ url κ²μ¦ api ꡬν (#15)
young0206 4055648
feat: μ νλΈ μμ λ±λ‘ api μμ κΈ°λ‘ μΆκ° (#15)
young0206 de20e41
feat: μμ μμ api ꡬν (#15)
young0206 2cc951d
feat: μμ μμ api ꡬν (#15)
young0206 c1d5f75
fix: μ½λ 리뷰 μμ (#15)
young0206 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
139 changes: 139 additions & 0 deletions
139
src/main/java/com/slatto/domain/video/client/YoutubeApiClient.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package com.slatto.domain.video.client; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.slatto.global.exception.BaseException; | ||
| import com.slatto.global.response.code.CommonErrorCode; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.client.SimpleClientHttpRequestFactory; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.client.RestClient; | ||
| import org.springframework.web.client.RestClientException; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| @Component | ||
| public class YoutubeApiClient { | ||
|
|
||
| private static final String YOUTUBE_API_BASE_URL = "https://www.googleapis.com"; | ||
| private static final List<String> THUMBNAIL_PRIORITIES = | ||
| List.of("maxres", "standard", "high", "medium", "default"); | ||
|
|
||
| private final RestClient restClient; | ||
| private final String apiKey; | ||
|
|
||
| @Autowired | ||
| public YoutubeApiClient( | ||
| RestClient.Builder restClientBuilder, | ||
| @Value("${youtube.api.key}") String apiKey, | ||
| @Value("${youtube.api.connect-timeout}") Duration connectTimeout, | ||
| @Value("${youtube.api.read-timeout}") Duration readTimeout | ||
| ) { | ||
| SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); | ||
| requestFactory.setConnectTimeout(connectTimeout); | ||
| requestFactory.setReadTimeout(readTimeout); | ||
|
|
||
| this.restClient = restClientBuilder | ||
| .baseUrl(YOUTUBE_API_BASE_URL) | ||
| .requestFactory(requestFactory) | ||
| .build(); | ||
| this.apiKey = apiKey; | ||
| } | ||
|
|
||
| YoutubeApiClient(RestClient restClient, String apiKey) { | ||
| this.restClient = restClient; | ||
| this.apiKey = apiKey; | ||
| } | ||
|
|
||
| public Optional<YoutubeVideoInfo> getVideo(String youtubeVideoId) { | ||
| YoutubeVideosResponse response; | ||
| try { | ||
| response = restClient.get() | ||
| .uri(uriBuilder -> uriBuilder | ||
| .path("/youtube/v3/videos") | ||
| .queryParam("part", "snippet,contentDetails,status") | ||
| .queryParam("id", youtubeVideoId) | ||
| .queryParam("key", apiKey) | ||
| .build()) | ||
| .retrieve() | ||
| .body(YoutubeVideosResponse.class); | ||
| } catch (RestClientException exception) { | ||
| throw new BaseException(CommonErrorCode.INTERNAL_SERVER_ERROR); | ||
| } | ||
|
|
||
| if (response == null || response.items() == null || response.items().isEmpty()) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| YoutubeVideoItem item = response.items().getFirst(); | ||
| String thumbnailUrl = selectThumbnailUrl(item.snippet().thumbnails()); | ||
| int durationSeconds = Math.toIntExact(Duration.parse(item.contentDetails().duration()).getSeconds()); | ||
|
|
||
| return Optional.of(new YoutubeVideoInfo( | ||
| item.snippet().title(), | ||
| thumbnailUrl, | ||
| durationSeconds, | ||
| item.status().embeddable(), | ||
| item.status().privacyStatus() | ||
| )); | ||
| } | ||
|
|
||
| private String selectThumbnailUrl(Map<String, YoutubeThumbnail> thumbnails) { | ||
| if (thumbnails == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return THUMBNAIL_PRIORITIES.stream() | ||
| .map(thumbnails::get) | ||
| .filter(thumbnail -> thumbnail != null && thumbnail.url() != null) | ||
| .map(YoutubeThumbnail::url) | ||
| .findFirst() | ||
| .orElse(null); | ||
| } | ||
|
|
||
| public record YoutubeVideoInfo( | ||
| String title, | ||
| String thumbnailUrl, | ||
| int durationSeconds, | ||
| boolean embeddable, | ||
| String privacyStatus | ||
| ) { | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| record YoutubeVideosResponse(List<YoutubeVideoItem> items) { | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| record YoutubeVideoItem( | ||
| YoutubeSnippet snippet, | ||
| YoutubeContentDetails contentDetails, | ||
| YoutubeStatus status | ||
| ) { | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| record YoutubeSnippet( | ||
| String title, | ||
| Map<String, YoutubeThumbnail> thumbnails | ||
| ) { | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| record YoutubeThumbnail(String url) { | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| record YoutubeContentDetails(String duration) { | ||
| } | ||
|
|
||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| record YoutubeStatus( | ||
| boolean embeddable, | ||
| String privacyStatus | ||
| ) { | ||
| } | ||
| } |
99 changes: 99 additions & 0 deletions
99
src/main/java/com/slatto/domain/video/controller/VideoController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package com.slatto.domain.video.controller; | ||
|
|
||
| import com.slatto.domain.video.dto.request.VideoRequest.VideoCreateReqDTO; | ||
| import com.slatto.domain.video.dto.request.VideoRequest.VideoUpdateReqDTO; | ||
| import com.slatto.domain.video.dto.response.VideoResponse.VideoCreateResDTO; | ||
| import com.slatto.domain.video.dto.response.VideoResponse.VideoDeleteResDTO; | ||
| import com.slatto.domain.video.dto.response.VideoResponse.VideoListResDTO; | ||
| import com.slatto.domain.video.dto.response.VideoResponse.VideoUpdateResDTO; | ||
| import com.slatto.domain.video.service.VideoService; | ||
| import com.slatto.global.response.ApiResponse; | ||
| import com.slatto.global.response.code.CommonSuccessCode; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.Max; | ||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.Positive; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.validation.annotation.Validated; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PatchMapping; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RequestParam; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.ResponseStatus; | ||
|
|
||
| @Validated | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/v1/projects/{projectId}/videos") | ||
| public class VideoController { | ||
|
|
||
| private final VideoService videoService; | ||
|
|
||
| @PatchMapping("/{videoId}") | ||
| @Operation(summary = "μμ μμ ", | ||
| description = "νλ‘μ νΈμ λ±λ‘λ μμμ μ λͺ©κ³Ό λ©λͺ¨λ₯Ό μμ ν©λλ€.<br>" + | ||
| "μμ νμ§ μμ λΆλΆμ nullλ‘ λ£μΌλ©΄ λ©λλ€.") | ||
| public ApiResponse<VideoUpdateResDTO> updateVideo( | ||
| @PathVariable @Positive Long projectId, | ||
| @PathVariable @Positive Long videoId, | ||
| @Valid @RequestBody VideoUpdateReqDTO request | ||
| ) { | ||
| // TODO: μΈμ¦/μΈκ° ꡬν ν JWTμμ memberId μΆμΆνλλ‘ λ³κ²½ | ||
| Long memberId = 1L; | ||
| return ApiResponse.success( | ||
| CommonSuccessCode.OK, | ||
| videoService.updateVideo(memberId, projectId, videoId, request) | ||
| ); | ||
| } | ||
|
|
||
| @DeleteMapping("/{videoId}") | ||
| @Operation(summary = "μμ μμ ", description = "νλ‘μ νΈμ λ±λ‘λ μμμ μμ ν©λλ€.") | ||
| public ApiResponse<VideoDeleteResDTO> deleteVideo( | ||
| @PathVariable @Positive Long projectId, | ||
| @PathVariable @Positive Long videoId | ||
| ) { | ||
| // TODO: μΈμ¦/μΈκ° ꡬν ν JWTμμ memberId μΆμΆνλλ‘ λ³κ²½ | ||
| Long memberId = 1L; | ||
| return ApiResponse.success( | ||
| CommonSuccessCode.OK, | ||
| videoService.deleteVideo(memberId, projectId, videoId) | ||
| ); | ||
| } | ||
|
|
||
| @PostMapping | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| @Operation(summary = "μμ λ±λ‘", description = "νλ‘μ νΈμ μλ‘μ΄ YouTube μμμ λ±λ‘ν©λλ€.") | ||
| public ApiResponse<VideoCreateResDTO> createVideo( | ||
| @PathVariable @Positive Long projectId, | ||
| @Valid @RequestBody VideoCreateReqDTO request | ||
| ) { | ||
| // TODO: μΈμ¦/μΈκ° ꡬν ν JWTμμ memberId μΆμΆνλλ‘ λ³κ²½ | ||
| Long memberId = 1L; | ||
| return ApiResponse.success( | ||
| CommonSuccessCode.CREATED, | ||
| videoService.createVideo(memberId, projectId, request) | ||
| ); | ||
| } | ||
|
|
||
| @GetMapping | ||
| @Operation(summary = "μμ λͺ©λ‘ μ‘°ν", description = "νλ‘μ νΈ λ©€λ²κ° νλ‘μ νΈμ λ±λ‘λ μμμ 컀μ λ°©μμΌλ‘ μ‘°νν©λλ€.") | ||
| public ApiResponse<VideoListResDTO> getVideos( | ||
| @PathVariable @Positive Long projectId, | ||
| @RequestParam(required = false) @Positive Long cursor, | ||
| @RequestParam(required = false) @Min(1) @Max(100) Integer size | ||
| ) { | ||
| // TODO: μΈμ¦/μΈκ° ꡬν ν JWTμμ memberId μΆμΆνλλ‘ λ³κ²½ | ||
| Long memberId = 1L; | ||
| return ApiResponse.success( | ||
| CommonSuccessCode.OK, | ||
| videoService.getVideos(memberId, projectId, cursor, size) | ||
| ); | ||
| } | ||
| } | ||
35 changes: 35 additions & 0 deletions
35
src/main/java/com/slatto/domain/video/controller/YoutubeController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.slatto.domain.video.controller; | ||
|
|
||
| import com.slatto.domain.video.dto.request.VideoRequest.YoutubeValidateReqDTO; | ||
| import com.slatto.domain.video.dto.response.VideoResponse.YoutubeValidateResDTO; | ||
| import com.slatto.domain.video.service.VideoService; | ||
| import com.slatto.global.response.ApiResponse; | ||
| import com.slatto.global.response.code.CommonSuccessCode; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/v1/videos/youtube") | ||
| public class YoutubeController { | ||
|
|
||
| private final VideoService videoService; | ||
|
|
||
| @PostMapping("/validate") | ||
| @Operation(summary = "YouTube URL κ²μ¦", description = "YouTube μμ μ 보μ νλ‘μ νΈ λ΄ μ€λ³΅ λ±λ‘ μ¬λΆλ₯Ό κ²μ¦ν©λλ€.") | ||
| public ApiResponse<YoutubeValidateResDTO> validateYoutubeUrl( | ||
| @Valid @RequestBody YoutubeValidateReqDTO request | ||
| ) { | ||
| // TODO: μΈμ¦/μΈκ° ꡬν ν JWTμμ memberId μΆμΆνλλ‘ λ³κ²½ | ||
| Long memberId = 1L; | ||
| return ApiResponse.success( | ||
| CommonSuccessCode.OK, | ||
| videoService.validateYoutubeUrl(memberId, request) | ||
| ); | ||
| } | ||
| } |
54 changes: 54 additions & 0 deletions
54
src/main/java/com/slatto/domain/video/dto/request/VideoRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.slatto.domain.video.dto.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Positive; | ||
| import jakarta.validation.constraints.Pattern; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
| public class VideoRequest { | ||
|
|
||
| @Schema(description = "μμ μμ μμ²") | ||
| public record VideoUpdateReqDTO( | ||
| @Pattern(regexp = "(?s).*\\S.*", message = "μμ μ λͺ©μ κ³΅λ°±μΌ μ μμ΅λλ€.") | ||
| @Size(max = 255, message = "μμ μ λͺ©μ μ΅λ 255μκΉμ§ μ λ ₯ν μ μμ΅λλ€.") | ||
| @Schema(example = "μμ λ μμ μ λͺ©", nullable = true) | ||
| String title, | ||
|
young0206 marked this conversation as resolved.
|
||
|
|
||
| @Schema(example = "μμ λ μμ λ©λͺ¨", nullable = true) | ||
| String memo | ||
| ) { | ||
| } | ||
|
|
||
| @Schema(description = "YouTube URL κ²μ¦ μμ²") | ||
| public record YoutubeValidateReqDTO( | ||
| @NotBlank(message = "YouTube URLμ νμμ λλ€.") | ||
| @Size(max = 500, message = "YouTube URLμ μ΅λ 500μκΉμ§ μ λ ₯ν μ μμ΅λλ€.") | ||
| @Schema(example = "https://www.youtube.com/watch?v=abc123") | ||
| String youtubeUrl, | ||
|
|
||
| @NotNull(message = "νλ‘μ νΈ IDλ νμμ λλ€.") | ||
| @Positive(message = "νλ‘μ νΈ IDλ μμμ¬μΌ ν©λλ€.") | ||
| @Schema(example = "10") | ||
| Long projectId | ||
| ) { | ||
| } | ||
|
|
||
| @Schema(description = "μμ λ±λ‘ μμ²") | ||
| public record VideoCreateReqDTO( | ||
| @NotBlank(message = "YouTube URLμ νμμ λλ€.") | ||
| @Size(max = 500, message = "YouTube URLμ μ΅λ 500μκΉμ§ μ λ ₯ν μ μμ΅λλ€.") | ||
| @Schema(example = "https://www.youtube.com/watch?v=abc123") | ||
| String youtubeUrl, | ||
|
|
||
| @NotBlank(message = "μμ μ λͺ©μ νμμ λλ€.") | ||
| @Size(max = 255, message = "μμ μ λͺ©μ μ΅λ 255μκΉμ§ μ λ ₯ν μ μμ΅λλ€.") | ||
| @Schema(example = "νλ‘μ νΈ λͺ ") | ||
| String title, | ||
|
|
||
| @Schema(example = "μμμ κ΄λ ¨λ λ©λͺ¨", nullable = true) | ||
| String memo | ||
| ) { | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.