Complete Lavalink replacement for Discord.py — local music player with built-in JioSaavn and Gaana support
Project description
Voicecord.py
Complete Lavalink replacement for Discord.py. Local music player with built-in JioSaavn and Gaana support, queue management, autoplay, filters, and plugin system. No external server needed.
No Lavalink. No Java. No separate server. Just Python + FFmpeg.
Installation
pip install voicecord
Requires: FFmpeg installed and in PATH.
Quick Start
import discord
from voicecord import Player, Pool, LoopMode
bot = discord.Bot()
@bot.slash_command()
async def play(ctx, query: str):
if not ctx.author.voice:
return await ctx.respond("Join a voice channel first.")
player = await Pool.connect(ctx.author.voice.channel, cls_type=Player)
tracks = await player.search(query)
track = tracks[0]
track.requester = ctx.author
player.queue.add(track)
await ctx.respond(f"Added **{track.title}** to queue.")
if not player.playing:
await player.play(player.queue.next())
@bot.slash_command()
async def skip(ctx):
player = Pool.get_player(ctx.guild)
if player:
await player.skip()
await ctx.respond("Skipped.")
@bot.slash_command()
async def queue(ctx):
player = Pool.get_player(ctx.guild)
if not player or player.queue.is_empty:
return await ctx.respond("Queue is empty.")
tracks = "\n".join(f"{i+1}. {t.title}" for i, t in enumerate(player.queue))
await ctx.respond(f"**Queue:**\n{tracks}")
@bot.slash_command()
async def loop(ctx, mode: str):
player = Pool.get_player(ctx.guild)
if not player:
return
modes = {"off": LoopMode.NONE, "track": LoopMode.TRACK, "queue": LoopMode.QUEUE}
player.loop = modes.get(mode, LoopMode.NONE)
await ctx.respond(f"Loop: **{mode}**")
@bot.event
async def on_voicecord_track_start(player, track):
print(f"Now playing: {track.title} by {track.author}")
@bot.event
async def on_voicecord_queue_end(player):
await player.disconnect()
bot.run("TOKEN")
Player
The Player extends discord.VoiceClient — it IS the voice connection.
Playback
| Method | Description |
|---|---|
await player.play(track) |
Play a track |
await player.pause() |
Pause playback |
await player.resume() |
Resume playback |
await player.skip() |
Skip to next track |
await player.previous() |
Play previous from history |
await player.stop() |
Stop and clear queue |
await player.seek(position_ms) |
Seek to position |
await player.set_volume(0-200) |
Set volume |
await player.disconnect() |
Leave voice channel |
Search
| Method | Description |
|---|---|
await player.search(query) |
Search default source (JioSaavn) |
await player.search(query, source="gaana") |
Search specific source |
await player.fetch_tracks(query) |
Auto-detect URLs or search |
Properties
| Property | Type | Description |
|---|---|---|
player.current |
Track |
Currently playing track |
player.queue |
Queue |
Track queue |
player.state |
PlayerState |
IDLE/PLAYING/PAUSED/STOPPED |
player.volume |
int |
Current volume (0-200) |
player.position |
float |
Playback position in ms |
player.loop |
LoopMode |
NONE/TRACK/QUEUE |
player.playing |
bool |
Is playing or paused |
player.connected |
bool |
Is connected to voice |
player.autoplay |
AutoPlay |
Autoplay manager |
player.filters |
FilterChain |
Active filters |
Queue
| Method | Description |
|---|---|
queue.add(track) |
Add to end |
queue.add_many(tracks) |
Add multiple |
queue.add_at(index, track) |
Insert at position |
queue.remove(index) |
Remove by index |
queue.clear() |
Clear all |
queue.shuffle() |
Shuffle queue |
queue.reverse() |
Reverse order |
queue.move(from, to) |
Move track |
queue.swap(i, j) |
Swap two tracks |
queue.skip_to(index) |
Jump to index |
queue.next() |
Get next (respects loop) |
queue.previous() |
Go back in history |
queue.peek() |
See next without popping |
| Property | Type | Description |
|---|---|---|
queue.current |
Track |
Current track |
queue.upcoming |
List[Track] |
Upcoming tracks |
queue.history |
List[Track] |
Previously played |
queue.count |
int |
Queue length |
queue.is_empty |
bool |
Is empty |
queue.duration |
int |
Total duration (ms) |
queue.loop |
LoopMode |
Loop mode |
Sources
JioSaavn is the default source. All sources implement BaseSource.
Built-in Sources
| Source | Search | Albums | Playlists | Artists | Recommendations | Stream |
|---|---|---|---|---|---|---|
jiosaavn (default) |
✅ | ✅ | ✅ | ✅ | ✅ | 320kbps AAC |
gaana |
✅ | ✅ | ✅ | ✅ | ❌ | HLS |
url |
❌ | ❌ | ❌ | ❌ | ❌ | Direct |
Switch Source
player.set_default_source("gaana")
tracks = await player.search("query", source="jiosaavn")
Custom Source
from voicecord import BaseSource, Track
class SpotifySource(BaseSource):
name = "spotify"
supports_search = True
async def search(self, query, limit=10):
# your implementation
return [Track(title="...", author="...", source=self.name)]
async def get_track(self, identifier):
return Track(...)
async def get_stream_url(self, track):
return "https://..."
player.register_source(SpotifySource())
Filters
from voicecord import BassBoost, Nightcore, Slowed, Volume, Karaoke, EightD, Tremolo
await player.set_filter(BassBoost(level=10))
await player.set_filter(Nightcore(speed=1.3))
await player.set_filter(BassBoost(5), Volume(0.8)) # combine
await player.clear_filters()
| Filter | Parameter | Range |
|---|---|---|
Volume(level) |
Volume multiplier | 0.0 - 2.0 |
BassBoost(level) |
Bass gain dB | 0 - 20 |
Nightcore(speed) |
Speed multiplier | 1.0 - 2.0 |
Slowed(speed) |
Speed multiplier | 0.5 - 1.0 |
Tremolo(freq, depth) |
Wobble effect | freq: 0.1-20, depth: 0-1 |
Karaoke() |
Remove vocals | — |
EightD(speed) |
Panning effect | 0.01 - 1.0 |
Autoplay
player.autoplay.enabled = True
When the queue is empty and autoplay is enabled, the player automatically fetches recommendations from JioSaavn based on the last played track and continues playing. Avoids repeats via internal history.
Loop Modes
from voicecord import LoopMode
player.loop = LoopMode.NONE # no repeat
player.loop = LoopMode.TRACK # repeat current track
player.loop = LoopMode.QUEUE # repeat entire queue
Events
@bot.event
async def on_voicecord_track_start(player, track):
print(f"Playing: {track.title}")
@bot.event
async def on_voicecord_track_end(player, track, reason):
print(f"Finished: {track.title}")
@bot.event
async def on_voicecord_queue_end(player):
await player.disconnect()
@bot.event
async def on_voicecord_autoplay(player, track):
print(f"Autoplay: {track.title}")
@bot.event
async def on_voicecord_track_exception(player, track, error):
print(f"Error: {error}")
| Event | Arguments |
|---|---|
on_voicecord_track_start |
player, track |
on_voicecord_track_end |
player, track, reason |
on_voicecord_track_exception |
player, track, error |
on_voicecord_queue_end |
player |
on_voicecord_autoplay |
player, track |
on_voicecord_player_paused |
player |
on_voicecord_player_resumed |
player |
on_voicecord_player_disconnect |
player |
Plugin System
from voicecord import BasePlugin
class LogPlugin(BasePlugin):
name = "logger"
async def on_track_start(self, player, track):
print(f"[LOG] Playing: {track}")
async def on_track_end(self, player, track):
print(f"[LOG] Ended: {track}")
async def on_queue_end(self, player):
print(f"[LOG] Queue empty in {player.guild.name}")
player.plugins.load(LogPlugin())
Pool
from voicecord import Pool, Player
player = await Pool.connect(channel, cls_type=Player)
player = Pool.get_player(guild)
await Pool.disconnect(guild)
await Pool.disconnect_all()
Track
| Field | Type |
|---|---|
title |
str |
author |
str |
identifier |
str |
uri |
str |
length |
int (ms) |
artwork |
str |
source |
str |
stream_url |
str |
is_seekable |
bool |
is_stream |
bool |
album_name |
str |
isrc |
str |
requester |
Any |
duration |
int (seconds) |
duration_ms |
int (ms) |
Comparison with Lavalink
| Feature | Voicecord.py | Wavelink/Pomice |
|---|---|---|
| Server Required | ❌ No | ✅ Lavalink (Java) |
| Setup | pip install voicecord |
Install Java + Lavalink + config |
| JioSaavn Built-in | ✅ (default) | ❌ Needs plugin |
| Gaana Built-in | ✅ | ❌ |
| Custom Sources | ✅ Python class | ❌ Java plugin |
| Audio Filters | ✅ FFmpeg | ✅ Lavalink |
| Autoplay | ✅ Built-in | ❌ Manual |
| Queue | ✅ Built-in | ✅ Built-in |
| Plugin System | ✅ Python | ❌ Java |
| Latency | Local FFmpeg | Network to server |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file voicecord_py-1.0.0.tar.gz.
File metadata
- Download URL: voicecord_py-1.0.0.tar.gz
- Upload date:
- Size: 18.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
446e335405d598416a4670a79878a9abec4c5dc47e9f5514e0198d0e9f9e67a5
|
|
| MD5 |
a46d741623d1aee48c748095a2c69aae
|
|
| BLAKE2b-256 |
4a3228c917084c0036f56f63eeefd6aa9e8ec3b8d5aca9f1c5efa8bd7a750d46
|
File details
Details for the file voicecord_py-1.0.0-py3-none-any.whl.
File metadata
- Download URL: voicecord_py-1.0.0-py3-none-any.whl
- Upload date:
- Size: 20.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1993d5dba8be976186f569105332a8049b88b735f8ee0e9079a40e04539e7a6a
|
|
| MD5 |
d9e0f7fe3f14a6c863714c4f0a5a976d
|
|
| BLAKE2b-256 |
6bdf246d0091a30037d05b6cc9d3d6c23ac6cbeba95aeae5b2d1dc7d72ee8d4d
|