๐ถ Spotify
โ ์์ ์คํธ๋ฆฌ๋ฐ ์๋น์ค
โ๏ธ ์ฃผ์ ๊ธฐ๋ฅ: ์ ์ธ๊ณ์ ์์ , ํ์บ์คํธ๋ฅผ ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ์ผ๋ก ๊ฐ์ํ ์ ์์
โ๏ธ ํ๋ซํผ: ๋ชจ๋ฐ์ผ, ์น, ๋ฐ์คํฌํ ๋ฑ ๋ค์ํ ๊ธฐ๊ธฐ์์ ์ฌ์ฉ ๊ฐ๋ฅ
๐ฉ๊ฐ์ธ ํ๋ก์ ํธ์ ๋ง์ถฐ ์์ฑํ ์ฝ๋์ ๋๋ค. ํ์์ ๋ง๊ฒ ์์ ํ์๊ธฐ ๋ฐ๋๋๋ค.
๐ถ API ์ค์
โจ Spotify ์์กด์ฑ ์ถ๊ฐ ๋ฐ ํค ๊ฐ ์ค์
โ
build.gradle
implementation 'se.michaelthelin.spotify:spotify-web-api-java:7.0.0'
โ
application.properties (ํค ๊ฐ์ ํ๊ฒฝ๋ณ์๋ก ๋ฑ๋ก)
spotify.client.id=${SPOTIFY_CLIENT_ID}
spotify.client.secret=${SPOTIFY_CLIENT_SECRET}
โจ SpotifyConfig ์ค์
package com.sigorzav.singmate.config;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.ParseException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials;
import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest;
import java.io.IOException;
/**
* โ
Spotify API Config
* - Access Token ์์ฑ
* - ๋ง๋ฃ์ฌ๋ถ ์ฒดํฌ ๋ฐ ์๋ ๊ฐฑ์
* - Refresh Token ๊ฐฑ์ ๋ฏธ์์ฑ (Client Credentials Flow ๋ฐฉ์)
*/
@Slf4j
@Component
public class SpotifyConfig {
@Getter
private final SpotifyApi spotifyApi;
private final ClientCredentialsRequest clientCredentialsRequest;
// Access Token์ ๋ง๋ฃ ์๊ฐ (๋ฐ๋ฆฌ์ด)
private long tokenExpirationTime;
public SpotifyConfig(
@Value("${spotify.client.id}") String spotifyClientId,
@Value("${spotify.client.secret}") String spotifyClientSecret
) {
// Spotify API ์ค์
this.spotifyApi = new SpotifyApi.Builder()
.setClientId(spotifyClientId)
.setClientSecret(spotifyClientSecret)
.build();
// Client Credentials Flow ์์ฒญ ๊ฐ์ฒด ์์ฑ
this.clientCredentialsRequest = spotifyApi.clientCredentials().build();
log.info("SpotifyConfig initialized with clientId: {}", spotifyClientId);
// Access Token ์์ฒญ
requestAccessToken();
}
/**
* Access Token ์์ฒญ ๋ฐ ์ค์
*/
private void requestAccessToken() {
try {
// Client Credentials Flow๋ฅผ ํตํด Access Token ์์ฒญ
ClientCredentials clientCredentials = clientCredentialsRequest.execute();
// Spotify API์ Access Token ์ค์
spotifyApi.setAccessToken(clientCredentials.getAccessToken());
// ์ ํจ์๊ฐ ์ค์ : 1์๊ฐ
this.tokenExpirationTime = System.currentTimeMillis() + 3600 * 1000;
log.info("Access Token retrieved successfully! Expires in: {} seconds", clientCredentials.getExpiresIn());
} catch (IOException | SpotifyWebApiException | RuntimeException | ParseException e) {
log.error("Error while retrieving access token: ", e);
}
}
// Access Token ๋ง๋ฃ ๋ฐ ๊ฐฑ์
public void ensureAccessToken() {
if (isAccessTokenExpired()) {
requestAccessToken();
}
}
// Access Token ๋ง๋ฃ ์ฌ๋ถ ํ์ธ
private boolean isAccessTokenExpired() {
return System.currentTimeMillis() >= tokenExpirationTime;
}
}
โจ Spotify API ํธ์ถ
โ
Controller
package com.sigorzav.singmate.song.controller;
import com.sigorzav.singmate.api.response.ApiResponse;
import com.sigorzav.singmate.song.dto.SongDTO;
import com.sigorzav.singmate.song.service.SongService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("api/v1/song/")
@RequiredArgsConstructor
@Tag(name = "SongControllerV1", description="๋
ธ๋ ๊ด๋ จ ๊ธฐ๋ฅ API - V1")
public class SongControllerV1 {
private final SongService songService;
@GetMapping("search")
@ResponseBody
@Operation(summary = "๋
ธ๋ ๊ฒ์", description = "๊ฐ์๋, ๊ณก์ ์ด๋ฆ ๋ฑ์ผ๋ก ๋
ธ๋๋ฅผ ๊ฒ์ํฉ๋๋ค.", tags = {"๋
ธ๋ ๊ด๋ฆฌ"})
public ApiResponse<Object> searchSongs(@RequestParam String query) {
return songService.searchSongs(query);
}
}
โ
Service
package com.sigorzav.singmate.song.service;
import com.sigorzav.singmate.api.response.ApiResponse;
import com.sigorzav.singmate.api.service.SpotifyService;
import com.sigorzav.singmate.common.enums.MessageEnum;
import com.sigorzav.singmate.exception.CustomException;
import com.sigorzav.singmate.song.dto.SongDTO;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class SongServiceImpl implements SongService {
private final SpotifyService spotifyService;
/**
* โ
๋
ธ๋ ๊ฒ์
* โ๏ธ ex) query :: ์ข์๋
*/
@Override
public ApiResponse<Object> searchSongs(String query) {
log.info("songSearch request processing. Query: {}", query);
try {
List<SongDTO> songs = spotifyService.searchSongsFromSpotify(query);
return ApiResponse.success(songs);
} catch (Exception e) {
log.error("searchSongs error", e);
throw new CustomException(MessageEnum.SONG_SEARCH_FAIL.getMsg(), HttpStatus.BAD_REQUEST);
}
}
}
โ
Spotify Service
package com.sigorzav.singmate.api.service;
import com.sigorzav.singmate.config.SpotifyConfig;
import com.sigorzav.singmate.song.dto.SongDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.ParseException;
import org.springframework.stereotype.Service;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.specification.Track;
import se.michaelthelin.spotify.requests.data.search.simplified.SearchTracksRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class SpotifyService {
private final SpotifyConfig spotifyConfig;
/**
* ๋
ธ๋ ๊ฒ์
*/
public List<SongDTO> searchSongsFromSpotify(String query) {
SpotifyApi spotifyApi = spotifyConfig.getSpotifyApi();
// Access Token ๋ง๋ฃ ํ์ธ ๋ฐ ๊ฐฑ์
spotifyConfig.ensureAccessToken();
try {
SearchTracksRequest searchTracksRequest = spotifyApi.searchTracks(query).build();
Track[] tracks = searchTracksRequest.execute().getItems();
return convertToSongs(tracks);
} catch (IOException | SpotifyWebApiException | RuntimeException | ParseException e) {
log.error("Spotify API ๊ฒ์ ์ค๋ฅ ๋ฐ์", e);
throw new RuntimeException("Spotify API ๊ฒ์์ด ์คํจํ์ต๋๋ค.", e);
}
}
/**
* ๋
ธ๋ ๊ฒ์ - ๊ฒฐ๊ณผ ๋ฆฌ์คํธ ์์ฑ
*/
private List<SongDTO> convertToSongs(Track[] tracks) {
List<SongDTO> songs = new ArrayList<>();
for (Track track : tracks) {
if (track.getName() == null) {
continue;
}
String songTitle = track.getName();
String songArtist = "Unknown Artist";
String songReleaseDate = "Unknown Release Date";
String songThumbnailUrl = "Unknown Image Url";
if (track.getArtists() != null && track.getArtists().length > 0 && track.getArtists()[0].getName() != null) {
songArtist = track.getArtists()[0].getName();
}
if (track.getAlbum() != null && track.getAlbum().getReleaseDate() != null) {
songReleaseDate = track.getAlbum().getReleaseDate();
}
if (track.getAlbum() != null && track.getAlbum().getImages() != null && track.getAlbum().getImages().length > 0) {
songThumbnailUrl = track.getAlbum().getImages()[0].getUrl();
}
SongDTO songDTO = new SongDTO();
songDTO.setSongTitle(songTitle);
songDTO.setSongArtist(songArtist);
songDTO.setSongReleaseDate(songReleaseDate);
songDTO.setSongThumbnailUrl(songThumbnailUrl);
songs.add(songDTO);
}
return songs;
}
}