import fp from 'fastify-plugin'; import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId } from './api.js'; import bots from '../../config/bots.js'; import { CATEGORY_IDS } from '../../config/index.js'; import { withTransaction } from '../../utils/transaction.js'; import { syncScheduleById } from '../meilisearch/index.js'; const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE; const PLAYLIST_CACHE_PREFIX = 'yt_uploads:'; async function youtubeBotPlugin(fastify, opts) { /** * uploads playlist ID 조회 (Redis 캐싱) */ async function getCachedUploadsPlaylistId(channelId) { const cacheKey = `${PLAYLIST_CACHE_PREFIX}${channelId}`; // Redis 캐시 확인 const cached = await fastify.redis.get(cacheKey); if (cached) { return cached; } // API 호출 후 캐싱 (영구 저장 - 값이 변하지 않음) const playlistId = await getUploadsPlaylistId(channelId); await fastify.redis.set(cacheKey, playlistId); return playlistId; } /** * 멤버 이름 맵 조회 */ async function getMemberNameMap() { const [rows] = await fastify.db.query('SELECT id, name FROM members'); const map = {}; for (const r of rows) { map[r.name] = r.id; } return map; } /** * description에서 멤버 추출 */ function extractMemberIds(description, memberNameMap) { if (!description) return []; const ids = []; for (const [name, id] of Object.entries(memberNameMap)) { if (description.includes(name)) { ids.push(id); } } return ids; } /** * 영상을 DB에 저장 */ async function saveVideo(video, bot) { // 중복 체크 (video_id로) - 트랜잭션 전에 수행 const [existing] = await fastify.db.query( 'SELECT id FROM schedule_youtube WHERE video_id = ?', [video.videoId] ); if (existing.length > 0) { return null; } // 커스텀 설정 적용 if (bot.titleFilter && !video.title.includes(bot.titleFilter)) { return null; } // 멤버 이름 맵 미리 조회 (트랜잭션 전에) let nameMap = null; if (bot.extractMembersFromDesc) { nameMap = await getMemberNameMap(); } // 트랜잭션으로 INSERT 작업 수행 return withTransaction(fastify.db, async (connection) => { // schedules 테이블에 저장 const [result] = await connection.query( 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', [YOUTUBE_CATEGORY_ID, video.title, video.date, video.time] ); const scheduleId = result.insertId; // schedule_youtube 테이블에 저장 await connection.query( 'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)', [scheduleId, video.videoId, video.videoType, video.channelId, bot.channelName] ); // 멤버 연결 (커스텀 설정) if (bot.defaultMemberId || bot.extractMembersFromDesc) { const memberIds = []; if (bot.defaultMemberId) { memberIds.push(bot.defaultMemberId); } if (nameMap) { memberIds.push(...extractMemberIds(video.description, nameMap)); } if (memberIds.length > 0) { const uniqueIds = [...new Set(memberIds)]; const values = uniqueIds.map(id => [scheduleId, id]); await connection.query( 'INSERT INTO schedule_members (schedule_id, member_id) VALUES ?', [values] ); } } return scheduleId; }); } /** * 최근 영상 동기화 (정기 실행) */ async function syncNewVideos(bot) { const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId); const videos = await fetchRecentVideos(bot.channelId, 10, uploadsPlaylistId); let addedCount = 0; 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 }; } /** * 전체 영상 동기화 (초기화) */ async function syncAllVideos(bot) { const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId); const videos = await fetchAllVideos(bot.channelId, uploadsPlaylistId); let addedCount = 0; 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 }; } /** * 관리 중인 채널 ID 목록 */ function getManagedChannelIds() { return bots .filter(b => b.type === 'youtube') .map(b => b.channelId); } fastify.decorate('youtubeBot', { syncNewVideos, syncAllVideos, getManagedChannelIds, }); } export default fp(youtubeBotPlugin, { name: 'youtubeBot', dependencies: ['db', 'redis'], });