From 9a2ef876d381a646f0d66145d8ed3cfa8da7fac3 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Sat, 18 Sep 2021 16:55:50 -0400 Subject: [PATCH] Correctly skip song if unavailable Also lets user know in text channel that song is unavailable after skipping. Fixes #324 --- package.json | 2 +- src/commands/play.ts | 5 ++-- src/managers/player.ts | 7 ++++-- src/services/get-songs.ts | 19 ++++++++------ src/services/natural-language-commands.ts | 30 ++++++++++++++++++++--- src/services/player.ts | 22 ++++++++++++++--- 6 files changed, 66 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index d5f98f3..eec72d8 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ ], "scripts": { "lint": "eslint 'src/**/*.ts'", - "lint-fix": "eslint 'src/**/*.ts' --fix", + "lint:fix": "eslint 'src/**/*.ts' --fix", "clean": "rm -rf dist dts", "test": "npm run lint", "build": "tsc", diff --git a/src/commands/play.ts b/src/commands/play.ts index a67b318..4886da2 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -1,5 +1,6 @@ import {TextChannel, Message} from 'discord.js'; import {URL} from 'url'; +import {Except} from 'type-fest'; import {TYPES} from '../types'; import {inject, injectable} from 'inversify'; import {QueuedSong, STATUS} from '../services/player'; @@ -68,7 +69,7 @@ export default class implements Command { const addToFrontOfQueue = args[args.length - 1] === 'i' || args[args.length - 1] === 'immediate'; - const newSongs: QueuedSong[] = []; + const newSongs: Array> = []; let extraMsg = ''; // Test if it's a complete URL @@ -133,7 +134,7 @@ export default class implements Command { return; } - newSongs.forEach(song => player.add(song, {immediate: addToFrontOfQueue})); + newSongs.forEach(song => player.add({...song, addedInChannelId: msg.channel.id}, {immediate: addToFrontOfQueue})); const firstSong = newSongs[0]; diff --git a/src/managers/player.ts b/src/managers/player.ts index b05d506..0afcc71 100644 --- a/src/managers/player.ts +++ b/src/managers/player.ts @@ -1,22 +1,25 @@ import {inject, injectable} from 'inversify'; import {TYPES} from '../types'; import Player from '../services/player'; +import {Client} from 'discord.js'; @injectable() export default class { private readonly guildPlayers: Map; private readonly cacheDir: string; + private readonly discordClient: Client; - constructor(@inject(TYPES.Config.CACHE_DIR) cacheDir: string) { + constructor(@inject(TYPES.Config.CACHE_DIR) cacheDir: string, @inject(TYPES.Client) client: Client) { this.guildPlayers = new Map(); this.cacheDir = cacheDir; + this.discordClient = client; } get(guildId: string): Player { let player = this.guildPlayers.get(guildId); if (!player) { - player = new Player(this.cacheDir); + player = new Player(this.cacheDir, this.discordClient); this.guildPlayers.set(guildId, player); } diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts index 8a040c0..d5ccdef 100644 --- a/src/services/get-songs.ts +++ b/src/services/get-songs.ts @@ -7,10 +7,13 @@ import Spotify from 'spotify-web-api-node'; import YouTube, {YoutubePlaylistItem} from 'youtube.ts'; import pLimit from 'p-limit'; import shuffle from 'array-shuffle'; +import {Except} from 'type-fest'; import {QueuedSong, QueuedPlaylist} from '../services/player'; import {TYPES} from '../types'; import {cleanUrl} from '../utils/url'; +type QueuedSongWithoutChannel = Except; + @injectable() export default class { private readonly youtube: YouTube; @@ -23,7 +26,7 @@ export default class { this.spotify = spotify; } - async youtubeVideoSearch(query: string): Promise { + async youtubeVideoSearch(query: string): Promise { try { const {items: [video]} = await this.youtube.videos.search({q: query, maxResults: 1, type: 'video'}); @@ -33,7 +36,7 @@ export default class { } } - async youtubeVideo(url: string): Promise { + async youtubeVideo(url: string): Promise { try { const videoDetails = await this.youtube.videos.get(cleanUrl(url)); @@ -50,7 +53,7 @@ export default class { } } - async youtubePlaylist(listId: string): Promise { + async youtubePlaylist(listId: string): Promise { // YouTube playlist const playlist = await this.youtube.playlists.get(listId); @@ -93,7 +96,7 @@ export default class { const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id}; - const songsToReturn: QueuedSong[] = []; + const songsToReturn: QueuedSongWithoutChannel[] = []; for (let video of playlistVideos) { try { @@ -115,7 +118,7 @@ export default class { return songsToReturn; } - async spotifySource(url: string): Promise<[QueuedSong[], number, number]> { + async spotifySource(url: string): Promise<[QueuedSongWithoutChannel[], number, number]> { const parsed = spotifyURI.parse(url); let tracks: SpotifyApi.TrackObjectSimplified[] = []; @@ -195,7 +198,7 @@ export default class { let nSongsNotFound = 0; // Get rid of null values - songs = songs.reduce((accum: QueuedSong[], song) => { + songs = songs.reduce((accum: QueuedSongWithoutChannel[], song) => { if (song) { accum.push(song); } else { @@ -205,10 +208,10 @@ export default class { return accum; }, []); - return [songs as QueuedSong[], nSongsNotFound, originalNSongs]; + return [songs as QueuedSongWithoutChannel[], nSongsNotFound, originalNSongs]; } - private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise { + private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise { try { const {items} = await this.youtube.videos.search({q: `"${track.name}" "${track.artists[0].name}"`, maxResults: 10}); const videoResult = items[0]; // Items.find(item => item.type === 'video'); diff --git a/src/services/natural-language-commands.ts b/src/services/natural-language-commands.ts index 442cbb0..c6348c1 100644 --- a/src/services/natural-language-commands.ts +++ b/src/services/natural-language-commands.ts @@ -24,7 +24,15 @@ export default class { if (msg.content.toLowerCase().includes('packers')) { await Promise.all([ msg.channel.send('GO PACKERS GO!!!'), - this.playClip(msg.guild!, msg.member!, {title: 'GO PACKERS!', artist: 'Unknown', url: 'https://www.youtube.com/watch?v=qkdtID7mY3E', length: 204, playlist: null, isLive: false}, 8, 10) + this.playClip(msg.guild!, msg.member!, { + title: 'GO PACKERS!', + artist: 'Unknown', + url: 'https://www.youtube.com/watch?v=qkdtID7mY3E', + length: 204, + playlist: null, + isLive: false, + addedInChannelId: msg.channel.id + }, 8, 10) ]); return true; @@ -33,7 +41,15 @@ export default class { if (msg.content.toLowerCase().includes('bears')) { await Promise.all([ msg.channel.send('F*** THE BEARS'), - this.playClip(msg.guild!, msg.member!, {title: 'GO PACKERS!', artist: 'Charlie Berens', url: 'https://www.youtube.com/watch?v=UaqlE9Pyy_Q', length: 385, playlist: null, isLive: false}, 358, 5.5) + this.playClip(msg.guild!, msg.member!, { + title: 'GO PACKERS!', + artist: 'Charlie Berens', + url: 'https://www.youtube.com/watch?v=UaqlE9Pyy_Q', + length: 385, + playlist: null, + isLive: false, + addedInChannelId: msg.channel.id + }, 358, 5.5) ]); return true; @@ -42,7 +58,15 @@ export default class { if (msg.content.toLowerCase().includes('bitconnect')) { await Promise.all([ msg.channel.send('🌊 🌊 🌊 🌊'), - this.playClip(msg.guild!, msg.member!, {title: 'BITCONNEEECCT', artist: 'Carlos Matos', url: 'https://www.youtube.com/watch?v=lCcwn6bGUtU', length: 227, playlist: null, isLive: false}, 50, 13) + this.playClip(msg.guild!, msg.member!, { + title: 'BITCONNEEECCT', + artist: 'Carlos Matos', + url: 'https://www.youtube.com/watch?v=lCcwn6bGUtU', + length: 227, + playlist: null, + isLive: false, + addedInChannelId: msg.channel.id + }, 50, 13) ]); return true; diff --git a/src/services/player.ts b/src/services/player.ts index 63f6445..5a9fa83 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -1,4 +1,4 @@ -import {VoiceConnection, VoiceChannel, StreamDispatcher} from 'discord.js'; +import {VoiceConnection, VoiceChannel, StreamDispatcher, Snowflake, Client, TextChannel} from 'discord.js'; import {promises as fs, createWriteStream} from 'fs'; import {Readable, PassThrough} from 'stream'; import path from 'path'; @@ -7,6 +7,7 @@ import ytdl from 'ytdl-core'; import {WriteStream} from 'fs-capacitor'; import ffmpeg from 'fluent-ffmpeg'; import shuffle from 'array-shuffle'; +import errorMsg from '../utils/error-msg'; export interface QueuedPlaylist { title: string; @@ -20,6 +21,7 @@ export interface QueuedSong { length: number; playlist: QueuedPlaylist | null; isLive: boolean; + addedInChannelId: Snowflake; } export enum STATUS { @@ -40,8 +42,11 @@ export default class { private positionInSeconds = 0; - constructor(cacheDir: string) { + private readonly discordClient: Client; + + constructor(cacheDir: string, client: Client) { this.cacheDir = cacheDir; + this.discordClient = client; } async connect(channel: VoiceChannel): Promise { @@ -142,7 +147,18 @@ export default class { this.lastSongURL = currentSong.url; } } catch (error: unknown) { - this.removeCurrent(); + const currentSong = this.getCurrent(); + await this.forward(1); + + if ((error as {statusCode: number}).statusCode === 410 && currentSong) { + const channelId = currentSong.addedInChannelId; + + if (channelId) { + await (this.discordClient.channels.cache.get(channelId) as TextChannel).send(errorMsg(`${currentSong.title} is unavailable`)); + return; + } + } + throw error; } }