mirror of
https://github.com/BluemediaDev/muse.git
synced 2025-04-16 19:43:56 +02:00
Refactor play command
This commit is contained in:
parent
c058ec95fe
commit
7844e80991
8 changed files with 239 additions and 210 deletions
|
@ -1,15 +1,8 @@
|
||||||
import {TextChannel, Message} from 'discord.js';
|
import {TextChannel, Message} from 'discord.js';
|
||||||
import YouTube from 'youtube.ts';
|
|
||||||
import Spotify from 'spotify-web-api-node';
|
|
||||||
import {URL} from 'url';
|
import {URL} from 'url';
|
||||||
import ytsr from 'ytsr';
|
|
||||||
import pLimit from 'p-limit';
|
|
||||||
import spotifyURI from 'spotify-uri';
|
|
||||||
import got from 'got';
|
|
||||||
import {parse, toSeconds} from 'iso8601-duration';
|
|
||||||
import {TYPES} from '../types';
|
import {TYPES} from '../types';
|
||||||
import {inject, injectable} from 'inversify';
|
import {inject, injectable} from 'inversify';
|
||||||
import {QueuedSong, QueuedPlaylist} from '../services/queue';
|
import {QueuedSong} from '../services/queue';
|
||||||
import {STATUS} from '../services/player';
|
import {STATUS} from '../services/player';
|
||||||
import QueueManager from '../managers/queue';
|
import QueueManager from '../managers/queue';
|
||||||
import PlayerManager from '../managers/player';
|
import PlayerManager from '../managers/player';
|
||||||
|
@ -17,6 +10,7 @@ import {getMostPopularVoiceChannel} from '../utils/channels';
|
||||||
import LoadingMessage from '../utils/loading-message';
|
import LoadingMessage from '../utils/loading-message';
|
||||||
import errorMsg from '../utils/error-msg';
|
import errorMsg from '../utils/error-msg';
|
||||||
import Command from '.';
|
import Command from '.';
|
||||||
|
import GetSongs from '../services/get-songs';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export default class implements Command {
|
export default class implements Command {
|
||||||
|
@ -33,16 +27,12 @@ export default class implements Command {
|
||||||
|
|
||||||
private readonly queueManager: QueueManager;
|
private readonly queueManager: QueueManager;
|
||||||
private readonly playerManager: PlayerManager;
|
private readonly playerManager: PlayerManager;
|
||||||
private readonly youtube: YouTube;
|
private readonly getSongs: GetSongs;
|
||||||
private readonly youtubeKey: string;
|
|
||||||
private readonly spotify: Spotify;
|
|
||||||
|
|
||||||
constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Lib.YouTube) youtube: YouTube, @inject(TYPES.Config.YOUTUBE_API_KEY) youtubeKey: string, @inject(TYPES.Lib.Spotify) spotify: Spotify) {
|
constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Services.GetSongs) getSongs: GetSongs) {
|
||||||
this.queueManager = queueManager;
|
this.queueManager = queueManager;
|
||||||
this.playerManager = playerManager;
|
this.playerManager = playerManager;
|
||||||
this.youtube = youtube;
|
this.getSongs = getSongs;
|
||||||
this.youtubeKey = youtubeKey;
|
|
||||||
this.spotify = spotify;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async execute(msg: Message, args: string []): Promise<void> {
|
public async execute(msg: Message, args: string []): Promise<void> {
|
||||||
|
@ -79,18 +69,7 @@ export default class implements Command {
|
||||||
|
|
||||||
const newSongs: QueuedSong[] = [];
|
const newSongs: QueuedSong[] = [];
|
||||||
|
|
||||||
const addSingleSong = async (source: string): Promise<void> => {
|
let nSongsNotFound = 0;
|
||||||
const videoDetails = await this.youtube.videos.get(source);
|
|
||||||
|
|
||||||
newSongs.push({
|
|
||||||
title: videoDetails.snippet.title,
|
|
||||||
artist: videoDetails.snippet.channelTitle,
|
|
||||||
length: toSeconds(parse(videoDetails.contentDetails.duration)),
|
|
||||||
url: videoDetails.id,
|
|
||||||
playlist: null,
|
|
||||||
isLive: videoDetails.snippet.liveBroadcastContent === 'live'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test if it's a complete URL
|
// Test if it's a complete URL
|
||||||
try {
|
try {
|
||||||
|
@ -102,169 +81,48 @@ export default class implements Command {
|
||||||
// YouTube source
|
// YouTube source
|
||||||
if (url.searchParams.get('list')) {
|
if (url.searchParams.get('list')) {
|
||||||
// YouTube playlist
|
// YouTube playlist
|
||||||
const playlist = await this.youtube.playlists.get(url.searchParams.get('list') as string);
|
newSongs.push(...await this.getSongs.youtubePlaylist(url.searchParams.get('list') as string));
|
||||||
const {items} = await this.youtube.playlists.items(url.searchParams.get('list') as string, {maxResults: '50'});
|
|
||||||
|
|
||||||
// Unfortunately, package doesn't provide a method for this
|
|
||||||
const res: any = await got('https://www.googleapis.com/youtube/v3/videos', {searchParams: {
|
|
||||||
part: 'contentDetails',
|
|
||||||
id: items.map(item => item.contentDetails.videoId).join(','),
|
|
||||||
key: this.youtubeKey
|
|
||||||
}}).json();
|
|
||||||
|
|
||||||
const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id};
|
|
||||||
|
|
||||||
items.forEach(video => {
|
|
||||||
const length = toSeconds(parse(res.items.find((i: any) => i.id === video.contentDetails.videoId).contentDetails.duration));
|
|
||||||
|
|
||||||
newSongs.push({
|
|
||||||
title: video.snippet.title,
|
|
||||||
artist: video.snippet.channelTitle,
|
|
||||||
length,
|
|
||||||
url: video.contentDetails.videoId,
|
|
||||||
playlist: queuedPlaylist,
|
|
||||||
isLive: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Single video
|
// Single video
|
||||||
try {
|
const song = await this.getSongs.youtubeVideo(url.href);
|
||||||
await addSingleSong(url.href);
|
|
||||||
} catch (error) {
|
if (song) {
|
||||||
await res.stop('that doesn\'t exist');
|
newSongs.push(song);
|
||||||
|
} else {
|
||||||
|
await res.stop(errorMsg('that doesn\'t exist'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
|
} else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
|
||||||
// Spotify source
|
const [convertedSongs, nMissing] = await this.getSongs.spotifySource(args[0]);
|
||||||
const parsed = spotifyURI.parse(args[0]);
|
|
||||||
|
|
||||||
const tracks: SpotifyApi.TrackObjectSimplified[] = [];
|
nSongsNotFound = nMissing;
|
||||||
|
|
||||||
let playlist: QueuedPlaylist | null = null;
|
newSongs.push(...convertedSongs);
|
||||||
|
|
||||||
switch (parsed.type) {
|
|
||||||
case 'album': {
|
|
||||||
const uri = parsed as spotifyURI.Album;
|
|
||||||
|
|
||||||
const [{body: album}, {body: {items}}] = await Promise.all([this.spotify.getAlbum(uri.id), this.spotify.getAlbumTracks(uri.id, {limit: 50})]);
|
|
||||||
|
|
||||||
tracks.push(...items);
|
|
||||||
|
|
||||||
playlist = {title: album.name, source: album.href};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'playlist': {
|
|
||||||
const uri = parsed as spotifyURI.Playlist;
|
|
||||||
|
|
||||||
let [{body: playlistResponse}, {body: tracksResponse}] = await Promise.all([this.spotify.getPlaylist(uri.id), this.spotify.getPlaylistTracks(uri.id, {limit: 1})]);
|
|
||||||
|
|
||||||
playlist = {title: playlistResponse.name, source: playlistResponse.href};
|
|
||||||
|
|
||||||
tracks.push(...tracksResponse.items.map(playlistItem => playlistItem.track));
|
|
||||||
|
|
||||||
while (tracksResponse.next) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
({body: tracksResponse} = await this.spotify.getPlaylistTracks(uri.id, {
|
|
||||||
limit: parseInt(new URL(tracksResponse.next).searchParams.get('limit') ?? '1', 10),
|
|
||||||
offset: parseInt(new URL(tracksResponse.next).searchParams.get('offset') ?? '0', 10)
|
|
||||||
}));
|
|
||||||
|
|
||||||
tracks.push(...tracksResponse.items.map(playlistItem => playlistItem.track));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'track': {
|
|
||||||
const uri = parsed as spotifyURI.Track;
|
|
||||||
|
|
||||||
const {body} = await this.spotify.getTrack(uri.id);
|
|
||||||
|
|
||||||
tracks.push(body);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'artist': {
|
|
||||||
// Await res.stop('ope, can\'t add a whole artist');
|
|
||||||
const uri = parsed as spotifyURI.Artist;
|
|
||||||
|
|
||||||
const {body} = await this.spotify.getArtistTopTracks(uri.id, 'US');
|
|
||||||
|
|
||||||
tracks.push(...body.tracks);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
await res.stop('huh?');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search YouTube for each track
|
|
||||||
const searchForTrack = async (track: SpotifyApi.TrackObjectSimplified): Promise<QueuedSong|null> => {
|
|
||||||
try {
|
|
||||||
const {items} = await ytsr(`${track.name} ${track.artists[0].name} offical`, {limit: 5});
|
|
||||||
const video = items.find((item: { type: string }) => item.type === 'video');
|
|
||||||
|
|
||||||
if (!video) {
|
|
||||||
throw new Error('No video found for query.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: video.title,
|
|
||||||
artist: track.artists[0].name,
|
|
||||||
length: track.duration_ms / 1000,
|
|
||||||
url: video.link,
|
|
||||||
playlist,
|
|
||||||
isLive: video.live
|
|
||||||
};
|
|
||||||
} catch (_) {
|
|
||||||
// TODO: handle error
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Limit concurrency so hopefully we don't get banned
|
|
||||||
const limit = pLimit(3);
|
|
||||||
let songs = await Promise.all(tracks.map(async track => limit(async () => searchForTrack(track))));
|
|
||||||
|
|
||||||
// Get rid of null values
|
|
||||||
songs = songs.reduce((accum: QueuedSong[], song) => {
|
|
||||||
if (song) {
|
|
||||||
accum.push(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
return accum;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
newSongs.push(...(songs as QueuedSong[]));
|
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Not a URL, must search YouTube
|
// Not a URL, must search YouTube
|
||||||
const query = args.join(' ');
|
const query = args.join(' ');
|
||||||
|
|
||||||
try {
|
const song = await this.getSongs.youtubeVideoSearch(query);
|
||||||
const {items: [video]} = await this.youtube.videos.search({q: query, maxResults: 1, type: 'video'});
|
|
||||||
|
|
||||||
await addSingleSong(video.id.videoId);
|
if (song) {
|
||||||
} catch (_) {
|
newSongs.push(song);
|
||||||
await res.stop('that doesn\'t exist');
|
} else {
|
||||||
|
await res.stop(errorMsg('that doesn\'t exist'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSongs.length === 0) {
|
if (newSongs.length === 0) {
|
||||||
// TODO: better response
|
await res.stop(errorMsg('no songs found'));
|
||||||
await res.stop('huh?');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
newSongs.forEach(song => this.queueManager.get(msg.guild!.id).add(song));
|
newSongs.forEach(song => this.queueManager.get(msg.guild!.id).add(song));
|
||||||
|
|
||||||
// TODO: better response
|
// TODO: better response
|
||||||
await res.stop('song(s) queued');
|
await res.stop(`song(s) queued (${nSongsNotFound} not found)`);
|
||||||
|
|
||||||
if (this.playerManager.get(msg.guild!.id).voiceConnection === null) {
|
if (this.playerManager.get(msg.guild!.id).voiceConnection === null) {
|
||||||
await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel);
|
await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel);
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default class implements Command {
|
||||||
|
|
||||||
await msg.channel.send('keep \'er movin\'');
|
await msg.channel.send('keep \'er movin\'');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
console.log(_);
|
||||||
await msg.channel.send('no song to skip to');
|
await msg.channel.send('no song to skip to');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ import {
|
||||||
import PlayerManager from './managers/player';
|
import PlayerManager from './managers/player';
|
||||||
import QueueManager from './managers/queue';
|
import QueueManager from './managers/queue';
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
import GetSongs from './services/get-songs';
|
||||||
|
|
||||||
// Comands
|
// Comands
|
||||||
import Command from './commands';
|
import Command from './commands';
|
||||||
import Clear from './commands/clear';
|
import Clear from './commands/clear';
|
||||||
|
@ -44,6 +47,9 @@ container.bind<Client>(TYPES.Client).toConstantValue(new Client());
|
||||||
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
|
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
|
||||||
container.bind<QueueManager>(TYPES.Managers.Queue).to(QueueManager).inSingletonScope();
|
container.bind<QueueManager>(TYPES.Managers.Queue).to(QueueManager).inSingletonScope();
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
container.bind<GetSongs>(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope();
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
container.bind<Command>(TYPES.Command).to(Clear).inSingletonScope();
|
container.bind<Command>(TYPES.Command).to(Clear).inSingletonScope();
|
||||||
container.bind<Command>(TYPES.Command).to(Config).inSingletonScope();
|
container.bind<Command>(TYPES.Command).to(Config).inSingletonScope();
|
||||||
|
|
185
src/services/get-songs.ts
Normal file
185
src/services/get-songs.ts
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
import {URL} from 'url';
|
||||||
|
import {inject, injectable} from 'inversify';
|
||||||
|
import {toSeconds, parse} from 'iso8601-duration';
|
||||||
|
import got from 'got';
|
||||||
|
import spotifyURI from 'spotify-uri';
|
||||||
|
import Spotify from 'spotify-web-api-node';
|
||||||
|
import ytsr from 'ytsr';
|
||||||
|
import YouTube from 'youtube.ts';
|
||||||
|
import pLimit from 'p-limit';
|
||||||
|
import {QueuedSong, QueuedPlaylist} from '../services/queue';
|
||||||
|
import {TYPES} from '../types';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export default class {
|
||||||
|
private readonly youtube: YouTube;
|
||||||
|
private readonly youtubeKey: string;
|
||||||
|
private readonly spotify: Spotify;
|
||||||
|
|
||||||
|
constructor(@inject(TYPES.Lib.YouTube) youtube: YouTube, @inject(TYPES.Config.YOUTUBE_API_KEY) youtubeKey: string, @inject(TYPES.Lib.Spotify) spotify: Spotify) {
|
||||||
|
this.youtube = youtube;
|
||||||
|
this.youtubeKey = youtubeKey;
|
||||||
|
this.spotify = spotify;
|
||||||
|
}
|
||||||
|
|
||||||
|
async youtubeVideoSearch(query: string): Promise<QueuedSong|null> {
|
||||||
|
try {
|
||||||
|
const {items: [video]} = await this.youtube.videos.search({q: query, maxResults: 1, type: 'video'});
|
||||||
|
|
||||||
|
return await this.youtubeVideo(video.id.videoId);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async youtubeVideo(url: string): Promise<QueuedSong|null> {
|
||||||
|
try {
|
||||||
|
const videoDetails = await this.youtube.videos.get(url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: videoDetails.snippet.title,
|
||||||
|
artist: videoDetails.snippet.channelTitle,
|
||||||
|
length: toSeconds(parse(videoDetails.contentDetails.duration)),
|
||||||
|
url: videoDetails.id,
|
||||||
|
playlist: null,
|
||||||
|
isLive: videoDetails.snippet.liveBroadcastContent === 'live'
|
||||||
|
};
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async youtubePlaylist(listId: string): Promise<QueuedSong[]> {
|
||||||
|
// YouTube playlist
|
||||||
|
const playlist = await this.youtube.playlists.get(listId);
|
||||||
|
const {items} = await this.youtube.playlists.items(listId, {maxResults: '50'});
|
||||||
|
|
||||||
|
// Unfortunately, package doesn't provide a method for this
|
||||||
|
const res: any = await got('https://www.googleapis.com/youtube/v3/videos', {searchParams: {
|
||||||
|
part: 'contentDetails',
|
||||||
|
id: items.map(item => item.contentDetails.videoId).join(','),
|
||||||
|
key: this.youtubeKey
|
||||||
|
}}).json();
|
||||||
|
|
||||||
|
const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id};
|
||||||
|
|
||||||
|
return items.map(video => {
|
||||||
|
const length = toSeconds(parse(res.items.find((i: any) => i.id === video.contentDetails.videoId).contentDetails.duration));
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: video.snippet.title,
|
||||||
|
artist: video.snippet.channelTitle,
|
||||||
|
length,
|
||||||
|
url: video.contentDetails.videoId,
|
||||||
|
playlist: queuedPlaylist,
|
||||||
|
isLive: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async spotifySource(url: string): Promise<[QueuedSong[], number]> {
|
||||||
|
const parsed = spotifyURI.parse(url);
|
||||||
|
|
||||||
|
const tracks: SpotifyApi.TrackObjectSimplified[] = [];
|
||||||
|
|
||||||
|
let playlist: QueuedPlaylist | null = null;
|
||||||
|
|
||||||
|
switch (parsed.type) {
|
||||||
|
case 'album': {
|
||||||
|
const uri = parsed as spotifyURI.Album;
|
||||||
|
|
||||||
|
const [{body: album}, {body: {items}}] = await Promise.all([this.spotify.getAlbum(uri.id), this.spotify.getAlbumTracks(uri.id, {limit: 50})]);
|
||||||
|
|
||||||
|
tracks.push(...items);
|
||||||
|
|
||||||
|
playlist = {title: album.name, source: album.href};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'playlist': {
|
||||||
|
const uri = parsed as spotifyURI.Playlist;
|
||||||
|
|
||||||
|
let [{body: playlistResponse}, {body: tracksResponse}] = await Promise.all([this.spotify.getPlaylist(uri.id), this.spotify.getPlaylistTracks(uri.id, {limit: 1})]);
|
||||||
|
|
||||||
|
playlist = {title: playlistResponse.name, source: playlistResponse.href};
|
||||||
|
|
||||||
|
tracks.push(...tracksResponse.items.map(playlistItem => playlistItem.track));
|
||||||
|
|
||||||
|
while (tracksResponse.next) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
({body: tracksResponse} = await this.spotify.getPlaylistTracks(uri.id, {
|
||||||
|
limit: parseInt(new URL(tracksResponse.next).searchParams.get('limit') ?? '1', 10),
|
||||||
|
offset: parseInt(new URL(tracksResponse.next).searchParams.get('offset') ?? '0', 10)
|
||||||
|
}));
|
||||||
|
|
||||||
|
tracks.push(...tracksResponse.items.map(playlistItem => playlistItem.track));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'track': {
|
||||||
|
const uri = parsed as spotifyURI.Track;
|
||||||
|
|
||||||
|
const {body} = await this.spotify.getTrack(uri.id);
|
||||||
|
|
||||||
|
tracks.push(body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'artist': {
|
||||||
|
const uri = parsed as spotifyURI.Artist;
|
||||||
|
|
||||||
|
const {body} = await this.spotify.getArtistTopTracks(uri.id, 'US');
|
||||||
|
|
||||||
|
tracks.push(...body.tracks);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return [[], 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit concurrency so hopefully we don't get banned for searching
|
||||||
|
const limit = pLimit(3);
|
||||||
|
let songs = await Promise.all(tracks.map(async track => limit(async () => this.spotifyToYouTube(track, playlist))));
|
||||||
|
|
||||||
|
let nSongsNotFound = 0;
|
||||||
|
|
||||||
|
// Get rid of null values
|
||||||
|
songs = songs.reduce((accum: QueuedSong[], song) => {
|
||||||
|
if (song) {
|
||||||
|
accum.push(song);
|
||||||
|
} else {
|
||||||
|
nSongsNotFound++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accum;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [songs as QueuedSong[], nSongsNotFound];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, playlist: QueuedPlaylist | null): Promise<QueuedSong | null> {
|
||||||
|
try {
|
||||||
|
const {items} = await ytsr(`${track.name} ${track.artists[0].name} offical`, {limit: 5});
|
||||||
|
const video = items.find((item: { type: string }) => item.type === 'video');
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
throw new Error('No video found for query.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: video.title,
|
||||||
|
artist: track.artists[0].name,
|
||||||
|
length: track.duration_ms / 1000,
|
||||||
|
url: video.link,
|
||||||
|
playlist,
|
||||||
|
isLive: video.live
|
||||||
|
};
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,9 +64,12 @@ export default class {
|
||||||
throw new Error('No song currently playing');
|
throw new Error('No song currently playing');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.waitForCache(currentSong.url);
|
if (await this.isCached(currentSong.url)) {
|
||||||
|
|
||||||
this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url), {seek: positionSeconds});
|
this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url), {seek: positionSeconds});
|
||||||
|
} else {
|
||||||
|
const stream = await this.getStream(currentSong.url, {seek: positionSeconds});
|
||||||
|
this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'});
|
||||||
|
}
|
||||||
|
|
||||||
this.attachListeners();
|
this.attachListeners();
|
||||||
this.startTrackingPosition(positionSeconds);
|
this.startTrackingPosition(positionSeconds);
|
||||||
|
@ -147,32 +150,7 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForCache(url: string, maxRetries = 500, retryDelay = 200): Promise<void> {
|
private async getStream(url: string, options: {seek?: number} = {}): Promise<Readable|string> {
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
if (await this.isCached(url)) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
let nOfChecks = 0;
|
|
||||||
|
|
||||||
const cachedCheck = setInterval(async () => {
|
|
||||||
if (await this.isCached(url)) {
|
|
||||||
clearInterval(cachedCheck);
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
nOfChecks++;
|
|
||||||
|
|
||||||
if (nOfChecks > maxRetries) {
|
|
||||||
clearInterval(cachedCheck);
|
|
||||||
reject(new Error('Timed out waiting for file to become cached.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, retryDelay);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getStream(url: string): Promise<Readable|string> {
|
|
||||||
const cachedPath = this.getCachedPath(url);
|
const cachedPath = this.getCachedPath(url);
|
||||||
|
|
||||||
if (await this.isCached(url)) {
|
if (await this.isCached(url)) {
|
||||||
|
@ -187,7 +165,6 @@ export default class {
|
||||||
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
|
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
|
||||||
|
|
||||||
let format = formats.find(filter);
|
let format = formats.find(filter);
|
||||||
let canDirectPlay = true;
|
|
||||||
|
|
||||||
const nextBestFormat = (formats: ytdl.videoFormat[]): ytdl.videoFormat | undefined => {
|
const nextBestFormat = (formats: ytdl.videoFormat[]): ytdl.videoFormat | undefined => {
|
||||||
if (formats[0].live) {
|
if (formats[0].live) {
|
||||||
|
@ -204,7 +181,6 @@ export default class {
|
||||||
|
|
||||||
if (!format) {
|
if (!format) {
|
||||||
format = nextBestFormat(info.formats);
|
format = nextBestFormat(info.formats);
|
||||||
canDirectPlay = false;
|
|
||||||
|
|
||||||
if (!format) {
|
if (!format) {
|
||||||
// If still no format is found, throw
|
// If still no format is found, throw
|
||||||
|
@ -212,21 +188,21 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let youtubeStream: Readable;
|
const inputOptions = [
|
||||||
|
|
||||||
if (canDirectPlay) {
|
|
||||||
youtubeStream = ytdl.downloadFromInfo(info, {format});
|
|
||||||
} else {
|
|
||||||
youtubeStream = ffmpeg(format.url).inputOptions([
|
|
||||||
'-reconnect',
|
'-reconnect',
|
||||||
'1',
|
'1',
|
||||||
'-reconnect_streamed',
|
'-reconnect_streamed',
|
||||||
'1',
|
'1',
|
||||||
'-reconnect_delay_max',
|
'-reconnect_delay_max',
|
||||||
'5'
|
'5'
|
||||||
]).noVideo().audioCodec('libopus').outputFormat('webm').pipe() as PassThrough;
|
];
|
||||||
|
|
||||||
|
if (options.seek) {
|
||||||
|
inputOptions.push('-ss', options.seek.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const youtubeStream = ffmpeg(format.url).inputOptions(inputOptions).noVideo().audioCodec('libopus').outputFormat('webm').pipe() as PassThrough;
|
||||||
|
|
||||||
const capacitor = new WriteStream();
|
const capacitor = new WriteStream();
|
||||||
|
|
||||||
youtubeStream.pipe(capacitor);
|
youtubeStream.pipe(capacitor);
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class {
|
||||||
private position = 0;
|
private position = 0;
|
||||||
|
|
||||||
forward(): void {
|
forward(): void {
|
||||||
if (this.position + 1 <= this.size()) {
|
if (this.position <= this.size() + 1) {
|
||||||
this.position++;
|
this.position++;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No songs in queue to forward to.');
|
throw new Error('No songs in queue to forward to.');
|
||||||
|
|
|
@ -16,5 +16,8 @@ export const TYPES = {
|
||||||
Managers: {
|
Managers: {
|
||||||
Player: Symbol('PlayerManager'),
|
Player: Symbol('PlayerManager'),
|
||||||
Queue: Symbol('QueueManager')
|
Queue: Symbol('QueueManager')
|
||||||
|
},
|
||||||
|
Services: {
|
||||||
|
GetSongs: Symbol('GetSongs')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default (error?: string | Error): string => {
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
str = `🚫 ${error}`;
|
str = `🚫 ${error}`;
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
str = `🚫 error: ${error.name}`;
|
str = `🚫 ope: ${error.name}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue