<div align="center">
<h1>Hoshimi (BETA)</h1>
<p>A lavalink@v4 client easy to use, up-to-date, and of course</p>
<div align="center">
    <img src="https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white" />
    <img src="https://img.shields.io/badge/MIT-green?style=for-the-badge" />
</div>
<br/>
<img alt="hoshimi" src="./assets/logo.png" />

![NPM Version](https://img.shields.io/npm/v/hoshimi?style=for-the-badge&logo=npm)
![NPM Downloads](https://img.shields.io/npm/dm/hoshimi?style=for-the-badge)

<p>
    <a href="https://www.npmjs.com/package/hoshimi">
        <img src="https://nodei.co/npm/hoshimi.png?downloads=true&stars=true" alt="NPM Install: hoshimi" />
    </a>
</p>
</div>

## 📦 Features
- 📋 **Lavalink V4**: Works with lavalink v4 and their features (wip).
- 🔗 **Node Manager**: Manage nodes, auto least‑used selection, session resume and more.
- ▶️ **Autoplay**: YouTube and Spotify recommendations out of the box; easily extend with your own function.
- 📝 **Lyrics**: Control your lyrics with live-lyrics updates; validates required plugins.
- 🌐 **REST + WebSocket**: Typed REST helpers, player/session control, decode single/multiple tracks.
- 📣 **Events**: Granular events with debug levels.
- 🧩 **Extensible**: Override structures with your own ones.
- 🧪 **Safety & DX**: Strict validation, descriptive errors, TypeScript-first API build, and formatting/linting.
- 📜 **Filters**: Built-in filters, plugin filters, and anything your fork exposes — no registration needed.  

## ⚙️ Requirements
- **Runtime** - atleast one of:
  - [Node.js](https://nodejs.org) v22+
  - [Bun](https://bun.com) v1.3+
  - [Deno](https://deno.com) v2.5+ (unstable)

## 📦 Installation

```sh
# Stable... and the development one (unstable)...

# Using NPM
npm install hoshimi # Stable
npm install hoshimi@dev # Development

# Or any package manager you use...

```

## 📜 Basic Setup

You can read the [test bot](https://github.com/Ganyu-Studios/hoshimi-bot) or you can follow this one:

```typescript
import { Hoshimi } from "hoshimi"; // She is all ears!
import { Client } from "seyfert"; // Only example client, you can use whatever you want...

const client = new Client(); // https://www.seyfert.dev/guide

const hoshimi = new Hoshimi({
    nodes: [
        {
            host: "localhost",
            port: 2333,
            password: "youshallnotpass",
        },
    ], // Add more nodes if you want!
    sendPayload(guildId, payload) {
        // Your client send to shard payload function
        client.gateway.send(client.gateway.calculateShardId(guildId), payload);
    },
});

// Bind the manager into your client!
client.hoshimi = hoshimi;

// FOLLOW YOUR CLIENT EVENT IMPLEMENTATION
// THIS IS ONLY A EXAMPLE, NOT A REAL USAGE
client.events.values.READY = {
    __filePath: null,
    data: { name: "ready", once: true },
    run(user, client) {
        client.logger.info(`Logged in as ${user.username}`);
        
        // Call the manager to initialize hoshimi
        hoshimi.init({ ...user, username: user.username });
    },
};

client.events.values.RAW = {
    __filePath: null,
    data: { name: "raw" },
    async run(data, client) {
        // Call the handler on the gateway dispatch events
        await hoshimi.updateVoiceState(data);
    },
};

(async () => {
    await client.start();
})();
```

## 📣 Events

The manager is an `EventEmitter`, and every event is typed by name — the handler's arguments come from
the event you listen to, so there is nothing to annotate:

```typescript
import { DebugLevels, EventNames } from "hoshimi";

hoshimi.on(EventNames.NodeReady, (node) => {
    console.log(`Node ${node.id} is ready.`);
});

hoshimi.on(EventNames.TrackStart, (player, track) => {
    console.log(`Now playing "${track?.info.title}" in ${player.guildId}`);
    // `player.textId` is the channel the player was created with, if you set one.
});

hoshimi.on(EventNames.QueueEnd, async (player) => {
    console.log(`Nothing left to play in ${player.guildId}`);
    await player.destroy();
});

hoshimi.on(EventNames.PlayerDestroy, (player, reason) => {
    console.log(`Player for ${player.guildId} destroyed: ${reason}`);
});

hoshimi.on(EventNames.NodeError, (node, error) => console.error(`Node ${node.id} failed:`, error));
hoshimi.on(EventNames.Error, (error) => console.error(error));
```

Debug is a single event carrying its level, so you decide how much of it reaches your logs:

```typescript
hoshimi.on(EventNames.Debug, (level, message) => {
    if (level === DebugLevels.Player) console.debug(message);
});
```

`EventNames` is only a convenience — `hoshimi.on("trackStart", ...)` is the same listener, typed the
same way. There are events for nodes, players, tracks, the queue and lyrics; your editor will list
them all from the enum.

## 📜 Filters

A filter is active while its key is in the payload. There is no "off" payload: `clear` removes the key,
and `isEnabled` is presence.

```typescript
import { FilterType } from "hoshimi";

const player = hoshimi.getPlayer("guildId");

await player.filterManager.setNightcore();
await player.filterManager.set(FilterType.Echo, { delay: 200, decay: 0.5 });

player.filterManager.isEnabled(FilterType.Echo); // true
player.filterManager.getEnabled();               // ["timescale", "echo"]
player.filterManager.get(FilterType.Timescale);  // TimescaleSettings | undefined

await player.filterManager.clear(FilterType.Echo);
await player.filterManager.reset();              // drops every filter
```

`set` and `get` are typed per filter, so the payload of a built-in is checked for you — `set(FilterType.Volume, { nope: true })` does not compile.

Filters Hoshimi has never heard of work too — a fork's own filters, a plugin you wrote, anything. The
envelope comes from the options:

```typescript
// pluginFilters.myFilter — the extension point the Lavalink v4 spec defines
await player.filterManager.set("myFilter", { gain: 2 });

// pluginFilters["my-plugin"].boost — nested, per the spec's plugin shape
await player.filterManager.set("boost", { gain: 2 }, { plugin: "my-plugin" });

// filters.forkEcho — top level, next to the built-ins, where forks expose theirs
await player.filterManager.set("forkEcho", { decay: 0.5 }, { top: true });

// Clear it from the same envelope it was written to
await player.filterManager.clear("boost", { plugin: "my-plugin" });
```

Hoshimi does not check which server it is talking to, so whether a fork-specific filter is safe to send
is up to you: point the player at a node that understands it.

Registering a filter is **optional**. Do it to get routing by name — no options at the call site — plus a
check against what the node advertises in `/v4/info`:

```typescript
import { FilterRegistry, FilterScope, PluginCapabilities } from "hoshimi";

FilterRegistry.register({
    name: "boost",
    scope: FilterScope.Plugin,      // Plugin -> pluginFilters · Core -> top level
    pluginName: "my-plugin",        // omit to write it flat under pluginFilters
    capability: PluginCapabilities.Filters,
});

await player.filterManager.set("boost", { gain: 2 }); // routed and validated
```

`set(name, payload, { validate: false })` skips that check when a node fails to advertise a filter it
actually supports.

To type a filter of your own, declare its payload — the key is the filter name, the value is what it
takes. That gives you autocompletion for the name and a checked payload in `set` and `get`:

```typescript
declare module "hoshimi" {
    interface CustomizableFilters {
        forkEcho: { decay: number; delay: number };
    }
}

await player.filterManager.set("forkEcho", { decay: 0.5, delay: 200 }, { top: true });
player.filterManager.get("forkEcho", { top: true }); // { decay: number; delay: number } | undefined
```

A filter nobody declared takes `unknown`, so ad-hoc payloads keep working without any of this.

## 💖 Used By

Hoshimi powers these bots:

- **Official Bots**:
    - **[Stelle](https://github.com/Ganyu-Studios/stelle-music)**: by [Ganyu Studios](https://github.com/Ganyu-Studios/stelle-music)

- **Community Bots**:
    - **[Miyu](https://ptb.discord.com/oauth2/authorize?client_id=1277180179273482280)**: by [Kenver](https://github.com/Kenver123)
    - **[GoTTY](https://discord.com/oauth2/authorize?client_id=1352131392993230869)**: by [Void](https://github.com/voidemx)
    - **[Flixo](https://discord.com/oauth2/authorize?client_id=1380994881731952741&permissions=7107797346413761&integration_type=0&scope=bot)**: by [Ansh](https://github.com/titanxdevz)

## 📝 Additional Notes
I'm currently working on this package.</br> This package takes some ideas provided from libraries like:

- 📦 [`lavalink-client`](https://github.com/Tomato6966/lavalink-client/)
- 📦 [`kazagumo`](https://github.com/Takiyo0/Kazagumo)
- 📦 [`distube`](https://github.com/skick1234/DisTube)
- 📦 [`discord-player`](https://github.com/Androz2091/discord-player)
- 📦 [`shoukaku`](https://github.com/shipgirlproject/Shoukaku)

**I'm taking their job as a base for this project, I love their job, all of them, I just took some
stuff because i'm too lazy to make my own.**</br> If anyone of them wants to
talk to me to remove their stuff, they can.</br>

But made with my code style and my knowledge and of course up-to-date.

## 📝 License

Copyright © 2026 [Ganyu Studios](https://github.com/Ganyu-Studios).

This project is [MIT](LICENSE) licensed.

- *The character and assets are not my property, property of miHoYo Co. Ltd. (HoYoverse)*

> *Made with 🐐❤️💪... A project made by the community, for the community.*
