Skip to main content

Playing Tracks

This section demonstrates how to search for and play tracks using Salada. It covers the entire process from handling a user command to getting music playing in a voice channel.

Complete Play Command Example

from discord import app_commands
import discord

@bot.tree.command(name='play')
@app_commands.describe(query='Song name or URL')
async def play_command(interaction: discord.Interaction, query: str):
    await interaction.response.defer()
    
    if not bot.salad:
        await interaction.followup.send('❌ Music system is not ready')
        return

    if not interaction.user.voice:
        await interaction.followup.send('❌ You must be in a voice channel to play music!')
        return

    voice_channel = interaction.user.voice.channel
    
    permissions = voice_channel.permissions_for(interaction.guild.me)
    if not permissions.connect or not permissions.speak:
        await interaction.followup.send('❌ I need Connect and Speak permissions in your voice channel!')
        return

    try:
        player = bot.salad.getPlayer(interaction.guild.id)
        if not player:
            player = await bot.salad.createConnection({
                'guildId': interaction.guild.id,
                'voiceChannel': voice_channel.id,
                'textChannel': interaction.channel.id
            })
            await voice_channel.connect()
            await player.setVolume(65)

        result = await bot.salad.resolve(query, requester=interaction.user)
        load_type = result.get('loadType')
        
        if load_type == 'empty':
            await interaction.followup.send('❌ No tracks found for your query.')
            return
        
        if load_type == 'error':
            await interaction.followup.send('❌ An error occurred while searching.')
            return

        if load_type == 'playlist':
            tracks = result.get('tracks', [])
            for track in tracks:
                player.addToQueue(track)
            
            playlist_info = result.get('playlistInfo', {})
            playlist_name = playlist_info.get('name', 'Unknown Playlist')
            
            embed = discord.Embed(
                title='📋 Playlist Added to Queue',
                description=f'**{playlist_name}**',
                color=discord.Color.green()
            )
            embed.add_field(name='Tracks', value=str(len(tracks)), inline=True)
            embed.add_field(name='Requested by', value=interaction.user.display_name, inline=True)
            
            await interaction.followup.send(embed=embed)
            
        elif load_type in ('search', 'track'):
            track = result['tracks'][0]
            player.addToQueue(track)
            
            embed = discord.Embed(
                title='🎵 Track Added to Queue',
                description=f'**{track.title}**\nBy {track.author}',
                color=discord.Color.green()
            )
            embed.set_thumbnail(url=track.thumbnail)
            embed.add_field(name='Duration', value=format_duration(track.duration), inline=True)
            embed.add_field(name='Position in Queue', value=str(len(player.queue) + 1), inline=True)
            embed.add_field(name='Requested by', value=interaction.user.display_name, inline=True)
            
            await interaction.followup.send(embed=embed)

        if not player.playing and not player.paused:
            await player.play()
            
    except Exception as error:
        print(f'Play command error: {error}')
        await interaction.followup.send('❌ An error occurred while processing your request.')

def format_duration(ms: int) -> str:
    if not ms or ms == 0:
        return '0:00'
    
    seconds = ms // 1000
    minutes = seconds // 60
    hours = minutes // 60
    
    if hours > 0:
        return f'{hours}:{(minutes % 60):02d}:{(seconds % 60):02d}'
    return f'{minutes}:{(seconds % 60):02d}'

Search Methods

Salada supports multiple ways to search for tracks through the resolve() method.
result = await bot.salad.resolve('never gonna give you up', requester=interaction.user)

if result.get('tracks'):
    track = result['tracks'][0]
    player.addToQueue(track)
    await player.play()
yt_result = await bot.salad.resolve('ytsearch:despacito', requester=interaction.user)

sc_result = await bot.salad.resolve('scsearch:lo-fi hip hop', requester=interaction.user)

sp_result = await bot.salad.resolve('spsearch:imagine dragons', requester=interaction.user)

url_result = await bot.salad.resolve('https://www.youtube.com/watch?v=dQw4w9WgXcQ', requester=interaction.user)

Advanced Search with Load Types

result = await bot.salad.resolve(query, requester=interaction.user)

load_type = result.get('loadType')

