fromis_9/backend/src/services/youtube/api.js

187 lines
5.4 KiB
JavaScript
Raw Normal View History

import config from '../../config/index.js';
import { formatDate, formatTime } from '../../utils/date.js';
const API_KEY = config.google.apiKey;
const API_BASE = 'https://www.googleapis.com/youtube/v3';
/**
* ISO 8601 duration (PT1M30S) 변환
*/
function parseDuration(duration) {
const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!match) return 0;
return (
parseInt(match[1] || 0) * 3600 +
parseInt(match[2] || 0) * 60 +
parseInt(match[3] || 0)
);
}
/**
* 영상 URL 생성
*/
function getVideoUrl(videoId, isShorts) {
return isShorts
? `https://www.youtube.com/shorts/${videoId}`
: `https://www.youtube.com/watch?v=${videoId}`;
}
/**
* 채널의 업로드 플레이리스트 ID 조회
*/
export async function getUploadsPlaylistId(channelId) {
const url = `${API_BASE}/channels?part=contentDetails&id=${channelId}&key=${API_KEY}`;
const res = await fetch(url);
const data = await res.json();
if (data.error) {
throw new Error(data.error.message);
}
if (!data.items?.length) {
throw new Error('채널을 찾을 수 없습니다');
}
return data.items[0].contentDetails.relatedPlaylists.uploads;
}
/**
* 영상 ID 목록으로 duration 조회 (Shorts 판별용)
*/
async function getVideoDurations(videoIds) {
const url = `${API_BASE}/videos?part=contentDetails&id=${videoIds.join(',')}&key=${API_KEY}`;
const res = await fetch(url);
const data = await res.json();
const durations = {};
if (data.items) {
for (const v of data.items) {
const seconds = parseDuration(v.contentDetails.duration);
durations[v.id] = seconds <= 60;
}
}
return durations;
}
/**
* 최근 N개 영상 조회
* @param {string} channelId - 채널 ID
* @param {number} maxResults - 최대 결과
* @param {string} uploadsPlaylistId - 캐싱된 uploads playlist ID (선택)
*/
export async function fetchRecentVideos(channelId, maxResults = 10, uploadsPlaylistId = null) {
const uploadsId = uploadsPlaylistId || await getUploadsPlaylistId(channelId);
const url = `${API_BASE}/playlistItems?part=snippet&playlistId=${uploadsId}&maxResults=${maxResults}&key=${API_KEY}`;
const res = await fetch(url);
const data = await res.json();
if (data.error) {
throw new Error(data.error.message);
}
const videoIds = data.items.map(item => item.snippet.resourceId.videoId);
const shortsMap = await getVideoDurations(videoIds);
return data.items.map(item => {
const { snippet } = item;
const videoId = snippet.resourceId.videoId;
const isShorts = shortsMap[videoId] || false;
const publishedAt = new Date(snippet.publishedAt);
return {
videoId,
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(videoId, isShorts),
};
});
}
/**
* 전체 영상 조회 (페이지네이션)
* @param {string} channelId - 채널 ID
* @param {string} uploadsPlaylistId - 캐싱된 uploads playlist ID (선택)
*/
export async function fetchAllVideos(channelId, uploadsPlaylistId = null) {
const uploadsId = uploadsPlaylistId || await getUploadsPlaylistId(channelId);
const videos = [];
let pageToken = '';
do {
const url = `${API_BASE}/playlistItems?part=snippet&playlistId=${uploadsId}&maxResults=50&key=${API_KEY}${pageToken ? `&pageToken=${pageToken}` : ''}`;
const res = await fetch(url);
const data = await res.json();
if (data.error) {
throw new Error(data.error.message);
}
const videoIds = data.items.map(item => item.snippet.resourceId.videoId);
const shortsMap = await getVideoDurations(videoIds);
for (const item of data.items) {
const { snippet } = item;
const videoId = snippet.resourceId.videoId;
const isShorts = shortsMap[videoId] || false;
const publishedAt = new Date(snippet.publishedAt);
videos.push({
videoId,
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(videoId, isShorts),
});
}
pageToken = data.nextPageToken || '';
} while (pageToken);
// 과거순 정렬
videos.sort((a, b) => a.publishedAt - b.publishedAt);
return videos;
}
/**
* 단일 영상 정보 조회
*/
export async function fetchVideoInfo(videoId) {
const url = `${API_BASE}/videos?part=snippet,contentDetails&id=${videoId}&key=${API_KEY}`;
const res = await fetch(url);
const data = await res.json();
if (!data.items?.length) {
return null;
}
const video = data.items[0];
const { snippet, contentDetails } = video;
const seconds = parseDuration(contentDetails.duration);
const isShorts = seconds > 0 && seconds <= 60;
const publishedAt = new Date(snippet.publishedAt);
return {
videoId,
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(videoId, isShorts),
};
}