refactor(youtube): API 호출 최적화 - 새 영상만 상세 조회

기존: 매 폴링마다 activities.list + videos.list (2 units)
변경: activities.list로 videoId 확인 후 DB에 없는 새 영상만 videos.list 호출
결과: 일일 API 사용량 약 50% 감소 (1분 간격 3채널도 가능)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-03-02 15:52:42 +09:00
parent f8acb5450f
commit 45adaaf0dc
3 changed files with 32 additions and 48 deletions

View file

@ -257,6 +257,7 @@ async function schedulerPlugin(fastify, opts) {
}
}
}
}
/**

View file

@ -129,12 +129,11 @@ async function getVideoDurations(videoIds) {
}
/**
* 최근 N개 영상 조회 (Activities API 사용 - playlistItems보다 빠른 반영)
* 최근 영상 ID 목록만 조회 (Activities API - 1 unit)
* @param {string} channelId - 채널 ID
* @param {number} maxResults - 최대 결과
*/
export async function fetchRecentVideos(channelId, maxResults = 10) {
// Activities API에 type=upload 지정 (다른 활동이 섞일 수 있어 2배 조회)
export async function fetchRecentVideoIds(channelId, maxResults = 10) {
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 res = await fetch(url);
@ -144,46 +143,10 @@ export async function fetchRecentVideos(channelId, maxResults = 10) {
throw new Error(data.error.message);
}
// 'upload' 타입만 필터링 (API가 완벽히 필터링하지 않을 수 있음)
const uploadItems = (data.items || [])
return (data.items || [])
.filter(item => item.snippet.type === 'upload')
.slice(0, maxResults);
if (uploadItems.length === 0) {
return [];
}
// video ID 목록 추출
const videoIds = uploadItems.map(item => item.contentDetails.upload.videoId);
// videos API로 상세 정보 조회 (duration, description 등)
const videosUrl = `${API_BASE}/videos?part=snippet,contentDetails&id=${videoIds.join(',')}&key=${API_KEY}`;
const videosRes = await fetch(videosUrl);
const videosData = await videosRes.json();
if (videosData.error) {
throw new Error(videosData.error.message);
}
return (videosData.items || []).map(video => {
const { snippet, contentDetails } = video;
const seconds = parseDuration(contentDetails.duration);
const isShorts = seconds > 0 && seconds <= 60;
const publishedAt = new Date(snippet.publishedAt);
return {
videoId: video.id,
title: snippet.title,
description: snippet.description || '',
channelId: snippet.channelId,
channelTitle: snippet.channelTitle,
publishedAt,
date: formatDate(publishedAt),
time: formatTime(publishedAt),
videoType: isShorts ? 'shorts' : 'video',
videoUrl: getVideoUrl(video.id, isShorts),
};
});
.slice(0, maxResults)
.map(item => item.contentDetails.upload.videoId);
}
/**

View file

@ -1,5 +1,5 @@
import fp from 'fastify-plugin';
import { fetchRecentVideos, fetchAllVideos } from './api.js';
import { fetchRecentVideoIds, fetchVideoInfo, fetchAllVideos } from './api.js';
import { CATEGORY_IDS } from '../../config/index.js';
import { withTransaction } from '../../utils/transaction.js';
import { syncScheduleById, deleteSchedule } from '../meilisearch/index.js';
@ -337,19 +337,38 @@ async function youtubeBotPlugin(fastify) {
await checkScheduledDeadline(bot);
}
const videos = await fetchRecentVideos(bot.channelId, 10);
let addedCount = 0;
// 1. 최근 영상 ID 목록만 조회 (activities.list - 1 unit)
const videoIds = await fetchRecentVideoIds(bot.channelId, 10);
if (videoIds.length === 0) {
return { addedCount: 0, total: 0 };
}
// 2. DB에서 이미 존재하는 영상 필터링
const [existing] = await fastify.db.query(
'SELECT video_id FROM schedule_youtube WHERE video_id IN (?)',
[videoIds]
);
const existingIds = new Set(existing.map(r => r.video_id));
const newVideoIds = videoIds.filter(id => !existingIds.has(id));
if (newVideoIds.length === 0) {
return { addedCount: 0, total: videoIds.length };
}
// 3. 새 영상만 상세 정보 조회 (videos.list - 새 영상당 1 unit)
let addedCount = 0;
for (const videoId of newVideoIds) {
const video = await fetchVideoInfo(videoId);
if (!video) continue;
for (const video of videos) {
const scheduleId = await saveVideo(video, bot);
if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++;
}
}
return { addedCount, total: videos.length };
return { addedCount, total: videoIds.length };
}
/**
@ -385,6 +404,7 @@ async function youtubeBotPlugin(fastify) {
syncNewVideos,
syncAllVideos,
getManagedChannelIds,
saveVideo,
});
}