Add music player
This commit is contained in:
1 parent
c820672803
commit
40604b4a04
17 files changed
+1153
No files matched your search
@@ -0,0 +1,78 @@
|
||||
import { Quality, Ytdl, parseVideoId } from '@distube/ytdl-core';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { Readable } from 'node:stream';
|
||||
import { playlist } from '#lib/content';
|
||||
import type { Track } from '#lib/music/types';
|
||||
|
||||
const downloadBufferBytes = 32 * 1024 * 1024;
|
||||
const mp3Bitrate = '160k';
|
||||
|
||||
const youtube = new Ytdl();
|
||||
|
||||
const playlistVideoIds = playlist.videos.flatMap((video) => {
|
||||
const videoId = parseVideoId(video);
|
||||
return videoId ? [videoId] : [];
|
||||
});
|
||||
const allowedVideoIds = new Set(playlistVideoIds);
|
||||
const trackCache = new Map<string, Track>();
|
||||
|
||||
export function isPlaylistVideo(videoId: string): boolean {
|
||||
return allowedVideoIds.has(videoId);
|
||||
}
|
||||
|
||||
async function getTrack(videoId: string): Promise<Track> {
|
||||
const cachedTrack = trackCache.get(videoId);
|
||||
if (cachedTrack) return cachedTrack;
|
||||
|
||||
const { videoDetails } = await youtube.getInfo(videoId);
|
||||
const track: Track = {
|
||||
id: videoId,
|
||||
title: videoDetails.title,
|
||||
artist: videoDetails.author.name,
|
||||
durationSeconds: videoDetails.lengthSeconds,
|
||||
cover: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
|
||||
};
|
||||
trackCache.set(videoId, track);
|
||||
return track;
|
||||
}
|
||||
|
||||
export async function getPlaylistTracks(): Promise<Track[]> {
|
||||
const results = await Promise.allSettled(playlistVideoIds.map(getTrack));
|
||||
return results.flatMap((result, index) => {
|
||||
if (result.status === 'fulfilled') return [result.value];
|
||||
console.error(`Music lookup failed for ${playlistVideoIds[index]}:`, result.reason);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
export function streamTrackAsMp3(videoId: string, startSeconds: number): ReadableStream<Uint8Array> {
|
||||
const downloadController = new AbortController();
|
||||
const youtubeAudio = youtube.download(videoId, {
|
||||
quality: Quality.HighestAudio,
|
||||
filter: { hasAudio: true, hasVideo: false },
|
||||
highWaterMark: downloadBufferBytes,
|
||||
signal: downloadController.signal
|
||||
});
|
||||
const ffmpegArguments = ['-loglevel', 'error', '-ss', String(startSeconds), '-i', 'pipe:0', '-vn', '-f', 'mp3', '-b:a', mp3Bitrate, 'pipe:1'];
|
||||
const ffmpeg = spawn('ffmpeg', ffmpegArguments, { stdio: ['pipe', 'pipe', 'inherit'] });
|
||||
|
||||
const stop = () => {
|
||||
downloadController.abort();
|
||||
youtubeAudio.destroy();
|
||||
if (ffmpeg.exitCode === null) ffmpeg.kill('SIGKILL');
|
||||
};
|
||||
|
||||
youtubeAudio.on('error', (error) => {
|
||||
console.error(`Music download failed for ${videoId}:`, error);
|
||||
stop();
|
||||
});
|
||||
ffmpeg.on('error', (error) => {
|
||||
console.error('ffmpeg failed to start:', error);
|
||||
stop();
|
||||
});
|
||||
ffmpeg.stdin.on('error', stop);
|
||||
ffmpeg.stdout.on('close', stop);
|
||||
youtubeAudio.pipe(ffmpeg.stdin);
|
||||
|
||||
return Readable.toWeb(ffmpeg.stdout) as ReadableStream<Uint8Array>;
|
||||
}
|
||||
Reference in new issue
Block a user