Metadata-Version: 2.2
Name: voicecord-py
Version: 1.0.0
Summary: Complete Lavalink replacement for Discord.py — local music player with built-in JioSaavn and Gaana support
Author: xylen-py
Project-URL: Homepage, https://github.com/xylen-py/Voicecord.py
Project-URL: Repository, https://github.com/xylen-py/Voicecord.py
Project-URL: Issues, https://github.com/xylen-py/Voicecord.py/issues
Keywords: discord,music,voice,player,lavalink,jiosaavn,gaana,bot,audio
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Framework :: AsyncIO
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: discord.py[voice]>=2.0.0
Requires-Dist: jiosaavnclient>=1.0.0
Requires-Dist: gaanaclient>=1.0.0
Requires-Dist: PyNaCl>=1.5.0

# Voicecord.py

[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://python.org)
[![discord.py](https://img.shields.io/badge/discord.py-2.0+-blue.svg)](https://discordpy.readthedocs.io)

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

```bash
pip install voicecord
```

**Requires**: FFmpeg installed and in PATH.

---

## Quick Start

```python
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

```python
player.set_default_source("gaana")
tracks = await player.search("query", source="jiosaavn")
```

### Custom Source

```python
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

```python
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

```python
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

```python
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

```python
@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

```python
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

```python
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
