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:
parent
f8acb5450f
commit
45adaaf0dc
3 changed files with 32 additions and 48 deletions
|
|
@ -257,6 +257,7 @@ async function schedulerPlugin(fastify, opts) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue