지금까지 내용 커밋
This commit is contained in:
130
bot/src/utils/api/Spotify.ts
Normal file
130
bot/src/utils/api/Spotify.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/// <reference types="spotify-api" />
|
||||
import "dotenv/config";
|
||||
import { Logger } from "../Logger";
|
||||
import { SongItem } from "../../types/Track";
|
||||
|
||||
const SPOTIFY_CLIENTID = process.env.SPOTIFY_CLIENTID?.trim() ?? "";
|
||||
const SPOTIFY_SECRET = process.env.SPOTIFY_SECRET?.trim() ?? "";
|
||||
|
||||
const SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token";
|
||||
const SPOTIFY_API_URL = "https://api.spotify.com/v1";
|
||||
const TOKENR_URL = "http://192.168.10.5:8075/api/token";
|
||||
|
||||
const searchCache = new Map<string, string>();
|
||||
|
||||
type TOKENER_RESPONSE = {
|
||||
clientId: string;
|
||||
accessToken: string;
|
||||
accessTokenExpirationTimestampMs: number;
|
||||
isAnonymous: boolean;
|
||||
_notes: string;
|
||||
};
|
||||
|
||||
export const Spotify = {
|
||||
_token: {
|
||||
cached: "",
|
||||
expiresAt: 0,
|
||||
},
|
||||
|
||||
async getTokenByTokener(): Promise<string | null> {
|
||||
if (this._token.cached && Date.now() < this._token.expiresAt) return this._token.cached;
|
||||
try {
|
||||
const response = await fetch(TOKENR_URL);
|
||||
if (!response.ok) throw new Error(`토크너 서버 응답 에러: ${response.status}`);
|
||||
const jsonData = await response.json() as TOKENER_RESPONSE;
|
||||
this._token.cached = jsonData.accessToken;
|
||||
this._token.expiresAt = jsonData.accessTokenExpirationTimestampMs;
|
||||
return jsonData.accessToken;
|
||||
} catch (err) {
|
||||
Logger.error(`토크너 접근 실패: ${err}`);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async getToken(): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch(SPOTIFY_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: SPOTIFY_CLIENTID,
|
||||
client_secret: SPOTIFY_SECRET,
|
||||
}),
|
||||
});
|
||||
|
||||
const textData = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
Logger.error(`스포티파이 정식 토큰 발급 에러 (${response.status}): ${textData}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const jsonData = JSON.parse(textData);
|
||||
|
||||
this._token.cached = jsonData.access_token;
|
||||
// expires_in은 초(초당 1000ms) 단위입니다. 만료 1분 전(60000ms)에 미리 갱신되게 세팅!
|
||||
this._token.expiresAt = Date.now() + (jsonData.expires_in * 1000) - 60000;
|
||||
|
||||
Logger.info("🟢 [Spotify] 공식 API 키로 정식 토큰 발급 완료!");
|
||||
return jsonData.access_token;
|
||||
} catch (err) {
|
||||
Logger.error(`스포티파이 토큰 요청 실패: ${err}`);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async getSearchFull(query: string): Promise<SongItem[]> {
|
||||
try {
|
||||
const token = await this.getToken();
|
||||
if (!token) return [];
|
||||
|
||||
const response = await fetch(
|
||||
`${SPOTIFY_API_URL}/search?q=${encodeURIComponent(query)}&type=track&market=KR&limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const textData = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
Logger.warn("⚠️ 스포티파이 요청 제한(Rate Limit)에 걸렸습니다.");
|
||||
} else {
|
||||
Logger.error(`스포티파이 API 에러 (${response.status}): ${textData}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = JSON.parse(textData) as SpotifyApi.TrackSearchResponse;
|
||||
|
||||
if (!data.tracks || data.tracks.items.length === 0) return [];
|
||||
|
||||
return data.tracks.items.map((track) => ({
|
||||
videoId: track.id,
|
||||
title: track.name,
|
||||
artist: track.artists.map(artist => artist.name).join(", "),
|
||||
duration: track.duration_ms,
|
||||
thumbnail: track.album.images[2]?.url,
|
||||
}));
|
||||
} catch (err) {
|
||||
Logger.error(`스포티파이 검색 실패: ${err}`);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
async getSearchUrl(query: string): Promise<string | null> {
|
||||
const lowerQuery = query.toLocaleLowerCase().trim();
|
||||
if (searchCache.has(lowerQuery)) return searchCache.get(lowerQuery) ?? null;
|
||||
const track = (await this.getSearchFull(query) ?? [])?.[0];
|
||||
const url = track?.videoId ? `https://open.spotify.com/track/${track.videoId}` : null;
|
||||
if (url) searchCache.set(lowerQuery, url);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user