๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๊ฐœ๋ฐœ/Spring

(Spring) Spotify API ์„ค์ • (searchTracks)

by gomdeng 2025. 4. 18.

๐Ÿถ 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;
    }
}