2026-01-16 21:11:02 +09:00
|
|
|
import fp from 'fastify-plugin';
|
2026-01-19 12:32:04 +09:00
|
|
|
import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId } from './api.js';
|
2026-01-16 21:11:02 +09:00
|
|
|
import bots from '../../config/bots.js';
|
2026-01-21 13:45:08 +09:00
|
|
|
import { CATEGORY_IDS } from '../../config/index.js';
|
2026-01-16 21:11:02 +09:00
|
|
|
|
2026-01-21 13:45:08 +09:00
|
|
|
const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE;
|
2026-01-19 12:32:04 +09:00
|
|
|
const PLAYLIST_CACHE_PREFIX = 'yt_uploads:';
|
2026-01-16 21:11:02 +09:00
|
|
|
|
|
|
|
|
async function youtubeBotPlugin(fastify, opts) {
|
2026-01-19 12:32:04 +09:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 21:11:02 +09:00
|
|
|
/**
|
|
|
|
|
* 멤버 이름 맵 조회
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// schedules 테이블에 저장
|
|
|
|
|
const [result] = await fastify.db.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 fastify.db.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 (bot.extractMembersFromDesc) {
|
|
|
|
|
const nameMap = await getMemberNameMap();
|
|
|
|
|
memberIds.push(...extractMemberIds(video.description, nameMap));
|
|
|
|
|
}
|
|
|
|
|
if (memberIds.length > 0) {
|
|
|
|
|
const uniqueIds = [...new Set(memberIds)];
|
|
|
|
|
const values = uniqueIds.map(id => [scheduleId, id]);
|
|
|
|
|
await fastify.db.query(
|
|
|
|
|
'INSERT INTO schedule_members (schedule_id, member_id) VALUES ?',
|
|
|
|
|
[values]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return scheduleId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 최근 영상 동기화 (정기 실행)
|
|
|
|
|
*/
|
|
|
|
|
async function syncNewVideos(bot) {
|
2026-01-19 12:32:04 +09:00
|
|
|
const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId);
|
|
|
|
|
const videos = await fetchRecentVideos(bot.channelId, 10, uploadsPlaylistId);
|
2026-01-16 21:11:02 +09:00
|
|
|
let addedCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const video of videos) {
|
|
|
|
|
const scheduleId = await saveVideo(video, bot);
|
|
|
|
|
if (scheduleId) {
|
|
|
|
|
addedCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { addedCount, total: videos.length };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 전체 영상 동기화 (초기화)
|
|
|
|
|
*/
|
|
|
|
|
async function syncAllVideos(bot) {
|
2026-01-19 12:32:04 +09:00
|
|
|
const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId);
|
|
|
|
|
const videos = await fetchAllVideos(bot.channelId, uploadsPlaylistId);
|
2026-01-16 21:11:02 +09:00
|
|
|
let addedCount = 0;
|
|
|
|
|
|
|
|
|
|
for (const video of videos) {
|
|
|
|
|
const scheduleId = await saveVideo(video, bot);
|
|
|
|
|
if (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',
|
2026-01-19 12:32:04 +09:00
|
|
|
dependencies: ['db', 'redis'],
|
2026-01-16 21:11:02 +09:00
|
|
|
});
|