Skip to main content

Overview

The !play command is the core functionality of MusicBot. It downloads audio from YouTube and plays it in your Discord voice channel.

Basic Usage

Syntax

!play <song name or URL>
The command accepts two types of input:
  1. Direct YouTube URLs - Play a specific video
  2. Search queries - Search YouTube and play the first result

YouTube URLs

Supported URL Formats

The bot detects URLs by checking if the input starts with http:// or https:// (bot.py:264):
if search_query.startswith('http://') or search_query.startswith('https://'):
    url_to_download = search_query

Example Usage

!play https://www.youtube.com/watch?v=dQw4w9WgXcQ
!play https://youtu.be/dQw4w9WgXcQ

Playlist Handling

Playlist URLs are not supported - the bot will only extract the first video:
ydl_opts = {
    'noplaylist': True,  # Disable downloading entire playlists
    # ...
}
(bot.py:75)

Search Queries

How Search Works

When you provide a non-URL input, the bot:
  1. Sends a “Searching for…” message (bot.py:269)
  2. Calls the search_youtube() function (bot.py:114-159)
  3. Uses YouTube Data API v3 to find videos
  4. Returns the first matching video URL
  5. Proceeds to download that video

Search Implementation

From bot.py:129-135:
params = {
    'q': prompt,  # The search query
    'part': 'snippet',
    'type': 'video',  # Restrict to videos only
    'key': YOUTUBE_API_KEY,
    'maxResults': 1  # Only get first result
}

Example Searches

!play Never Gonna Give You Up
!play lofi hip hop
!play beethoven symphony 9
!play guitar tutorial

Search Fallback

yt-dlp also has a built-in search capability as a fallback:
ydl_opts = {
    'default_search': 'ytsearch',
    # ...
}
(bot.py:77)

Audio Download Process

Download Configuration

Audio is downloaded using yt-dlp with these settings (bot.py:67-78):
ydl_opts = {
    'format': 'bestaudio/best',  # Best audio quality available
    'postprocessors': [{
        'key': 'FFmpegExtractAudio',  # Extract audio using FFmpeg
        'preferredcodec': 'mp3',  # Convert to MP3
        'preferredquality': '192',  # 192 kbps quality
    }],
    'quiet': True,  # Suppress console output
    'default_search': 'ytsearch',
}

Audio Quality

  • Format: MP3
  • Bitrate: 192 kbps
  • Source: Best available audio stream from YouTube

Download Steps

The download process (bot.py:288-377):
  1. Extract video info without downloading
    pre_info_dict = ydl_info_extractor.extract_info(url_to_download, download=False)
    
  2. Check cache for existing file
    video_id = pre_info_dict.get('id')
    potential_cached_path = os.path.join(guild_music_dir, f"{video_id}.mp3")
    if os.path.exists(potential_cached_path):
        # Use cached file
    
  3. Download if not cached
    with yt_dlp.YoutubeDL(download_opts) as ydl_downloader:
        info_dict_download = ydl_downloader.extract_info(url_to_download, download=True)
    
  4. Add to queue and start playback

Caching System

Per-Guild Cache Directories

Each Discord server has its own cache directory (bot.py:278-282):
guild_id = str(ctx.guild.id)
guild_music_dir = os.path.join('music', guild_id)
os.makedirs(guild_music_dir, exist_ok=True)
Directory Structure:
music/
├── 123456789012345678/  (Guild ID 1)
│   ├── dQw4w9WgXcQ.mp3
│   └── jNQXAC9IVRw.mp3
└── 987654321098765432/  (Guild ID 2)
    ├── dQw4w9WgXcQ.mp3
    └── L_jWHffIx5E.mp3

Cache Benefits

  1. Faster playback - No re-download for repeated songs
  2. Reduced bandwidth - Same video cached per server
  3. Reduced API calls - No repeated YouTube API requests

Cache Hit vs Cache Miss

Cache Hit (bot.py:315-319):
if os.path.exists(potential_cached_path):
    logger.info(f"Cache hit: Using existing file {potential_cached_path}")
    file_path_to_play = potential_cached_path
    await ctx.send(f"Found '{title_to_play}' in cache. Adding to queue.")
Cache Miss (bot.py:320-322):
else:
    logger.info(f"Cache miss for video ID {video_id}. Proceeding with download.")
    await ctx.send(f"Downloading audio for '{title_to_play}'... This may take a moment.")

Cache Naming

Files are named using YouTube video IDs:
f"{video_id}.mp3"  # e.g., "dQw4w9WgXcQ.mp3"
This ensures:
  • Unique filenames per video
  • No conflicts between different videos
  • Easy identification of cached videos

Cache Persistence

Cached files are retained after playback (bot.py:560):
# os.remove(file_path) # File deletion disabled for caching
logger.info(f'Song finished, file retained in cache: {file_path}')

Cache Cleanup

Cache is automatically cleared when:
  • Using !leave command (deletes entire guild cache)
  • Using !clearcache command (manual cleanup)

Voice Channel Connection

Prerequisites

Before playing music:
  1. User must be in a voice channel (bot.py:232-234):
    if not ctx.author.voice or not ctx.author.voice.channel:
        await ctx.send("You are not connected to a voice channel...")
    
  2. Bot must not be in a different channel (bot.py:238-240):
    if guild_state.voice_client and guild_state.voice_client.channel != user_voice_channel:
        await ctx.send(f"I am already in a different voice channel...")
    

Auto-Join

The bot automatically joins your channel (bot.py:243-259):
if not guild_state.voice_client or not guild_state.voice_client.is_connected():
    guild_state.voice_client = await user_voice_channel.connect()
    logger.info(f"Bot joined voice channel: '{user_voice_channel.name}'")

Error Handling

Common Errors

No search query provided:
Please provide a song name or YouTube URL after the `!play` command.
User not in voice channel:
You are not connected to a voice channel. Please join one to play music.
Video not found:
Could not find a video for: 'your search'. Please try a different search term or URL.
Download error (region-locked, private, deleted):
Error downloading the song. It might be region-locked, private, deleted, or an invalid link.
File not found after download:
Error: Could not locate the downloaded file for 'Song Title' after download.

Error Logging

All errors are logged with full details (bot.py:403-410):
except yt_dlp.utils.DownloadError as e:
    logger.error(f"yt-dlp download error for '{url_to_download}': {str(e)}")
    await ctx.send(f"Error downloading the song...")
except Exception as e:
    logger.error(f"Unexpected error in play command: {e}", exc_info=True)

Playback Flow

  1. User invokes !play <query>
  2. Bot validates user is in voice channel
  3. Bot connects to voice channel if needed
  4. Query is processed (URL or search)
  5. Video info is extracted
  6. Cache is checked
  7. Audio is downloaded (if not cached)
  8. Song is added to queue
  9. Playback starts if nothing is playing
  10. Otherwise, song waits in queue

Performance Considerations

  • First play: Requires download (may take 5-30 seconds depending on video length)
  • Subsequent plays: Instant (from cache)
  • Search queries: Add ~1-2 seconds for API lookup
  • Direct URLs: Skip search, go straight to download

Tips

  • Use direct URLs for faster, more accurate results
  • Search queries return the first result - be specific
  • Songs are cached automatically - no manual action needed
  • Cache persists across bot restarts
  • Each server has isolated cache

Build docs developers (and LLMs) love