Skip to content

Commit

Permalink
feat: allow audio resource as a track source
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite committed Jan 1, 2025
1 parent 2e6321b commit 73f4931
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Discord Player is a robust framework for developing Discord Music bots using Jav
- Out-of-the-box voice states handling
- IP Rotation support
- Easy serialization and deserialization
- Limited support for [Eris](https://npmjs.com/eris)
- Experimental support for [Eris](https://npmjs.com/eris) client

## Installation

Expand Down
46 changes: 24 additions & 22 deletions apps/website/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,30 @@ export default function HomePage() {
library and offers a comprehensive set of customizable tools, making it one of the most feature enrich framework
in town.
</p>
<Tabs items={['npm', 'yarn', 'pnpm', 'bun']} className="w-1/6 text-start">
<Tab value="npm">
<CodeBlock lang="bash">
<Pre>npm i --save discord-player</Pre>
</CodeBlock>
</Tab>
<Tab value="yarn">
<CodeBlock lang="bash">
<Pre>yarn add discord-player</Pre>
</CodeBlock>
</Tab>
<Tab value="pnpm">
<CodeBlock lang="bash">
<Pre>pnpm add discord-player</Pre>
</CodeBlock>
</Tab>
<Tab value="bun">
<CodeBlock lang="bash">
<Pre>bun add discord-player</Pre>
</CodeBlock>
</Tab>
</Tabs>
<div className="px-4 w-full md:w-[40%] lg:w-[30%]">
<Tabs items={['npm', 'yarn', 'pnpm', 'bun']} className="text-start" persist>
<Tab value="npm">
<CodeBlock lang="bash">
<Pre>npm add discord-player</Pre>
</CodeBlock>
</Tab>
<Tab value="yarn">
<CodeBlock lang="bash">
<Pre>yarn add discord-player</Pre>
</CodeBlock>
</Tab>
<Tab value="pnpm">
<CodeBlock lang="bash">
<Pre>pnpm add discord-player</Pre>
</CodeBlock>
</Tab>
<Tab value="bun">
<CodeBlock lang="bash">
<Pre>bun add discord-player</Pre>
</CodeBlock>
</Tab>
</Tabs>
</div>
<div className="inline-flex items-center gap-3 max-md:mx-auto">
<Link
className="inline-flex items-center justify-center text-sm font-medium ring-offset-fd-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring disabled:pointer-events-none disabled:opacity-50 bg-fd-background bg-gradient-to-b from-fd-primary to-fd-primary/60 text-fd-primary-foreground shadow-inner shadow-fd-background/20 hover:bg-fd-primary/90 h-11 px-6 rounded-full"
Expand Down
19 changes: 17 additions & 2 deletions apps/website/content/docs/common-actions/playing_raw_resource.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,23 @@ Discord Player natively allows you to play custom audio resource.
```js title="play-raw.js"
import { createAudioResource } from 'discord-voip';

const queue = player.nodes.create({...}); // create guild queue
const resource = createAudioResource(...); // create audio resource

await queue.node.playRaw(resource); // play that resource
await player.play(resource); // play that resource
```

<Callout type="warning">
If you do not plan on replaying the track, it is recommended to clear the assigned resource from the track to avoid
memory leaks.
</Callout>

The following example demonstrates how to clear the resource from the track. You may also utilize the events to clear the resource when the track ends. You can use `track.hasResource` to determine if the track has a resource assigned.

```js title="play-raw.js"
const { track } = await player.play(resource); // play that resource

// clear the resource from the track
if (track.hasResource) {
track.setResource(null);
}
```
12 changes: 12 additions & 0 deletions apps/website/content/docs/migrating/migrating_to_v7.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,15 @@ Discord Player will automatically resolve the guild from the context.
Passing arguments to hooks inside a context will ignore the context and use the provided arguments instead. This may cause unexpected issues when using multiple instances of `Player`.

See [Using Hooks](/docs/hooks/using_hooks) for more information.

### `queue.node.playRaw` method removed

The `queue.node.playRaw` method allowed you to play a custom audio resource. This method has been removed in v7. You can now directly use `player.play` to play a custom audio resource.

```js title="play-raw.js"
import { createAudioResource } from 'discord-voip';

const resource = createAudioResource(...); // create audio resource

await player.play(resource); // play that resource
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@
"use-macro": "^1.0.1",
"vitest": "^0.34.6"
}
}
}
50 changes: 44 additions & 6 deletions packages/discord-player/src/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { VoiceUtils } from './stream/VoiceUtils';
import { QueryResolver, QueryType, ResolvedQuery, SearchQueryType } from './utils/QueryResolver';
import { Util } from './utils/Util';
import { version as dVoiceVersion } from 'discord-voip';
import { AudioResource, version as dVoiceVersion } from 'discord-voip';
import { ExtractorExecutionContext } from './extractors/ExtractorExecutionContext';
import { BaseExtractor } from './extractors/BaseExtractor';
import { QueryCache, QueryCacheProvider } from './utils/QueryCache';
Expand Down Expand Up @@ -63,7 +63,7 @@ export interface PlayerNodeInitializationResult<T = unknown> {
queue: GuildQueue<T>;
}

export type TrackLike = string | Track | SearchResult | Track[] | Playlist;
export type TrackLike = string | Track | SearchResult | Track[] | Playlist | AudioResource;

export interface PlayerNodeInitializerOptions<T> extends SearchOptions {
nodeOptions?: GuildNodeCreateOptions<T>;
Expand Down Expand Up @@ -511,13 +511,15 @@ export class Player extends PlayerEventsEmitter<PlayerEvents> {
* console.log(result); // Logs `SearchResult` object
* ```
*/
public async search(
searchQuery: string | Track | Track[] | Playlist | SearchResult,
options: SearchOptions = {},
): Promise<SearchResult> {
public async search(searchQuery: TrackLike, options: SearchOptions = {}): Promise<SearchResult> {
if (searchQuery instanceof SearchResult) return searchQuery;

if (searchQuery instanceof AudioResource) {
searchQuery = this.createTrackFromAudioResource(searchQuery);
}

if (options.requestedBy != null) options.requestedBy = this.client.users.resolve(options.requestedBy)!;

options.blockExtractors ??= this.options.blockExtractors;
options.fallbackSearchEngine ??= QueryType.AUTO_SEARCH;

Expand Down Expand Up @@ -785,4 +787,40 @@ export class Player extends PlayerEventsEmitter<PlayerEvents> {
public createPlaylist(data: PlaylistInitData) {
return new Playlist(this, data);
}

/**
* Creates a track from an audio resource.
* @param resource The audio resource
*/
public createTrackFromAudioResource(resource: AudioResource) {
const metadata = resource.metadata as Record<string, unknown>;
const ref = SnowflakeUtil.generate().toString();
const maybeTitle = 'title' in metadata ? `${metadata.title}` : `Track ${ref}`;
const maybeAuthor = 'author' in metadata ? `${metadata.author}` : 'Unknown author';
const maybeDuration = 'duration' in metadata ? `${metadata.duration}` : '00:00';
const maybeThumbnail = 'thumbnail' in metadata ? `${metadata.thumbnail}` : undefined;
const maybeURL = 'url' in metadata ? `${metadata.url}` : `discord-player://blob/${ref}`;
const maybeDescription = 'description' in metadata ? `${metadata.description}` : 'No description available.';
const maybeViews = 'views' in metadata ? Number(metadata.views) || 0 : 0;

const track = new Track(this, {
title: maybeTitle,
author: maybeAuthor,
duration: maybeDuration,
thumbnail: maybeThumbnail,
url: maybeURL,
description: maybeDescription,
queryType: QueryType.DISCORD_PLAYER_BLOB,
source: 'arbitrary',
metadata,
live: false,
views: maybeViews,
});

resource.metadata = track;

track.setResource(resource as AudioResource<Track>);

return track;
}
}
25 changes: 25 additions & 0 deletions packages/discord-player/src/fabric/Track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SerializedType, tryIntoThumbnailString } from '../utils/serde';
import { InvalidArgTypeError } from '../errors';
import { Util } from '../utils/Util';
import { SearchQueryType } from '../utils/QueryResolver';
import { AudioResource } from 'discord-voip';

export type TrackResolvable = Track | string | number;

Expand Down Expand Up @@ -161,6 +162,8 @@ export class Track<T = unknown> {
public bridgedExtractor: BaseExtractor | null = null;
public bridgedTrack: Track | null = null;

#resource: AudioResource<Track> | null = null;

/**
* Track constructor
* @param player The player that instantiated this Track
Expand All @@ -184,6 +187,28 @@ export class Track<T = unknown> {
this.live = data.live ?? false;
}

/**
* Sets audio resource for this track. This is not useful outside of the library.
* @param resource Audio resource
*/
public setResource(resource: AudioResource<Track> | null) {
this.#resource = resource;
}

/**
* Gets audio resource for this track
*/
public get resource() {
return this.#resource;
}

/**
* Whether this track has an audio resource
*/
public get hasResource() {
return this.#resource != null;
}

/**
* Request metadata for this track
*/
Expand Down
19 changes: 11 additions & 8 deletions packages/discord-player/src/queue/GuildQueuePlayerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,14 +497,6 @@ export class GuildQueuePlayerNode<Meta = unknown> {
return true;
}

/**
* Play raw audio resource
* @param resource The audio resource to play
*/
public async playRaw(resource: AudioResource) {
await this.queue.dispatcher?.playStream(resource as AudioResource<Track>);
}

/**
* Play the given track
* @param res The track to play
Expand Down Expand Up @@ -544,6 +536,17 @@ export class GuildQueuePlayerNode<Meta = unknown> {
if (this.queue.hasDebugger) this.queue.debug('Requested option requires to play the track, initializing...');

try {
const assignedResource = track.resource;

if (assignedResource) {
if (this.queue.hasDebugger)
this.queue.debug('Track has an audio resource assigned, player will now play the resource directly...');

this.queue.setTransitioning(!!options.transitionMode);

return this.#performPlay(assignedResource);
}

if (this.queue.hasDebugger) this.queue.debug(`Initiating stream extraction process...`);
const src = track.raw?.source || track.source;
const qt: SearchQueryType =
Expand Down
8 changes: 8 additions & 0 deletions packages/discord-player/src/utils/QueryResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ const youtubePlaylistRegex =
const youtubeVideoURLRegex =
/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/;
const youtubeVideoIdRegex = /^[a-zA-Z0-9-_]{11}$/;
// discord-player://blob/uuid-v4
const discordPlayerBlobRegex = /^discord-player:\/\/blob\/\d+$/;
// #endregion scary things above *sigh*

const DomainsMap = {
DiscordPlayer: ['discord-player'],
YouTube: ['youtube.com', 'youtu.be', 'music.youtube.com', 'gaming.youtube.com', 'www.youtube.com', 'm.youtube.com'],
Spotify: ['open.spotify.com', 'embed.spotify.com'],
Vimeo: ['vimeo.com', 'player.vimeo.com'],
Expand Down Expand Up @@ -66,6 +69,7 @@ const redirectDomains = new Set([
* - APPLE_MUSIC_SEARCH
* - FILE
* - AUTO_SEARCH
* - DISCORD_PLAYER_BLOB
* @typedef {string} QueryType
*/
export const QueryType = {
Expand All @@ -92,6 +96,7 @@ export const QueryType = {
APPLE_MUSIC_SEARCH: 'appleMusicSearch',
FILE: 'file',
AUTO_SEARCH: 'autoSearch',
DISCORD_PLAYER_BLOB: 'discordPlayerBlob',
} as const;

export type QueryType = (typeof QueryType)[keyof typeof QueryType];
Expand Down Expand Up @@ -123,6 +128,7 @@ class QueryResolver {
soundcloudTrackRegex,
soundcloudPlaylistRegex,
youtubePlaylistRegex,
discordPlayerBlobRegex,
};
}

Expand Down Expand Up @@ -174,6 +180,8 @@ class QueryResolver {

const resolver = (type: typeof fallbackSearchEngine, query: string) => ({ type, query });

if (discordPlayerBlobRegex.test(query)) return resolver(QueryType.DISCORD_PLAYER_BLOB, query);

try {
const url = new URL(query);

Expand Down

0 comments on commit 73f4931

Please sign in to comment.