feat(route/youtube): subtitle attachment (#19772)
* feat(route/youtube): subtitle attachment * chore: update * update
This commit is contained in:
parent
74f0d40244
commit
37507f6553
|
|
@ -1,3 +1,4 @@
|
|||
import { getSubtitles } from 'youtube-caption-extractor';
|
||||
import cache from '@/utils/cache';
|
||||
import { Innertube } from 'youtubei.js';
|
||||
import utils, { getVideoUrl } from '../utils';
|
||||
|
|
@ -6,6 +7,38 @@ import { parseRelativeDate } from '@/utils/parse-date';
|
|||
|
||||
const innertubePromise = Innertube.create();
|
||||
|
||||
function pad(n: number, width: number = 2) {
|
||||
return String(n).padStart(width, '0');
|
||||
}
|
||||
|
||||
function toSrtTime(seconds: number): string {
|
||||
const totalMs = Math.floor(seconds * 1000);
|
||||
const hours = Math.floor(totalMs / 3_600_000);
|
||||
const minutes = Math.floor((totalMs % 3_600_000) / 60000);
|
||||
const secs = Math.floor((totalMs % 60000) / 1000);
|
||||
const millis = totalMs % 1000;
|
||||
return `${pad(hours)}:${pad(minutes)}:${pad(secs)},${pad(millis, 3)}`;
|
||||
}
|
||||
|
||||
type Subtitle = {
|
||||
start: string;
|
||||
dur: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
function convertToSrt(segments: Subtitle[]): string {
|
||||
return segments
|
||||
.map((seg, index) => {
|
||||
const start = Number.parseFloat(seg.start);
|
||||
const end = start + Number.parseFloat(seg.dur);
|
||||
return `${index + 1}
|
||||
${toSrtTime(start)} --> ${toSrtTime(end)}
|
||||
${seg.text}
|
||||
`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export const getChannelIdByUsername = (username: string) =>
|
||||
cache.tryGet(`youtube:getChannelIdByUsername:${username}`, async () => {
|
||||
const innertube = await innertubePromise;
|
||||
|
|
@ -29,27 +62,38 @@ export const getDataByChannelId = async ({ channelId, embed }: { channelId: stri
|
|||
image: channel.metadata.avatar?.[0].url,
|
||||
description: channel.metadata.description,
|
||||
|
||||
item: videos.videos
|
||||
.filter((video) => 'video_id' in video)
|
||||
.map((video) => {
|
||||
const img = 'best_thumbnail' in video ? video.best_thumbnail?.url : ('thumbnails' in video ? video.thumbnails?.[0]?.url : undefined);
|
||||
item: await Promise.all(
|
||||
videos.videos
|
||||
.filter((video) => 'video_id' in video)
|
||||
.map(async (video) => {
|
||||
const subtitles = await getSubtitles({ videoID: video.video_id });
|
||||
const srt = convertToSrt(subtitles);
|
||||
const dataUrl = `data:text/plain;charset=utf-8,${encodeURIComponent(srt)}`;
|
||||
|
||||
return {
|
||||
title: video.title.text || `YouTube Video ${video.video_id}`,
|
||||
description: 'description_snippet' in video ? utils.renderDescription(embed, video.video_id, img, utils.formatDescription(video.description_snippet?.toHTML())) : null,
|
||||
link: `https://www.youtube.com/watch?v=${video.video_id}`,
|
||||
author: typeof video.author === 'string' ? video.author : (video.author.name === 'N/A' ? undefined : video.author.name),
|
||||
image: img,
|
||||
pubDate: 'published' in video && video.published?.text ? parseRelativeDate(video.published.text) : undefined,
|
||||
attachments: [
|
||||
{
|
||||
url: getVideoUrl(video.video_id),
|
||||
mime_type: 'text/html',
|
||||
duration_in_seconds: video.duration && 'seconds' in video.duration ? video.duration.seconds : undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
const img = 'best_thumbnail' in video ? video.best_thumbnail?.url : ('thumbnails' in video ? video.thumbnails?.[0]?.url : undefined);
|
||||
|
||||
return {
|
||||
title: video.title.text || `YouTube Video ${video.video_id}`,
|
||||
description: 'description_snippet' in video ? utils.renderDescription(embed, video.video_id, img, utils.formatDescription(video.description_snippet?.toHTML())) : null,
|
||||
link: `https://www.youtube.com/watch?v=${video.video_id}`,
|
||||
author: typeof video.author === 'string' ? video.author : (video.author.name === 'N/A' ? undefined : video.author.name),
|
||||
image: img,
|
||||
pubDate: 'published' in video && video.published?.text ? parseRelativeDate(video.published.text) : undefined,
|
||||
attachments: [
|
||||
{
|
||||
url: getVideoUrl(video.video_id),
|
||||
mime_type: 'text/html',
|
||||
duration_in_seconds: video.duration && 'seconds' in video.duration ? video.duration.seconds : undefined,
|
||||
},
|
||||
{
|
||||
url: dataUrl,
|
||||
mime_type: 'text/srt',
|
||||
title: 'Subtitles',
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@
|
|||
"uuid": "11.1.0",
|
||||
"winston": "3.17.0",
|
||||
"xxhash-wasm": "1.1.0",
|
||||
"youtube-caption-extractor": "1.8.2",
|
||||
"youtubei.js": "15.0.1",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -287,6 +287,9 @@ importers:
|
|||
xxhash-wasm:
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0
|
||||
youtube-caption-extractor:
|
||||
specifier: 1.8.2
|
||||
version: 1.8.2
|
||||
youtubei.js:
|
||||
specifier: 15.0.1
|
||||
version: 15.0.1
|
||||
|
|
@ -5446,6 +5449,9 @@ packages:
|
|||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
striptags@3.2.0:
|
||||
resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==}
|
||||
|
||||
superagent@10.2.3:
|
||||
resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
|
@ -6106,6 +6112,10 @@ packages:
|
|||
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
youtube-caption-extractor@1.8.2:
|
||||
resolution: {integrity: sha512-4VNDP5rIjBmTF4Us2aPiu5c795Yf/yLqXtEQ3U33PPJsoutKrmeIyx/8NBYH0Qlzk+tEmaftQ7/6a6TgfYNtVA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
youtubei.js@15.0.1:
|
||||
resolution: {integrity: sha512-2slapqJS5NuXKHvcACEknyVz0AjH/TrXaOhDM0q2twQKa54kCmfj+7B/2nGfd20uzAe29zW1ejk2qOc4ABuGkg==}
|
||||
|
||||
|
|
@ -11649,6 +11659,8 @@ snapshots:
|
|||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
striptags@3.2.0: {}
|
||||
|
||||
superagent@10.2.3:
|
||||
dependencies:
|
||||
component-emitter: 1.3.1
|
||||
|
|
@ -12307,6 +12319,11 @@ snapshots:
|
|||
|
||||
yoctocolors-cjs@2.1.2: {}
|
||||
|
||||
youtube-caption-extractor@1.8.2:
|
||||
dependencies:
|
||||
he: 1.2.0
|
||||
striptags: 3.2.0
|
||||
|
||||
youtubei.js@15.0.1:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.6.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue