From 1da98b552d11e43a5153a4454b6073f42a2f10ec Mon Sep 17 00:00:00 2001 From: wlstj <22020306jinseoyeon@gmail.com> Date: Sun, 21 Jul 2024 16:01:06 +0900 Subject: [PATCH 01/20] =?UTF-8?q?build:=20youtube=20api=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ src/main/resources/application.yml | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4084531..094f539 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,12 @@ dependencies { //s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + //youtube api + implementation 'com.google.api-client:google-api-client:1.33.0' + implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0' + implementation 'com.google.apis:google-api-services-youtube:v3-rev20230816-2.0.0' + implementation 'com.google.http-client:google-http-client-jackson2:1.39.2' } tasks.named('test') { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ef00c8f..82bcf1e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,4 +27,8 @@ cloud: region.static: ap-northeast-2 credentials: accessKey: ${BUCKET_ACCESS} - secretKey: ${BUCKET_SECRET} \ No newline at end of file + secretKey: ${BUCKET_SECRET} + +youtube: + api: + key: ${API_KEY} From edabbf8bcdfeb2d1a2fe319629a95ff9e973da4f Mon Sep 17 00:00:00 2001 From: wlstj <22020306jinseoyeon@gmail.com> Date: Sun, 21 Jul 2024 16:01:06 +0900 Subject: [PATCH 02/20] =?UTF-8?q?build:=20youtube=20api=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 094f539..1e5a730 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From 18c03758d519abb4c47396959353a609235d9949 Mon Sep 17 00:00:00 2001 From: wlstj <22020306jinseoyeon@gmail.com> Date: Sun, 21 Jul 2024 16:01:24 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20playlist=20entity=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playlist/domain/Playlist.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java diff --git a/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java new file mode 100644 index 0000000..8b5cb40 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java @@ -0,0 +1,49 @@ +package com.example.beginnerfitbe.playlist.domain; + +import com.example.beginnerfitbe.user.domain.User; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; + +@Entity +@NoArgsConstructor +@Getter +public class Playlist { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column + private String description; + + @Column(nullable = false) + private int totalTime; + + @Column(nullable = false) + private boolean isCompleted; + + @CreatedDate + @Column(nullable = false) + private LocalDateTime createdAt; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Builder + public Playlist(String title, String description, int totalTime, boolean isCompleted, LocalDateTime createdAt, User user) { + this.title = title; + this.description = description; + this.totalTime = totalTime; + this.isCompleted = isCompleted; + this.createdAt = createdAt; + this.user = user; + } +} From 8b1715b7181b68927814114e66279621e25a5515 Mon Sep 17 00:00:00 2001 From: wlstj <22020306jinseoyeon@gmail.com> Date: Sun, 21 Jul 2024 16:12:27 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20youtube=20entity=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playlist/domain/Playlist.java | 8 ++- .../beginnerfitbe/youtube/domain/Youtube.java | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java diff --git a/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java index 8b5cb40..63f6011 100644 --- a/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java +++ b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java @@ -1,6 +1,7 @@ package com.example.beginnerfitbe.playlist.domain; import com.example.beginnerfitbe.user.domain.User; +import com.example.beginnerfitbe.youtube.domain.Youtube; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; @@ -8,6 +9,7 @@ import org.springframework.data.annotation.CreatedDate; import java.time.LocalDateTime; +import java.util.List; @Entity @NoArgsConstructor @@ -37,13 +39,17 @@ public class Playlist { @JoinColumn(name = "user_id", nullable = false) private User user; + @OneToMany(mappedBy = "playlist", fetch = FetchType.LAZY) + private List videos; + @Builder - public Playlist(String title, String description, int totalTime, boolean isCompleted, LocalDateTime createdAt, User user) { + public Playlist(String title, String description, int totalTime, boolean isCompleted, LocalDateTime createdAt, User user, List videos) { this.title = title; this.description = description; this.totalTime = totalTime; this.isCompleted = isCompleted; this.createdAt = createdAt; this.user = user; + this.videos = videos; } } diff --git a/src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java b/src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java new file mode 100644 index 0000000..56d2863 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java @@ -0,0 +1,57 @@ +package com.example.beginnerfitbe.youtube.domain; + +import com.example.beginnerfitbe.playlist.domain.Playlist; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Youtube { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String url; + + @Column(nullable = false) + private String thumbnail; + + @Column + private String description; + + @Column(nullable = false) + private String channel; + + @Column(nullable = false) + private String publishedAt; + + @Column(nullable = false) + private String duration; + + @Column(nullable = false) + private Boolean isWatched; + + @ManyToOne + @JoinColumn(name = "playlist_id", nullable = false) + private Playlist playlist; + + @Builder + public Youtube(String title, String url, String thumbnail, String description, String channel, String publishedAt, String duration, Boolean isWatched, Playlist playlist) { + this.title = title; + this.url = url; + this.thumbnail = thumbnail; + this.description = description; + this.channel = channel; + this.publishedAt = publishedAt; + this.duration = duration; + this.isWatched = isWatched; + this.playlist = playlist; + } +} From 5badc1b458e8eeb66f755efee261548b15967c6d Mon Sep 17 00:00:00 2001 From: wlstj <22020306jinseoyeon@gmail.com> Date: Sun, 21 Jul 2024 17:45:38 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20youtube=20api=20search=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playlist/domain/Playlist.java | 6 +- .../{Youtube.java => YoutubeVideo.java} | 8 +- .../youtube/dto/YoutubeSearchResDto.java | 30 ++++ .../repository/YoutubeVideoRepository.java | 9 ++ .../youtube/service/YoutubeVideoService.java | 27 ++++ .../youtube/util/YoutubeUtil.java | 129 ++++++++++++++++++ 6 files changed, 204 insertions(+), 5 deletions(-) rename src/main/java/com/example/beginnerfitbe/youtube/domain/{Youtube.java => YoutubeVideo.java} (78%) create mode 100644 src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeSearchResDto.java create mode 100644 src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java create mode 100644 src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java create mode 100644 src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java diff --git a/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java index 63f6011..f76b2ef 100644 --- a/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java +++ b/src/main/java/com/example/beginnerfitbe/playlist/domain/Playlist.java @@ -1,7 +1,7 @@ package com.example.beginnerfitbe.playlist.domain; import com.example.beginnerfitbe.user.domain.User; -import com.example.beginnerfitbe.youtube.domain.Youtube; +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; @@ -40,10 +40,10 @@ public class Playlist { private User user; @OneToMany(mappedBy = "playlist", fetch = FetchType.LAZY) - private List videos; + private List videos; @Builder - public Playlist(String title, String description, int totalTime, boolean isCompleted, LocalDateTime createdAt, User user, List videos) { + public Playlist(String title, String description, int totalTime, boolean isCompleted, LocalDateTime createdAt, User user, List videos) { this.title = title; this.description = description; this.totalTime = totalTime; diff --git a/src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java b/src/main/java/com/example/beginnerfitbe/youtube/domain/YoutubeVideo.java similarity index 78% rename from src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java rename to src/main/java/com/example/beginnerfitbe/youtube/domain/YoutubeVideo.java index 56d2863..e737b00 100644 --- a/src/main/java/com/example/beginnerfitbe/youtube/domain/Youtube.java +++ b/src/main/java/com/example/beginnerfitbe/youtube/domain/YoutubeVideo.java @@ -9,11 +9,14 @@ @Entity @Getter @NoArgsConstructor -public class Youtube { +public class YoutubeVideo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, unique = true) + private String videoId; + @Column(nullable = false) private String title; @@ -43,8 +46,9 @@ public class Youtube { private Playlist playlist; @Builder - public Youtube(String title, String url, String thumbnail, String description, String channel, String publishedAt, String duration, Boolean isWatched, Playlist playlist) { + public YoutubeVideo(String title, String videoId, String url, String thumbnail, String description, String channel, String publishedAt, String duration, Boolean isWatched, Playlist playlist) { this.title = title; + this.videoId = videoId; this.url = url; this.thumbnail = thumbnail; this.description = description; diff --git a/src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeSearchResDto.java b/src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeSearchResDto.java new file mode 100644 index 0000000..dd9d90c --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/dto/YoutubeSearchResDto.java @@ -0,0 +1,30 @@ +package com.example.beginnerfitbe.youtube.dto; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class YoutubeSearchResDto { + private String videoId; + private String title; + private String url; + private String description; + private String thumbnail; + private String channel; + private String publishedAt; + private String duration; + + @Builder + public YoutubeSearchResDto(String videoId, String title, String url, String description, String thumbnail, String channel, String publishedAt, String duration, Boolean isWatched) { + this.videoId = videoId; + this.title = title; + this.url = url; + this.description = description; + this.thumbnail = thumbnail; + this.channel = channel; + this.publishedAt = publishedAt; + this.duration = duration; + } +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java b/src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java new file mode 100644 index 0000000..3754631 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/repository/YoutubeVideoRepository.java @@ -0,0 +1,9 @@ +package com.example.beginnerfitbe.youtube.repository; + +import com.example.beginnerfitbe.youtube.domain.YoutubeVideo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface YoutubeVideoRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java b/src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java new file mode 100644 index 0000000..2013800 --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/service/YoutubeVideoService.java @@ -0,0 +1,27 @@ +package com.example.beginnerfitbe.youtube.service; + +import com.example.beginnerfitbe.youtube.dto.YoutubeSearchResDto; +import com.example.beginnerfitbe.youtube.util.YoutubeUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class YoutubeVideoService { + + private final YoutubeUtil youtubeUtil; + + public List search(String query) throws IOException { + return youtubeUtil.search(query); + } + + + + + + +} diff --git a/src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java b/src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java new file mode 100644 index 0000000..7f4d60c --- /dev/null +++ b/src/main/java/com/example/beginnerfitbe/youtube/util/YoutubeUtil.java @@ -0,0 +1,129 @@ +package com.example.beginnerfitbe.youtube.util; + +import com.example.beginnerfitbe.youtube.dto.YoutubeSearchResDto; +import com.example.beginnerfitbe.youtube.repository.YoutubeVideoRepository; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class YoutubeUtil { + + @Value("${youtube.api.key}") + private String apiKey; + + private final YoutubeVideoRepository youtubeRepository; + + + // 비디오 검색 + public List search(String keyword) throws IOException { + JsonFactory jsonFactory = new JacksonFactory(); + YouTube youtube = new YouTube.Builder( + new com.google.api.client.http.javanet.NetHttpTransport(), + jsonFactory, + request -> { + }) + .setApplicationName("youtube-search") + .build(); + + YouTube.Search.List search = youtube.search().list(Collections.singletonList("id,snippet")); + + search.setKey(apiKey); + search.setQ(keyword); + search.setMaxResults(10L); + + SearchListResponse searchResponse = search.execute(); + List searchResultList = searchResponse.getItems(); + + if (searchResultList != null && !searchResultList.isEmpty()) { + return searchResultList.stream() + .filter(searchResult -> "youtube#video".equals(searchResult.getId().getKind())) + .map(searchResult -> { + String videoId = searchResult.getId().getVideoId(); + String videoTitle = searchResult.getSnippet().getTitle(); + String videoDescription = searchResult.getSnippet().getDescription(); + String videoChannel = searchResult.getSnippet().getChannelTitle(); + String publishedAt = searchResult.getSnippet().getPublishedAt().toString(); + String videoThumbnail = searchResult.getSnippet().getThumbnails().getHigh().getUrl(); + String duration = videoInfo(youtube, videoId); + + return YoutubeSearchResDto.builder() + .videoId(videoId) + .url("https://www.youtube.com/watch?v=" + videoId) + .title(videoTitle) + .description(videoDescription) + .channel(videoChannel) + .publishedAt(publishedAt) + .thumbnail(videoThumbnail) + .duration(duration) + .build(); + }) + .collect(Collectors.toList()); + } + System.out.println("No search results found."); + return Collections.emptyList(); + } + + //비디오 메타데이터 + public String videoInfo(YouTube youtube, String videoId){ + try { + YouTube.Videos.List videosListByIdRequest = youtube.videos().list(Collections.singletonList("contentDetails")); + videosListByIdRequest.setKey(apiKey); + + videosListByIdRequest.setId(Collections.singletonList(videoId)); + VideoListResponse videoListResponse = videosListByIdRequest.execute(); + + List