Skip to content

Commit 8d25891

Browse files
authored
feat: Spotify 곡 조회 기능 구현 (#43)
* chore: spotify 의존성 추가 * chore: spotify.yml 생성 * chore: spotify properties 추가 * chore: spotify config 추가 * feat: 장르 기반 스포티파이 음악 검색 기능 구현 * refactor: 장르, 년도 기반 검색 로직 개선
1 parent 3760b68 commit 8d25891

File tree

10 files changed

+188
-1
lines changed

10 files changed

+188
-1
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ dependencies {
5555

5656
// MySQL
5757
implementation 'mysql:mysql-connector-java:8.0.33'
58+
59+
// Spotify
60+
implementation 'se.michaelthelin.spotify:spotify-web-api-java:8.4.1'
5861
}
5962

6063
tasks.named('test') {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.amcamp.domain.spotify.api;
2+
3+
import com.amcamp.domain.spotify.application.SpotifyService;
4+
import com.amcamp.domain.spotify.dto.response.SpotifySearchResponse;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RequestParam;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import java.util.List;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
@RequestMapping("/music")
16+
public class SpotifyController {
17+
18+
private final SpotifyService spotifyService;
19+
20+
@GetMapping("/search")
21+
public List<SpotifySearchResponse> searchSpotify(@RequestParam List<String> genres) {
22+
return spotifyService.searchByGenre(genres);
23+
}
24+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.amcamp.domain.spotify.application;
2+
3+
import com.amcamp.domain.spotify.dto.response.SpotifySearchResponse;
4+
import com.amcamp.global.error.exception.CustomException;
5+
import com.amcamp.global.error.exception.ErrorCode;
6+
import com.amcamp.global.util.MemberUtil;
7+
import com.amcamp.infra.config.spotify.SpotifyConfig;
8+
import lombok.RequiredArgsConstructor;
9+
import org.apache.hc.core5.http.ParseException;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
import se.michaelthelin.spotify.SpotifyApi;
13+
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
14+
import se.michaelthelin.spotify.model_objects.specification.*;
15+
import se.michaelthelin.spotify.requests.data.search.simplified.SearchTracksRequest;
16+
17+
import java.io.IOException;
18+
import java.util.Arrays;
19+
import java.util.LinkedHashSet;
20+
import java.util.List;
21+
import java.util.Set;
22+
23+
import static com.neovisionaries.i18n.CountryCode.KR;
24+
25+
@Transactional
26+
@Service
27+
@RequiredArgsConstructor
28+
public class SpotifyService {
29+
30+
private final SpotifyConfig spotifyConfig;
31+
private final MemberUtil memberUtil;
32+
33+
public List<SpotifySearchResponse> searchByGenre(List<String> genres) {
34+
memberUtil.getCurrentMember();
35+
36+
Track[] tracks = getTrackInfoByGenre(genres);
37+
38+
return Arrays.stream(tracks)
39+
.map(this::getTrackData)
40+
.toList();
41+
}
42+
43+
/*
44+
* 장르에 따른 검색 결과를 반환합니다.
45+
*/
46+
public Track[] getTrackInfoByGenre(List<String> genres) {
47+
try {
48+
SpotifyApi spotifyApi = new SpotifyApi.Builder()
49+
.setAccessToken(spotifyConfig.generateAccessToken())
50+
.build();
51+
52+
Set<Track> uniqueTracks = new LinkedHashSet<>();
53+
54+
for (String genre : genres) {
55+
String query = "genre:" + genre + " year:2024-2025";
56+
57+
SearchTracksRequest searchTrackRequest = spotifyApi.searchTracks(query)
58+
.market(KR)
59+
.limit(10)
60+
.build();
61+
62+
Paging<Track> searchResult = searchTrackRequest.execute();
63+
64+
List<Track> selectedTracks = Arrays.stream(searchResult.getItems())
65+
.limit(3)
66+
.toList();
67+
68+
uniqueTracks.addAll(selectedTracks);
69+
70+
if (uniqueTracks.size() >= 10) {
71+
break;
72+
}
73+
}
74+
75+
return uniqueTracks.toArray(new Track[0]);
76+
} catch (IOException | ParseException | SpotifyWebApiException e) {
77+
throw new CustomException(ErrorCode.SPOTIFY_EXCEPTION);
78+
}
79+
}
80+
81+
private SpotifySearchResponse getTrackData(Track track) {
82+
ArtistSimplified[] artists = track.getArtists();
83+
String artistName = artists[0].getName();
84+
85+
AlbumSimplified album = track.getAlbum();
86+
String albumName = album.getName();
87+
88+
Image[] images = album.getImages();
89+
String imageUrl = (images.length > 0) ? images[0].getUrl() : "NO_IMAGE";
90+
91+
return SpotifySearchResponse.of(artistName, track.getName(), albumName, imageUrl);
92+
}
93+
}
94+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.amcamp.domain.spotify.dto.response;
2+
3+
public record SpotifySearchResponse(String artistName, String title, String albumName, String imageUrl) {
4+
public static SpotifySearchResponse of(String artistName, String title, String albumName, String imageUrl) {
5+
return new SpotifySearchResponse(artistName, title, albumName, imageUrl);
6+
}
7+
}

src/main/java/com/amcamp/global/error/exception/ErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public enum ErrorCode {
1212
AUTH_NOT_FOUND(HttpStatus.UNAUTHORIZED, "사용자 인증 정보를 찾을 수 없습니다. 올바른 토큰으로 요청해주세요."),
1313

1414
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다."),
15+
16+
SPOTIFY_EXCEPTION(HttpStatus.SERVICE_UNAVAILABLE, "스포티파이 라이브러리 사용 중 예외가 발생했습니다."),
1517
;
1618

1719
private final HttpStatus httpStatus;

src/main/java/com/amcamp/infra/config/properties/PropertiesConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.amcamp.infra.config.jwt.JwtProperties;
55
import com.amcamp.infra.config.oauth.KakaoProperties;
66
import com.amcamp.infra.config.redis.RedisProperties;
7+
import com.amcamp.infra.config.spotify.SpotifyProperties;
78
import org.springframework.boot.context.properties.EnableConfigurationProperties;
89
import org.springframework.context.annotation.Configuration;
910

@@ -12,6 +13,7 @@
1213
KakaoProperties.class,
1314
JwtProperties.class,
1415
ChatProperties.class,
16+
SpotifyProperties.class,
1517
})
1618
@Configuration
1719
public class PropertiesConfig {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.amcamp.infra.config.spotify;
2+
3+
import com.amcamp.global.error.exception.CustomException;
4+
import com.amcamp.global.error.exception.ErrorCode;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.apache.hc.core5.http.ParseException;
7+
import org.springframework.stereotype.Component;
8+
import se.michaelthelin.spotify.SpotifyApi;
9+
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
10+
import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials;
11+
import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest;
12+
13+
import java.io.IOException;
14+
15+
@Component
16+
@Slf4j
17+
public class SpotifyConfig {
18+
19+
private final SpotifyApi spotifyApi;
20+
21+
private SpotifyConfig(SpotifyProperties spotifyProperties) {
22+
this.spotifyApi =
23+
new SpotifyApi.Builder()
24+
.setClientId(spotifyProperties.clientId())
25+
.setClientSecret(spotifyProperties.clientSecret())
26+
.build();
27+
}
28+
29+
public String generateAccessToken() {
30+
ClientCredentialsRequest clientCredentialsRequest = spotifyApi.clientCredentials().build();
31+
try {
32+
final ClientCredentials clientCredentials = clientCredentialsRequest.execute();
33+
spotifyApi.setAccessToken(clientCredentials.getAccessToken());
34+
return spotifyApi.getAccessToken();
35+
} catch (IOException | SpotifyWebApiException | ParseException e) {
36+
log.error(e.getMessage());
37+
throw new CustomException(ErrorCode.SPOTIFY_EXCEPTION);
38+
}
39+
}
40+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.amcamp.infra.config.spotify;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties("spotify")
6+
public record SpotifyProperties(String clientId, String clientSecret) {
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
spring:
2+
config:
3+
activate:
4+
on-profile: "spotify"
5+
spotify:
6+
client-id: ${SPOTIFY_CLIENT_ID}
7+
client-secret: ${SPOTIFY_CLIENT_SECRET}

src/main/resources/application.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ spring:
66
include:
77
- security
88
- redis
9-
- gemini
9+
- gemini
10+
- spotify

0 commit comments

Comments
 (0)