/// 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(); type TOKENER_RESPONSE = { clientId: string; accessToken: string; accessTokenExpirationTimestampMs: number; isAnonymous: boolean; _notes: string; }; export const Spotify = { _token: { cached: "", expiresAt: 0, }, async getTokenByTokener(): Promise { 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 { 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 { 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, url: `https://open.spotify.com/track/${track.id}`, title: track.name, artist: track.artists.map(artist => artist.name).join(", "), duration: track.duration_ms, thumbnail: track.album.images[0]?.url, })); } catch (err) { Logger.error(`스포티파이 검색 실패: ${err}`); return []; } }, async getSearchUrl(query: string): Promise { const lowerQuery = query.toLocaleLowerCase().trim(); if (searchCache.has(lowerQuery)) return searchCache.get(lowerQuery) ?? null; const track = (await this.getSearchFull(query) ?? [])?.[0]; if (track.url) searchCache.set(lowerQuery, track.url); return track.url; } }