diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index 7db9623..6b07cb9 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -257,6 +257,7 @@ async function schedulerPlugin(fastify, opts) { } } } + } /** diff --git a/backend/src/services/youtube/api.js b/backend/src/services/youtube/api.js index 6faad9b..039d292 100644 --- a/backend/src/services/youtube/api.js +++ b/backend/src/services/youtube/api.js @@ -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); } /** diff --git a/backend/src/services/youtube/index.js b/backend/src/services/youtube/index.js index 51e6f08..e4a9de4 100644 --- a/backend/src/services/youtube/index.js +++ b/backend/src/services/youtube/index.js @@ -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, }); }