first commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { Innertube } from 'youtubei.js';
|
||||
import { AppError } from '../../utils/errors.js';
|
||||
import { canonicalSourceUrl, extractYouTubeId } from '../../utils/url.js';
|
||||
|
||||
function textValue(value) {
|
||||
if (value == null) return '';
|
||||
return typeof value === 'string' ? value : value.toString();
|
||||
}
|
||||
|
||||
export function readTranscriptSegments(transcriptInfo) {
|
||||
const segments = transcriptInfo?.transcript?.content?.body?.initial_segments ?? [];
|
||||
return segments
|
||||
.filter((segment) => segment?.snippet && segment?.start_ms != null)
|
||||
.map((segment) => ({
|
||||
startSec: Number(segment.start_ms) / 1000,
|
||||
endSec: Number(segment.end_ms) / 1000,
|
||||
text: textValue(segment.snippet).trim(),
|
||||
}))
|
||||
.filter((segment) => Number.isFinite(segment.startSec) && segment.text);
|
||||
}
|
||||
|
||||
export class YouTubeExtractor {
|
||||
constructor({ clientFactory } = {}) {
|
||||
this.clientFactory = clientFactory ?? (() => Innertube.create({
|
||||
lang: 'ko',
|
||||
location: 'KR',
|
||||
retrieve_player: false,
|
||||
}));
|
||||
}
|
||||
|
||||
async extract(sourceUrl) {
|
||||
const sourceId = extractYouTubeId(sourceUrl);
|
||||
let info;
|
||||
|
||||
try {
|
||||
const client = await this.clientFactory();
|
||||
info = await client.getInfo(sourceId);
|
||||
} catch (error) {
|
||||
throw new AppError('YouTube 정보를 가져오지 못했습니다.', {
|
||||
statusCode: 502,
|
||||
code: 'YOUTUBE_EXTRACTION_FAILED',
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
const basic = info.basic_info ?? {};
|
||||
const description = basic.short_description?.trim() ?? '';
|
||||
let transcriptSegments = [];
|
||||
|
||||
try {
|
||||
transcriptSegments = readTranscriptSegments(await info.getTranscript());
|
||||
} catch {
|
||||
// 자막이 비활성화된 영상은 설명만으로 계속 분석한다.
|
||||
}
|
||||
|
||||
if (!description && transcriptSegments.length === 0) {
|
||||
throw new AppError('YouTube 자막과 설명을 찾을 수 없습니다.', {
|
||||
statusCode: 422,
|
||||
code: 'YOUTUBE_TEXT_NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
||||
const transcriptText = transcriptSegments
|
||||
.map((segment) => `[${segment.startSec}] ${segment.text}`)
|
||||
.join('\n');
|
||||
const rawText = [
|
||||
description && `[DESCRIPTION]\n${description}`,
|
||||
transcriptText && `[TRANSCRIPT]\n${transcriptText}`,
|
||||
].filter(Boolean).join('\n\n');
|
||||
const thumbnails = basic.thumbnail ?? [];
|
||||
const thumbnail = thumbnails.length > 0 ? thumbnails[thumbnails.length - 1] : null;
|
||||
|
||||
return {
|
||||
platform: 'youtube',
|
||||
sourceUrl: canonicalSourceUrl(sourceUrl),
|
||||
sourceId,
|
||||
title: basic.title?.trim() || null,
|
||||
author: basic.author?.trim() || basic.channel?.name?.trim() || null,
|
||||
thumbnailUrl: thumbnail?.url ?? null,
|
||||
rawText,
|
||||
metadata: {
|
||||
durationSec: basic.duration ?? null,
|
||||
transcript: transcriptSegments,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user