if load_type == 'error':
    await interaction.reply('❌ Failed to load track!')
    return

if load_type == 'empty':
    await interaction.reply('❌ No tracks found!')
    return

if load_type in ('track', 'search'):
    track = result['tracks'][0]
    player.addToQueue(track)
    await player.play()

if load_type == 'playlist':
    tracks = result.get('tracks', [])
    for track in tracks:
        player.addToQueue(track)
    await interaction.reply(f'✅ Added {len(tracks)} tracks from playlist!')
    await player.play()

Track Information

Accessing Track Properties

current_track = player.currentTrackObj

if current_track:
    print('Title:', current_track.title)
    print('Author:', current_track.author)
    print('Duration:', current_track.duration)
    print('URL:', current_track.uri)
    print('Thumbnail:', current_track.thumbnail)
    print('Encoded:', current_track.track)
    print('Requester:', current_track.requester)

Creating Track Display

def create_track_embed(track, position: int = 1):
    embed = discord.Embed(
        title='🎵 Now Playing',
        description=f'**{track.title}**',
        color=0x1db954
    )
    
    embed.set_thumbnail(url=track.thumbnail)
    
    embed.add_field(name='Artist', value=track.author, inline=True)
    embed.add_field(name='Duration', value=format_duration(track.duration), inline=True)
    embed.add_field(name='Requested by', value=track.requester.display_name, inline=True)
    embed.add_field(name='Position', value=f'{position} of {len(player.queue) + 1}', inline=True)
    
    embed.set_footer(text='Use /skip to skip this track')
    
    return embed

Playlist Handling

Loading Playlists

async def handle_playlist_load(interaction, query):
    result = await bot.salad.resolve(query, requester=interaction.user)
    
    if result.get('loadType') == 'playlist':
        playlist_info = result.get('playlistInfo', {})
        tracks = result.get('tracks', [])
        
        for track in tracks:
            player.addToQueue(track)
        
        total_duration = sum(track.duration for track in tracks if hasattr(track, 'duration'))
        
        embed = discord.Embed(
            title='📋 Playlist Added',
            description=f'**{playlist_info.get("name", "Unknown")}**',
            color=0x9932cc
        )
        
        embed.add_field(name='Tracks', value=str(len(tracks)), inline=True)
        embed.add_field(name='Duration', value=format_duration(total_duration), inline=True)
        embed.add_field(name='Added by', value=interaction.user.display_name, inline=True)
        
        await interaction.followup.send(embed=embed)
        
        if not player.playing and not player.paused:
            await player.play()

Playlist Information

if result.get('playlistInfo'):
    playlist = result['playlistInfo']
    print('Playlist Name:', playlist.get('name'))
    print('Track Count:', len(result.get('tracks', [])))
    print('Selected Track:', playlist.get('selectedTrack', -1))

Queue Management

Adding Tracks to Queue

player.addToQueue(track)

for track in tracks:
    player.addToQueue(track)

player.queue.add(track)

player.queue.insert(track, 0)

Queue Information

print('Queue length:', len(player.queue))
print('Current track:', player.currentTrackObj)
print('Is playing:', player.playing)
print('Is paused:', player.paused)

all_tracks = player.queue.getAll()

player.queue.clear()
player.queue.remove(0)
player.queue.shuffle()

Player Controls

Basic Playback Controls

await player.play()

await player.skip()

await player.stop()

await player.pause()

await player.resume()

await player.setVolume(75)

await player.seek(30000)

position = player.position

Player State Checking

if player.playing:
    print('Currently playing')

if player.paused:
    print('Playback is paused')

if player.connected:
    print('Connected to voice')

if player.destroyed:
    print('Player has been destroyed')

Event Handling

Track Events

@bot.salad.event()
async def on_track_start(player, track):
    channel = bot.get_channel(player.textChannel)
    if channel:
        embed = create_track_embed(track)
        await channel.send(embed=embed)

@bot.salad.event()
async def on_track_end(player, track, reason):
    if reason == 'finished':
        if len(player.queue) > 0:
            await player.play()
        else:
            await player.destroy()

