Use play-dl instead node-ytdl-core
This commit is contained in:
parent
8d7b841d9a
commit
7e4bfc1499
11 changed files with 1237 additions and 1634 deletions
82
README.md
82
README.md
|
@ -1,18 +1,15 @@
|
|||
# Discord Player
|
||||
# Discord Player Play-dl
|
||||
Complete framework to facilitate music commands using **[discord.js](https://discord.js.org)**.
|
||||
|
||||
[![downloadsBadge](https://img.shields.io/npm/dt/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
|
||||
[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
|
||||
[![discordBadge](https://img.shields.io/discord/558328638911545423?style=for-the-badge&color=7289da)](https://androz2091.fr/discord)
|
||||
[![wakatime](https://wakatime.com/badge/github/Androz2091/discord-player.svg)](https://wakatime.com/badge/github/Androz2091/discord-player)
|
||||
[![CodeFactor](https://www.codefactor.io/repository/github/androz2091/discord-player/badge/v5)](https://www.codefactor.io/repository/github/androz2091/discord-player/overview/v5)
|
||||
[![wakatime](https://wakatime.com/badge/github/JonnyBro/discord-player.svg)](https://wakatime.com/badge/github/Androz2091/discord-player)
|
||||
[![CodeFactor](https://www.codefactor.io/repository/github/jonnybro/discord-player/badge/v5)](https://www.codefactor.io/repository/github/androz2091/discord-player/overview/v5)
|
||||
|
||||
## Installation
|
||||
|
||||
### Install **[discord-player](https://npmjs.com/package/discord-player)**
|
||||
### Install **[discord-player-play-dl](https://github.com/JonnyBro/discord-player-play-dl)**
|
||||
|
||||
```sh
|
||||
$ npm install --save discord-player
|
||||
$ npm install git+https://github.com/JonnyBro/discord-player-play-dl
|
||||
```
|
||||
|
||||
### Install **[@discordjs/opus](https://npmjs.com/package/@discordjs/opus)**
|
||||
|
@ -145,7 +142,7 @@ By default, discord-player supports **YouTube**, **Spotify** and **SoundCloud**
|
|||
|
||||
### Optional dependencies
|
||||
|
||||
Discord Player provides an **Extractor API** that enables you to use your custom stream extractor with it. Some packages have been made by the community to add new features using this API.
|
||||
Discord Player Play-dl provides an **Extractor API** that enables you to use your custom stream extractor with it. Some packages have been made by the community to add new features using this API.
|
||||
|
||||
#### [@discord-player/extractor](https://github.com/DevAndromeda/discord-player-extractors) (optional)
|
||||
|
||||
|
@ -156,23 +153,17 @@ You just need to install it using `npm i --save @discord-player/extractor` (disc
|
|||
|
||||
`@discord-player/downloader` is an optional package that brings support for +700 websites. The documentation is available [here](https://github.com/DevAndromeda/discord-player-downloader).
|
||||
|
||||
## Examples of bots made with Discord Player
|
||||
## Examples of bots made with Discord Player Play-dl
|
||||
|
||||
These bots are made by the community, they can help you build your own!
|
||||
|
||||
* **[Discord Music Bot](https://github.com/Androz2091/discord-music-bot)** by [Androz2091](https://github.com/Androz2091)
|
||||
* [Dodong](https://github.com/nizeic/Dodong) by [nizeic](https://github.com/nizeic)
|
||||
* [Musico](https://github.com/Whirl21/Musico) by [Whirl21](https://github.com/Whirl21)
|
||||
* [Eyesense-Music-Bot](https://github.com/naseif/Eyesense-Music-Bot) by [naseif](https://github.com/naseif)
|
||||
* [Music-bot](https://github.com/ZerioDev/Music-bot) by [ZerioDev](https://github.com/ZerioDev)
|
||||
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [Androz2091](https://github.com/Androz2091) (**outdated**)
|
||||
* [Discord-Music](https://github.com/inhydrox/discord-music) by [inhydrox](https://github.com/inhydrox) (**outdated**)
|
||||
* **[JaBa Bot](https://canary.discord.com/api/oauth2/authorize?client_id=708637495054565426&permissions=8&scope=bot%20applications.commands)** by [Jonny_Bro](https://github.com/JonnyBro)
|
||||
|
||||
## Advanced
|
||||
|
||||
### Smooth Volume
|
||||
|
||||
Discord Player will by default try to implement this. If smooth volume does not work, you need to add this line at the top of your main file:
|
||||
Discord Player Play-dl will by default try to implement this. If smooth volume does not work, you need to add this line at the top of your main file:
|
||||
|
||||
```js
|
||||
// CJS
|
||||
|
@ -184,61 +175,10 @@ import "discord-player/smoothVolume"
|
|||
|
||||
> ⚠️ Make sure that line is situated at the **TOP** of your **main** file.
|
||||
|
||||
### Use cookies
|
||||
|
||||
```js
|
||||
const player = new Player(client, {
|
||||
ytdlOptions: {
|
||||
requestOptions: {
|
||||
headers: {
|
||||
cookie: "YOUR_YOUTUBE_COOKIE"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Use custom proxies
|
||||
|
||||
```js
|
||||
const HttpsProxyAgent = require("https-proxy-agent");
|
||||
|
||||
// Remove "user:pass@" if you don't need to authenticate to your proxy.
|
||||
const proxy = "http://user:pass@111.111.111.111:8080";
|
||||
const agent = HttpsProxyAgent(proxy);
|
||||
|
||||
const player = new Player(client, {
|
||||
ytdlOptions: {
|
||||
requestOptions: { agent }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
> You may also create a simple proxy server and forward requests through it.
|
||||
> See **[https://github.com/http-party/node-http-proxy](https://github.com/http-party/node-http-proxy)** for more info.
|
||||
|
||||
### Custom stream Engine
|
||||
|
||||
Discord Player by default uses **[node-ytdl-core](https://github.com/fent/node-ytdl-core)** for youtube and some other extractors for other sources.
|
||||
If you need to modify this behavior without touching extractors, you need to use `createStream` functionality of discord player.
|
||||
Here's an example on how you can use **[play-dl](https://npmjs.com/package/play-dl)** to download youtube streams instead of using ytdl-core.
|
||||
|
||||
```js
|
||||
const playdl = require("play-dl");
|
||||
|
||||
// other code
|
||||
const queue = player.createQueue(..., {
|
||||
...,
|
||||
async onBeforeCreateStream(track, source, _queue) {
|
||||
// only trap youtube source
|
||||
if (source === "youtube") {
|
||||
// track here would be youtube track
|
||||
return (await playdl.stream(track.url, { discordPlayerCompatibility : true })).stream;
|
||||
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
Discord Player Play-dl by default uses **[play-dl](https://github.com/play-dl/play-dl)** for youtube and some other extractors for other sources.
|
||||
If you need to modify this behavior without touching extractors, you need to use `onBeforeCreateStream` functionality of Discord player Play-dl.
|
||||
|
||||
`<Queue>.onBeforeCreateStream` is called before actually downloading the stream. It is a different concept from extractors, where you are **just** downloading
|
||||
streams. `source` here will be a video source. Streams from `onBeforeCreateStream` are then piped to `FFmpeg` and finally sent to Discord voice servers.
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
# Create Stream
|
||||
|
||||
This is a checkpoint where discord-player calls `createStream` before downloading stream.
|
||||
|
||||
### Custom stream Engine
|
||||
|
||||
Discord Player by default uses **[node-ytdl-core](https://github.com/fent/node-ytdl-core)** for youtube and some other extractors for other sources.
|
||||
If you need to modify this behavior without touching extractors, you need to use `createStream` functionality of discord player.
|
||||
Here's an example on how you can use **[play-dl](https://npmjs.com/package/play-dl)** to download youtube streams instead of using ytdl-core.
|
||||
|
||||
```js
|
||||
const playdl = require("play-dl");
|
||||
|
||||
// other code
|
||||
const queue = player.createQueue(..., {
|
||||
...,
|
||||
async onBeforeCreateStream(track, source, _queue) {
|
||||
// only trap youtube source
|
||||
if (source === "youtube") {
|
||||
// track here would be youtube track
|
||||
return (await playdl.stream(track.url, { discordPlayerCompatibility : true })).stream;
|
||||
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`<Queue>.onBeforeCreateStream` is called before actually downloading the stream. It is a different concept from extractors, where you are **just** downloading
|
||||
streams. `source` here will be a video source. Streams from `onBeforeCreateStream` are then piped to `FFmpeg` and finally sent to Discord voice servers.
|
||||
|
||||
# FAQ
|
||||
## How can I remove this?
|
||||
|
||||
> If you already made this change and want to switch to default mode in runtime,
|
||||
> you can set `queue.onBeforeCreateStream` to `null` which will make `discord-player` use default config.
|
||||
|
||||
## Which stream format should I return?
|
||||
|
||||
> It's not necessary to return opus format or whatever, since every streams have to be converted to s16le, due to inline volume.
|
||||
|
||||
## Can I use ytdl-core-discord?
|
||||
|
||||
> Yes, you can.
|
||||
|
||||
## Can I use this for other sources, like soundcloud?
|
||||
|
||||
> Absolutely.
|
||||
|
||||
## This is not working properly
|
||||
|
||||
> `onBeforeCreateStream` may not work properly if you have `spotifyBridge` enabled (enabled by default).
|
|
@ -1,18 +0,0 @@
|
|||
# Using Cookies to avoid 429
|
||||
|
||||
```js
|
||||
const { Player } = require("discord-player");
|
||||
|
||||
const player = new Player(client, {
|
||||
ytdlOptions: {
|
||||
requestOptions: {
|
||||
headers: {
|
||||
cookie: "YOUR_YOUTUBE_COOKIE"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
> Keep in mind that using `cookies` after getting `429` **does not fix the problem**.
|
||||
> You should use `cookies` before getting `429` which helps to **_reduce_** `Error: Status Code 429`
|
|
@ -1,19 +0,0 @@
|
|||
# Using Proxy to avoid 429
|
||||
|
||||
```js
|
||||
const { Player } = require("discord-player");
|
||||
const HttpsProxyAgent = require("https-proxy-agent");
|
||||
|
||||
// Remove "user:pass@" if you don't need to authenticate to your proxy.
|
||||
const proxy = "http://user:pass@111.111.111.111:8080";
|
||||
const agent = HttpsProxyAgent(proxy);
|
||||
|
||||
const player = new Player(client, {
|
||||
ytdlOptions: {
|
||||
requestOptions: { agent }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
> You may also create a simple proxy server and forward requests through it.
|
||||
> See **[https://github.com/http-party/node-http-proxy](https://github.com/http-party/node-http-proxy)** for more info.
|
20
package.json
20
package.json
|
@ -30,13 +30,13 @@
|
|||
"prepare": "husky install",
|
||||
"lint:fix": "eslint src --ext .ts --fix"
|
||||
},
|
||||
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
||||
"contributors": [
|
||||
"DevAndromeda <devandromeda@snowflakedev.org>"
|
||||
"DevAndromeda <devandromeda@snowflakedev.org>",
|
||||
"Jonny_Bro#4226"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Androz2091/discord-player.git"
|
||||
"url": "git+https://github.com/JonnyBro/discord-player.git"
|
||||
},
|
||||
"keywords": [
|
||||
"music",
|
||||
|
@ -52,27 +52,21 @@
|
|||
"discord-music-player",
|
||||
"discord-music",
|
||||
"music-player",
|
||||
"youtube-dl",
|
||||
"ytdl-core",
|
||||
"ytdl",
|
||||
"lavalink",
|
||||
"api"
|
||||
],
|
||||
"author": "Androz2091",
|
||||
"author": "Androz2091 & Jonny_Bro",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Androz2091/discord-player/issues"
|
||||
"url": "https://github.com/JonnyBro/discord-player/issues"
|
||||
},
|
||||
"homepage": "https://discord-player.js.org",
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "^0.11.0",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"soundcloud-scraper": "^5.0.3",
|
||||
"play-dl": "^1.9.5",
|
||||
"spotify-url-info": "^3.1.2",
|
||||
"tiny-typed-emitter": "^2.1.0",
|
||||
"tslib": "^2.4.0",
|
||||
"youtube-sr": "^4.3.0",
|
||||
"ytdl-core": "^4.11.0"
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/ts-docgen": "^0.4.1",
|
||||
|
|
104
src/Player.ts
104
src/Player.ts
|
@ -4,26 +4,19 @@ import { Queue } from "./Structures/Queue";
|
|||
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
||||
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
|
||||
import Track from "./Structures/Track";
|
||||
import { QueryResolver } from "./utils/QueryResolver";
|
||||
import YouTube from "youtube-sr";
|
||||
import { Util } from "./utils/Util";
|
||||
import play, { SoundCloudPlaylist, YouTubePlayList } from "play-dl";
|
||||
import Spotify from "spotify-url-info";
|
||||
import { QueryResolver } from "./utils/QueryResolver";
|
||||
import { Util } from "./utils/Util";
|
||||
import { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
|
||||
import { getInfo as ytdlGetInfo } from "ytdl-core";
|
||||
import { Client as SoundCloud, SearchResult as SoundCloudSearchResult } from "soundcloud-scraper";
|
||||
import { Playlist } from "./Structures/Playlist";
|
||||
import { ExtractorModel } from "./Structures/ExtractorModel";
|
||||
import { generateDependencyReport } from "@discordjs/voice";
|
||||
|
||||
const soundcloud = new SoundCloud();
|
||||
|
||||
class Player extends EventEmitter<PlayerEvents> {
|
||||
public readonly client: Client;
|
||||
public readonly options: PlayerInitOptions = {
|
||||
autoRegisterExtractor: true,
|
||||
ytdlOptions: {
|
||||
highWaterMark: 1 << 25
|
||||
},
|
||||
connectionTimeout: 20000
|
||||
};
|
||||
public readonly queues = new Collection<Snowflake, Queue>();
|
||||
|
@ -165,7 +158,6 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
const _meta = queueInitOptions.metadata;
|
||||
delete queueInitOptions["metadata"];
|
||||
queueInitOptions.volumeSmoothness ??= 0.08;
|
||||
queueInitOptions.ytdlOptions ??= this.options.ytdlOptions;
|
||||
const queue = new Queue(this, guild, queueInitOptions);
|
||||
queue.metadata = _meta;
|
||||
this.queues.set(guild.id, queue);
|
||||
|
@ -275,21 +267,21 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine;
|
||||
const qt = options.searchEngine === QueryType.AUTO ? await QueryResolver.resolve(query) : options.searchEngine;
|
||||
switch (qt) {
|
||||
case QueryType.YOUTUBE_VIDEO: {
|
||||
const info = await ytdlGetInfo(query, this.options.ytdlOptions).catch(Util.noop);
|
||||
const info = await play.video_info(query).catch(Util.noop);
|
||||
if (!info) return { playlist: null, tracks: [] };
|
||||
|
||||
const track = new Track(this, {
|
||||
title: info.videoDetails.title,
|
||||
description: info.videoDetails.description,
|
||||
author: info.videoDetails.author?.name,
|
||||
url: info.videoDetails.video_url,
|
||||
title: info.video_details.title,
|
||||
description: info.video_details.description,
|
||||
author: info.video_details.channel?.name,
|
||||
url: info.video_details.url,
|
||||
requestedBy: options.requestedBy as User,
|
||||
thumbnail: Util.last(info.videoDetails.thumbnails)?.url,
|
||||
views: parseInt(info.videoDetails.viewCount.replace(/[^0-9]/g, "")) || 0,
|
||||
duration: Util.buildTimeCode(Util.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)),
|
||||
thumbnail: Util.last(info.video_details.thumbnails)?.url,
|
||||
views: info.video_details.views || 0,
|
||||
duration: Util.buildTimeCode(Util.parseMS(info.video_details.durationInSec * 1000)),
|
||||
source: "youtube",
|
||||
raw: info
|
||||
});
|
||||
|
@ -297,12 +289,13 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
return { playlist: null, tracks: [track] };
|
||||
}
|
||||
case QueryType.YOUTUBE_SEARCH: {
|
||||
const videos = await YouTube.search(query, {
|
||||
type: "video"
|
||||
const videos = await play.search(query, {
|
||||
limit: 10,
|
||||
source: { youtube: "video" }
|
||||
}).catch(Util.noop);
|
||||
if (!videos) return { playlist: null, tracks: [] };
|
||||
|
||||
const tracks = videos.map((m) => {
|
||||
const tracks = videos.map(m => {
|
||||
(m as any).source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
return new Track(this, {
|
||||
title: m.title,
|
||||
|
@ -310,34 +303,37 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
author: m.channel?.name,
|
||||
url: m.url,
|
||||
requestedBy: options.requestedBy as User,
|
||||
thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"),
|
||||
thumbnail: Util.last(m.thumbnails).url,
|
||||
views: m.views,
|
||||
duration: m.durationFormatted,
|
||||
duration: m.durationRaw,
|
||||
source: "youtube",
|
||||
raw: m
|
||||
});
|
||||
});
|
||||
|
||||
return { playlist: null, tracks };
|
||||
return { playlist: null, tracks, searched: true };
|
||||
}
|
||||
case QueryType.SOUNDCLOUD_TRACK:
|
||||
case QueryType.SOUNDCLOUD_SEARCH: {
|
||||
const result: SoundCloudSearchResult[] = QueryResolver.resolve(query) === QueryType.SOUNDCLOUD_TRACK ? [{ url: query }] : await soundcloud.search(query, "track").catch(() => []);
|
||||
const result = await QueryResolver.resolve(query) === QueryType.SOUNDCLOUD_TRACK ? [{ url: query }] : await play.search(query, {
|
||||
limit: 5,
|
||||
source: { soundcloud: "tracks" }
|
||||
}).catch(() => []);
|
||||
if (!result || !result.length) return { playlist: null, tracks: [] };
|
||||
const res: Track[] = [];
|
||||
|
||||
for (const r of result) {
|
||||
const trackInfo = await soundcloud.getSongInfo(r.url).catch(Util.noop);
|
||||
const trackInfo = await play.soundcloud(r.url).catch(Util.noop);
|
||||
if (!trackInfo) continue;
|
||||
|
||||
const track = new Track(this, {
|
||||
title: trackInfo.title,
|
||||
title: trackInfo.name,
|
||||
url: trackInfo.url,
|
||||
duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)),
|
||||
description: trackInfo.description,
|
||||
thumbnail: trackInfo.thumbnail,
|
||||
views: trackInfo.playCount,
|
||||
author: trackInfo.author.name,
|
||||
duration: Util.buildTimeCode(Util.parseMS(trackInfo.durationInMs)),
|
||||
description: "",
|
||||
thumbnail: trackInfo.user.thumbnail,
|
||||
views: 0,
|
||||
author: trackInfo.user.name,
|
||||
requestedBy: options.requestedBy,
|
||||
source: "soundcloud",
|
||||
engine: trackInfo
|
||||
|
@ -440,18 +436,18 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
return { playlist: playlist, tracks: playlist.tracks };
|
||||
}
|
||||
case QueryType.SOUNDCLOUD_PLAYLIST: {
|
||||
const data = await soundcloud.getPlaylist(query).catch(Util.noop);
|
||||
const data = await play.soundcloud(query).catch(Util.noop) as unknown as SoundCloudPlaylist;
|
||||
if (!data) return { playlist: null, tracks: [] };
|
||||
|
||||
const res = new Playlist(this, {
|
||||
title: data.title,
|
||||
description: data.description ?? "",
|
||||
thumbnail: data.thumbnail ?? "https://soundcloud.com/pwa-icon-192.png",
|
||||
title: data.name,
|
||||
description: "",
|
||||
thumbnail: "https://soundcloud.com/pwa-icon-192.png",
|
||||
type: "playlist",
|
||||
source: "soundcloud",
|
||||
author: {
|
||||
name: data.author?.name ?? data.author?.username ?? "Unknown Artist",
|
||||
url: data.author?.profile
|
||||
name: data.user.name ?? "Unknown Owner",
|
||||
url: data.user.url
|
||||
},
|
||||
tracks: [],
|
||||
id: `${data.id}`, // stringified
|
||||
|
@ -459,15 +455,16 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
rawPlaylist: data
|
||||
});
|
||||
|
||||
for (const song of data.tracks) {
|
||||
const songs = await data.all_tracks();
|
||||
for (const song of songs) {
|
||||
const track = new Track(this, {
|
||||
title: song.title,
|
||||
description: song.description ?? "",
|
||||
author: song.author?.username ?? song.author?.name ?? "Unknown Artist",
|
||||
title: song.name,
|
||||
description: "",
|
||||
author: song.publisher.name ?? "Unknown Publisher",
|
||||
url: song.url,
|
||||
thumbnail: song.thumbnail,
|
||||
duration: Util.buildTimeCode(Util.parseMS(song.duration)),
|
||||
views: song.playCount ?? 0,
|
||||
duration: Util.buildTimeCode(Util.parseMS(song.durationInMs)),
|
||||
views: 0,
|
||||
requestedBy: options.requestedBy,
|
||||
playlist: res,
|
||||
source: "soundcloud",
|
||||
|
@ -479,11 +476,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
return { playlist: res, tracks: res.tracks };
|
||||
}
|
||||
case QueryType.YOUTUBE_PLAYLIST: {
|
||||
const ytpl = await YouTube.getPlaylist(query).catch(Util.noop);
|
||||
const ytpl = await play.playlist_info(query, { incomplete: true }).catch(Util.noop) as unknown as YouTubePlayList;
|
||||
if (!ytpl) return { playlist: null, tracks: [] };
|
||||
|
||||
await ytpl.fetch().catch(Util.noop);
|
||||
|
||||
const playlist: Playlist = new Playlist(this, {
|
||||
title: ytpl.title,
|
||||
thumbnail: ytpl.thumbnail as unknown as string,
|
||||
|
@ -500,22 +495,21 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
rawPlaylist: ytpl
|
||||
});
|
||||
|
||||
playlist.tracks = ytpl.videos.map(
|
||||
(video) =>
|
||||
const videos = await ytpl.all_videos();
|
||||
playlist.tracks = videos.map(video =>
|
||||
new Track(this, {
|
||||
title: video.title,
|
||||
description: video.description,
|
||||
author: video.channel?.name,
|
||||
url: video.url,
|
||||
requestedBy: options.requestedBy as User,
|
||||
thumbnail: video.thumbnail.url,
|
||||
thumbnail: Util.last(video.thumbnails).url,
|
||||
views: video.views,
|
||||
duration: video.durationFormatted,
|
||||
duration: video.durationRaw,
|
||||
raw: video,
|
||||
playlist: playlist,
|
||||
source: "youtube"
|
||||
})
|
||||
);
|
||||
}));
|
||||
|
||||
return { playlist: playlist, tracks: playlist.tracks };
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@ import { Player } from "../Player";
|
|||
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||
import Track from "./Track";
|
||||
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
||||
import ytdl from "ytdl-core";
|
||||
import { AudioResource, StreamType } from "@discordjs/voice";
|
||||
import play from "play-dl";
|
||||
import { Util } from "../utils/Util";
|
||||
import YouTube from "youtube-sr";
|
||||
import AudioFilters from "../utils/AudioFilters";
|
||||
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
||||
import type { Readable } from "stream";
|
||||
|
@ -103,12 +102,8 @@ class Queue<T = unknown> {
|
|||
leaveOnEmpty: true,
|
||||
leaveOnEmptyCooldown: 1000,
|
||||
autoSelfDeaf: true,
|
||||
ytdlOptions: {
|
||||
highWaterMark: 1 << 25
|
||||
},
|
||||
initialVolume: 100,
|
||||
bufferingTimeout: 3000,
|
||||
spotifyBridge: true,
|
||||
disableVolume: false
|
||||
} as PlayerOptions,
|
||||
options
|
||||
|
@ -636,23 +631,15 @@ class Queue<T = unknown> {
|
|||
const hasCustomDownloader = typeof this.onBeforeCreateStream === "function";
|
||||
|
||||
if (["youtube", "spotify"].includes(track.raw.source)) {
|
||||
let spotifyResolved = false;
|
||||
if (this.options.spotifyBridge && track.raw.source === "spotify" && !track.raw.engine) {
|
||||
track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" })
|
||||
.then((res) => res[0].url)
|
||||
.catch(() => null);
|
||||
spotifyResolved = true;
|
||||
}
|
||||
|
||||
const url = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
||||
if (!url) return void this.play(this.tracks.shift(), { immediate: true });
|
||||
|
||||
if (hasCustomDownloader) {
|
||||
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) || null;
|
||||
stream = (await this.onBeforeCreateStream(track, "youtube", this)) || null;
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
stream = ytdl(url, this.options.ytdlOptions);
|
||||
stream = (await play.stream(url, { discordPlayerCompatibility: true })).stream;
|
||||
}
|
||||
} else {
|
||||
const arbitraryStream = (hasCustomDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
|
||||
|
@ -665,7 +652,7 @@ class Queue<T = unknown> {
|
|||
}
|
||||
|
||||
const ffmpegStream = createFFmpegStream(stream, {
|
||||
encoderArgs: options.encoderArgs || this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||
encoderArgs: options.encoderArgs || [],
|
||||
seek: options.seek ? options.seek / 1000 : 0,
|
||||
fmt: "s16le"
|
||||
}).on("error", (err) => {
|
||||
|
@ -704,27 +691,22 @@ class Queue<T = unknown> {
|
|||
if (this.options.leaveOnEnd) this.destroy();
|
||||
return void this.player.emit("queueEnd", this);
|
||||
}
|
||||
let info = await YouTube.getVideo(track.url)
|
||||
.then((x) => x.videos[0])
|
||||
.catch(Util.noop);
|
||||
// fallback
|
||||
if (!info)
|
||||
info = await YouTube.search(track.author)
|
||||
.then((x) => x[0])
|
||||
const info = await play.video_info(track.url)
|
||||
.catch(Util.noop);
|
||||
if (!info) {
|
||||
if (this.options.leaveOnEnd) this.destroy();
|
||||
return void this.player.emit("queueEnd", this);
|
||||
}
|
||||
|
||||
const related = await play.video_info(info.related_videos[1]);
|
||||
const nextTrack = new Track(this.player, {
|
||||
title: info.title,
|
||||
url: `https://www.youtube.com/watch?v=${info.id}`,
|
||||
duration: info.durationFormatted ? Util.buildTimeCode(Util.parseMS(info.duration * 1000)) : "0:00",
|
||||
title: related.video_details.title,
|
||||
url: related.video_details.url,
|
||||
duration: related.video_details.durationRaw ? Util.buildTimeCode(Util.parseMS(related.video_details.durationInSec * 1000)) : "0:00",
|
||||
description: "",
|
||||
thumbnail: typeof info.thumbnail === "string" ? info.thumbnail : info.thumbnail.url,
|
||||
views: info.views,
|
||||
author: info.channel.name,
|
||||
thumbnail: Util.last(related.video_details.thumbnails).url,
|
||||
views: related.video_details.views,
|
||||
author: related.video_details.channel.name,
|
||||
requestedBy: track.requestedBy,
|
||||
source: "youtube"
|
||||
});
|
||||
|
|
|
@ -110,6 +110,7 @@ class Track {
|
|||
|
||||
private _patch(data: RawTrackData) {
|
||||
this.title = escapeMarkdown(data.title ?? "");
|
||||
this.description = data.description ?? "";
|
||||
this.author = data.author ?? "";
|
||||
this.url = data.url ?? "";
|
||||
this.thumbnail = data.thumbnail ?? "";
|
||||
|
|
|
@ -4,13 +4,13 @@ import { Queue } from "../Structures/Queue";
|
|||
import Track from "../Structures/Track";
|
||||
import { Playlist } from "../Structures/Playlist";
|
||||
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||
import { downloadOptions } from "ytdl-core";
|
||||
|
||||
export type FiltersName = keyof QueueFilters;
|
||||
|
||||
export interface PlayerSearchResult {
|
||||
playlist: Playlist | null;
|
||||
tracks: Track[];
|
||||
searched?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,7 +70,7 @@ export type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary";
|
|||
* @property {string} url The url
|
||||
* @property {string} thumbnail The thumbnail
|
||||
* @property {string} duration The duration
|
||||
* @property {number} views The views
|
||||
* @property {number | boolean} views The views
|
||||
* @property {User} requestedBy The user who requested this track
|
||||
* @property {Playlist} [playlist] The playlist
|
||||
* @property {TrackSource} [source="arbitrary"] The source
|
||||
|
@ -131,10 +131,8 @@ export interface PlayerProgressbarOptions {
|
|||
* @property {boolean} [leaveOnEmpty=true] If it should leave on empty
|
||||
* @property {number} [leaveOnEmptyCooldown=1000] The cooldown in ms
|
||||
* @property {boolean} [autoSelfDeaf=true] If it should set the bot in deaf mode
|
||||
* @property {YTDLDownloadOptions} [ytdlOptions] The youtube download options
|
||||
* @property {number} [initialVolume=100] The initial player volume
|
||||
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
||||
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
|
||||
* @property {boolean} [disableVolume=false] If player should disable inline volume
|
||||
* @property {number} [volumeSmoothness=0] The volume transition smoothness between volume changes (lower the value to get better result)
|
||||
* Setting this or leaving this empty will disable this effect. Example: `volumeSmoothness: 0.1`
|
||||
|
@ -146,10 +144,8 @@ export interface PlayerOptions {
|
|||
leaveOnEmpty?: boolean;
|
||||
leaveOnEmptyCooldown?: number;
|
||||
autoSelfDeaf?: boolean;
|
||||
ytdlOptions?: downloadOptions;
|
||||
initialVolume?: number;
|
||||
bufferingTimeout?: number;
|
||||
spotifyBridge?: boolean;
|
||||
disableVolume?: boolean;
|
||||
volumeSmoothness?: number;
|
||||
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
||||
|
@ -477,11 +473,9 @@ export interface PlaylistJSON {
|
|||
/**
|
||||
* @typedef {object} PlayerInitOptions
|
||||
* @property {boolean} [autoRegisterExtractor=true] If it should automatically register `@discord-player/extractor`
|
||||
* @property {YTDLDownloadOptions} [ytdlOptions] The options passed to `ytdl-core`
|
||||
* @property {number} [connectionTimeout=20000] The voice connection timeout
|
||||
*/
|
||||
export interface PlayerInitOptions {
|
||||
autoRegisterExtractor?: boolean;
|
||||
ytdlOptions?: downloadOptions;
|
||||
connectionTimeout?: number;
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
import { validateID, validateURL } from "ytdl-core";
|
||||
import { YouTube } from "youtube-sr";
|
||||
import { QueryType } from "../types/types";
|
||||
import play from "play-dl";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { validateURL as SoundcloudValidateURL } from "soundcloud-scraper";
|
||||
|
||||
// scary things below *sigh*
|
||||
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
|
||||
|
@ -27,11 +24,11 @@ class QueryResolver {
|
|||
* @param {string} query The query
|
||||
* @returns {QueryType}
|
||||
*/
|
||||
static resolve(query: string): QueryType {
|
||||
if (SoundcloudValidateURL(query, "track")) return QueryType.SOUNDCLOUD_TRACK;
|
||||
if (SoundcloudValidateURL(query, "playlist") || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST;
|
||||
if (YouTube.isPlaylist(query)) return QueryType.YOUTUBE_PLAYLIST;
|
||||
if (validateID(query) || validateURL(query)) return QueryType.YOUTUBE_VIDEO;
|
||||
static async resolve(query: string): Promise<QueryType> {
|
||||
if (await play.so_validate(query) === "track") return QueryType.SOUNDCLOUD_TRACK;
|
||||
if (await play.so_validate(query) === "playlist" || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST;
|
||||
if (play.yt_validate(query) === "playlist") return QueryType.YOUTUBE_PLAYLIST;
|
||||
if (play.yt_validate(query) === "video") return QueryType.YOUTUBE_VIDEO;
|
||||
if (spotifySongRegex.test(query)) return QueryType.SPOTIFY_SONG;
|
||||
if (spotifyPlaylistRegex.test(query)) return QueryType.SPOTIFY_PLAYLIST;
|
||||
if (spotifyAlbumRegex.test(query)) return QueryType.SPOTIFY_ALBUM;
|
||||
|
@ -48,8 +45,8 @@ class QueryResolver {
|
|||
* @param {string} query The query
|
||||
* @returns {string}
|
||||
*/
|
||||
static getVimeoID(query: string): string {
|
||||
return QueryResolver.resolve(query) === QueryType.VIMEO
|
||||
static async getVimeoID(query: string): Promise<string> {
|
||||
return await QueryResolver.resolve(query) === QueryType.VIMEO
|
||||
? query
|
||||
.split("/")
|
||||
.filter((x) => !!x)
|
||||
|
|
Loading…
Reference in a new issue