Skip to content
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

feat: 스포티파이 검색 연동 #172

Merged
merged 15 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/showpot-dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=dev
run: ./gradlew clean build -Dspring.profiles.active=dev -x test
GaBaljaintheroom marked this conversation as resolved.
Show resolved Hide resolved

- name: Backend CI Discord Notification
uses: sarisia/actions-status-discord@v1
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ gradle-app.setting
### QClass ###
**/src/main/generated/

### application-cloud-local.yml
### yml ###
app/src/main/resources/application-cloud-local.yml
app/infrastructure/spotify/src/main/resources/application-spotify-local.yml

# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,macos,gradle
1 change: 1 addition & 0 deletions app/infrastructure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ dependencies {
implementation project(":app:infrastructure:redis")
implementation project(":app:infrastructure:s3")
implementation project(":app:infrastructure:message-queue")
implementation project(":app:infrastructure:spotify")
}
3 changes: 3 additions & 0 deletions app/infrastructure/spotify/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
GaBaljaintheroom marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.spotify.client;

import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.spotify.client.dto.request.AccessTokenSpotifyRequest;
import org.spotify.client.dto.request.ArtistSearchSpotifyRequest;
import org.spotify.client.dto.response.SpotifyAccessTokenResponse;
import org.spotify.client.dto.response.SpotifySearchResponse;
import org.spotify.property.SpotifyProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
@RequiredArgsConstructor
@Slf4j
public class SpotifyClient {

private final SpotifyProperty spotifyProperty;

public ResponseEntity<SpotifyAccessTokenResponse> requestToken() {
ResponseEntity<SpotifyAccessTokenResponse> result = WebClient.builder()
.baseUrl(spotifyProperty.tokenApiURL())
.build()
.post()
.contentType(APPLICATION_FORM_URLENCODED)
.body(
AccessTokenSpotifyRequest.builder()
.clientId(spotifyProperty.clientId())
.clientSecret(spotifyProperty.clientSecret())
.build()
.getFormInserter()
)
.retrieve()
.toEntity(SpotifyAccessTokenResponse.class)
.block();

if (result == null
|| !result.getStatusCode().is2xxSuccessful()
) {
log.error("Spotify API request access token failed: {}", result);
throw new RuntimeException("Spotify API request access token failed");
}

// TODO: handle error
return result;
}

public SpotifySearchResponse searchArtist(ArtistSearchSpotifyRequest request) {
ResponseEntity<SpotifySearchResponse> result = WebClient.builder()
.defaultHeader("Authorization", "Bearer " + request.accessToken())
.baseUrl(spotifyProperty.apiURL() + "/search?" + request.toQueryParameter())
.build()
.get()
.retrieve()
.toEntity(SpotifySearchResponse.class)
.block();

if (result == null
|| result.getStatusCode() == HttpStatus.UNAUTHORIZED
|| result.getStatusCode() == HttpStatus.FORBIDDEN
|| result.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS
GaBaljaintheroom marked this conversation as resolved.
Show resolved Hide resolved
) {
log.error("Spotify API search artist failed: {}", result);
throw new RuntimeException("Spotify API request search artist failed");
}

return result.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.spotify.client.dto.request;

import lombok.Builder;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.BodyInserters.FormInserter;

public record AccessTokenSpotifyRequest(
String grantType,
String clientId,
String clientSecret
) {

public AccessTokenSpotifyRequest(String grantType, String clientId, String clientSecret) {
this.grantType = grantType;
this.clientId = clientId;
this.clientSecret = clientSecret;
}

@Builder
public AccessTokenSpotifyRequest(String clientId, String clientSecret) {
this(
"client_credentials",
clientId,
clientSecret
);
}

public FormInserter<String> getFormInserter() {
return BodyInserters.fromFormData("grant_type", grantType)
.with("client_id", clientId)
.with("client_secret", clientSecret);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.spotify.client.dto.request;

import lombok.Builder;

/**
* @param accessToken Spotify API 요청을 위한 토큰
* @param search 검색어
* @param limit default: 20, range: 0-50
* @param offset default: 0, range: 0-1000
*/
@Builder
public record ArtistSearchSpotifyRequest(
String accessToken,
String search,
int limit,
int offset
) {

public String toQueryParameter() {
return String.format(
"q=%s&type=artist&limit=%d&offset=%d",
search,
limit,
offset
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.spotify.client.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;

public record SpotifyAccessTokenResponse(
@JsonProperty("access_token")
String accessToken,
@JsonProperty("token_type")
String tokenType,
@JsonProperty("expires_in")
int expiresIn
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.spotify.client.dto.response;

public record SpotifyArtistImageSearchResponse(
int height,
int width,
String url
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.spotify.client.dto.response;

import java.util.List;

public record SpotifyArtistItemSearchResponse(
String id,
String name,
List<String> genres,
List<SpotifyArtistImageSearchResponse> images
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.spotify.client.dto.response;

import java.util.List;

public record SpotifyArtistSearchResponse(
String href,
int limit,
String next,
int offset,
String previous,
int total,
List<SpotifyArtistItemSearchResponse> items
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.spotify.client.dto.response;

public record SpotifySearchResponse(
SpotifyArtistSearchResponse artists
) {
GaBaljaintheroom marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.spotify.config;

import org.spotify.property.SpotifyProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties({SpotifyProperty.class})
public class SpotifyConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.spotify.property;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spotify")
public record SpotifyProperty(
String clientId,
String clientSecret,
String tokenApiURL,
String apiURL
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.spotify;

import org.junit.jupiter.api.Test;
import org.spotify.config.SpotifyConfig;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(value = {SpotifyConfig.class})
class SpotifyApplicationTests {

@Test
void contextLoads() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.spotify.client;

import org.junit.jupiter.api.Test;
import org.spotify.client.dto.request.ArtistSearchSpotifyRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
@ActiveProfiles("spotify-local")
class SpotifyClientTest {

@Autowired
private SpotifyClient spotifyClient;

@Test
void requestToken() {
var result = spotifyClient.requestToken();
System.out.println(result.getBody().accessToken());
}

@Test
void searchArtist() {
String accessToken = spotifyClient.requestToken().getBody().accessToken();
var result = spotifyClient.searchArtist(
ArtistSearchSpotifyRequest.builder()
.accessToken(accessToken)
.search("BTS")
.limit(10)
.offset(0)
.build()
);
System.out.println(result);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.example.config;

import org.spotify.config.SpotifyConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({RedisConfig.class, PubSubConfig.class, S3Config.class})
@Import({RedisConfig.class, PubSubConfig.class, S3Config.class, SpotifyConfig.class})
public class InfrastructureConfig {

}
6 changes: 3 additions & 3 deletions app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ spring:
profiles:
active: local
group:
local: domain-local, cloud-local
dev: domain-dev, cloud-dev
prod: domain-prod, cloud-prod
local: domain-local, cloud-local, spotify-local
dev: domain-dev, cloud-dev, spotify-dev
prod: domain-prod, cloud-prod, spotify-prod
3 changes: 2 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ include(":app:api:show-api")
include (":app:infrastructure")
include (":app:infrastructure:redis")
include (":app:infrastructure:s3")
include (":app:infrastructure:message-queue")
include (":app:infrastructure:message-queue")
include (":app:infrastructure:spotify")
Loading