@bot.salad.event()
async def on_track_exception(player, track, exception):
    print(f'Track error: {track.title} - {exception}')
    if len(player.queue) > 0:
        await player.skip()

@bot.salad.event()
async def on_track_stuck(player, track, threshold):
    print(f'Track stuck: {track.title}')
    await player.skip()

Player Events

@bot.salad.event()
async def on_player_connect(player):
    print(f'Player connected in guild {player.guildId}')

@bot.salad.event()
async def on_player_destroy(player, reason):
    print(f'Player destroyed in guild {player.guildId}')

@bot.salad.event()
async def on_player_move(player, old_channel, new_channel):
    print(f'Player moved from {old_channel} to {new_channel}')

@bot.salad.event()
async def on_queue_end(player):
    print('Queue has ended')
    await player.destroy()

File Upload Support

Handling Local Audio Files

SUPPORTED_AUDIO_FORMATS = [
    '.mp3', '.wav', '.flac', '.aac', '.ogg', 
    '.wma', '.m4a', '.opus', '.mp4', '.avi', 
    '.mov', '.webm', '.mkv'
]

async def handle_file_upload(message, player):
    if message.attachments:
        attachment = message.attachments[0]
        file_name = attachment.filename.lower()
        
        has_valid_extension = any(
            file_name.endswith(ext) for ext in SUPPORTED_AUDIO_FORMATS
        )
        
        if has_valid_extension:
            try:
                result = await bot.salad.resolve(
                    attachment.url,
                    requester=message.author
                )
                
                if result.get('tracks'):
                    track = result['tracks'][0]
                    
                    track.title = attachment.filename.rsplit('.', 1)[0]
                    track.author = 'Local File'
                    track.uri = attachment.url
                    
                    player.addToQueue(track)
                    
                    file_size_mb = attachment.size / (1024 * 1024)
                    await message.channel.send(
                        f'✅ Added local file: {attachment.filename} ({file_size_mb:.2f} MB)'
                    )
                    
                    if not player.playing:
                        await player.play()
                    
                    return True
            except Exception as error:
                print(f'Local file processing error: {error}')
                return False
    
    return False

Error Handling

Comprehensive Error Handling

async def play_track_safe(interaction, query):
    try:
        if not interaction.user.voice:
            await interaction.followup.send('❌ You need to be in a voice channel!')
            return
        
        player = bot.salad.getPlayer(interaction.guild.id)
        if not player:
            player = await bot.salad.createConnection({
                'guildId': interaction.guild.id,
                'voiceChannel': interaction.user.voice.channel.id,
                'textChannel': interaction.channel.id
            })
            await interaction.user.voice.channel.connect()
        
        result = await asyncio.wait_for(
            bot.salad.resolve(query, requester=interaction.user),
            timeout=10.0
        )
        
        if not result or not result.get('tracks'):
            await interaction.followup.send('❌ No tracks found for your search.')
            return
        
        track = result['tracks'][0]
        player.addToQueue(track)
        
        await interaction.followup.send(f'✅ Added **{track.title}** to the queue!')
        
        if not player.playing and not player.paused:
            await player.play()
        
    except asyncio.TimeoutError:
        await interaction.followup.send('⏱️ Search timed out, please try again.')
    except Exception as error:
        print(f'Play command error: {error}')
        await interaction.followup.send('❌ An error occurred while processing your request.')

Permission Validation

def validate_voice_permissions(voice_channel, bot_member):
    permissions = voice_channel.permissions_for(bot_member)
    
    if not permissions.connect:
        raise Exception('Missing permission to connect to voice channel')
    
    if not permissions.speak:
        raise Exception('Missing permission to speak in voice channel')
    
    return True

Advanced Features

Auto-Play with Queue Management

@bot.salad.event()
async def on_track_end(player, track, reason):
    if reason == 'finished':
        if len(player.queue) > 0:
            await player.play()
        else:
            await asyncio.sleep(300)
            if len(player.queue) == 0 and not player.playing:
                await player.destroy()

@bot.salad.event()
async def on_track_exception(player, track, exception):
    print(f'Track failed: {track.title} - {exception}')
    
    if len(player.queue) > 0:
        await player.skip()
    else:
        channel = bot.get_channel(player.textChannel)
        if channel:
            await channel.send('❌ No more tracks to play!')

