mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-12 21:05:29 +01:00
parent
efcdeb78c8
commit
fd782219ef
12
package.json
12
package.json
|
@ -2,12 +2,16 @@
|
|||
"name": "muse",
|
||||
"version": "0.1.0",
|
||||
"description": "🎧 a self-hosted Discord music bot that doesn't suck ",
|
||||
"main": "dist/index.js",
|
||||
"exports": "./dist/index.js",
|
||||
"repository": "git@github.com:codetheweb/muse.git",
|
||||
"author": "Max Isom <hi@maxisom.me>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"types": "dts/types",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"dts"
|
||||
|
@ -83,13 +87,15 @@
|
|||
"node-emoji": "^1.10.0",
|
||||
"p-event": "^4.2.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"p-queue": "^7.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^5.22.4",
|
||||
"sequelize-typescript": "^1.1.0",
|
||||
"spotify-uri": "^2.2.0",
|
||||
"spotify-web-api-node": "^5.0.2",
|
||||
"sqlite3": "^5.0.2",
|
||||
"youtube.ts": "^0.2.0",
|
||||
"ytdl-core": "^4.9.1"
|
||||
"youtube.ts": "^0.2.2",
|
||||
"ytdl-core": "^4.9.1",
|
||||
"ytsr": "^3.5.3"
|
||||
}
|
||||
}
|
||||
|
|
22
src/bot.ts
22
src/bot.ts
|
@ -1,16 +1,16 @@
|
|||
import {Client, Message, Collection} from 'discord.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from './types';
|
||||
import {Settings, Shortcut} from './models';
|
||||
import container from './inversify.config';
|
||||
import Command from './commands';
|
||||
import debug from './utils/debug';
|
||||
import NaturalLanguage from './services/natural-language-commands';
|
||||
import handleGuildCreate from './events/guild-create';
|
||||
import handleVoiceStateUpdate from './events/voice-state-update';
|
||||
import errorMsg from './utils/error-msg';
|
||||
import {isUserInVoice} from './utils/channels';
|
||||
import Config from './services/config';
|
||||
import {TYPES} from './types.js';
|
||||
import {Settings, Shortcut} from './models/index.js';
|
||||
import container from './inversify.config.js';
|
||||
import Command from './commands/index.js';
|
||||
import debug from './utils/debug.js';
|
||||
import NaturalLanguage from './services/natural-language-commands.js';
|
||||
import handleGuildCreate from './events/guild-create.js';
|
||||
import handleVoiceStateUpdate from './events/voice-state-update.js';
|
||||
import errorMsg from './utils/error-msg.js';
|
||||
import {isUserInVoice} from './utils/channels.js';
|
||||
import Config from './services/config.js';
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import {Message} from 'discord.js';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {TextChannel, Message, GuildChannel} from 'discord.js';
|
||||
import {injectable} from 'inversify';
|
||||
import {Settings} from '../models';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import {Settings} from '../models/index.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Message, TextChannel} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import LoadingMessage from '../utils/loading-message';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import LoadingMessage from '../utils/loading-message.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {injectable} from 'inversify';
|
||||
import Command from '.';
|
||||
import {TYPES} from '../types';
|
||||
import {Settings} from '../models';
|
||||
import container from '../inversify.config';
|
||||
import {TYPES} from '../types.js';
|
||||
import {Settings} from '../models/index.js';
|
||||
import container from '../inversify.config.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import {STATUS} from '../services/player';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {STATUS} from '../services/player.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import {TextChannel, Message} from 'discord.js';
|
||||
import {URL} from 'url';
|
||||
import {Except} from 'type-fest';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import {QueuedSong, STATUS} from '../services/player';
|
||||
import PlayerManager from '../managers/player';
|
||||
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels';
|
||||
import LoadingMessage from '../utils/loading-message';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import {QueuedSong, STATUS} from '../services/player.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels.js';
|
||||
import LoadingMessage from '../utils/loading-message.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
import GetSongs from '../services/get-songs';
|
||||
import GetSongs from '../services/get-songs.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
@ -124,6 +124,7 @@ export default class implements Command {
|
|||
if (song) {
|
||||
newSongs.push(song);
|
||||
} else {
|
||||
console.log(_);
|
||||
await res.stop(errorMsg('that doesn\'t exist'));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import {Message, MessageEmbed} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import {STATUS} from '../services/player';
|
||||
import Command from '.';
|
||||
import getProgressBar from '../utils/get-progress-bar';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import {prettyTime} from '../utils/time';
|
||||
import getYouTubeID from 'get-youtube-id';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {STATUS} from '../services/player.js';
|
||||
import Command from '.';
|
||||
import getProgressBar from '../utils/get-progress-bar.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import {prettyTime} from '../utils/time.js';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {Message, TextChannel} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import LoadingMessage from '../utils/loading-message';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import LoadingMessage from '../utils/loading-message.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
import {parseTime} from '../utils/time';
|
||||
import {parseTime} from '../utils/time.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {injectable} from 'inversify';
|
||||
import {Shortcut, Settings} from '../models';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import {Shortcut, Settings} from '../models/index.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {Message, TextChannel} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import Command from '.';
|
||||
import LoadingMessage from '../utils/loading-message';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import LoadingMessage from '../utils/loading-message.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {Message} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import {TYPES} from '../types.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import PlayerManager from '../managers/player';
|
||||
import errorMsg from '../utils/error-msg';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
|
||||
@injectable()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {Guild, TextChannel, Message} from 'discord.js';
|
||||
import emoji from 'node-emoji';
|
||||
import pEvent from 'p-event';
|
||||
import {Settings} from '../models';
|
||||
import {chunk} from '../utils/arrays';
|
||||
import {Settings} from '../models/index.js';
|
||||
import {chunk} from '../utils/arrays.js';
|
||||
|
||||
const DEFAULT_PREFIX = '!';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {VoiceState} from 'discord.js';
|
||||
import container from '../inversify.config';
|
||||
import {TYPES} from '../types';
|
||||
import PlayerManager from '../managers/player';
|
||||
import {getSizeWithoutBots} from '../utils/channels';
|
||||
import container from '../inversify.config.js';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {getSizeWithoutBots} from '../utils/channels.js';
|
||||
|
||||
export default (oldState: VoiceState, _: VoiceState): void => {
|
||||
const playerManager = container.get<PlayerManager>(TYPES.Managers.Player);
|
||||
|
|
12
src/index.ts
12
src/index.ts
|
@ -1,10 +1,10 @@
|
|||
import makeDir from 'make-dir';
|
||||
import path from 'path';
|
||||
import container from './inversify.config';
|
||||
import {TYPES} from './types';
|
||||
import Bot from './bot';
|
||||
import {sequelize} from './utils/db';
|
||||
import Config from './services/config';
|
||||
import container from './inversify.config.js';
|
||||
import {TYPES} from './types.js';
|
||||
import Bot from './bot.js';
|
||||
import {sequelize} from './utils/db.js';
|
||||
import Config from './services/config.js';
|
||||
|
||||
const bot = container.get<Bot>(TYPES.Bot);
|
||||
|
||||
|
@ -16,7 +16,7 @@ const bot = container.get<Bot>(TYPES.Bot);
|
|||
await makeDir(config.CACHE_DIR);
|
||||
await makeDir(path.join(config.CACHE_DIR, 'tmp'));
|
||||
|
||||
await sequelize.sync({});
|
||||
await sequelize.sync({alter: true});
|
||||
|
||||
await bot.listen();
|
||||
})();
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
import 'reflect-metadata';
|
||||
import {Container} from 'inversify';
|
||||
import {TYPES} from './types';
|
||||
import Bot from './bot';
|
||||
import {TYPES} from './types.js';
|
||||
import Bot from './bot.js';
|
||||
import {Client} from 'discord.js';
|
||||
import ConfigProvider from './services/config';
|
||||
import ConfigProvider from './services/config.js';
|
||||
|
||||
// Managers
|
||||
import PlayerManager from './managers/player';
|
||||
import PlayerManager from './managers/player.js';
|
||||
|
||||
// Helpers
|
||||
import GetSongs from './services/get-songs';
|
||||
import NaturalLanguage from './services/natural-language-commands';
|
||||
import GetSongs from './services/get-songs.js';
|
||||
import NaturalLanguage from './services/natural-language-commands.js';
|
||||
|
||||
// Comands
|
||||
import Command from './commands';
|
||||
import Clear from './commands/clear';
|
||||
import Config from './commands/config';
|
||||
import Disconnect from './commands/disconnect';
|
||||
import ForwardSeek from './commands/fseek';
|
||||
import Help from './commands/help';
|
||||
import Pause from './commands/pause';
|
||||
import Play from './commands/play';
|
||||
import QueueCommad from './commands/queue';
|
||||
import Seek from './commands/seek';
|
||||
import Shortcuts from './commands/shortcuts';
|
||||
import Shuffle from './commands/shuffle';
|
||||
import Skip from './commands/skip';
|
||||
import Unskip from './commands/unskip';
|
||||
import ThirdParty from './services/third-party';
|
||||
import Clear from './commands/clear.js';
|
||||
import Config from './commands/config.js';
|
||||
import Disconnect from './commands/disconnect.js';
|
||||
import ForwardSeek from './commands/fseek.js';
|
||||
import Help from './commands/help.js';
|
||||
import Pause from './commands/pause.js';
|
||||
import Play from './commands/play.js';
|
||||
import QueueCommad from './commands/queue.js';
|
||||
import Seek from './commands/seek.js';
|
||||
import Shortcuts from './commands/shortcuts.js';
|
||||
import Shuffle from './commands/shuffle.js';
|
||||
import Skip from './commands/skip.js';
|
||||
import Unskip from './commands/unskip.js';
|
||||
import ThirdParty from './services/third-party.js';
|
||||
import CacheProvider from './services/cache.js';
|
||||
|
||||
let container = new Container();
|
||||
|
||||
|
@ -67,4 +68,6 @@ container.bind(TYPES.Config).toConstantValue(new ConfigProvider());
|
|||
// Static libraries
|
||||
container.bind(TYPES.ThirdParty).to(ThirdParty);
|
||||
|
||||
container.bind(TYPES.Cache).to(CacheProvider);
|
||||
|
||||
export default container;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from '../types';
|
||||
import Player from '../services/player';
|
||||
import {Client} from 'discord.js';
|
||||
import Config from '../services/config';
|
||||
import {TYPES} from '../types.js';
|
||||
import Player from '../services/player.js';
|
||||
import Config from '../services/config.js';
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
|
|
15
src/models/cache.ts
Normal file
15
src/models/cache.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript';
|
||||
import sequelize from 'sequelize';
|
||||
|
||||
@Table
|
||||
export default class Cache extends Model<Cache> {
|
||||
@PrimaryKey
|
||||
@Column
|
||||
key!: string;
|
||||
|
||||
@Column(sequelize.TEXT)
|
||||
value!: string;
|
||||
|
||||
@Column
|
||||
expiresAt!: Date;
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import Settings from './settings';
|
||||
import Shortcut from './shortcut';
|
||||
import Cache from './cache.js';
|
||||
import Settings from './settings.js';
|
||||
import Shortcut from './shortcut.js';
|
||||
|
||||
export {
|
||||
Cache,
|
||||
Settings,
|
||||
Shortcut
|
||||
};
|
||||
|
|
52
src/services/cache.ts
Normal file
52
src/services/cache.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {injectable} from 'inversify';
|
||||
import {Cache} from '../models/index.js';
|
||||
import debug from '../utils/debug.js';
|
||||
|
||||
type Seconds = number;
|
||||
|
||||
type Options = {
|
||||
expiresIn: Seconds;
|
||||
key?: string;
|
||||
};
|
||||
|
||||
const futureTimeToDate = (time: Seconds) => new Date(new Date().getTime() + (time * 1000));
|
||||
|
||||
@injectable()
|
||||
export default class CacheProvider {
|
||||
async wrap<T extends [...any[], Options], F>(func: (...options: any) => Promise<F>, ...options: T): Promise<F> {
|
||||
if (options.length === 0) {
|
||||
throw new Error('Missing cache options');
|
||||
}
|
||||
|
||||
const functionArgs = options.slice(0, options.length - 1);
|
||||
|
||||
const {
|
||||
key = JSON.stringify(functionArgs),
|
||||
expiresIn
|
||||
} = options[options.length - 1] as Options;
|
||||
|
||||
const cachedResult = await Cache.findByPk(key);
|
||||
|
||||
if (cachedResult) {
|
||||
if (new Date() < cachedResult.expiresAt) {
|
||||
debug(`Cache hit: ${key}`);
|
||||
return JSON.parse(cachedResult.value);
|
||||
}
|
||||
|
||||
await cachedResult.destroy();
|
||||
}
|
||||
|
||||
debug(`Cache miss: ${key}`);
|
||||
|
||||
const result = await func(...options as any[]);
|
||||
|
||||
// Save result
|
||||
await Cache.upsert({
|
||||
key,
|
||||
value: JSON.stringify(result),
|
||||
expiresAt: futureTimeToDate(expiresIn)
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -2,37 +2,73 @@ import {URL} from 'url';
|
|||
import {inject, injectable} from 'inversify';
|
||||
import {toSeconds, parse} from 'iso8601-duration';
|
||||
import got from 'got';
|
||||
import ytsr, {Video} from 'ytsr';
|
||||
import spotifyURI from 'spotify-uri';
|
||||
import Spotify from 'spotify-web-api-node';
|
||||
import YouTube, {YoutubePlaylistItem} from 'youtube.ts';
|
||||
import pLimit from 'p-limit';
|
||||
import PQueue from 'p-queue';
|
||||
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';
|
||||
import ThirdParty from './third-party';
|
||||
import Config from './config';
|
||||
import {QueuedSong, QueuedPlaylist} from '../services/player.js';
|
||||
import {TYPES} from '../types.js';
|
||||
import {cleanUrl} from '../utils/url.js';
|
||||
import ThirdParty from './third-party.js';
|
||||
import Config from './config.js';
|
||||
import CacheProvider from './cache.js';
|
||||
|
||||
type QueuedSongWithoutChannel = Except<QueuedSong, 'addedInChannelId'>;
|
||||
|
||||
const ONE_HOUR_IN_SECONDS = 60 * 60;
|
||||
const ONE_MINUTE_IN_SECONDS = 1 * 60;
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
private readonly youtube: YouTube;
|
||||
private readonly youtubeKey: string;
|
||||
private readonly spotify: Spotify;
|
||||
private readonly cache: CacheProvider;
|
||||
|
||||
constructor(@inject(TYPES.ThirdParty) thirdParty: ThirdParty, @inject(TYPES.Config) config: Config) {
|
||||
private readonly ytsrQueue: PQueue;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.ThirdParty) thirdParty: ThirdParty,
|
||||
@inject(TYPES.Config) config: Config,
|
||||
@inject(TYPES.Cache) cache: CacheProvider) {
|
||||
this.youtube = thirdParty.youtube;
|
||||
this.youtubeKey = config.YOUTUBE_API_KEY;
|
||||
this.spotify = thirdParty.spotify;
|
||||
this.cache = cache;
|
||||
|
||||
this.ytsrQueue = new PQueue({concurrency: 4});
|
||||
}
|
||||
|
||||
async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel|null> {
|
||||
try {
|
||||
const {items: [video]} = await this.youtube.videos.search({q: query, maxResults: 1, type: 'video'});
|
||||
const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
|
||||
ytsr,
|
||||
query,
|
||||
{
|
||||
limit: 10
|
||||
},
|
||||
{
|
||||
expiresIn: ONE_HOUR_IN_SECONDS
|
||||
}
|
||||
));
|
||||
|
||||
return await this.youtubeVideo(video.id.videoId);
|
||||
let firstVideo: Video | undefined;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type === 'video') {
|
||||
firstVideo = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstVideo) {
|
||||
throw new Error('No video found.');
|
||||
}
|
||||
|
||||
return await this.youtubeVideo(firstVideo.id);
|
||||
} catch (_: unknown) {
|
||||
return null;
|
||||
}
|
||||
|
@ -40,7 +76,13 @@ export default class {
|
|||
|
||||
async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel|null> {
|
||||
try {
|
||||
const videoDetails = await this.youtube.videos.get(cleanUrl(url));
|
||||
const videoDetails = await this.cache.wrap(
|
||||
this.youtube.videos.get,
|
||||
cleanUrl(url),
|
||||
{
|
||||
expiresIn: ONE_HOUR_IN_SECONDS
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
title: videoDetails.snippet.title,
|
||||
|
@ -57,7 +99,13 @@ export default class {
|
|||
|
||||
async youtubePlaylist(listId: string): Promise<QueuedSongWithoutChannel[]> {
|
||||
// YouTube playlist
|
||||
const playlist = await this.youtube.playlists.get(listId);
|
||||
const playlist = await this.cache.wrap(
|
||||
this.youtube.playlists.get,
|
||||
listId,
|
||||
{
|
||||
expiresIn: ONE_MINUTE_IN_SECONDS
|
||||
}
|
||||
);
|
||||
|
||||
interface VideoDetailsResponse {
|
||||
id: string;
|
||||
|
@ -75,7 +123,14 @@ export default class {
|
|||
|
||||
while (playlistVideos.length !== playlist.contentDetails.itemCount) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const {items, nextPageToken} = await this.youtube.playlists.items(listId, {maxResults: '50', pageToken: nextToken});
|
||||
const {items, nextPageToken} = await this.cache.wrap(
|
||||
this.youtube.playlists.items,
|
||||
listId,
|
||||
{maxResults: '50', pageToken: nextToken},
|
||||
{
|
||||
expiresIn: ONE_MINUTE_IN_SECONDS
|
||||
}
|
||||
);
|
||||
|
||||
nextToken = nextPageToken;
|
||||
|
||||
|
@ -84,11 +139,24 @@ export default class {
|
|||
// Start fetching extra details about videos
|
||||
videoDetailsPromises.push((async () => {
|
||||
// Unfortunately, package doesn't provide a method for this
|
||||
const {items: videoDetailItems}: {items: VideoDetailsResponse[]} = 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 {items: videoDetailItems} = await this.cache.wrap(
|
||||
() => {
|
||||
return got(
|
||||
'https://www.googleapis.com/youtube/v3/videos',
|
||||
{
|
||||
searchParams: {
|
||||
part: 'contentDetails',
|
||||
id: items.map(item => item.contentDetails.videoId).join(','),
|
||||
key: this.youtubeKey,
|
||||
responseType: 'json'
|
||||
}
|
||||
}
|
||||
).json();
|
||||
},
|
||||
{
|
||||
expiresIn: ONE_MINUTE_IN_SECONDS
|
||||
}
|
||||
);
|
||||
|
||||
videoDetails.push(...videoDetailItems);
|
||||
})());
|
||||
|
@ -193,9 +261,7 @@ export default class {
|
|||
tracks = shuffled.slice(0, 50);
|
||||
}
|
||||
|
||||
// Limit concurrency so hopefully we don't get banned for searching
|
||||
const limit = pLimit(5);
|
||||
let songs = await Promise.all(tracks.map(async track => limit(async () => this.spotifyToYouTube(track, playlist))));
|
||||
let songs = await Promise.all(tracks.map(async track => this.spotifyToYouTube(track, playlist)));
|
||||
|
||||
let nSongsNotFound = 0;
|
||||
|
||||
|
@ -215,14 +281,7 @@ export default class {
|
|||
|
||||
private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel | null> {
|
||||
try {
|
||||
const {items} = await this.youtube.videos.search({q: `"${track.name}" "${track.artists[0].name}"`, maxResults: 10});
|
||||
const videoResult = items[0];
|
||||
|
||||
if (!videoResult) {
|
||||
throw new Error('No video found for query.');
|
||||
}
|
||||
|
||||
return await this.youtubeVideo(videoResult.id.videoId);
|
||||
return await this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
|
||||
} catch (_: unknown) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {inject, injectable} from 'inversify';
|
||||
import {Message, Guild, GuildMember} from 'discord.js';
|
||||
import {TYPES} from '../types';
|
||||
import PlayerManager from '../managers/player';
|
||||
import {QueuedSong} from '../services/player';
|
||||
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {QueuedSong} from '../services/player.js';
|
||||
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels.js';
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
|
|
|
@ -7,7 +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';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
|
||||
export interface QueuedPlaylist {
|
||||
title: string;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {inject, injectable} from 'inversify';
|
||||
import SpotifyWebApi from 'spotify-web-api-node';
|
||||
import Youtube from 'youtube.ts';
|
||||
import {TYPES} from '../types';
|
||||
import Config from './config';
|
||||
import Youtube from 'youtube.ts/dist/youtube.js';
|
||||
import {TYPES} from '../types.js';
|
||||
import Config from './config.js';
|
||||
|
||||
@injectable()
|
||||
export default class ThirdParty {
|
||||
|
@ -12,7 +12,8 @@ export default class ThirdParty {
|
|||
private spotifyTokenTimerId?: NodeJS.Timeout;
|
||||
|
||||
constructor(@inject(TYPES.Config) config: Config) {
|
||||
this.youtube = new Youtube(config.YOUTUBE_API_KEY);
|
||||
// Library is transpiled incorrectly
|
||||
this.youtube = new ((Youtube as any).default)(config.YOUTUBE_API_KEY);
|
||||
this.spotify = new SpotifyWebApi({
|
||||
clientId: config.SPOTIFY_CLIENT_ID,
|
||||
clientSecret: config.SPOTIFY_CLIENT_SECRET
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const TYPES = {
|
||||
Bot: Symbol('Bot'),
|
||||
Cache: Symbol('Cache'),
|
||||
Client: Symbol('Client'),
|
||||
Config: Symbol('Config'),
|
||||
Command: Symbol('Command'),
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import {Sequelize} from 'sequelize-typescript';
|
||||
import path from 'path';
|
||||
import {DATA_DIR} from '../services/config';
|
||||
import {Settings, Shortcut} from '../models';
|
||||
import {DATA_DIR} from '../services/config.js';
|
||||
import {Cache, Settings, Shortcut} from '../models/index.js';
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
database: 'muse',
|
||||
storage: path.join(DATA_DIR, 'db.sqlite'),
|
||||
models: [Settings, Shortcut],
|
||||
models: [Cache, Settings, Shortcut],
|
||||
logging: false
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"lib": ["ES2019"],
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -1153,6 +1153,11 @@ event-target-shim@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
eventemitter3@^4.0.7:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
|
@ -1997,7 +2002,7 @@ mimic-response@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
|
||||
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
|
||||
|
||||
miniget@^4.0.0:
|
||||
miniget@^4.0.0, miniget@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/miniget/-/miniget-4.2.1.tgz#11a1c24817a059e292378eb9cff4328d9240c665"
|
||||
integrity sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ==
|
||||
|
@ -2332,6 +2337,14 @@ p-locate@^5.0.0:
|
|||
dependencies:
|
||||
p-limit "^3.0.2"
|
||||
|
||||
p-queue@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-7.1.0.tgz#c2bb28f8dc0ebf3fadb985b8706cf2ce5fe5f275"
|
||||
integrity sha512-V+0vPJbhYkBqknPp0qnaz+dWcj8cNepfXZcsVIVEHPbFQXMPwrzCNIiM4FoxGtwHXtPzVCPHDvqCr1YrOJX2Gw==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.7"
|
||||
p-timeout "^5.0.0"
|
||||
|
||||
p-timeout@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
|
||||
|
@ -2339,6 +2352,11 @@ p-timeout@^3.1.0:
|
|||
dependencies:
|
||||
p-finally "^1.0.0"
|
||||
|
||||
p-timeout@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-5.0.0.tgz#0f5dc08422e7243f8317669c461734cd1257a8dc"
|
||||
integrity sha512-z+bU/N7L1SABsqKnQzvAnINgPX7NHdzwUV+gHyJE7VGNDZSr03rhcPODCZSWiiT9k+gf74QPmzcZzqJRvxYZow==
|
||||
|
||||
package-json@^6.3.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
|
||||
|
@ -3287,22 +3305,13 @@ yocto-queue@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
youtube.ts@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/youtube.ts/-/youtube.ts-0.2.0.tgz#a6cbc153bda9aa712a10098b22060877fb2995f3"
|
||||
integrity sha512-cRHN/7L9FO+DkdKTzvUAiB/hQo+ZtzLb4GfVywPTTtf9JBol87fqzLKf86cBWwuXLOJzJNLB/wvZmUmsxpbByQ==
|
||||
youtube.ts@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/youtube.ts/-/youtube.ts-0.2.2.tgz#d068bd13da7d2a98743d78682d54535d9c1a2833"
|
||||
integrity sha512-2p5RGp66h77XlLlc2YyxSDU1Gql9cWDhq35TS1kAr5wSU/yIWxeXeuqKyKaFBW8uNqWUkQCiyUbn5tfqPgyzeQ==
|
||||
dependencies:
|
||||
axios "^0.19.0"
|
||||
ytdl-core "^4.0.3"
|
||||
|
||||
ytdl-core@^4.0.3:
|
||||
version "4.8.3"
|
||||
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.8.3.tgz#21570d1834db13dec7828cf87bbf4c83c0fe68d7"
|
||||
integrity sha512-cWCBeX4FCgjcKmuVK384MT582RIAakpUSeMF/NPVmhO8cWiG+LeQLnBordvLolb0iXYzfUvalgmycYAE5Sy6Xw==
|
||||
dependencies:
|
||||
m3u8stream "^0.8.3"
|
||||
miniget "^4.0.0"
|
||||
sax "^1.1.3"
|
||||
ytdl-core "^4.9.1"
|
||||
|
||||
ytdl-core@^4.9.1:
|
||||
version "4.9.1"
|
||||
|
@ -3312,3 +3321,10 @@ ytdl-core@^4.9.1:
|
|||
m3u8stream "^0.8.3"
|
||||
miniget "^4.0.0"
|
||||
sax "^1.1.3"
|
||||
|
||||
ytsr@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/ytsr/-/ytsr-3.5.3.tgz#88e8e2df11ce53c28b456b5510272495cb42ac3a"
|
||||
integrity sha512-BEyIKbQULmk27hiVUQ1cBszAqP8roPBOQTWPZpBioKxjSZBeicfgF2qPIQoY7koodQwRuo1DmCFz3DyrXjADxg==
|
||||
dependencies:
|
||||
miniget "^4.2.1"
|
||||
|
|
Loading…
Reference in a new issue