fix(youtube): YouTube API fetch에 타임아웃 추가

업스트림 행 시 봇 동기화/요청 핸들러가 무한 대기하던 문제 방지.
fetchWithTimeout(AbortController, 10s)으로 7개 fetch 호출 일괄 래핑.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-07 15:42:53 +09:00
parent 6e559a52b7
commit b0a2e69711

View file

@ -3,6 +3,20 @@ import { formatDate, formatTime } from '../../utils/date.js';
const API_KEY = config.google.apiKey; const API_KEY = config.google.apiKey;
const API_BASE = 'https://www.googleapis.com/youtube/v3'; const API_BASE = 'https://www.googleapis.com/youtube/v3';
const FETCH_TIMEOUT = 10000;
/**
* 타임아웃이 있는 fetch (업스트림 행으로 /요청이 무한 대기하는 방지)
*/
async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
/** /**
* ISO 8601 duration (PT1M30S) 변환 * ISO 8601 duration (PT1M30S) 변환
@ -31,7 +45,7 @@ function getVideoUrl(videoId, isShorts) {
*/ */
export async function getUploadsPlaylistId(channelId) { export async function getUploadsPlaylistId(channelId) {
const url = `${API_BASE}/channels?part=contentDetails&id=${channelId}&key=${API_KEY}`; const url = `${API_BASE}/channels?part=contentDetails&id=${channelId}&key=${API_KEY}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
@ -52,7 +66,7 @@ export async function getChannelByHandle(handle) {
// @ 제거 // @ 제거
const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle; const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle;
const url = `${API_BASE}/channels?part=snippet,brandingSettings&forHandle=${cleanHandle}&key=${API_KEY}`; const url = `${API_BASE}/channels?part=snippet,brandingSettings&forHandle=${cleanHandle}&key=${API_KEY}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
@ -84,7 +98,7 @@ export async function getChannelByHandle(handle) {
*/ */
export async function getChannelInfo(channelId) { export async function getChannelInfo(channelId) {
const url = `${API_BASE}/channels?part=snippet,brandingSettings&id=${channelId}&key=${API_KEY}`; const url = `${API_BASE}/channels?part=snippet,brandingSettings&id=${channelId}&key=${API_KEY}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
@ -115,7 +129,7 @@ export async function getChannelInfo(channelId) {
*/ */
async function getVideoDurations(videoIds) { async function getVideoDurations(videoIds) {
const url = `${API_BASE}/videos?part=contentDetails&id=${videoIds.join(',')}&key=${API_KEY}`; const url = `${API_BASE}/videos?part=contentDetails&id=${videoIds.join(',')}&key=${API_KEY}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
const durations = {}; const durations = {};
@ -136,7 +150,7 @@ async function getVideoDurations(videoIds) {
export async function fetchRecentVideoIds(channelId, maxResults = 10) { export async function fetchRecentVideoIds(channelId, maxResults = 10) {
const fetchCount = Math.min(maxResults * 2, 50); const fetchCount = Math.min(maxResults * 2, 50);
const url = `${API_BASE}/activities?part=snippet,contentDetails&channelId=${channelId}&type=upload&maxResults=${fetchCount}&key=${API_KEY}`; const url = `${API_BASE}/activities?part=snippet,contentDetails&channelId=${channelId}&type=upload&maxResults=${fetchCount}&key=${API_KEY}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
@ -161,7 +175,7 @@ export async function fetchAllVideos(channelId, uploadsPlaylistId = null) {
do { do {
const url = `${API_BASE}/playlistItems?part=snippet&playlistId=${uploadsId}&maxResults=50&key=${API_KEY}${pageToken ? `&pageToken=${pageToken}` : ''}`; const url = `${API_BASE}/playlistItems?part=snippet&playlistId=${uploadsId}&maxResults=50&key=${API_KEY}${pageToken ? `&pageToken=${pageToken}` : ''}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
@ -204,7 +218,7 @@ export async function fetchAllVideos(channelId, uploadsPlaylistId = null) {
*/ */
export async function fetchVideoInfo(videoId) { export async function fetchVideoInfo(videoId) {
const url = `${API_BASE}/videos?part=snippet,contentDetails&id=${videoId}&key=${API_KEY}`; const url = `${API_BASE}/videos?part=snippet,contentDetails&id=${videoId}&key=${API_KEY}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const data = await res.json(); const data = await res.json();
if (!data.items?.length) { if (!data.items?.length) {