@bot.salad.event()
async def on_track_stuck(player, track, threshold):
    print(f'Track stuck: {track.title}')
    await player.skip()

Position Tracking

def get_queue_position(player):
    is_currently_playing = player.playing and player.currentTrackObj
    position = len(player.queue) + (2 if is_currently_playing else 1)
    return position

def get_position_suffix(num: int) -> str:
    if num == 1:
        return '1st'
    elif num == 2:
        return '2nd'
    elif num == 3:
        return '3rd'
    else:
        return f'{num}th'

position = get_queue_position(player)
position_text = get_position_suffix(position)
print(f'Track added to {position_text} position in queue')
Always validate user permissions and bot permissions before attempting to create connections or play tracks. This prevents common errors and improves user experience.
Use embeds to provide rich information about tracks and playlists. Users appreciate seeing thumbnails, duration, and other metadata when tracks are added to the queue.
from discord import app_commands
import discord

@bot.tree.command(name='play')
@app_commands.describe(query='Song name or URL')
async def play_command(interaction: discord.Interaction, query: str):
    await interaction.response.defer()
    
    if not bot.salad:
        await interaction.followup.send('❌ Music system is not ready')
        return

    if not interaction.user.voice:
        await interaction.followup.send('❌ You must be in a voice channel to play music!')
        return

    voice_channel = interaction.user.voice.channel
    
    permissions = voice_channel.permissions_for(interaction.guild.me)
    if not permissions.connect or not permissions.speak:
        await interaction.followup.send('❌ I need Connect and Speak permissions in your voice channel!')
        return

    try:
        player = bot.salad.getPlayer(interaction.guild.id)
        if not player:
            player = await bot.salad.createConnection({
                'guildId': interaction.guild.id,
                'voiceChannel': voice_channel.id,
                'textChannel': interaction.channel.id
            })
            await voice_channel.connect()
            await player.setVolume(65)

        result = await bot.salad.resolve(query, requester=interaction.user)
        
        load_type = result.get('loadType')
        if load_type == 'empty':
            await interaction.followup.send('❌ No tracks found for your query.')
            return
        
        if load_type == 'error':
            await interaction.followup.send('❌ An error occurred while searching.')
            return

        if load_type == 'playlist':
            tracks = result.get('tracks', [])
            for track in tracks:
                player.addToQueue(track)
            
            playlist_info = result.get('playlistInfo', {})
            playlist_name = playlist_info.get('name', 'Unknown Playlist')
            
            embed = discord.Embed(
                title='📋 Playlist Added to Queue',
                description=f'**{playlist_name}**',
                color=discord.Color.green()
            )
            embed.add_field(name='Tracks', value=str(len(tracks)), inline=True)
            embed.add_field(name='Requested by', value=interaction.user.display_name, inline=True)
            
            await interaction.followup.send(embed=embed)
            
        elif load_type in ('search', 'track'):
            track = result['tracks'][0]
            player.addToQueue(track)
            
            embed = discord.Embed(
                title='🎵 Track Added to Queue',
                description=f'**{track.title}**\nBy {track.author}',
                color=discord.Color.green()
            )
            embed.set_thumbnail(url=track.thumbnail)
            embed.add_field(name='Duration', value=format_duration(track.duration), inline=True)
            embed.add_field(name='Position in Queue', value=str(len(player.queue) + 1), inline=True)
            embed.add_field(name='Requested by', value=interaction.user.display_name, inline=True)
            
            await interaction.followup.send(embed=embed)

        if not player.playing and not player.paused:
            await player.play()
            
    except Exception as error:
        print(f'Play command error: {error}')
        await interaction.followup.send('❌ An error occurred while processing your request.')

def format_duration(ms: int) -> str:
    if not ms or ms == 0:
        return '0:00'
    
    seconds = ms // 1000
    minutes = seconds // 60
    hours = minutes // 60
    
    if hours > 0:
        return f'{hours}:{(minutes % 60):02d}:{(seconds % 60):02d}'
    return f'{minutes}:{(seconds % 60):02d}'