Skip to content

Autoplay System

Ryanlink includes a built-in Autoplay class that automatically finds and queues similar tracks when the queue empties. It uses a multi-source fetch chain with weighted scoring to pick the best match.

Autoplay is triggered via the onEmptyQueue.autoPlayFunction option. Set player.autoplay = true to activate it per-player:

import { RyanlinkManager, Autoplay } from 'ryanlink';
const manager = new RyanlinkManager({
playerOptions: {
onEmptyQueue: {
autoPlayFunction: async (player, lastTrack) => {
if (!player.autoplay) return;
await Autoplay.defaultAutoplay(player, lastTrack);
},
destroyAfterMs: 20000, // leave VC after 20s if queue empty and autoplay off
},
},
});
// Enable per-player
player.autoplay = true;
// Disable
player.autoplay = false;

Fine-tune the selection algorithm via playerOptions.autoplayConfig:

| Option | Type | Default | Description | | :--- | :--- | :--- | :--- | | enabled | boolean | true | Whether the built-in Autoplay class is active | | defaultSource | string | 'ytsearch' | Fallback search source | | limit | number | 1 | Tracks to add per autoplay trigger | | minDuration | number | 20000 | Minimum track duration (ms) | | maxDuration | number | 900000 | Maximum track duration (ms) | | durationTolerance | number | 90000 | Max delta (ms) between last track and candidate for full duration score | | historyLimit | number | 20 | Circular buffer size — tracks in this buffer are never re-queued | | prefetchThreshold | number | 1 | Trigger autoplay early when queue drops to this many tracks | | excludeKeywords | string[] | See below | Keyword blacklist applied to track titles | | fetchRelatedTracks | function | undefined | Custom async function to fetch candidates: (player, lastTrack) => Promise<Track[]> |

Default excluded keywords: nightcore, bass boosted, 8d audio, slowed, reverb, bass boost, pitch shift, speed up, sped up.

const manager = new RyanlinkManager({
playerOptions: {
autoplayConfig: {
limit: 1,
minDuration: 20000,
maxDuration: 900000,
durationTolerance: 90000,
historyLimit: 20,
prefetchThreshold: 1,
excludeKeywords: ['nightcore', 'slowed', 'reverb', '8d'],
},
},
});

The engine tries sources in order until enough candidates are found:

  1. YouTube — ytrec:{identifier} recommendation query (if source is YouTube and track is not a stream)
  2. Spotify — sprec:seed_artists={artistId}&seed_tracks={trackId} (if Spotify IDs are available)
  3. Same-artist search — {source}search:{artist} on the native source
  4. Title+artist search — {source}search:{title} {artist} on the native source
  5. Final fallback — ytsearch:{artist} search

Each candidate is scored (higher = better):

| Factor | Points | | :--- | :--- | | Duration within durationTolerance | 0–40 | | Same artist (exact match) | +50 | | Partial artist match | +25 | | Random jitter | 0–10 |

Candidates are sorted by score descending. The top limit tracks are selected.

Before scoring, candidates are filtered out if:

  • Their identifier is in the history buffer
  • Their ISRC is in the history buffer
  • Their normalized title is in the history buffer
  • Their title contains an excluded keyword
  • Their normalized title matches the last track’s normalized title
  • Their duration is outside [minDuration, maxDuration]

Track identifiers are stored in a per-player buffer (autoplay_history_buf). Any track in this buffer is hard-excluded from selection. The buffer is capped at historyLimit entries and synced with player.recentHistory.

When queue.tracks.length <= prefetchThreshold, autoplay triggers early — the next track is resolved and queued before the current one ends, eliminating the silence gap.


Override the entire candidate-fetching logic:

const manager = new RyanlinkManager({
playerOptions: {
autoplayConfig: {
fetchRelatedTracks: async (player, lastTrack) => {
// Return an array of Track objects
const result = await player.search(
{ query: `${lastTrack.info.author} radio`, source: 'ytsearch' },
'Autoplay'
);
return result.tracks;
},
},
},
});

Keywords are matched case-insensitively against the candidate’s title. The last track’s own keywords are not used to filter candidates.

autoplayConfig: {
excludeKeywords: ['nightcore', 'bass boosted', '8d audio', 'slowed', 'reverb'],
}

player.autoplay = true; // enable
player.autoplay = false; // disable

The autoplay flag is also settable via createPlayer:

const player = manager.createPlayer({
guildId: '...',
voiceChannelId: '...',
autoplay: true,
});