Metadata-Version: 2.4
Name: nebula-client-py
Version: 1.5.0
Summary: An asyncio/discord.py client for NebulaAudio
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: aiohttp>=3.9
Requires-Dist: discord.py>=2.3

# nebula_client

An asyncio/discord.py client for [NebulaAudio](.), styled after wavelink /
lavalink.py: connect a `Node`, use `Player` as your voice client, `await
node.search(...)` to load tracks, `await player.play(track)` to play them.

## Install

```
pip install nebula-client-py
```
**Repository**: https://github.com/ItzAntonio101/nebula-client-js

## Status — read before wiring this into a real bot

**Fully working:** Node REST calls (`search`, `fetch_info`, `fetch_stats`,
player PATCH/DELETE), the WebSocket listener (reconnect w/ backoff, event
parsing), all events (`TrackStart`, `TrackEnd`, `TrackException`,
`TrackStuck`, `PlayerUpdate`, `NodeReady`), queueing/looping, Filters
builder, Track/Playlist/SearchResult models.

**The one thing that doesn't produce audio yet:** NebulaAudio's Java server
does not implement Discord's voice UDP/RTP transport (this is documented in
its own README/javadoc — `AudioPlayer.sendFrame()` is a labeled no-op seam).
That means calling `player.play(track)` here will correctly tell the node to
start decoding and will fire real `TrackStartEvent`/`TrackEndEvent`s — but no
Opus packets currently leave the server to Discord's voice socket. This
client's `on_voice_server_update`/`on_voice_state_update` hooks capture the
endpoint/token/session_id discord.py hands them (that part's correct and
matches Lavalink's contract), but forwarding them anywhere is a no-op with a
log line pointing back at the gap, rather than a silent pretend-success.

Everything in this client is written to be correct *for when* that gap is
closed server-side — the REST/WS contract, event flow, and queue logic don't
need to change once real audio transmission exists, only
`_maybe_dispatch_voice_update` needs to actually send the voice payload.

## Example bot

```python
import discord
from discord.ext import commands
import nebula_client

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready():
    node = nebula_client.Node(uri="http://localhost:2333", password="supersecret")
    await nebula_client.NodePool.connect(client=bot, nodes=[node])
    print(f"Logged in as {bot.user}")

@bot.event
async def on_nebula_track_start(event: nebula_client.TrackStartEvent):
    print(f"Now playing: {event.track.title} in guild {event.player.guild_id}")

@bot.command()
async def play(ctx: commands.Context, *, query: str):
    if not ctx.author.voice:
        return await ctx.send("Join a voice channel first.")

    player: nebula_client.Player = ctx.voice_client or await ctx.author.voice.channel.connect(cls=nebula_client.Player)

    result = await player.node.search(query if "://" in query or ":" in query.split(" ")[0] else f"ytsearch:{query}")
    if not result:
        return await ctx.send("Nothing found.")

    track = result.tracks[0]
    track.requester = ctx.author

    if player.is_playing:
        player.add_to_queue(track)
        await ctx.send(f"Queued: **{track.title}**")
    else:
        await player.play(track)
        await ctx.send(f"Now playing: **{track.title}**")

@bot.command()
async def skip(ctx: commands.Context):
    if ctx.voice_client:
        await ctx.voice_client.skip()
        await ctx.send("Skipped.")

@bot.command()
async def volume(ctx: commands.Context, value: int):
    if ctx.voice_client:
        await ctx.voice_client.set_volume(value)
        await ctx.send(f"Volume set to {value}%")

@bot.command()
async def nightcore(ctx: commands.Context):
    if ctx.voice_client:
        await ctx.voice_client.set_filters(nebula_client.Filters().set_nightcore())
        await ctx.send("Nightcore on.")

bot.run("YOUR_TOKEN")
```

## Layout

```
nebula_client/
├── __init__.py       - public API surface
├── node.py            - Node (REST + WebSocket) and NodePool registry
├── player.py          - Player(discord.VoiceProtocol)
├── track.py           - Track / Playlist / SearchResult
├── filters.py         - Filters builder
├── events.py           - event dataclasses dispatched via client.dispatch()
├── enums.py            - LoadType, TrackEndReason, NodeStatus
└── exceptions.py       - exception hierarchy
```
