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:

OptionTypeDefaultDescription
enabledbooleantrueWhether the built-in Autoplay class is active
defaultSourcestring'ytsearch'Fallback search source
limitnumber1Tracks to add per autoplay trigger
minDurationnumber20000Minimum track duration (ms)
maxDurationnumber900000Maximum track duration (ms)
durationTolerancenumber90000Max delta (ms) between last track and candidate for full duration score
historyLimitnumber20Circular buffer size — tracks in this buffer are never re-queued
prefetchThresholdnumber1Trigger autoplay early when queue drops to this many tracks
excludeKeywordsstring[]See belowKeyword blacklist applied to track titles
fetchRelatedTracksfunctionundefinedCustom 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. YouTubeytrec:{identifier} recommendation query (if source is YouTube and track is not a stream)
  2. Spotifysprec: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 fallbackytsearch:{artist} search

Each candidate is scored (higher = better):

FactorPoints
Duration within durationTolerance0–40
Same artist (exact match)+50
Partial artist match+25
Random jitter0–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,
});