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)**.
|
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)
|
[![wakatime](https://wakatime.com/badge/github/JonnyBro/discord-player.svg)](https://wakatime.com/badge/github/Androz2091/discord-player)
|
||||||
[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/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)
|
||||||
[![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)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Install **[discord-player](https://npmjs.com/package/discord-player)**
|
### Install **[discord-player-play-dl](https://github.com/JonnyBro/discord-player-play-dl)**
|
||||||
|
|
||||||
```sh
|
```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)**
|
### Install **[@discordjs/opus](https://npmjs.com/package/@discordjs/opus)**
|
||||||
|
@ -145,7 +142,7 @@ By default, discord-player supports **YouTube**, **Spotify** and **SoundCloud**
|
||||||
|
|
||||||
### Optional dependencies
|
### 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)
|
#### [@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).
|
`@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!
|
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)
|
* **[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)
|
||||||
* [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**)
|
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
### Smooth Volume
|
### 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
|
```js
|
||||||
// CJS
|
// CJS
|
||||||
|
@ -184,61 +175,10 @@ import "discord-player/smoothVolume"
|
||||||
|
|
||||||
> ⚠️ Make sure that line is situated at the **TOP** of your **main** file.
|
> ⚠️ 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
|
### 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.
|
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 `createStream` functionality of discord player.
|
If you need to modify this behavior without touching extractors, you need to use `onBeforeCreateStream` functionality of Discord player Play-dl.
|
||||||
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
|
`<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.
|
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",
|
"prepare": "husky install",
|
||||||
"lint:fix": "eslint src --ext .ts --fix"
|
"lint:fix": "eslint src --ext .ts --fix"
|
||||||
},
|
},
|
||||||
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"DevAndromeda <devandromeda@snowflakedev.org>"
|
"DevAndromeda <devandromeda@snowflakedev.org>",
|
||||||
|
"Jonny_Bro#4226"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Androz2091/discord-player.git"
|
"url": "git+https://github.com/JonnyBro/discord-player.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"music",
|
"music",
|
||||||
|
@ -52,27 +52,21 @@
|
||||||
"discord-music-player",
|
"discord-music-player",
|
||||||
"discord-music",
|
"discord-music",
|
||||||
"music-player",
|
"music-player",
|
||||||
"youtube-dl",
|
|
||||||
"ytdl-core",
|
|
||||||
"ytdl",
|
|
||||||
"lavalink",
|
|
||||||
"api"
|
"api"
|
||||||
],
|
],
|
||||||
"author": "Androz2091",
|
"author": "Androz2091 & Jonny_Bro",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Androz2091/discord-player/issues"
|
"url": "https://github.com/JonnyBro/discord-player/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://discord-player.js.org",
|
"homepage": "https://discord-player.js.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.11.0",
|
"@discordjs/voice": "^0.11.0",
|
||||||
"libsodium-wrappers": "^0.7.10",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
"soundcloud-scraper": "^5.0.3",
|
"play-dl": "^1.9.5",
|
||||||
"spotify-url-info": "^3.1.2",
|
"spotify-url-info": "^3.1.2",
|
||||||
"tiny-typed-emitter": "^2.1.0",
|
"tiny-typed-emitter": "^2.1.0",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0"
|
||||||
"youtube-sr": "^4.3.0",
|
|
||||||
"ytdl-core": "^4.11.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@discordjs/ts-docgen": "^0.4.1",
|
"@discordjs/ts-docgen": "^0.4.1",
|
||||||
|
|
124
src/Player.ts
124
src/Player.ts
|
@ -4,26 +4,19 @@ import { Queue } from "./Structures/Queue";
|
||||||
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
||||||
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
|
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
|
||||||
import Track from "./Structures/Track";
|
import Track from "./Structures/Track";
|
||||||
import { QueryResolver } from "./utils/QueryResolver";
|
import play, { SoundCloudPlaylist, YouTubePlayList } from "play-dl";
|
||||||
import YouTube from "youtube-sr";
|
|
||||||
import { Util } from "./utils/Util";
|
|
||||||
import Spotify from "spotify-url-info";
|
import Spotify from "spotify-url-info";
|
||||||
|
import { QueryResolver } from "./utils/QueryResolver";
|
||||||
|
import { Util } from "./utils/Util";
|
||||||
import { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
|
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 { Playlist } from "./Structures/Playlist";
|
||||||
import { ExtractorModel } from "./Structures/ExtractorModel";
|
import { ExtractorModel } from "./Structures/ExtractorModel";
|
||||||
import { generateDependencyReport } from "@discordjs/voice";
|
import { generateDependencyReport } from "@discordjs/voice";
|
||||||
|
|
||||||
const soundcloud = new SoundCloud();
|
|
||||||
|
|
||||||
class Player extends EventEmitter<PlayerEvents> {
|
class Player extends EventEmitter<PlayerEvents> {
|
||||||
public readonly client: Client;
|
public readonly client: Client;
|
||||||
public readonly options: PlayerInitOptions = {
|
public readonly options: PlayerInitOptions = {
|
||||||
autoRegisterExtractor: true,
|
autoRegisterExtractor: true,
|
||||||
ytdlOptions: {
|
|
||||||
highWaterMark: 1 << 25
|
|
||||||
},
|
|
||||||
connectionTimeout: 20000
|
connectionTimeout: 20000
|
||||||
};
|
};
|
||||||
public readonly queues = new Collection<Snowflake, Queue>();
|
public readonly queues = new Collection<Snowflake, Queue>();
|
||||||
|
@ -165,7 +158,6 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
const _meta = queueInitOptions.metadata;
|
const _meta = queueInitOptions.metadata;
|
||||||
delete queueInitOptions["metadata"];
|
delete queueInitOptions["metadata"];
|
||||||
queueInitOptions.volumeSmoothness ??= 0.08;
|
queueInitOptions.volumeSmoothness ??= 0.08;
|
||||||
queueInitOptions.ytdlOptions ??= this.options.ytdlOptions;
|
|
||||||
const queue = new Queue(this, guild, queueInitOptions);
|
const queue = new Queue(this, guild, queueInitOptions);
|
||||||
queue.metadata = _meta;
|
queue.metadata = _meta;
|
||||||
this.queues.set(guild.id, queue);
|
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) {
|
switch (qt) {
|
||||||
case QueryType.YOUTUBE_VIDEO: {
|
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: [] };
|
if (!info) return { playlist: null, tracks: [] };
|
||||||
|
|
||||||
const track = new Track(this, {
|
const track = new Track(this, {
|
||||||
title: info.videoDetails.title,
|
title: info.video_details.title,
|
||||||
description: info.videoDetails.description,
|
description: info.video_details.description,
|
||||||
author: info.videoDetails.author?.name,
|
author: info.video_details.channel?.name,
|
||||||
url: info.videoDetails.video_url,
|
url: info.video_details.url,
|
||||||
requestedBy: options.requestedBy as User,
|
requestedBy: options.requestedBy as User,
|
||||||
thumbnail: Util.last(info.videoDetails.thumbnails)?.url,
|
thumbnail: Util.last(info.video_details.thumbnails)?.url,
|
||||||
views: parseInt(info.videoDetails.viewCount.replace(/[^0-9]/g, "")) || 0,
|
views: info.video_details.views || 0,
|
||||||
duration: Util.buildTimeCode(Util.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)),
|
duration: Util.buildTimeCode(Util.parseMS(info.video_details.durationInSec * 1000)),
|
||||||
source: "youtube",
|
source: "youtube",
|
||||||
raw: info
|
raw: info
|
||||||
});
|
});
|
||||||
|
@ -297,12 +289,13 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
return { playlist: null, tracks: [track] };
|
return { playlist: null, tracks: [track] };
|
||||||
}
|
}
|
||||||
case QueryType.YOUTUBE_SEARCH: {
|
case QueryType.YOUTUBE_SEARCH: {
|
||||||
const videos = await YouTube.search(query, {
|
const videos = await play.search(query, {
|
||||||
type: "video"
|
limit: 10,
|
||||||
|
source: { youtube: "video" }
|
||||||
}).catch(Util.noop);
|
}).catch(Util.noop);
|
||||||
if (!videos) return { playlist: null, tracks: [] };
|
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
|
(m as any).source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
return new Track(this, {
|
return new Track(this, {
|
||||||
title: m.title,
|
title: m.title,
|
||||||
|
@ -310,34 +303,37 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
author: m.channel?.name,
|
author: m.channel?.name,
|
||||||
url: m.url,
|
url: m.url,
|
||||||
requestedBy: options.requestedBy as User,
|
requestedBy: options.requestedBy as User,
|
||||||
thumbnail: m.thumbnail?.displayThumbnailURL("maxresdefault"),
|
thumbnail: Util.last(m.thumbnails).url,
|
||||||
views: m.views,
|
views: m.views,
|
||||||
duration: m.durationFormatted,
|
duration: m.durationRaw,
|
||||||
source: "youtube",
|
source: "youtube",
|
||||||
raw: m
|
raw: m
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { playlist: null, tracks };
|
return { playlist: null, tracks, searched: true };
|
||||||
}
|
}
|
||||||
case QueryType.SOUNDCLOUD_TRACK:
|
case QueryType.SOUNDCLOUD_TRACK:
|
||||||
case QueryType.SOUNDCLOUD_SEARCH: {
|
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: [] };
|
if (!result || !result.length) return { playlist: null, tracks: [] };
|
||||||
const res: Track[] = [];
|
const res: Track[] = [];
|
||||||
|
|
||||||
for (const r of result) {
|
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;
|
if (!trackInfo) continue;
|
||||||
|
|
||||||
const track = new Track(this, {
|
const track = new Track(this, {
|
||||||
title: trackInfo.title,
|
title: trackInfo.name,
|
||||||
url: trackInfo.url,
|
url: trackInfo.url,
|
||||||
duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)),
|
duration: Util.buildTimeCode(Util.parseMS(trackInfo.durationInMs)),
|
||||||
description: trackInfo.description,
|
description: "",
|
||||||
thumbnail: trackInfo.thumbnail,
|
thumbnail: trackInfo.user.thumbnail,
|
||||||
views: trackInfo.playCount,
|
views: 0,
|
||||||
author: trackInfo.author.name,
|
author: trackInfo.user.name,
|
||||||
requestedBy: options.requestedBy,
|
requestedBy: options.requestedBy,
|
||||||
source: "soundcloud",
|
source: "soundcloud",
|
||||||
engine: trackInfo
|
engine: trackInfo
|
||||||
|
@ -440,18 +436,18 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
return { playlist: playlist, tracks: playlist.tracks };
|
return { playlist: playlist, tracks: playlist.tracks };
|
||||||
}
|
}
|
||||||
case QueryType.SOUNDCLOUD_PLAYLIST: {
|
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: [] };
|
if (!data) return { playlist: null, tracks: [] };
|
||||||
|
|
||||||
const res = new Playlist(this, {
|
const res = new Playlist(this, {
|
||||||
title: data.title,
|
title: data.name,
|
||||||
description: data.description ?? "",
|
description: "",
|
||||||
thumbnail: data.thumbnail ?? "https://soundcloud.com/pwa-icon-192.png",
|
thumbnail: "https://soundcloud.com/pwa-icon-192.png",
|
||||||
type: "playlist",
|
type: "playlist",
|
||||||
source: "soundcloud",
|
source: "soundcloud",
|
||||||
author: {
|
author: {
|
||||||
name: data.author?.name ?? data.author?.username ?? "Unknown Artist",
|
name: data.user.name ?? "Unknown Owner",
|
||||||
url: data.author?.profile
|
url: data.user.url
|
||||||
},
|
},
|
||||||
tracks: [],
|
tracks: [],
|
||||||
id: `${data.id}`, // stringified
|
id: `${data.id}`, // stringified
|
||||||
|
@ -459,15 +455,16 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
rawPlaylist: data
|
rawPlaylist: data
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const song of data.tracks) {
|
const songs = await data.all_tracks();
|
||||||
|
for (const song of songs) {
|
||||||
const track = new Track(this, {
|
const track = new Track(this, {
|
||||||
title: song.title,
|
title: song.name,
|
||||||
description: song.description ?? "",
|
description: "",
|
||||||
author: song.author?.username ?? song.author?.name ?? "Unknown Artist",
|
author: song.publisher.name ?? "Unknown Publisher",
|
||||||
url: song.url,
|
url: song.url,
|
||||||
thumbnail: song.thumbnail,
|
thumbnail: song.thumbnail,
|
||||||
duration: Util.buildTimeCode(Util.parseMS(song.duration)),
|
duration: Util.buildTimeCode(Util.parseMS(song.durationInMs)),
|
||||||
views: song.playCount ?? 0,
|
views: 0,
|
||||||
requestedBy: options.requestedBy,
|
requestedBy: options.requestedBy,
|
||||||
playlist: res,
|
playlist: res,
|
||||||
source: "soundcloud",
|
source: "soundcloud",
|
||||||
|
@ -479,11 +476,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
return { playlist: res, tracks: res.tracks };
|
return { playlist: res, tracks: res.tracks };
|
||||||
}
|
}
|
||||||
case QueryType.YOUTUBE_PLAYLIST: {
|
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: [] };
|
if (!ytpl) return { playlist: null, tracks: [] };
|
||||||
|
|
||||||
await ytpl.fetch().catch(Util.noop);
|
|
||||||
|
|
||||||
const playlist: Playlist = new Playlist(this, {
|
const playlist: Playlist = new Playlist(this, {
|
||||||
title: ytpl.title,
|
title: ytpl.title,
|
||||||
thumbnail: ytpl.thumbnail as unknown as string,
|
thumbnail: ytpl.thumbnail as unknown as string,
|
||||||
|
@ -500,22 +495,21 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
rawPlaylist: ytpl
|
rawPlaylist: ytpl
|
||||||
});
|
});
|
||||||
|
|
||||||
playlist.tracks = ytpl.videos.map(
|
const videos = await ytpl.all_videos();
|
||||||
(video) =>
|
playlist.tracks = videos.map(video =>
|
||||||
new Track(this, {
|
new Track(this, {
|
||||||
title: video.title,
|
title: video.title,
|
||||||
description: video.description,
|
description: video.description,
|
||||||
author: video.channel?.name,
|
author: video.channel?.name,
|
||||||
url: video.url,
|
url: video.url,
|
||||||
requestedBy: options.requestedBy as User,
|
requestedBy: options.requestedBy as User,
|
||||||
thumbnail: video.thumbnail.url,
|
thumbnail: Util.last(video.thumbnails).url,
|
||||||
views: video.views,
|
views: video.views,
|
||||||
duration: video.durationFormatted,
|
duration: video.durationRaw,
|
||||||
raw: video,
|
raw: video,
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
source: "youtube"
|
source: "youtube"
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
return { playlist: playlist, tracks: playlist.tracks };
|
return { playlist: playlist, tracks: playlist.tracks };
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { Player } from "../Player";
|
||||||
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||||
import Track from "./Track";
|
import Track from "./Track";
|
||||||
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
||||||
import ytdl from "ytdl-core";
|
|
||||||
import { AudioResource, StreamType } from "@discordjs/voice";
|
import { AudioResource, StreamType } from "@discordjs/voice";
|
||||||
|
import play from "play-dl";
|
||||||
import { Util } from "../utils/Util";
|
import { Util } from "../utils/Util";
|
||||||
import YouTube from "youtube-sr";
|
|
||||||
import AudioFilters from "../utils/AudioFilters";
|
import AudioFilters from "../utils/AudioFilters";
|
||||||
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
|
@ -103,12 +102,8 @@ class Queue<T = unknown> {
|
||||||
leaveOnEmpty: true,
|
leaveOnEmpty: true,
|
||||||
leaveOnEmptyCooldown: 1000,
|
leaveOnEmptyCooldown: 1000,
|
||||||
autoSelfDeaf: true,
|
autoSelfDeaf: true,
|
||||||
ytdlOptions: {
|
|
||||||
highWaterMark: 1 << 25
|
|
||||||
},
|
|
||||||
initialVolume: 100,
|
initialVolume: 100,
|
||||||
bufferingTimeout: 3000,
|
bufferingTimeout: 3000,
|
||||||
spotifyBridge: true,
|
|
||||||
disableVolume: false
|
disableVolume: false
|
||||||
} as PlayerOptions,
|
} as PlayerOptions,
|
||||||
options
|
options
|
||||||
|
@ -636,23 +631,15 @@ class Queue<T = unknown> {
|
||||||
const hasCustomDownloader = typeof this.onBeforeCreateStream === "function";
|
const hasCustomDownloader = typeof this.onBeforeCreateStream === "function";
|
||||||
|
|
||||||
if (["youtube", "spotify"].includes(track.raw.source)) {
|
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;
|
const url = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
||||||
if (!url) return void this.play(this.tracks.shift(), { immediate: true });
|
if (!url) return void this.play(this.tracks.shift(), { immediate: true });
|
||||||
|
|
||||||
if (hasCustomDownloader) {
|
if (hasCustomDownloader) {
|
||||||
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) || null;
|
stream = (await this.onBeforeCreateStream(track, "youtube", this)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
stream = ytdl(url, this.options.ytdlOptions);
|
stream = (await play.stream(url, { discordPlayerCompatibility: true })).stream;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const arbitraryStream = (hasCustomDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
|
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, {
|
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,
|
seek: options.seek ? options.seek / 1000 : 0,
|
||||||
fmt: "s16le"
|
fmt: "s16le"
|
||||||
}).on("error", (err) => {
|
}).on("error", (err) => {
|
||||||
|
@ -704,27 +691,22 @@ class Queue<T = unknown> {
|
||||||
if (this.options.leaveOnEnd) this.destroy();
|
if (this.options.leaveOnEnd) this.destroy();
|
||||||
return void this.player.emit("queueEnd", this);
|
return void this.player.emit("queueEnd", this);
|
||||||
}
|
}
|
||||||
let info = await YouTube.getVideo(track.url)
|
const info = await play.video_info(track.url)
|
||||||
.then((x) => x.videos[0])
|
|
||||||
.catch(Util.noop);
|
.catch(Util.noop);
|
||||||
// fallback
|
|
||||||
if (!info)
|
|
||||||
info = await YouTube.search(track.author)
|
|
||||||
.then((x) => x[0])
|
|
||||||
.catch(Util.noop);
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
if (this.options.leaveOnEnd) this.destroy();
|
if (this.options.leaveOnEnd) this.destroy();
|
||||||
return void this.player.emit("queueEnd", this);
|
return void this.player.emit("queueEnd", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const related = await play.video_info(info.related_videos[1]);
|
||||||
const nextTrack = new Track(this.player, {
|
const nextTrack = new Track(this.player, {
|
||||||
title: info.title,
|
title: related.video_details.title,
|
||||||
url: `https://www.youtube.com/watch?v=${info.id}`,
|
url: related.video_details.url,
|
||||||
duration: info.durationFormatted ? Util.buildTimeCode(Util.parseMS(info.duration * 1000)) : "0:00",
|
duration: related.video_details.durationRaw ? Util.buildTimeCode(Util.parseMS(related.video_details.durationInSec * 1000)) : "0:00",
|
||||||
description: "",
|
description: "",
|
||||||
thumbnail: typeof info.thumbnail === "string" ? info.thumbnail : info.thumbnail.url,
|
thumbnail: Util.last(related.video_details.thumbnails).url,
|
||||||
views: info.views,
|
views: related.video_details.views,
|
||||||
author: info.channel.name,
|
author: related.video_details.channel.name,
|
||||||
requestedBy: track.requestedBy,
|
requestedBy: track.requestedBy,
|
||||||
source: "youtube"
|
source: "youtube"
|
||||||
});
|
});
|
||||||
|
|
|
@ -110,6 +110,7 @@ class Track {
|
||||||
|
|
||||||
private _patch(data: RawTrackData) {
|
private _patch(data: RawTrackData) {
|
||||||
this.title = escapeMarkdown(data.title ?? "");
|
this.title = escapeMarkdown(data.title ?? "");
|
||||||
|
this.description = data.description ?? "";
|
||||||
this.author = data.author ?? "";
|
this.author = data.author ?? "";
|
||||||
this.url = data.url ?? "";
|
this.url = data.url ?? "";
|
||||||
this.thumbnail = data.thumbnail ?? "";
|
this.thumbnail = data.thumbnail ?? "";
|
||||||
|
|
|
@ -4,13 +4,13 @@ import { Queue } from "../Structures/Queue";
|
||||||
import Track from "../Structures/Track";
|
import Track from "../Structures/Track";
|
||||||
import { Playlist } from "../Structures/Playlist";
|
import { Playlist } from "../Structures/Playlist";
|
||||||
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||||
import { downloadOptions } from "ytdl-core";
|
|
||||||
|
|
||||||
export type FiltersName = keyof QueueFilters;
|
export type FiltersName = keyof QueueFilters;
|
||||||
|
|
||||||
export interface PlayerSearchResult {
|
export interface PlayerSearchResult {
|
||||||
playlist: Playlist | null;
|
playlist: Playlist | null;
|
||||||
tracks: Track[];
|
tracks: Track[];
|
||||||
|
searched?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,7 +70,7 @@ export type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary";
|
||||||
* @property {string} url The url
|
* @property {string} url The url
|
||||||
* @property {string} thumbnail The thumbnail
|
* @property {string} thumbnail The thumbnail
|
||||||
* @property {string} duration The duration
|
* @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 {User} requestedBy The user who requested this track
|
||||||
* @property {Playlist} [playlist] The playlist
|
* @property {Playlist} [playlist] The playlist
|
||||||
* @property {TrackSource} [source="arbitrary"] The source
|
* @property {TrackSource} [source="arbitrary"] The source
|
||||||
|
@ -131,10 +131,8 @@ export interface PlayerProgressbarOptions {
|
||||||
* @property {boolean} [leaveOnEmpty=true] If it should leave on empty
|
* @property {boolean} [leaveOnEmpty=true] If it should leave on empty
|
||||||
* @property {number} [leaveOnEmptyCooldown=1000] The cooldown in ms
|
* @property {number} [leaveOnEmptyCooldown=1000] The cooldown in ms
|
||||||
* @property {boolean} [autoSelfDeaf=true] If it should set the bot in deaf mode
|
* @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} [initialVolume=100] The initial player volume
|
||||||
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
* @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 {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)
|
* @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`
|
* Setting this or leaving this empty will disable this effect. Example: `volumeSmoothness: 0.1`
|
||||||
|
@ -146,10 +144,8 @@ export interface PlayerOptions {
|
||||||
leaveOnEmpty?: boolean;
|
leaveOnEmpty?: boolean;
|
||||||
leaveOnEmptyCooldown?: number;
|
leaveOnEmptyCooldown?: number;
|
||||||
autoSelfDeaf?: boolean;
|
autoSelfDeaf?: boolean;
|
||||||
ytdlOptions?: downloadOptions;
|
|
||||||
initialVolume?: number;
|
initialVolume?: number;
|
||||||
bufferingTimeout?: number;
|
bufferingTimeout?: number;
|
||||||
spotifyBridge?: boolean;
|
|
||||||
disableVolume?: boolean;
|
disableVolume?: boolean;
|
||||||
volumeSmoothness?: number;
|
volumeSmoothness?: number;
|
||||||
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
||||||
|
@ -477,11 +473,9 @@ export interface PlaylistJSON {
|
||||||
/**
|
/**
|
||||||
* @typedef {object} PlayerInitOptions
|
* @typedef {object} PlayerInitOptions
|
||||||
* @property {boolean} [autoRegisterExtractor=true] If it should automatically register `@discord-player/extractor`
|
* @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
|
* @property {number} [connectionTimeout=20000] The voice connection timeout
|
||||||
*/
|
*/
|
||||||
export interface PlayerInitOptions {
|
export interface PlayerInitOptions {
|
||||||
autoRegisterExtractor?: boolean;
|
autoRegisterExtractor?: boolean;
|
||||||
ytdlOptions?: downloadOptions;
|
|
||||||
connectionTimeout?: number;
|
connectionTimeout?: number;
|
||||||
}
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
import { validateID, validateURL } from "ytdl-core";
|
|
||||||
import { YouTube } from "youtube-sr";
|
|
||||||
import { QueryType } from "../types/types";
|
import { QueryType } from "../types/types";
|
||||||
|
import play from "play-dl";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
|
||||||
import { validateURL as SoundcloudValidateURL } from "soundcloud-scraper";
|
|
||||||
|
|
||||||
// scary things below *sigh*
|
// scary things below *sigh*
|
||||||
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
|
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
|
||||||
|
@ -27,11 +24,11 @@ class QueryResolver {
|
||||||
* @param {string} query The query
|
* @param {string} query The query
|
||||||
* @returns {QueryType}
|
* @returns {QueryType}
|
||||||
*/
|
*/
|
||||||
static resolve(query: string): QueryType {
|
static async resolve(query: string): Promise<QueryType> {
|
||||||
if (SoundcloudValidateURL(query, "track")) return QueryType.SOUNDCLOUD_TRACK;
|
if (await play.so_validate(query) === "track") return QueryType.SOUNDCLOUD_TRACK;
|
||||||
if (SoundcloudValidateURL(query, "playlist") || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST;
|
if (await play.so_validate(query) === "playlist" || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST;
|
||||||
if (YouTube.isPlaylist(query)) return QueryType.YOUTUBE_PLAYLIST;
|
if (play.yt_validate(query) === "playlist") return QueryType.YOUTUBE_PLAYLIST;
|
||||||
if (validateID(query) || validateURL(query)) return QueryType.YOUTUBE_VIDEO;
|
if (play.yt_validate(query) === "video") return QueryType.YOUTUBE_VIDEO;
|
||||||
if (spotifySongRegex.test(query)) return QueryType.SPOTIFY_SONG;
|
if (spotifySongRegex.test(query)) return QueryType.SPOTIFY_SONG;
|
||||||
if (spotifyPlaylistRegex.test(query)) return QueryType.SPOTIFY_PLAYLIST;
|
if (spotifyPlaylistRegex.test(query)) return QueryType.SPOTIFY_PLAYLIST;
|
||||||
if (spotifyAlbumRegex.test(query)) return QueryType.SPOTIFY_ALBUM;
|
if (spotifyAlbumRegex.test(query)) return QueryType.SPOTIFY_ALBUM;
|
||||||
|
@ -48,8 +45,8 @@ class QueryResolver {
|
||||||
* @param {string} query The query
|
* @param {string} query The query
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
static getVimeoID(query: string): string {
|
static async getVimeoID(query: string): Promise<string> {
|
||||||
return QueryResolver.resolve(query) === QueryType.VIMEO
|
return await QueryResolver.resolve(query) === QueryType.VIMEO
|
||||||
? query
|
? query
|
||||||
.split("/")
|
.split("/")
|
||||||
.filter((x) => !!x)
|
.filter((x) => !!x)
|
||||||
|
|
Loading…
Reference in a new issue