# Configuration import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; # Configuration The configuration file controls the server behavior, audio quality, sources, and cluster scaling. ## Server Settings Core settings for the server instance. `useBunServer` is currently experimental. Use with caution in production environments. *** ## Cluster & Scaling Settings related to worker clustering and auto-scaling behavior. ### General Cluster Options ### Specialized Source Workers ### Hibernation ### Scaling Configuration These settings control the `cluster.scaling` behavior. ### Workers Endpoint *** ## Connection Health Monitors network health and connection quality for Discord voice. *** ## Limits *** ## Audio & Player Configuration for audio quality, updates, and playback behavior. *** ## Filters Toggle built-in audio filters that can be used via the player filters API. *** ## Voice Receive Configuration for receiving Discord voice frames over a dedicated WebSocket. *** ## Audio Mixer Configuration for the built-in audio mixer (mix layers). *** ## SponsorBlock Automatic skipping of sponsored or unwanted segments in YouTube tracks, powered by the [SponsorBlock](https://sponsor.ajay.app) community database. This feature is YouTube-only; segments are fetched when a YouTube track starts playing. *** ## Sources Configuration for specific music and media providers. ### Primary Platforms ### YouTube ### Spotify ### Apple Music ### Tidal ### Songlink ### Eternal Box ### Gaana ### Audius ### Qobuz ### Yandex Music ### Piper TTS ### VK Music ### Bilibili ### Pandora ### SoundCloud ### Local Files ### Deezer ### JioSaavn ### Google TTS ### Flowery TTS ### Google Drive Plays audio files hosted on Google Drive. Use the `gdsearch:` prefix or a direct Google Drive share URL with `/v4/loadtracks`. *** ## Meanings Configuration for track meaning/background providers. *** ## Lyrics Configuration for lyrics providers. **Supported Providers:** YouTube, Genius, Musixmatch, LRCLib, Letras.mus.br, Bilibili, Yandex Music, Apple Music. *** ## Security & Rate Limiting ### DOS Protection ### Rate Limit (Global & Per Entity) ### Route Planner (IP Rotation) ## Logging & Metrics *** ## Plugins Configuration for loading and managing plugins. # NodeLink vs Lavalink import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; # NodeLink vs Other Solutions You might be familiar with various audio engines. NodeLink was built to stand on its own as a high-density, performance-first solution for modern infrastructure. NodeLink maintains broad industry-standard API compatibility. Your existing integrations work out of the box, with significantly enhanced performance and a richer feature set. *** ## The Extra Spice 🌢️ NodeLink isn't just a clone. We took what worked and added features that made sense to us. Synced lyrics from YouTube captions. Plain text from Genius, Musixmatch, and LRCLib. Load video chapters automatically. Perfect for podcasts and long-form content. Real-time network speed tracking. Know exactly how fast your audio is flowing. Stream audio directly without going through Discord. For when you need that flexibility. Overlay TTS, sound effects, or voiceovers on top of your music. Built-in mixing engine. Google TTS built-in. No plugins needed. *** ## What's Actually Different? Let's break it down. No fluff. Just facts. ### Lyrics Support **Lavalink** has community plugins for lyrics (like LavaLyrics, Lyrics.kt).\ **NodeLink** has it built-in with multiple sources. ```bash # Get lyrics with automatic source detection curl "http://your-server.com/v4/loadlyrics?encodedTrack=QAAAjQI..." \ -H "Authorization: yourPassword" # Prefer specific language curl "http://your-server.com/v4/loadlyrics?encodedTrack=QAAAjQI...&lang=pt" \ -H "Authorization: yourPassword" ``` **Supported sources:** * **YouTube Captions**: Synced, timestamped lyrics (when available) * **Genius**: Plain text lyrics, community-sourced * **Musixmatch**: Synced lyrics database (auto-obtains signature) * **LRCLib**: Open-source synced lyrics repository ```json { "loadType": "lyrics", "data": { "synced": true, "lang": "en", "source": "youtube", "lines": [ { "text": "β™ͺ We're no strangers to love β™ͺ", "time": 18640, "duration": 3240 }, { "text": "β™ͺ You know the rules and so do I β™ͺ", "time": 22640, "duration": 4320 } ] } } ``` ```json { "loadType": "lyrics", "data": { "synced": false, "lang": "en", "source": "genius", "lines": [ { "text": "We're no strangers to love", "time": null, "duration": null }, { "text": "You know the rules and so do I", "time": null, "duration": null } ] } } ``` *** ### Chapter Markers **Lavalink** has the SponsorBlock plugin for chapters.\ **NodeLink** has automatic chapter extraction built-in. Perfect for podcasts, tutorials, and music albums with multiple parts. ```bash curl "http://your-server.com/v4/loadchapters?encodedTrack=QAAAjQI..." \ -H "Authorization: yourPassword" ``` **Returns:** ```json [ { "title": "An intro to video chapters", "startTime": 0, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 23000, "endTime": 23000 }, { "title": "How to set up video chapters", "startTime": 23000, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 44000, "endTime": 67000 }, { "title": "See video chapters in action", "startTime": 67000, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 12000, "endTime": 79000 } ] ``` *** ### Connection Metrics Real-time connection health and speed tracking. `/v4/connection` Know exactly how your connection is performing. Useful for debugging slow audio or buffering issues. ```bash curl "http://your-server.com/v4/connection" \ -H "Authorization: yourPassword" ``` **Response:** ```json { "status": "good", "metrics": { "speed": { "bps": 23669887.133182842, "kbps": 184920.99322799096, "mbps": 180.59 }, "downloadedBytes": 10485760, "durationSeconds": 0.44, "timestamp": 1764813556344 } } ``` *** ### Direct Streaming **Lavalink** has `/youtube/stream/{videoId}` via YouTube plugin.\ **NodeLink** has `/v4/trackstream` for all sources. Get direct streaming URLs and available formats for any track. Enable in config with `enableTrackStreamEndpoint: true`. ```bash # Works with ANY source (YouTube, Spotify, Deezer, etc.) # Returns JSON with streaming URL and available formats curl "http://your-server.com/v4/trackstream?encodedTrack=QAAAjQI...&itag=251" \ -H "Authorization: yourPassword" ``` NodeLink also supports raw PCM streaming via `/v4/loadstream` (enable with `enableLoadStreamEndpoint: true`) when you need direct audio bytes for recording or custom processing. **Response:** ```json { "url": "https://[...].googlevideo.com/videoplayback?...", "protocol": "http", "format": "webm/opus", "formats": [ { "itag": 251, "mimeType": "audio/webm; codecs=\"opus\"", "bitrate": 146128, "audioQuality": "AUDIO_QUALITY_MEDIUM" } ] } ``` ```bash # Only works with YouTube curl "http://your-server.com/youtube/stream/{videoId}" \ -H "Authorization: yourPassword" \ --output audio.mp3 ``` **Use cases:** * Getting direct streaming URLs for custom players * Downloading audio for offline playback * Custom audio processing pipelines * Building audio preview features *** ### Text-to-Speech **Lavalink** has Google Cloud TTS plugin (requires API keys).\ **NodeLink** has Google TTS built-in (no keys needed). ```bash # Free, no API keys required curl "http://your-server.com/v4/loadtracks?identifier=gtts:Hello World" \ -H "Authorization: yourPassword" ``` Requires [Google Cloud TTS Plugin](https://github.com/DuncteBot/tts-plugin) with Google Cloud API keys and billing enabled. *** ## Enhanced Monitoring ### Server Info Both provide `/v4/info`, but NodeLink adds the `isNodelink` field for identification: ```json { "version": { "semver": "3.3.0", "major": 3, "minor": 3, "patch": 0 }, "node": "v22.12.0", "isNodelink": true, "sourceManagers": [ "deezer", "bandcamp", "soundcloud", "local", "http", "applemusic", "youtube", "instagram", "kwai", "twitch", "spotify", "tidal", "nicovideo", "reddit", "lastfm" ], "filters": [ "tremolo", "vibrato", "lowpass", "highpass", "rotation", "karaoke", "distortion", "channelMix", "equalizer", "chorus", "compressor", "echo", "phaser", "timescale" ] } ``` ```json { "version": { "semver": "4.0.0", "major": 4, "minor": 0, "patch": 0 }, "jvm": "18.0.2.1", "lavaplayer": "1.3.98.4", "sourceManagers": [ "youtube", "soundcloud" ], "filters": [ "equalizer", "karaoke", "timescale", "channelMix" ] } ``` ### Detailed Statistics NodeLink's `/v4/stats` includes **detailedStats** with granular tracking: ```json { "players": 5, "playingPlayers": 3, "uptime": 17930, "detailedStats": { "api": { "requests": { "/v4/loadtracks": 150, "/v4/info": 5, "/v4/loadlyrics": 42 }, "errors": {} }, "sources": { "youtube": 120, "spotify": 30 }, "playback": { "events": { "TrackStartEvent": 100, "TrackEndEvent": 95 } } } } ``` *** ## Audio Sources Comparison Here's where things get interesting. Let's see what's **built-in** vs what needs **plugins**. These sources stream audio **directly** without needing to search elsewhere: | Source | Lavalink | NodeLink | | ------------ | ----------------------- | ---------- | | YouTube | πŸ”Œ Plugin | βœ… Built-in | | SoundCloud | βœ… Built-in | βœ… Built-in | | Deezer | πŸ”Œ Plugin | βœ… Built-in | | Bandcamp | πŸ”Œ Plugin | βœ… Built-in | | Audiomack | ❌ Not available | βœ… Built-in | | Gaana | ❌ Not available | βœ… Built-in | | Twitch | βœ… Built-in | βœ… Built-in | | Instagram | ❌ Not available | βœ… Built-in | | Reddit | ❌ Not available | βœ… Built-in | | Kwai | ❌ Not available | βœ… Built-in | | NicoVideo | ⚠️ Built-in\Not working | βœ… Built-in | | Vimeo | ⚠️ Built-in\Not working | βœ… Built-in | | Pandora | ❌ Not available | βœ… Built-in | | JioSaavn | πŸ”Œ Plugin | βœ… Built-in | | Local Files | βœ… Built-in | βœ… Built-in | | HTTP URLs | βœ… Built-in | βœ… Built-in | | Amazon Music | ❌ Not available | βœ… Built-in | | Mixcloud | πŸ”Œ Plugin | βœ… Built-in | | VK Music | πŸ”Œ Plugin | βœ… Built-in | | Telegram | πŸ”Œ Plugin | βœ… Built-in | | Bilibili | πŸ”Œ Plugin | βœ… Built-in | | Pinterest | ❌ Not available | βœ… Built-in | These sources fetch metadata then **search YouTube** for actual audio: | Source | Lavalink | NodeLink | Notes | | ----------- | --------------- | ---------- | ---------------------------------------------- | | Spotify | πŸ”Œ Plugin | βœ… Built-in | Requires Client ID/Secret (free) | | Apple Music | πŸ”Œ Plugin | βœ… Built-in | **Auto-generates token** (no $99/year needed!) | | Tidal | πŸ”Œ Plugin | βœ… Built-in | Requires web token (free) | | Last.fm | ❌ Not available | βœ… Built-in | Track discovery, mirrors to YouTube | | Shazam | ❌ Not available | βœ… Built-in | Track discovery, mirrors to YouTube | | Genius | ❌ Not available | βœ… Built-in | Metadata resolve, mirrors to default search | **NodeLink automatically generates Apple Music tokens** without requiring the $99/year Apple Developer Program membership. Just set `mediaApiToken: "token_here"` in config and NodeLink handles the rest. Lavalink's LavaSrc plugin requires you to pay Apple and manually obtain tokens. **How mirroring works:** 1. Fetch track metadata from Spotify/Apple Music/Tidal 2. Search YouTube with: `"song title" "artist name"` 3. Find best match based on duration and metadata 4. Stream from YouTube **Why mirror?** * These platforms don't provide direct streaming APIs * Mirroring is the only way without paying for premium accounts * YouTube has almost everything **Standard (both support):** * `ytsearch:` - YouTube search * `ytmsearch:` - YouTube Music search * `scsearch:` - SoundCloud search **NodeLink extras:** * `spsearch:` - Spotify search (then mirrors to YouTube) * `amsearch:` - Apple Music search (then mirrors to YouTube) * `dzsearch:` - Deezer direct search * `tdsearch:` - Tidal search (then mirrors to YouTube) * `bcsearch:` - Bandcamp search * `admsearch:` / `audiomack:` - Audiomack search * `gaanasearch:` - Gaana search * `jssearch:` - JioSaavn search * `lfsearch:` - Last.fm search * `ncsearch:` / `nicovideo:` - NicoVideo search * `pdsearch:` - Pandora search * `vksearch:` - VK Music search * `mcsearch:` - Mixcloud search * `bilibili:` - Bilibili search * `shsearch:` / `szsearch:` - Shazam search * `gtts:` - Google Text-to-Speech * `speak:` - Google Text-to-Speech alias * `flowery:` / `ftts:` - Flowery TTS * `local:` / `file:` - Local file path * `http:` - Direct HTTP URL * `search:` - Unified search across configured sources **Recommendation prefixes:** * `ytrec:` - YouTube automix recommendations * `sprec:` - Spotify recommendations * `dzrec:` - Deezer recommendations * `tdrec:` - Tidal recommendations * `jsrec:` - JioSaavn recommendations * `vkrec:` - VK Music recommendations **Try it:** ```bash # Search Spotify then play via YouTube curl "http://your-server.com/v4/loadtracks?identifier=spsearch:never gonna give you up" \ -H "Authorization: yourPassword" # Search Deezer directly curl "http://your-server.com/v4/loadtracks?identifier=dzsearch:daft punk" \ -H "Authorization: yourPassword" # Text-to-speech curl "http://your-server.com/v4/loadtracks?identifier=gtts:Hello World" \ -H "Authorization: yourPassword" ``` *** ## The Plugin Situation Let's address the elephant in the room: **Lavalink has plugins. NodeLink doesn't.** But here's the thing... **Lavalink**: Modular ecosystem. Want a feature? Install a plugin.\ **NodeLink**: Batteries included. Want a feature? It's probably already there. Neither is objectively better. It depends on what **you** need. ### Lavalink's Approach Lavalink has a **thriving plugin ecosystem** built by the community: * **[LavaSrc](https://github.com/topi314/LavaSrc)**: Spotify, Apple Music, Deezer, Yandex Music support * **[LavaLyrics](https://github.com/topi314/LavaLyrics)**: Lyrics API for other plugins to consume * **[SponsorBlock](https://github.com/topi314/SponsorBlock-Plugin)**: YouTube chapters and sponsor segment skipping * **[YouTube Plugin](https://github.com/lavalink-devs/youtube-source)**: Enhanced YouTube with OAuth, age-restriction bypass * **[Google TTS Plugin](https://github.com/DuncteBot/tts-plugin)**: Text-to-speech via Google Cloud API * **[LavaSearch](https://github.com/topi314/LavaSearch)**: Advanced search across platforms * **[LavaDSPX](https://github.com/Devoxin/LavaDSPX-Plugin)**: Additional audio filters (echo, compressor, phaser) * **And many more...** **How plugin installation works:** Lavalink can **auto-download plugins** from Maven repositories. You declare them in `application.yml` with their dependency coordinates, and Lavalink downloads the `.jar` files on startup. **Advantages:** * **Flexibility**: Install only what you need * **Community-driven**: Third-party developers can extend functionality * **Modularity**: Each plugin is independent * **Auto-download**: No manual `.jar` management needed * **Extensibility**: You can create custom plugins in Java/Kotlin **Trade-offs:** * **Version compatibility**: Plugins must match your Lavalink version (v3 vs v4) * **Configuration complexity**: Each plugin has its own config section * **Dependency management**: Need to track plugin versions and updates * **Restart required**: Adding/updating plugins requires restarting Lavalink ### NodeLink's Approach NodeLink has **most common features built-in**: * **30+ sources** out of the box * **Lyrics** from 4 providers * **Chapters** extraction * **TTS** without API keys * **Connection metrics** * **Direct streaming** **Advantages:** * Simplicity: One package, everything works * No version conflicts: Everything is tested together * Unified config: One config file for all features **Trade-offs:** * Less flexibility: Can't add custom plugins * Larger base package: Includes features you might not use * Fixed feature set: Dependent on core team for new features Most bots use: YouTube, Spotify, SoundCloud, lyrics, and basic playback. NodeLink covers this **without any plugins**. If you need highly specialized features (custom audio sources, unique filters), Lavalink's plugin ecosystem might serve you better. *** ## Configuration Tokens Some sources need tokens. Here's the reality: **These require free tokens (no payment):** **Spotify** (`clientId` + `clientSecret`): * Requires a Client ID and Secret from the Spotify Developer Dashboard. **Deezer** (`arl`): * Requires a valid `arl` cookie from a web session. **Tidal** (`token`): * Requires a valid authentication token. **VK Music** (`userToken` or `userCookie`): * Requires a user token or cookie from a VK web session. All these services have **free tiers**. You don't need premium accounts. **Apple Music** - The magic one: ```js applemusic: { enabled: true, mediaApiToken: "token_here" // NodeLink generates it automatically! } ``` That's it. NodeLink handles Apple Music authentication **without** requiring: * $99/year Apple Developer membership * Manual token extraction * Token refresh management **How?** NodeLink reverse-engineered Apple's web player authentication and can generate valid tokens on-demand. We automate what others require you to do manually or pay for. **These work without ANY tokens:** * YouTube * SoundCloud * Bandcamp * Instagram * Reddit * Kwai * NicoVideo * Pandora (auto-fetches csrf token) * Twitch * Last.fm * Amazon Music * Mixcloud * Telegram * Bilibili (optional sessdata) * Pinterest * Local files * HTTP URLs * Google TTS Just enable them in config and they work immediately. *** ## WebSocket Events NodeLink emits **additional events** for fine-grained control: * **TrackStartEvent**: Track started playing * **TrackEndEvent**: Track finished/stopped * **TrackExceptionEvent**: Track threw an error * **TrackStuckEvent**: Track got stuck * **WebSocketClosedEvent**: Voice connection closed * **PlayerCreatedEvent**: Player initialized * **PlayerDestroyedEvent**: Player cleaned up * **PlayerConnectedEvent**: Connected to voice * **PlayerReconnectingEvent**: Attempting reconnection * **VolumeChangedEvent**: Volume adjusted * **FiltersChangedEvent**: Filters modified * **SeekEvent**: Track position changed * **PauseEvent**: Playback paused/resumed * **ConnectionStatusEvent**: Voice connection status update These give you **real-time visibility** into everything happening with the player. *** ## So... Which One? Still unsure? Let's make it personal. **β†’ NodeLink** You don't want to manage plugins. You don't want to hunt down .jar files or deal with version conflicts. You just want Spotify, lyrics, and chapters to work out of the box. NodeLink gives you everything in one package. Install once, enable sources in config, done. **β†’ Lavalink** You have specific needs. Maybe you need a custom audio filter that doesn't exist yet. Maybe you're building something that requires a unique plugin architecture. Lavalink's plugin ecosystem is mature with community-built solutions for edge cases. **β†’ NodeLink** Time is money. You need a proof of concept by Friday. You want Spotify, YouTube, lyrics, and chapters without spending hours configuring plugins and reading documentation. Install NodeLink. Add tokens to config. Write your bot logic. Ship it. Done. **β†’ NodeLink** NodeLink uses a **multi-worker cluster architecture**. Each player session can run in its own dedicated process, not just the voice connection, but the entire player logic. This means: * Better resource utilization on multi-core systems * Isolation between players (one crash doesn't affect others) * Automatic scaling based on load * Hot-reload workers without downtime It's built for modern infrastructure from day one. **β†’ NodeLink** You're a JavaScript/TypeScript developer. You want to read the code, understand the internals, maybe even contribute features. NodeLink is 100% JavaScript. The codebase is approachable. No JVM bytecode, no compiled .class files. Just readable JS you can debug with Chrome DevTools. **β†’ Lavalink** Your entire infrastructure is Java. You have JVM monitoring tools, deployment pipelines optimized for .jar files, and a team with Java expertise. Lavalink fits naturally into JVM-based infrastructure. **β†’ NodeLink** You want Apple Music support but don't want to pay $99/year for Apple Developer Program membership or manually extract tokens every time they expire. NodeLink auto-generates Apple Music tokens. While Lavalink can use manually extracted tokens, NodeLink handles the generation and renewal automatically. **β†’ NodeLink** NodeLink is community-driven. Yes, there might be bugs. Yes, some features are still being refined. But that's the beauty of it. **You can help shape it.** Report bugs, suggest features, contribute code. The community is actively building the best experience possible for everyone. It's not about being "more stable" or "less stable" β€” it's about being **actively developed** with a community that cares. ### The Real Answer **NodeLink is API-compatible with Lavalink v4.** Your client works with both seamlessly. Want to try NodeLink? Install it. Test your use case. If you love it, stick with it. If you need something Lavalink-specific, you can switch β€” **your client code doesn't change**. The beauty is that **you're not locked in**. These are tools, not commitments. NodeLink represents a **modern approach** to audio streaming: * **Worker-based architecture**: Each player can run in its own process * **Built-in features**: No plugin hunting or version conflicts * **Open development**: Community-driven improvements * **JavaScript ecosystem**: Native integration with Node.js projects * **Cost-effective**: Free features that cost money elsewhere (Apple Music tokens) It's not about replacing Lavalink. It's about offering **a better fit** for certain use cases. NodeLink might have rough edges. We own that. But **we're actively improving it every day**. Found a bug? [Report it](https://github.com/PerformanC/NodeLink/issues) and we'll fix it.\ Need a feature? Let us know and we'll consider it.\ Want to contribute? The codebase is waiting for you. **Together, we make NodeLink better.** πŸ’š *** NodeLink is a **drop-in replacement** for Lavalink v4. Change your connection URL, everything works. Then gradually enable NodeLink-specific features (lyrics, chapters, extra sources) when you're ready. *** ## Architecture (v3.5.0+) NodeLink v3.5.0 introduced a major architectural shift nicknamed **"The End of Stutter."** ### Two-Tiered Cluster Model Unlike Lavalink which typically runs as a single process (or requires external load balancers), NodeLink features a built-in tiered clustering system: 1. **Source Workers**: Dedicated to non-deterministic I/O tasks. * Searching (YouTube, Spotify, etc.) * Resolving metadata * Scraping lyrics & chapters * Delegating heavy JSON parsing away from the main thread. 2. **Voice Workers**: Specialized for real-time DSP (Digital Signal Processing). * Opus encoding/decoding * Audio filtering (Equalizer, Echo, etc.) * Mixing layers * Running with **High OS Priority** to ensure audio frames are delivered every 20ms without fail. ### Binary Socket Bus To achieve maximum throughput with minimum latency, NodeLink uses a custom **Binary Socket Bus** for inter-process communication (IPC). * **Unix Sockets / Named Pipes**: Bypasses the overhead of the network stack. * **V8 Serialization**: Directly transfers complex JavaScript objects between processes without slow JSON stringification. * **Ring Buffers**: Eliminates Garbage Collection (GC) pauses during audio processing. *** ## Security & Protocols ### DAVE (Discord Audio Visual Encryption) NodeLink fully supports Discord's new **DAVE** protocol (`aead_aes256_gcm_rtpsize`). This ensures your audio is end-to-end encrypted and remains compatible with Discord's latest security requirements without any manual configuration. ### SABR & PO Tokens For YouTube playback, NodeLink integrates the **SABR** adaptive streaming protocol and a built-in **PO Token (Proof of Origin)** manager. This significantly reduces "Video Unavailable" or "Bot Detected" errors by providing valid attestation for every stream request. *** ## Detecting NodeLink Want to detect if you're connected to NodeLink vs Lavalink? Check the `/v4/info` endpoint: ```javascript const info = await fetch('http://your-server.com/v4/info', { headers: { 'Authorization': 'yourPassword' } }).then(r => r.json()); if (info.isNodelink) { console.log(`🌿 NodeLink v${info.version.semver}`); // Use NodeLink-specific features // - /v4/loadlyrics // - /v4/loadchapters // - /v4/connection // - Extra sources (Instagram, Reddit, etc.) } else { console.log(`🎡 Lavalink v${info.version.semver}`); // Standard Lavalink features only } ``` *** ## What's Next? Now that you know the differences, it's time to try it out. ### Install NodeLink Follow the [Quick Start](/docs) guide to get NodeLink running in 5 minutes. ### Configure Sources Add tokens for Spotify, Deezer, Tidal (all free). Apple Music auto-generates. ### Try Extra Features Experiment with lyrics, chapters, connection metrics, and TTS. ### Build Something Cool Make a bot, share your creation, join the community! *** ## Questions? Found something unclear? Want to suggest a feature? Have a cool use case? Join our community or [open an issue](https://github.com/PerformanC/NodeLink/issues). We're always listening. πŸ’™ # Welcome to NodeLink import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; # HELLOOOOOOOOOOO WORLD! πŸ‘‹ Welcome to the **NodeLink** documentation! If you are here, you are probably looking for an audio server that flows with your code. You want something that feels natural, something that you can tinker with, break, and fix again. **You are in the right place.** NodeLink was born from a simple desire: to understand and master every detail of an audio server. It is **100% Node.js**, lightweight, modular, and built with passion. It doesn’t try to reinvent the wheel β€” it just makes it spin in our own rhythm. *** ## NodeLink? Why though? Seriously, why? We love the existing options. Truly. But sometimes, you just want to build in the language you speak every day. We built NodeLink because we wanted a server that felt like home. We wanted to peek under the hood, see the gears turning, and understand the magic of audio processing without switching contexts. If you write your bot in **JavaScript** or **TypeScript**, NodeLink feels like a natural extension of your project. It fits right into your existing ecosystem, making debugging and contributing a breeze. We aim for simplicity. NodeLink handles the complexities of various source managers internally, so you can focus on playing music rather than fighting with configuration files and access tokens. We utilize a **worker-based architecture**. This ensures that heavy tasks like parsing playlists happen in separate processes, keeping your main audio loop smooth and uninterrupted. ### The Real Truth It's about **control** and **knowledge**. You can go deeper into anything here. If you don't know how something works, **go deep**. Learn it. Imagine it. Search for it. Run after it. Find it. > If you don't seek knowledge, it won't just fall into your hands. We built this so you can read the code, understand the stack traces, and realize that **you can do anything**. Don't let anyone tell you otherwise. The code is open. The door is open. Come in and build something amazing. πŸ’™ *** ## Let's get you running Enough talking, let's make some noise. NodeLink requires only Node.js, which runs on Linux, Windows, and macOS. ### Prerequisites Before we start, make sure you have **Node.js (v22+)** and **Git** installed. The easiest way is using `winget` in PowerShell: ```powershell winget install OpenJS.NodeJS winget install Git.Git ``` *Alternatively, download the installers from [nodejs.org](https://nodejs.org) and [git-scm.com](https://git-scm.com).* Using a package manager (Ubuntu/Debian example): ```bash # Install Git sudo apt update && sudo apt install git -y # Install Node.js 22.x curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs ``` If you have **Homebrew** installed, it's a breeze: ```bash brew install node brew install git ``` ### Clone the Magic Bring the project to your machine. ```bash git clone https://github.com/PerformanC/NodeLink.git cd NodeLink ``` ### Install Dependencies Install the project dependencies using your preferred package manager. ```bash npm install ``` ```bash yarn install ``` ```bash pnpm install ``` ```bash bun install ``` ### Config & Fly Copy the default configuration and start the server. ```bash cp config.default.js config.js npm run start ``` ```powershell Copy-Item config.default.js -Destination config.js npm run start ``` ```cmd copy config.default.js config.js npm run start ``` If anything strange happens, report it on our GitHub or join our Discord!\ Fast feedback means fast fixes. Thanks to everyone helping improve NodeLink. πŸ’™ ## Where to go next? Tweak the engine. Configure your sources, filters, and limits. For the curious minds who want to see the raw endpoints. # Minimum Client Requirements import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; # Minimum Client Requirements This page defines the minimum client-side features required to be considered officially compatible with NodeLink. It focuses only on endpoints, WebSocket flows, and payload extensions that a client must implement. Administrative and monitoring concerns are intentionally excluded. *** ## Introduction NodeLink is compatible with the Lavalink v4 API, but it adds client-facing features that go beyond the standard specification. A client that wants to be considered an official NodeLink client must implement the minimum set below. These items are not server-only toggles: they require client logic. ## Method The list is derived from the NodeLink server implementation and limited to differences that require explicit client support. Items that are operational, administrative, or monitoring-oriented are excluded from the minimum. ## Results ### Minimum Feature Set REST endpoints and WS events for layered audio playback. Load synced or plain lyrics via /v4/loadlyrics. Load video chapters via /v4/loadchapters. Filters not present in Lavalink. Dedicated WS stream for voice frames. channelId and audioTrackId support. Additional search prefixes beyond Lavalink. REST endpoints and WS events for automatic segment skipping. *** ### Audio Mixer **Minimum support** * REST: `POST/GET/PATCH/DELETE /v4/sessions/:sessionId/players/:guildId/mix` * WS events: `MixStartedEvent`, `MixEndedEvent` The mixer introduces new endpoints and event types. Clients must provide methods to create, manage, and remove layers, plus handle mix lifecycle events from the WebSocket. ### Lyrics **Minimum support** * REST: `GET /v4/loadlyrics` Lyrics are retrieved through a NodeLink-specific endpoint. Clients must implement this call and handle the response format. ### Chapters **Minimum support** * REST: `GET /v4/loadchapters` Chapters require a NodeLink-specific endpoint. Clients must implement the request and expose the chapter data to users. ### Extra Filters **Minimum support** * Filters: `echo`, `chorus`, `compressor`, `highpass`, `phaser`, `spatial` These filters extend the Lavalink filter schema. Clients must allow these filter objects in the update payload and avoid rejecting unknown keys. ### Voice Receive **Minimum support** * WS: `/v4/websocket/voice/:guildId` Voice Receive uses a second WebSocket stream. Clients must handle the binary frame format and reconnect logic for this channel. ### Update Player Extras **Minimum support** * `voice.channelId` in Update Player payloads NodeLink extends the Update Player schema. Clients must pass and preserve `voice.channelId` for DAVE stability during voice connections. ### Search Prefixes **Minimum support** * Additional prefixes: `admsearch`, `amsearch`, `audiomack`, `bcsearch`, `bilibili`, `dzsearch`, `flowery`, `ftts`, `gaanasearch`, `gdsearch`, `gtts`, `jssearch`, `lfsearch`, `mcsearch`, `ncsearch`, `nicovideo`, `pdsearch`, `shsearch`, `speak`, `spsearch`, `szsearch`, `tdsearch`, `vksearch` These prefixes are resolved by NodeLink sources. Clients must allow them as valid search identifiers and not block them as invalid inputs. *** ## Optional (Not Required for Official Compatibility) These features are available in NodeLink but are not part of the minimum compatibility bar for clients: * `/v4/connection` (connection metrics endpoint used for diagnostics) * `/v4/encodedtracks` (batch track encoding helper) * `search:` unified search prefix for `/v4/loadtracks` * `:playlist:` type modifier in search prefixes (example: `scsearch:playlist:lofi`) * `track.audioTrackId` in Update Player payloads (optional; defaults to original audio unless the client selects an alternate YouTube auto-dubbed track) *** ### SponsorBlock **Minimum support** * REST: `GET/PATCH/POST/DELETE /v4/sessions/:sessionId/players/:guildId/sponsorblock` * WS events: `SponsorBlockSegmentsLoadedEvent`, `SponsorBlockSegmentSkippedEvent` SponsorBlock introduces new REST endpoints for reading and managing per-player segment state, plus two new WebSocket events. Clients must implement the REST calls and handle the events to provide full SponsorBlock integration. *** ## Conclusion If a client implements the eight minimum areas above, it can be marked as an official NodeLink-compatible client in the README feature matrix. # Community Plugins import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; # Plugins NodeLink supports third-party plugins to add additional functionality such as custom audio sources, filters, WebSocket handling, and REST endpoints. To install a plugin, typically you place the plugin folder into the `plugins/` directory of your NodeLink instance or install it via npm. Check each plugin's repository for specific installation instructions. ## Official & Community Plugins Below is a list of known plugins developed by the community and the NodeLink team.

Cloudflare Tunnel

Official

Automatically exposes your NodeLink instance to the internet using Cloudflare Tunnel.

By 1Lucas1apk
View Source β†—

Sample Plugin

Template

A starter template demonstrating how to create custom sources, filters, and API routes.

By NodeLink Team
View Source β†—
Plugins are executed with full privileges within the NodeLink process. Only install plugins from trusted sources. The NodeLink team is not responsible for malicious code running via third-party plugins. ## Want to submit a plugin? If you have created a plugin and want to see it listed here: 1. Ensure your plugin has a public repository (GitHub, GitLab, etc.). 2. Include a clear `README.md` with installation and usage instructions. 3. Open a Pull Request to the [NodeLink Documentation](https://github.com/PerformanC/nodelink-docs) adding your plugin to this file. # Troubleshooting import { Callout } from 'fumadocs-ui/components/callout'; # Common Problems NodeLink was recently rewritten from the ground up. While we strive for perfection, we are humans (mostly), and humans make mistakes. If you encounter something that isn't listed here or seems like a bug, please join our [PerformanC Discord](https://discord.gg/uPveNfTuCJ) or [Ecliptia Discord](https://discord.gg/fzjksWS65v) and let us know! Here are specific errors you might encounter in your console or logs, and exactly what they mean. ## Startup & Connection Errors **Message:** `Port 3000 is already in use.` Another application (or a zombie instance of NodeLink) is already running on the port you configured. * **Linux/Mac:** Run `lsof -i :3000` to see what's using it, then `kill -9 `. * **Windows:** Use Task Manager or `netstat -ano | findstr :3000`. * **Alternative:** Change `server.port` in your `config.js`. **Message:** `Unauthorized connection attempt from [IP] - Invalid password provided` Your Lavalink client (the bot) is sending a password that does not match `server.password` in `config.js`. * **Check:** Ensure both your bot's config and NodeLink's config match exactly. * **Client Name:** Some clients fail to send a `Client-Name` header. NodeLink rejects these connections. Ensure your client library is up to date. **Message:** `Track identifier loading is not supported.` You attempted to play a track by passing an `identifier` (URL or search query) directly to the `UpdatePlayer` or `Add Mix Layer` endpoints. NodeLink (and standard Lavalink v4) requires tracks to be **encoded** strings for these operations. **Solution:** 1. Call `/v4/loadtracks?identifier=...` first. 2. Take the `encoded` string from the response. 3. Pass that `encoded` string to the player update. ## YouTube Specifics **Message:** `Cipher server at ... is offline or unreachable.` NodeLink relies on a signature deciphering service (like the one hosted by Kikkia) to play YouTube videos. * **Check Config:** Look at `sources.youtube.cipher.url` in `config.js`. * **Status:** The public server might be down. You may need to host your own or find an alternative. Some videos require a logged-in account. NodeLink attempts to use OAuth tokens if configured, but they can expire. * **OAuth:** Set `sources.youtube.getOAuthToken` to `true` in config, restart, and follow the instructions in the console to refresh your token. * **TV Client:** We simulate a TV client to bypass some restrictions, but `refreshToken` must be valid. ## Voice & Audio **Message:** `Voice connection for guild ... is not ready (missing UDP info).` NodeLink connected to Discord's WebSocket, but failed to establish the UDP connection required to send audio. * **Firewall:** Ensure your server allows **outgoing UDP** traffic to high ports. * **Docker:** If running in Docker, ensure you aren't blocking UDP. * **Discord:** Sometimes Discord's voice servers are just having a bad day. NodeLink relies on native modules like `sodium-native` and `opusscript` (or others) for encryption and encoding. * If you see errors related to `sodium` or `opus`, try reinstalling dependencies: `rm -rf node_modules && npm install`. * Ensure you have build tools installed (Python, C++ compiler) if you are building from source. ## Worker & Process Issues If a worker process dies, it usually means something went very wrong internally. NodeLink isolates players in workers, so if a worker fails, it dumps the error and the responsibility back to the main process (and you). **What to do:** 1. Enable debug logging in `config.js` (set `logging.level` to `'debug'`) to catch the stack trace. 2. This is likely a bug in the core logic. Please copy the logs and report it on our Discord servers. ## Configuration & Updates NodeLink evolves fast. If you updated the server but forgot to update your `config.js`, you might be missing critical settings. Always check the `config.default.js` file in the root directory after an update. It contains the source of truth for all available options. If an option says it doesn't exist, it probably just needs to be copied over. ## Connection & Network NodeLink includes a recovery system that attempts to handle disconnections gracefully. However, if you see frequent `ECONNRESET` errors, check your environment: * **Firewalls:** Ensure nothing is aggressively terminating long-lived connections or blocking specific packets. * **Router Channels:** Some routers automatically switch WiFi channels (frequencies) to "optimize" the network. When this happens (channel 1 β†’ 6, for example), the router often momentarily disconnects all clients to re-establish the link on the new frequency. This brief interruption is enough to kill an active stream. * **YouTube:** Sometimes YouTube simply closes the connection. This is "normal" behavior we try to mitigate. ## Limits & Protection NodeLink protects itself with a built-in rate limiter and DoS protection system. If you are running a **private** client that executes many requests in a few seconds (e.g., a dashboard or heavy load testing), you might trigger these protections. It is recommended to **disable** `rateLimit` and `dosProtection` in your configuration if you trust the clients connecting to your node. # API Reference import { Card, Cards } from 'fumadocs-ui/components/card'; import { Callout } from 'fumadocs-ui/components/callout'; # NodeLink API Reference NodeLink is a high-density standalone audio engine that maintains full industry-standard API compatibility while adding unique features for enhanced functionality. This reference covers all available endpoints, events, and data structures. ## Core APIs
Real-time communication. * Handshaking & Resuming * Player Events (Start, End, Exception) * Server Statistics HTTP endpoints for management. * Resolving Tracks & Searching * Player Control (Play, Stop, Filter) * Session Management
## NodeLink Extensions These features are exclusive to NodeLink or extend the standard behavior.
Exclusive capabilities. * **Audio Mixer:** Overlay TTS/SFX * **Holo Tracks:** Rich Metadata * **Lyrics:** Synced & Plain * **Chapters:** Video markers * **Direct Streaming:** HTTP access Server setup. * Enabling sources * Tuning limits * Docker environment
## Quick Reference: Search Prefixes When calling `/v4/loadtracks`, you can use these prefixes to search specific sources: | Prefix | Source | Description | | :------------- | :------------ | :--------------------------------------- | | `ytsearch:` | YouTube | Standard video search | | `ytmsearch:` | YouTube Music | Music-only search | | `scsearch:` | SoundCloud | Track search | | `search:` | Unified | Search across `unifiedSearchSources` | | `spsearch:` | Spotify | Metadata search (mirrors to YouTube) | | `amsearch:` | Apple Music | Metadata search (mirrors to YouTube) | | `dzsearch:` | Deezer | Direct audio search | | `tdsearch:` | Tidal | Metadata search (mirrors to YouTube) | | `bcsearch:` | Bandcamp | Track search | | `admsearch:` | Audiomack | Track search | | `audiomack:` | Audiomack | Alias of `admsearch:` | | `gaanasearch:` | Gaana | Track search | | `jssearch:` | JioSaavn | Track search | | `lfsearch:` | Last.fm | Metadata search (mirrors to YouTube) | | `pdsearch:` | Pandora | Track search | | `vksearch:` | VK Music | Track search | | `mcsearch:` | Mixcloud | Track search | | `ncsearch:` | NicoVideo | Video search | | `nicovideo:` | NicoVideo | Alias of `ncsearch:` | | `bilibili:` | Bilibili | Video search | | `shsearch:` | Shazam | Metadata search (mirrors to YouTube) | | `szsearch:` | Shazam | Alias of `shsearch:` | | `ebox:` | Eternal Box | Infinite Jukebox search | | `jukebox:` | Eternal Box | Alias of `ebox:` | | `slsearch:` | Songlink | Aggregated search (Odesli) | | `qbsearch:` | Qobuz | Track search | | `ymsearch:` | Yandex Music | Track search | | `ausearch:` | Audius | Track search | | `azsearch:` | Amazon Music | Track search | | `agsearch:` | Anghami | Track search | | `bksearch:` | Bluesky | Media search | | `lmsearch:` | Letras.mus.br | Meaning/Lyrics search | | `pipertts:` | Piper TTS | Local Text-to-Speech | | `gtts:` | Google TTS | Generate speech audio | | `speak:` | Google TTS | Alias of `gtts:` | | `ftts:` | Flowery TTS | Generate speech via Flowery API | | `flowery:` | Flowery TTS | Alias of `ftts:` | | `gdsearch:` | Google Drive | Plays audio files hosted on Google Drive | You can also pass direct URLs (e.g., `https://www.youtube.com/watch?v=...`) without any prefix. ## Search Type Modifiers Some sources accept a **type modifier** right after the prefix. This is not exclusive to YouTube; SoundCloud and other sources also parse type modifiers. The format is: `::` Supported types: `track` (default), `playlist`, `album`, `artist`, `channel`. Examples: * `ytsearch:playlist:billie eilish` (returns playlist results) * `ytmsearch:album:daft punk` (returns album results) * `scsearch:playlist:lofi beats` (returns SoundCloud playlists) * `scsearch:user:trap nation` (returns SoundCloud users) Playlist and channel search results are **not playable tracks**. Use the returned `info.uri` to call `/v4/loadtracks` and then play the resulting playlist tracks. ## Recommendation Prefixes Some sources can return recommended tracks when you use a `*rec` prefix. | Prefix | Source | Description | | :------- | :------- | :---------------------------------------------------------- | | `ytrec:` | YouTube | Automix based on video id or search query | | `sprec:` | Spotify | Recommendations (pass `seed_tracks=...` or just a track id) | | `dzrec:` | Deezer | Recommendations by track id, or `artist=`/`track=` query | | `tdrec:` | Tidal | Recommendations by track id or search query | | `jsrec:` | JioSaavn | Recommendations by track id or search query | | `vkrec:` | VK Music | Recommendations (requires VK auth) | Local files can be loaded by absolute path (e.g., `C:\music\track.mp3`) or with `local:` + a relative path from `sources.local.basePath`. # NodeLink Features import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { ApiCard, ApiSection, ApiSchema } from '@/components/api-ui'; import { Callout } from 'fumadocs-ui/components/callout'; # NodeLink Features Features unique to NodeLink that extend beyond standard Lavalink compatibility. ## Audio Mixer The Audio Mixer allows you to play secondary audio tracks simultaneously with the main track. This is perfect for: * **Voiceovers & TTS**: Announce song names or read chat messages without stopping the music. * **Sound Effects**: Play airhorns, applause, or memes over the active track. * **Background Music**: Keep a low-volume ambient track running while playing other media. The mixer works by intercepting the main audio stream and digitally mixing the PCM data from auxiliary sources before encoding. It features intelligent buffering to handle sources that download faster than real-time (like TTS) and automatic clamping to prevent audio distortion. The mixer exposes a full REST API for managing layers: * `POST /v4/sessions/:sessionId/players/:guildId/mix` - Add a new layer * `GET /v4/sessions/:sessionId/players/:guildId/mix` - List active layers * `PATCH /v4/sessions/:sessionId/players/:guildId/mix/:mixId` - Update layer volume * `DELETE /v4/sessions/:sessionId/players/:guildId/mix/:mixId` - Remove a layer Emitted when a layer is successfully added and begins buffering. ```json { "op": "event", "type": "MixStartedEvent", "guildId": "990369410344701964", "mixId": "835e8475f0a53815", "track": { ... }, "volume": 0.9 } ``` Emitted when a layer is removed (finished, error, or manual stop). ```json { "op": "event", "type": "MixEndedEvent", "guildId": "990369410344701964", "mixId": "835e8475f0a53815", "reason": "FINISHED" } ``` **Possible Reasons:** * `FINISHED`: Playback completed naturally. * `REMOVED`: Manually removed via API. * `ERROR`: Stream error occurred. * `MAIN_ENDED`: The main track ended, triggering auto-cleanup. ## Advanced Playback NodeLink supports high-fidelity volume fading to prevent jarring audio clips. This can be configured globally or per-request in the player update payload. The `fading` object contains settings for different scenarios: * `trackStart`: Fade in when a new track begins. * `trackEnd`: Fade out as a track reaches its end. * `trackStop`: Fade out when playback is stopped. * `seek`: Fade out before and fade in after a seek operation. * `ducking`: Lower volume temporarily (e.g., during a mix layer). Each section accepts: * `duration`: Time in milliseconds. * `curve`: Mathematical curve for the fade (`linear`, `exponential`, `logarithmic`, `s-curve`). ```json { "fading": { "enabled": true, "trackStart": { "duration": 1500, "curve": "s-curve" }, "seek": { "duration": 500, "curve": "linear" } } } ``` By providing a `nextTrack` in the player update, NodeLink will begin resolving and buffering the subsequent track while the current one is still playing. When the active track finishes, NodeLink immediately switches to the preloaded resource, resulting in a "gapless" experience. ```json { "nextTrack": { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5...", "userData": { "context": "queued" } } } ``` When enabled, NodeLink fetches community-reported segments from [SponsorBlock](https://sponsor.ajay.app) as soon as a YouTube track starts. Matching segments are automatically skipped at runtime based on the configured categories and action types. Each player maintains its own SponsorBlock state, which can be read and updated via the REST API. This feature is **YouTube-only** β€” segments are only fetched for tracks whose `sourceName` is `youtube`. ```js // config.js sponsorblock: { enabled: true, api: 'https://sponsor.ajay.app', categories: ['sponsor', 'selfpromo', 'intro', 'outro'], actionTypes: ['skip'], skipMarginMs: 150 } ``` See [SponsorBlock config](/docs/config#sponsorblock) for all available options. The SponsorBlock state for each player is managed via four endpoints: * `GET /v4/sessions/:sessionId/players/:guildId/sponsorblock` β€” Read current state * `PATCH /v4/sessions/:sessionId/players/:guildId/sponsorblock` β€” Update settings (`enabled`, `categories`, `actionTypes`, `skipMarginMs`) * `POST /v4/sessions/:sessionId/players/:guildId/sponsorblock` β€” Override the segments array with a custom list * `DELETE /v4/sessions/:sessionId/players/:guildId/sponsorblock` β€” Clear all SponsorBlock state See the [REST API docs](/docs/api/rest#sponsorblock) for full request/response details. Fired when segments have been fetched from the SponsorBlock API for the current track. ```json { "op": "event", "type": "SponsorBlockSegmentsLoadedEvent", "guildId": "987654321098765432", "segments": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start": 12.5, "end": 45.0, "category": "sponsor", "actionType": "skip", "votes": 42, "locked": false, "videoDuration": 213.0, "description": "" } ] } ``` Fired each time a segment is automatically skipped during playback. ```json { "op": "event", "type": "SponsorBlockSegmentSkippedEvent", "guildId": "987654321098765432", "segment": { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start": 12.5, "end": 45.0, "category": "sponsor", "actionType": "skip", "votes": 42, "locked": false, "videoDuration": 213.0, "description": "" } } ``` ## Enhanced Track Metadata When enabled in config.js, NodeLink makes an additional API call to YouTube when a track starts, gathering rich metadata that would normally require multiple separate API calls. The enriched data includes channel information (name, subscribers, verified status), track details (views, publish date, keywords), media quality options, external links from the channel (Spotify, Instagram), and multiple thumbnail resolutions. All data is returned in the `pluginInfo` object with `type: "holo"` during the TrackStartEvent. ```js // config.js enableHoloTracks: true ``` ```bash # Docker environment variable NODELINK_ENABLEHOLOTRACKS=true ``` Ideal for music bots displaying rich embeds with artist info, applications showing detailed video metadata without extra API calls, and clients wanting comprehensive track information automatically. Adds one extra YouTube API request per track start (100-300ms latency). Only works for YouTube/YouTube Music sources. Uses \~5-20 KB extra bandwidth per track. ```json { "op": "event", "type": "TrackStartEvent", "guildId": "...", "track": { "encoded": "QAAAjQIAJVJpY2sgQXN0bGV5...", "info": { "identifier": "dQw4w9WgXcQ", "title": "Rick Astley - Never Gonna Give You Up", "author": "Rick Astley", "length": 213000, "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "sourceName": "youtube" }, "pluginInfo": { "type": "holo", "accessibility": "Rick Astley - Never Gonna Give You Up by Rick Astley", "description": "The official video for Never Gonna Give You Up...", "keywords": ["rick astley", "never gonna give you up", "80s music"], "externalLinks": [ {"platform": "spotify", "url": "https://open.spotify.com/artist/..."}, {"platform": "instagram", "url": "https://instagram.com/rickastley"} ], "details": { "isSeekable": true, "isLive": false, "isExplicit": false, "genres": ["pop", "80s"], "publishedAt": "2009-10-25T06:57:33Z" }, "links": { "source": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "preview": "https://www.youtube.com/embed/dQw4w9WgXcQ", "artist": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw" }, "media": { "thumbnails": { "default": "https://i.ytimg.com/vi/dQw4w9WgXcQ/default.jpg", "medium": "https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg", "high": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg", "standard": "https://i.ytimg.com/vi/dQw4w9WgXcQ/sddefault.jpg", "maxres": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" }, "videoQualities": ["1080p", "720p", "480p", "360p"], "audioFormats": ["251 (opus)", "140 (m4a)"] }, "channel": { "name": "Rick Astley", "id": "UCuAXFkgsw1L7xaCfnd5JJOw", "url": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", "icon": "https://yt3.ggpht.com/...", "subscribers": 4500000, "verified": true, "description": "Official Rick Astley YouTube Channel...", "videoCount": 156 }, "metrics": { "views": 1500000000, "likes": 15000000, "badges": ["verified"] } } } } ``` ## Meaning System The Meaning System allows clients to retrieve encyclopedic information, history, and context for tracks and artists. It aggregates data from multiple providers: * **Wikipedia**: Detailed historical and biographical data. * **Letras.mus.br**: Track meanings and cultural context (primarily for Portuguese/Spanish content). This feature is perfect for bots that want to provide "Did you know?" facts about the music being played. ```bash curl -X GET "http://your-server.com/v4/meaning?encodedTrack=..." ``` ```json { "loadType": "meaning", "data": { "title": "Never Gonna Give You Up", "description": "1987 song by Rick Astley", "paragraphs": [ ""Never Gonna Give You Up" is the debut single recorded by English singer-songwriter Rick Astley...", "The song was a worldwide number-one hit, initially in the United Kingdom in 1987..." ], "url": "https://en.wikipedia.org/wiki/Never_Gonna_Give_You_Up", "provider": "wikipedia", "type": "track" } } ``` ## Eternal Box (Infinite Jukebox) Eternal Box (based on the Infinite Jukebox algorithm) analyzes a track's rhythm, beats, and acoustic segments to create a graph of similar-sounding points. During playback, the engine can "jump" from one part of the song to another seamlessly, extending the track indefinitely without sounding repetitive or broken. Use the `eternalbox:` prefix or a supported URL: * `eternalbox:SPOTIFY_ID` * `https://eternalboxmirror.xyz/jukebox_go.html?id=SPOTIFY_ID` Sent when analysis is complete, containing the full beat map. Sent every time the engine performs a seamless jump or loop. ```json { "op": "event", "type": "EternalBoxJumpEvent", "guildId": "...", "fromBeat": 124, "toBeat": 42, "jumpType": "jump" } ``` ## Lyrics & Chapters NodeLink automatically tries multiple sources in order until lyrics are found: 1. **YouTube Captions** - Synced, timestamped lyrics from captions 2. **Musixmatch** - Large database of synced lyrics 3. **LRCLib** - Open-source community synced lyrics 4. **Genius** - Community-sourced plain text lyrics The `source` field in the response indicates which provider returned the lyrics. ```bash curl -X GET "http://your-server.com/v4/loadlyrics?encodedTrack=QAAAjQIAJVJpY2sgQXN0bGV5...&lang=en" \ -H "Authorization: yourPassword" ``` ```json { "loadType": "lyrics", "data": { "synced": true, "lang": "en", "source": "youtube", "lines": [ { "text": "β™ͺ We're no strangers to love β™ͺ", "time": 18640, "duration": 3240 }, { "text": "β™ͺ You know the rules and so do I β™ͺ", "time": 22640, "duration": 4320 }, { "text": "β™ͺ A full commitment's what I'm thinking of β™ͺ", "time": 27640, "duration": 4100 } ] } } ``` ```json { "loadType": "lyrics", "data": { "synced": false, "lang": "en", "source": "genius", "lines": [ { "text": "We're no strangers to love", "time": null, "duration": null }, { "text": "You know the rules and so do I", "time": null, "duration": null }, { "text": "A full commitment's what I'm thinking of", "time": null, "duration": null } ] } } ``` ```json { "loadType": "empty", "data": {} } ``` ```bash curl -X GET "http://your-server.com/v4/loadchapters?encodedTrack=QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA" \ -H "Authorization: yourPassword" ``` ```json [ { "title": "An intro to video chapters", "startTime": 0, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 23000, "endTime": 23000 }, { "title": "How to set up video chapters", "startTime": 23000, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 44000, "endTime": 67000 }, { "title": "See video chapters in action", "startTime": 67000, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 12000, "endTime": 79000 } ] ``` ```json [] ``` ## Direct Streaming This feature must be explicitly enabled in your configuration: ```js // config.js enableTrackStreamEndpoint: true ``` ```bash curl -X GET "http://your-server.com/v4/trackstream?encodedTrack=QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA&itag=251" \ -H "Authorization: yourPassword" ``` * Radio/streaming applications that need raw audio * Bot audio recording and archiving systems * Audio preview functionality * Custom audio processing pipelines * Downloading tracks for offline playback Enable this feature carefully as it allows direct audio downloads. Consider rate limiting and authentication. ```json { "url": "https://[CENSORED].googlevideo.com/videoplayback?expire=[CENSORED]&ei=[CENSORED]&ip=[CENSORED]&id=[CENSORED]&itag=251&source=youtube&requiressl=yes...", "protocol": "http", "format": "webm/opus", "hlsUrl": null, "formats": [ { "itag": 137, "mimeType": "video/mp4; codecs=\"avc1.640028\"", "qualityLabel": "1080p", "bitrate": 705901 }, { "itag": 248, "mimeType": "video/webm; codecs=\"vp9\"", "qualityLabel": "1080p", "bitrate": 1076128 }, { "itag": 140, "mimeType": "audio/mp4; codecs=\"mp4a.40.2\"", "bitrate": 130778, "audioQuality": "AUDIO_QUALITY_MEDIUM" }, { "itag": 251, "mimeType": "audio/webm; codecs=\"opus\"", "bitrate": 146128, "audioQuality": "AUDIO_QUALITY_MEDIUM" } ] } ```

The response includes: url (Direct streaming URL that expires after some time), protocol (Streaming protocol http/https), format (Audio format being streamed), hlsUrl (HLS playlist URL if available), and formats (All available quality/format options with their itags).

Enable the endpoint in config: ```js // config.js enableLoadStreamEndpoint: true ``` ```bash curl -X GET "http://your-server.com/v4/loadstream?encodedTrack=QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA&position=30000" \ -H "Authorization: yourPassword" \ --output track.pcm ``` Returns a raw PCM stream with `Content-Type: audio/l16;rate=48000;channels=2`. ## Monitoring & Metrics ```bash curl -X GET "http://your-server.com/v4/metrics" \ -H "Authorization: yourPassword" ``` **`nodelink_players`**\ Total number of active players across all guilds **`nodelink_playing_players`**\ Players currently playing audio **`nodelink_frames_sent`**\ Total audio frames sent to Discord **`nodelink_frames_nulled`**\ Frames dropped due to processing issues **`nodelink_frames_deficit`**\ Frame timing deficit (negative = ahead, positive = behind) **`nodelink_frames_expected`**\ Expected frames based on player count **`nodelink_uptime_ms`**\ Server uptime in milliseconds **`nodelink_cpu_nodelink_load`**\ NodeLink process CPU usage (0.0 to 1.0) **`nodejs_heap_size_used_bytes`**\ V8 heap memory usage **`nodejs_external_memory_bytes`**\ C++ objects and buffers memory **`process_resident_memory_bytes`**\ Total physical RAM used (RSS) **`nodejs_eventloop_lag_*`**\ Event loop lag (mean, p50, p99) **`nodejs_gc_duration_seconds`**\ Garbage collection pause times **`process_open_fds` / `process_max_fds`**\ File descriptor usage **`nodelink_api_requests_total`**\ Total API requests by endpoint **`nodejs_active_handles`**\ Active network handles (sockets, TCP connections) **`nodejs_active_handles_total`**\ Total active handles by type See the [Prometheus page](/prometheus) for a complete Grafana dashboard with pre-configured panels for all NodeLink metrics. ```bash curl -X GET "http://your-server.com/v4/stats" \ -H "Authorization: yourPassword" ``` Beyond standard Lavalink stats, NodeLink includes `detailedStats` with granular usage metrics. ```json { "players": 5, "playingPlayers": 3, "uptime": 3600000, "memory": {...}, "cpu": {...}, "frameStats": {...}, "detailedStats": { "api": { "requests": { "/v4/stats": 45, "/v4/loadtracks": 1203, "/v4/loadlyrics": 89, "/v4/info": 12 }, "errors": { "/v4/loadtracks": 3 } }, "sources": { "youtube": 850, "spotify": 280, "soundcloud": 73 }, "playback": { "events": { "TrackStartEvent": 523, "TrackEndEvent": 520, "TrackStuckEvent": 2, "TrackExceptionEvent": 1 } } } } ``` * Identifying most used API endpoints * Tracking popular audio sources * Monitoring error rates per endpoint * Analyzing playback event distribution ## Additional Filters NodeLink includes additional audio filters beyond the standard Lavalink filters, all processed in real-time. ```json { "filters": { "echo": { "delay": 500, "feedback": 0.3, "mix": 0.5 } } } ``` ```json { "filters": { "chorus": { "rate": 1.5, "depth": 0.5, "delay": 25, "mix": 0.6, "feedback": 0.2 } } } ``` ```json { "filters": { "compressor": { "threshold": -20, "ratio": 4, "attack": 10, "release": 100, "gain": 5 } } } ``` ```json { "filters": { "highpass": { "smoothing": 20 } } } ``` ```json { "filters": { "phaser": { "stages": 6, "rate": 0.5, "depth": 0.7, "feedback": 0.5, "mix": 0.5, "minFrequency": 200, "maxFrequency": 2000 } } } ``` ```json { "filters": { "spatial": { "depth": 0.8, "rate": 0.3 } } } ``` ## Multi-Audio Track Support NodeLink supports selecting specific audio tracks for videos that contain multiple audio streams (e.g., Netflix-style dubs, multi-language YouTube videos). **1. Check for available tracks** When you load a track (especially from YouTube), the response includes an `audioTracks` array inside `pluginInfo`. This lists all available audio streams for that video. To see this information, you must load the track using `loadTracks`. ```json "pluginInfo": { "audioTracks": [ { "id": "en", "name": "English (Original)", "isDefault": true, "isAutoDubbed": false }, { "id": "pt-br", "name": "Portuguese (Brazil)", "isDefault": false, "isAutoDubbed": true } ] } ``` **2. Select a track** By default, NodeLink plays the **default/original** audio. To play a different track (e.g., Portuguese dub), pass the `audioTrackId` in the `track` object when updating the player. ```bash curl -X PATCH "http://your-server.com/v4/sessions/{sessionId}/players/{guildId}" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{ "track": { "encoded": "...", "audioTrackId": "pt-br" } }' ``` * **Multi-language Videos**: Allow users to switch between original audio and dubbed versions. * **Descriptive Audio**: Select audio tracks with descriptive commentary for accessibility. ## Event Behavior NodeLink modifies when certain events are emitted for better accuracy. Unlike Lavalink which emits TrackStartEvent immediately when you call update player, NodeLink waits until the audio stream actually starts flowing. This means you receive the event only when playback truly begins. This provides more accurate timing for tracking actual playback start times and prevents false positives when tracks fail to load after being queued. ## YouTube Source Configuration ```bash curl -X PATCH "http://your-server.com/v4/youtube/config" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "1//0gN...", "visitorData": "Cgt..." }' ``` ```json { "message": "YouTube configuration updated successfully.", "workersUpdated": 4, "fieldsUpdated": ["refreshToken", "visitorData"] } ``` ```json { "timestamp": 1733612345678, "status": 403, "error": "Forbidden", "message": "Token validation failed...", "path": "/v4/youtube/config" } ``` ```json { "refreshToken": "1//0h_Y...[hidden]", "visitorData": "Cgt...[hidden]", "isConfigured": true, "isValid": null } ``` ```json { "refreshToken": "1//0h_Y...[hidden]", "visitorData": null, "isConfigured": true, "isValid": true } ``` ```bash curl -X POST "http://your-server.com/v4/youtube/oauth" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"refreshToken": "..."}' ``` ```bash curl -X GET "http://your-server.com/v4/youtube/oauth?refreshToken=..." \ -H "Authorization: yourPassword" ``` ```json { "access_token": "ya29.a0...", "expires_in": 3599, "scope": "https://www.googleapis.com/auth/youtube", "token_type": "Bearer" } ``` # REST API import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { ApiCard, ApiSection, ApiSchema } from '@/components/api-ui'; # REST API The REST API allows you to manage sessions, players, and resolve audio tracks. All endpoints are prefixed with `/v4`. ## Common Types ```json { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "identifier": "dQw4w9WgXcQ", "isSeekable": true, "author": "Rick Astley", "length": 214000, "isStream": false, "position": 0, "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube" }, "pluginInfo": {} } ``` The `loadType` field tells you exactly what kind of result you received. You should always check this field first before attempting to parse the `data` object. Metadata about the playlist found in `data.info`. Details about why the load failed, found in `data` when type is `error`. Directly wraps a single [Track Object](#track-object). ```json { "loadType": "track", "data": { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5...", "info": { "identifier": "dQw4w9WgXcQ", "title": "Never Gonna Give You Up", "author": "Rick Astley", "length": 214000, "isSeekable": true, "isStream": false, "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "sourceName": "youtube" }, "pluginInfo": {}, "userData": {} } } ``` Contains playlist metadata and a list of tracks. ```json { "loadType": "playlist", "data": { "info": { "name": "My Favorite Songs", "selectedTrack": -1 }, "pluginInfo": {}, "tracks": [ { "encoded": "...", "info": { ... } }, { "encoded": "...", "info": { ... } } ] } } ``` Contains a list of tracks found by the query. ```json { "loadType": "search", "data": [ { "encoded": "...", "info": { "title": "Result 1", ... } }, { "encoded": "...", "info": { "title": "Result 2", ... } } ] } ``` Indicates no matches were found for the identifier. ```json { "loadType": "empty", "data": {} } ``` Indicates a failure during loading (e.g. Geo-blocked, Private). ```json { "loadType": "error", "data": { "message": "Video unavailable", "severity": "common", "cause": "The uploader has not made this video available in your country" } } ``` ## Tracks & Items ```bash curl -X GET "http://your-server.com/v4/loadtracks?identifier=ytsearch:never%20gonna%20give%20you%20up" \ -H "Authorization: yourPassword" ``` Use `search:query` to search across `unifiedSearchSources`. Use `::` to filter the result kind (where supported). * Types: `track` (default), `playlist`, `album`, `artist`, `channel` * Example: `ytsearch:playlist:billie eilish` Some searches (e.g., `ytsearch:playlist:...`) return **playlist/channel entries**, not playable tracks. These items include `pluginInfo.type` like `playlist_result` or `channel_result`.\ To play them, call `/v4/loadtracks` again using the returned `info.uri` (for playlists) and queue the resulting tracks. Use `*rec` prefixes to fetch recommendations (e.g., `ytrec:videoId`, `sprec:seed_tracks=...`). You can pass an absolute path (e.g., `C:\music\track.mp3`) or `local:relative/path.mp3` (resolved against `sources.local.basePath`). `frameStats` includes `expected` (total expected frames) and `deficit` when players are active. ```json { "loadType": "track", "data": { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "identifier": "dQw4w9WgXcQ", "isSeekable": true, "author": "Rick Astley", "length": 214000, "isStream": false, "position": 0, "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube" }, "pluginInfo": {} } } ``` ```json { "loadType": "search", "data": [ { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "identifier": "dQw4w9WgXcQ", "isSeekable": true, "author": "Rick Astley", "length": 214000, "isStream": false, "position": 0, "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube" }, "pluginInfo": {} } ] } ``` ```json { "loadType": "playlist", "data": { "info": { "name": "My Playlist", "selectedTrack": 0 }, "pluginInfo": {}, "tracks": [ { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "identifier": "dQw4w9WgXcQ", "isSeekable": true, "author": "Rick Astley", "length": 214000, "isStream": false, "position": 0, "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube" }, "pluginInfo": {} } ] } } ``` ```json { "loadType": "empty", "data": {} } ``` ```json { "loadType": "error", "data": { "message": "Failed to load track", "severity": "common", "cause": "Video unavailable" } } ``` ```bash curl -X GET "http://your-server.com/v4/decodetrack?encodedTrack=QAAAjQIAJVJpY2sgQXN0bGV5..." \ -H "Authorization: yourPassword" ``` ```json { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "author": "Rick Astley", "length": 214000, "identifier": "dQw4w9WgXcQ", "isSeekable": true, "isStream": false, "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube", "position": 0 }, "pluginInfo": {}, "userData": {} } ``` ```bash curl -X POST "http://your-server.com/v4/decodetracks" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '["QAAAjQIAJVJpY2sgQXN0bGV5...","QAAAkAIAJU90aGVyIFRyYWNr..."]' ``` ```json [ { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "author": "Rick Astley", "length": 214000, "identifier": "dQw4w9WgXcQ", "isSeekable": true, "isStream": false, "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube", "position": 0 }, "pluginInfo": {}, "userData": {} } ] ``` ```json "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA" ``` ```bash curl -X GET "http://your-server.com/v4/encodetrack?track=%7B%22identifier%22%3A%22dQw4w9WgXcQ%22%2C%22title%22%3A%22Rick%20Astley%20-%20Never%20Gonna%20Give%20You%20Up%22%2C%22author%22%3A%22Rick%20Astley%22%2C%22length%22%3A214000%2C%22uri%22%3A%22https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ%22%2C%22sourceName%22%3A%22youtube%22%7D" \ -H "Authorization: yourPassword" ``` Array of track objects to encode, where each track has: ```bash curl -X POST "http://your-server.com/v4/encodedtracks" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '[ { "info": { "identifier": "dQw4w9WgXcQ", "title": "Rick Astley - Never Gonna Give You Up", "author": "Rick Astley", "length": 214000, "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "sourceName": "youtube" } }, { "info": { "identifier": "invalid_track", "title": "Invalid Track", "author": "Unknown", "length": 0, "uri": "https://example.com/invalid", "sourceName": "http" } } ]' ``` ```json [ "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "QAAAAAIADUludmFsaWQgVHJhY2sAB1Vua25vd24AAAAAAAAAAAANaW52YWxpZF90cmFjawABAB1odHRwczovL2V4YW1wbGUuY29tL2ludmFsaWQAAARodHRwAAAAAAAAAAA=" ] ``` ## Players & Sessions ```bash curl -X GET "http://your-server.com/v4/sessions/yourSessionId/players" \ -H "Authorization: yourPassword" ``` ```json [ { "guildId": "990369410344701964", "track": { "encoded": "QAAAAAMAC2dvbGRlbiBob3VyAARKVktFAAAAAAADNFAAC3VWXzVlRXZhbW9RAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9dVZfNWVFdmFtb1EBADZodHRwczovL2kueXRpbWcuY29tL3ZpX3dlYnAvdVZfNWVFdmFtb1Evc2RkZWZhdWx0LndlYnAAAAd5b3V0dWJlAAAAAAAAAAA=", "info": { "title": "golden hour", "author": "JVKE", "length": 210000, "identifier": "uV_5eEvamoQ", "isSeekable": true, "isStream": false, "uri": "https://www.youtube.com/watch?v=uV_5eEvamoQ", "artworkUrl": "https://i.ytimg.com/vi_webp/uV_5eEvamoQ/sddefault.webp", "isrc": null, "sourceName": "youtube", "position": 0 }, "endTime": 0, "userData": { "requester": "978981769661513758" } }, "volume": 100, "paused": false, "filters": {}, "state": { "time": 1764813558539, "position": 5100, "connected": true, "ping": 184 }, "voice": { "sessionId": "5e3473e2000512df9fd4b25e5e614690", "token": "518000ca380f3f08", "endpoint": "c-gru17-9aec959a.discord.media:2083" } } ] ``` ```bash curl -X GET "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId" \ -H "Authorization: yourPassword" ``` ```json { "guildId": "990369410344701964", "track": { "encoded": "QAAAAAMAJmhlciAoZmVhdC4gQW5uaWthIFdlbGxzICYgS2FkZW4gSGF3a2UpAARKVktFAAAAAAADDUAAC0RObVRZVXgwd3FZAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9RE5tVFlVeDB3cVkBADZodHRwczovL2kueXRpbWcuY29tL3ZpX3dlYnAvRE5tVFlVeDB3cVkvc2RkZWZhdWx0LndlYnAAAAd5b3V0dWJlAAAAAAAAAAA=", "info": { "title": "her (feat. Annika Wells & Kaden Hawke)", "author": "JVKE", "length": 200000, "identifier": "DNmTYUx0wqY", "isSeekable": true, "isStream": false, "uri": "https://www.youtube.com/watch?v=DNmTYUx0wqY", "artworkUrl": "https://i.ytimg.com/vi_webp/DNmTYUx0wqY/sddefault.webp", "isrc": null, "sourceName": "youtube", "position": 0 }, "endTime": 0, "userData": { "requester": "978981769661513758" } }, "volume": 100, "paused": false, "filters": { "filters": {} }, "state": { "time": 1764813548928, "position": 196740, "connected": true, "ping": 184 }, "voice": { "sessionId": "5e3473e2000512df9fd4b25e5e614690", "token": "518000ca380f3f08", "endpoint": "c-gru17-9aec959a.discord.media:2083" } } ``` ```bash curl -X PATCH "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId?noReplace=false" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{ "track": { "encoded": "QAAAjQIAJVJpY2sgQXN0bGV5..." }, "nextTrack": { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5..." }, "fading": { "enabled": true, "trackStart": { "duration": 1000, "curve": "linear" } }, "volume": 100 }' ``` ```json { "guildId": "990369410344701964", "track": { "encoded": "QAAAAAMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA", "info": { "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)", "author": "Rick Astley", "length": 214000, "identifier": "dQw4w9WgXcQ", "isSeekable": true, "isStream": false, "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi_webp/dQw4w9WgXcQ/sddefault.webp", "isrc": null, "sourceName": "youtube", "position": 0 }, "endTime": 0, "userData": {} }, "volume": 100, "paused": false, "filters": { "equalizer": [ {"band": 0, "gain": 0.2} ] }, "fading": { "enabled": true, "trackStart": { "duration": 1000, "curve": "linear" }, "trackEnd": { "duration": 0, "curve": "linear" }, "trackStop": { "duration": 0, "curve": "linear" }, "seek": { "duration": 0, "curve": "linear" }, "ducking": { "enabled": false, "duration": 0, "targetVolume": 0.3, "curve": "linear" } }, "state": { "time": 1764813548928, "position": 0, "connected": true, "ping": 184 }, "voice": { "sessionId": "5e3473e2000512df9fd4b25e5e614690", "token": "518000ca380f3f08", "endpoint": "c-gru17-9aec959a.discord.media:2083" } } ``` ```bash curl -X DELETE "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId" \ -H "Authorization: yourPassword" ``` ``` 204 No Content ``` ## SponsorBlock ```bash curl -X GET "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/sponsorblock" \ -H "Authorization: yourPassword" ``` ```json { "enabled": true, "categories": ["sponsor", "selfpromo", "intro", "outro"], "actionTypes": ["skip"], "segments": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start": 12.5, "end": 45.0, "category": "sponsor", "actionType": "skip", "votes": 42, "locked": false, "videoDuration": 213.0, "description": "" } ], "lastSkippedUuid": null, "skipMarginMs": 150 } ``` ```bash curl -X PATCH "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/sponsorblock" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"enabled": true, "categories": ["sponsor", "intro"]}' ``` Returns the updated [PlayerSponsorBlockState](#playersponsorblockstate). ```bash curl -X POST "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/sponsorblock" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{ "segments": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start": 12.5, "end": 45.0, "category": "sponsor", "actionType": "skip", "votes": 42, "locked": false, "videoDuration": 213.0, "description": "" } ] }' ``` Returns the updated [PlayerSponsorBlockState](#playersponsorblockstate). ```bash curl -X DELETE "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/sponsorblock" \ -H "Authorization: yourPassword" ``` ``` 204 No Content ``` Enables sending lyrics events via the session WebSocket when the current track has available lyrics. Disables lyrics events for this player. ```bash curl -X POST "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/lyrics/subscribe?skipTrackSource=true" \ -H "Authorization: yourPassword" ``` `204 No Content` ```bash curl -X PATCH "http://your-server.com/v4/sessions/yourSessionId" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"resuming": true, "timeout": 60}' ``` ```json { "resuming": true, "timeout": 60 } ``` ## Node Information ```bash curl -X GET "http://your-server.com/v4/connection" \ -H "Authorization: yourPassword" ``` ```json { "status": "good", "metrics": { "speed": { "bps": 23669887.133182842, "kbps": 184920.99322799096, "mbps": 180.59 }, "downloadedBytes": 10485760, "durationSeconds": 0.44, "timestamp": 1764813556344 } } ``` ```bash curl -X GET "http://your-server.com/v4/info" \ -H "Authorization: yourPassword" ``` ```json { "version": { "semver": "3.3.0", "major": 3, "minor": 3, "patch": 0, "prerelease": [], "build": [] }, "buildTime": 1764691522000, "git": { "branch": "v3", "commit": "b2814b1", "commitTime": 1764691522000 }, "node": "v22.12.0", "voice": { "name": "@performanc/voice", "version": "github:PerformanC/voice" }, "isNodelink": true, "sourceManagers": [ "youtube", "spotify", "soundcloud", "applemusic", "deezer", "bandcamp", "twitch", "googledrive", "http", "local" ], "filters": [ "equalizer", "karaoke", "timescale", "tremolo", "vibrato", "rotation", "distortion", "channelMix", "lowpass", "highpass" ], "plugins": [] } ``` ```bash curl -X GET "http://your-server.com/v4/stats" \ -H "Authorization: yourPassword" ``` ```json { "players": 0, "playingPlayers": 0, "uptime": 17930, "memory": { "free": 904605696, "used": 39914392, "allocated": 51482624, "reservable": 8011513856 }, "cpu": { "cores": 12, "systemLoad": 0.93, "nodelinkLoad": 0 }, "frameStats": null, "eventLoopLagP50": 0.12, "eventLoopLagP95": 0.48, "eventLoopLagP99": 1.02, "stuckRecoveries": 0, "detailedStats": { "api": { "requests": { "/v4/stats": 1, "/v4/info": 5, "/v4/loadtracks": 23 }, "errors": {} }, "sources": { "youtube": 15, "spotify": 8 }, "playback": { "events": { "TrackStartEvent": 10, "TrackEndEvent": 9 } } } } ``` ```bash curl -X GET "http://your-server.com/v4/metrics" \ -H "Authorization: Bearer yourPassword" ``` This endpoint is protected by `metrics.authorization.type` and `metrics.authorization.password`.\ If `metrics.authorization.password` is empty, the server uses `server.password`. ```bash curl -X GET "http://your-server.com/v4/workers" \ -H "Authorization: yourPassword" ``` ```json [ { "id": 1, "clusterId": 1, "pid": 24560, "stats": { "players": 0, "playingPlayers": 0 }, "health": true, "uptime": 120 } ] ``` ```bash curl -X PATCH "http://your-server.com/v4/workers" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"code": "CAPYBARA", "id": 1}' ``` ```json { "killed": true, "id": 1, "clusterId": 1, "pid": 24560 } ``` ```bash curl -X GET "http://your-server.com/version" ``` ```text 3.5.0 ``` ## Cluster & Management ```bash curl -X GET "http://your-server.com/v4/workers" \ -H "Authorization: yourPassword" ``` ```json [ { "id": 1, "clusterId": 1, "pid": 24560, "stats": { "players": 12, "playingPlayers": 8 }, "health": true, "uptime": 1200 } ] ``` ```bash curl -X PATCH "http://your-server.com/v4/workers" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"code": "ADMIN_PATCH_CODE", "pid": 24560}' ``` ## YouTube Source Configuration ```json { "message": "YouTube configuration updated successfully.", "workersUpdated": 4, "fieldsUpdated": ["refreshToken", "visitorData"] } ``` ```bash curl -X GET "http://your-server.com/v4/loadlyrics?encodedTrack=QAAAjQIAJVJpY2sgQXN0bGV5..." \ -H "Authorization: yourPassword" ``` ```json { "loadType": "lyrics", "data": { "synced": true, "lang": "en", "lines": [ { "text": "β™ͺ We're no strangers to love β™ͺ", "time": 18640, "duration": 3240 }, { "text": "β™ͺ You know the rules and so do I β™ͺ", "time": 22640, "duration": 4320 } ] } } ``` ```json { "loadType": "lyrics", "data": { "synced": false, "lang": "en", "source": "genius", "lines": [ { "text": "We're no strangers to love", "time": null, "duration": null }, { "text": "You know the rules and so do I", "time": null, "duration": null }, { "text": "A full commitment's what I'm thinking of", "time": null, "duration": null } ] } } ``` ```json { "loadType": "empty", "data": {} } ``` ```bash curl -X GET "http://your-server.com/v4/loadchapters?encodedTrack=QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA" \ -H "Authorization: yourPassword" ``` ```json [ { "title": "An intro to video chapters", "startTime": 0, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 23000, "endTime": 23000 }, { "title": "How to set up video chapters", "startTime": 23000, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 44000, "endTime": 67000 }, { "title": "See video chapters in action", "startTime": 67000, "thumbnails": [ { "url": "https://i.ytimg.com/vi_webp/b1Fo_M_tj6w/hqdefault.webp", "width": 336, "height": 188 } ], "duration": 12000, "endTime": 79000 } ] ``` ```json [] ``` ```bash curl -X GET "http://your-server.com/v4/meaning?encodedTrack=QAAAAAMARFJpY2sgQXN0bGV5..." \ -H "Authorization: yourPassword" ``` ```json { "loadType": "meaning", "data": { "title": "Never Gonna Give You Up", "description": "1987 song by Rick Astley", "paragraphs": [ "\"Never Gonna Give You Up\" is the debut single recorded by English singer-songwriter Rick Astley...", "The song was a worldwide number-one hit, initially in the United Kingdom in 1987..." ], "url": "https://en.wikipedia.org/wiki/Never_Gonna_Give_You_Up", "provider": "wikipedia", "type": "track" } } ``` ```json { "loadType": "empty", "data": {} } ``` ```bash curl -X GET "http://your-server.com/v4/trackstream?encodedTrack=QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA&itag=251" \ -H "Authorization: yourPassword" ``` ```json { "url": "https://[CENSORED].googlevideo.com/videoplayback?expire=[CENSORED]&ei=[CENSORED]&ip=[CENSORED]&id=[CENSORED]&itag=251&source=youtube&requiressl=yes...", "protocol": "http", "format": "webm/opus", "hlsUrl": null, "formats": [ { "itag": 137, "mimeType": "video/mp4; codecs=\"avc1.640028\"", "qualityLabel": "1080p", "bitrate": 705901 }, { "itag": 248, "mimeType": "video/webm; codecs=\"vp9\"", "qualityLabel": "1080p", "bitrate": 1076128 }, { "itag": 140, "mimeType": "audio/mp4; codecs=\"mp4a.40.2\"", "bitrate": 130778, "audioQuality": "AUDIO_QUALITY_MEDIUM" }, { "itag": 251, "mimeType": "audio/webm; codecs=\"opus\"", "bitrate": 146128, "audioQuality": "AUDIO_QUALITY_MEDIUM" } ] } ``` Requires `enableLoadStreamEndpoint: true` in `config.js`. ```bash curl -X GET "http://your-server.com/v4/loadstream?encodedTrack=QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA&position=30000" \ -H "Authorization: yourPassword" \ --output track.pcm ``` ```bash curl -X POST "http://your-server.com/v4/loadstream" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"encodedTrack":"QAAAAAMAM0hvdyB0byBBZGQgQ2hhcHRlcnMgdG8gWW91ciBWaWRlb3MgVXNpbmcgVGltZXN0YW1wcwAQWW91VHViZSBDcmVhdG9ycwAAAAAAATSYAAtiMUZvX01fdGo2dwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWIxRm9fTV90ajZ3AQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2IxRm9fTV90ajZ3L3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA","volume":120,"filters":{"equalizer":[{"band":0,"gain":0.2}]}}' \ --output track.pcm ``` Returns a raw PCM stream with `Content-Type: audio/l16;rate=48000;channels=2`. ## Route Planner ```bash curl -X GET "http://your-server.com/v4/routeplanner/status" \ -H "Authorization: yourPassword" ``` ```bash curl -X POST "http://your-server.com/v4/routeplanner/free/address" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"address": "192.168.1.1"}' ``` ```bash curl -X POST "http://your-server.com/v4/routeplanner/free/all" \ -H "Authorization: yourPassword" ``` ## Audio Mixer The Audio Mixer allows overlaying auxiliary audio tracks (like TTS, sound effects, or background music) on top of the main active track. ```bash curl -X POST "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/mix" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{ "track": { "encoded": "QAAAAAIAEFRUUzogaGVsbG8gd29ybGQ..." }, "volume": 0.9 }' ``` ```json { "id": "835e8475f0a53815", "track": { "encoded": "...", "info": { ... }, "userData": {} }, "volume": 0.9 } ``` ```bash curl -X GET "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/mix" \ -H "Authorization: yourPassword" ``` ```json { "mixes": [ { "id": "835e8475f0a53815", "track": { ... }, "volume": 0.9, "position": 1280, "startTime": 1700000000000 } ] } ``` ```bash curl -X PATCH "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/mix/835e8475f0a53815" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"volume": 0.5}' ``` `204 No Content` ```bash curl -X DELETE "http://your-server.com/v4/sessions/yourSessionId/players/yourGuildId/mix/835e8475f0a53815" \ -H "Authorization: yourPassword" ``` `204 No Content` ## Audio Filters NodeLink supports a variety of audio filters that can be applied in real-time to modify playback. Filters are applied through the `filters` object in the Update Player request. ### How to Apply Filters Send filters in the Update Player request: ```bash curl -X PATCH "http://your-server.com/v4/sessions/{sessionId}/players/{guildId}" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{ "filters": { "equalizer": [ {"band": 0, "gain": 0.2} ], "echo": { "delay": 500, "feedback": 0.3, "mix": 0.5 } } }' ``` You can combine multiple filters simultaneously. NodeLink processes them efficiently in the audio pipeline. ### Standard Filters | Band | Frequency | Description | | ---- | --------- | ----------- | | 0 | 25 Hz | Sub-bass | | 1 | 40 Hz | Bass | | 2 | 63 Hz | Bass | | 3 | 100 Hz | Bass | | 4 | 160 Hz | Low-mid | | 5 | 250 Hz | Low-mid | | 6 | 400 Hz | Mid | | 7 | 630 Hz | Mid | | 8 | 1 kHz | Mid | | 9 | 1.6 kHz | Upper-mid | | 10 | 2.5 kHz | Upper-mid | | 11 | 4 kHz | Presence | | 12 | 6.3 kHz | Presence | | 13 | 10 kHz | Brilliance | | 14 | 16 kHz | Air | ```json { "filters": { "equalizer": [ {"band": 0, "gain": 0.2}, {"band": 1, "gain": 0.15}, {"band": 2, "gain": 0.1} ] } } ``` ### NodeLink Exclusive Filters These filters are unique to NodeLink, built from scratch with custom DSP implementations. ```json { "filters": { "echo": { "delay": 500, "feedback": 0.3, "mix": 0.5 } } } ``` ## YouTube Source Configuration ```bash curl -X PATCH "http://your-server.com/v4/youtube/config" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"refreshToken":"1//0gN...","visitorData":"Cgt..."}' ``` ```json { "message": "YouTube configuration updated successfully.", "workersUpdated": 4, "fieldsUpdated": ["refreshToken", "visitorData"] } ``` ```json { "timestamp": 1733612345678, "status": 403, "error": "Forbidden", "message": "Token validation failed...", "path": "/v4/youtube/config" } ``` ```json { "refreshToken": "1//0h_Y...[hidden]", "visitorData": "Cgt...[hidden]", "isConfigured": true, "isValid": null } ``` ```json { "refreshToken": "1//0h_Y...[hidden]", "visitorData": null, "isConfigured": true, "isValid": true } ``` ```bash curl -X POST "http://your-server.com/v4/youtube/oauth" \ -H "Authorization: yourPassword" \ -H "Content-Type: application/json" \ -d '{"refreshToken": "..."}' ``` ```bash curl -X GET "http://your-server.com/v4/youtube/oauth?refreshToken=..." \ -H "Authorization: yourPassword" ``` ```json { "access_token": "ya29.a0...", "expires_in": 3599, "scope": "https://www.googleapis.com/auth/youtube", "token_type": "Bearer" } ``` # WebSocket API import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { ApiCard, ApiSection, ApiSchema } from '@/components/api-ui'; # WebSocket API NodeLink uses WebSocket connections to provide real-time communication between the server and clients. This connection is essential for receiving player events, track updates, and server statistics. ## Connection ```json { "op": "ready", "resumed": false, "sessionId": "a1b2c3d4e5f6g7h8" } ``` ```json { "op": "ready", "resumed": true, "sessionId": "a1b2c3d4e5f6g7h8" } ``` When successfully resumed using the `Session-Id` header, the server sends a ready event with `resumed: true`. All queued events from the disconnection period are immediately sent, followed by current state updates from all active players. ## Incoming Events (Server β†’ Client) NodeLink sends various events to keep clients informed about playback status, player state, and server statistics. All events are sent as JSON messages. ### Ready Event ```json { "op": "ready", "resumed": false, "sessionId": "xY7kP2mN8vQ4rL1s" } ``` ### Player Update ```json { "op": "playerUpdate", "guildId": "987654321098765432", "state": { "time": 1701350400000, "position": 45230, "connected": true, "ping": 42 } } ``` ### Stats Event ```json { "op": "stats", "players": 5, "playingPlayers": 3, "uptime": 3600000, "memory": { "free": 4294967296, "used": 536870912, "allocated": 1073741824, "reservable": 8589934592 }, "cpu": { "cores": 8, "systemLoad": 0.25, "processLoad": 0.08 }, "frameStats": { "sent": 15000, "nulled": 0, "deficit": 0, "expected": 15000 } } ``` ### Track Start Event ```json { "op": "event", "type": "TrackStartEvent", "guildId": "987654321098765432", "track": { "encoded": "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAACyPgAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==", "info": { "identifier": "dQw4w9WgXcQ", "isSeekable": true, "author": "Rick Astley", "length": 212000, "isStream": false, "position": 0, "title": "Never Gonna Give You Up", "uri": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "artworkUrl": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg", "isrc": null, "sourceName": "youtube" } }, "playingQuality": 251 } ``` ### Track End Event ```json { "op": "event", "type": "TrackEndEvent", "guildId": "987654321098765432", "track": { "encoded": "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAACyPgAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==", "info": { "identifier": "dQw4w9WgXcQ", "title": "Never Gonna Give You Up", "author": "Rick Astley", "length": 212000, "sourceName": "youtube" } }, "reason": "finished" } ``` ### Track Exception Event ```json { "op": "event", "type": "TrackExceptionEvent", "guildId": "987654321098765432", "track": { "encoded": "...", "info": { "identifier": "invalid123", "title": "Invalid Track" } }, "exception": { "message": "Failed to load track", "severity": "fault", "cause": "Video unavailable" } } ``` ### Track Stuck Event ```json { "op": "event", "type": "TrackStuckEvent", "guildId": "987654321098765432", "track": { "encoded": "...", "info": { "identifier": "dQw4w9WgXcQ", "title": "Never Gonna Give You Up" } }, "thresholdMs": 10000, "reason": "Recovery attempt failed" } ``` ```json { "op": "event", "type": "SponsorBlockSegmentsLoadedEvent", "guildId": "987654321098765432", "segments": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start": 12.5, "end": 45.0, "category": "sponsor", "actionType": "skip", "votes": 42, "locked": false, "videoDuration": 213.0, "description": "" } ] } ``` ```json { "op": "event", "type": "SponsorBlockSegmentSkippedEvent", "guildId": "987654321098765432", "segment": { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "start": 12.5, "end": 45.0, "category": "sponsor", "actionType": "skip", "votes": 42, "locked": false, "videoDuration": 213.0, "description": "" } } ``` ```json { "op": "event", "type": "StreamMetadataEvent", "guildId": "...", "stream": { "metadata": { "streamTitle": "Artist - Song Name", "streamUrl": "http://radio.example.com" } } } ``` ### WebSocket Closed Event ```json { "op": "event", "type": "WebSocketClosedEvent", "guildId": "987654321098765432", "code": 4014, "reason": "Disconnected", "byRemote": true } ``` When a worker fails, all players assigned to that worker are destroyed. NodeLink will automatically emit a `WebSocketClosedEvent` for each affected guild with code `5001` and reason `worker_failed`. ```json { "op": "event", "type": "WorkerFailedEvent", "affectedGuilds": ["987654321098765432"], "message": "Players for guilds 987654321098765432 lost due to worker failure." } ``` ### Player Lifecycle Events ```json { "op": "event", "type": "PlayerCreatedEvent", "guildId": "987654321098765432", "player": { "guildId": "987654321098765432", "track": null, "paused": false, "volume": 100 } } ``` ```json { "op": "event", "type": "PlayerDestroyedEvent", "guildId": "987654321098765432" } ``` ```json { "op": "event", "type": "PlayerConnectedEvent", "guildId": "987654321098765432", "voice": { "sessionId": "abc", "token": "token", "endpoint": "us-central123.discord.media", "channelId": "123456789012345678" } } ``` ```json { "op": "event", "type": "PlayerReconnectingEvent", "guildId": "987654321098765432", "voice": { "sessionId": "abc", "token": "token", "endpoint": "us-central123.discord.media", "channelId": "123456789012345678" } } ``` ### Player State Events ```json { "op": "event", "type": "VolumeChangedEvent", "guildId": "987654321098765432", "volume": 80 } ``` ```json { "op": "event", "type": "FiltersChangedEvent", "guildId": "987654321098765432", "filters": { "volume": 1.0, "equalizer": [ { "band": 0, "gain": 0.2 } ] } } ``` ```json { "op": "event", "type": "SeekEvent", "guildId": "987654321098765432", "position": 60000 } ``` ```json { "op": "event", "type": "PauseEvent", "guildId": "987654321098765432", "paused": true } ``` ```json { "op": "event", "type": "ConnectionStatusEvent", "status": "connected", "metrics": { "speed": { "mbps": 180.59 }, "timestamp": 1764813556344 } } ``` ### Lyrics Events ### Audio Mixer Events ```json { "op": "event", "type": "MixStartedEvent", "guildId": "990369410344701964", "mixId": "835e8475f0a53815", "track": { "encoded": "QAAAAAIAEFRUUzogaGVsbG8gd29ybGQACkdvb2dsZSBUVFP//////////wAQZ3R0czpoZWxsbyB3b3JsZAEBAHBodHRwczovL3RyYW5zbGF0ZS5nb29nbGUuY29tL3RyYW5zbGF0ZV90dHM/aWU9VVRGLTgmcT1oZWxsbyUyMHdvcmxkJnRsPWVuLVVTJnRvdGFsPTEmaWR4PTAmdGV4dGxlbj0xMSZjbGllbnQ9Z3R4AApnb29nbGUtdHRzAAAAAAAAAAA=", "info": { ... }, "userData": {} }, "volume": 0.9 } ``` ```json { "op": "event", "type": "MixEndedEvent", "guildId": "990369410344701964", "mixId": "835e8475f0a53815", "reason": "FINISHED" } ``` # Docker import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Steps, Step } from 'fumadocs-ui/components/steps'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; # Docker: Because Production Should Just Work Look, we've all been there. You spend hours getting NodeLink running perfectly on your laptop. Everything works. Audio is crisp. No crashes. Life is good. Then you deploy to production and... it doesn't work. Wrong Node version. Missing dependencies. Something about native modules. Your weekend is gone. **Docker fixes this.** It's not magic. It's just consistency. Same environment, same dependencies, same behaviorβ€”whether you're running on your gaming PC, a $5 VPS, or an enterprise Kubernetes cluster. Docker wraps your application with everything it needs (Node.js, system libraries, native modules) into an image. That image runs the same way everywhere. No surprises. No "works on my machine" excuses. *** ## Why Bother With Containers? Because you have better things to do than fight with package managers at 3 AM trying to get `sodium-native` to compile. Docker gives you instant setup where you don't need to manually install Node.js v22+. No hunting down build tools for native modules. No dependency resolution nightmares. Just pull the image and run. Your NodeLink instance lives in perfect isolation, in its own world where it can't interfere with your system and nothing can interfere with it. Clean slate every time you start it. Updates become trivial. New version? Just `docker compose pull && docker compose up -d` and you're done. Need to rollback? Same commands, different tag. The portability is real tooβ€”it runs on Linux, Windows, macOS, ARM, x86, cloud, bare metal. If Docker exists there, NodeLink runs there. Native modules like `sodium-native` for encryption are already compiled in the image. No gcc, no python, no build-essential needed. The image is optimized for NodeLink's worker-based architecture. Cluster mode works out of the box without manual configuration. Health checks, restart policies, and security best practices are configured. Not something you cobbled together from a tutorial. The same image that runs on your laptop runs in production. No environment-specific bugs. No surprise differences. *** ## The Setup Is Already Done The NodeLink repository includes a production-ready Docker setup. No assembly required. We already built a multi-stage `Dockerfile` that compiles native modules in a build stage and copies only what's needed to the runtime stage. The result is a lean image optimized for size and security. The `docker-compose.yml` includes every single configuration option NodeLink supports, all documented inline with examples. Health checks verify NodeLink is actually responding, not just that the container is running. Restart policies ensure it comes back up automatically if it crashes. Volume mounts are configured for local music files and log persistence. You don't need to piece this together yourself. You don't need to understand Docker internals. Just clone the repo, adjust the handful of settings that matter to you, and launch. *** ## Getting Started ### Install Docker You need Docker and Docker Compose. If you don't have them yet, get them from the official sources. For Ubuntu/Debian, use the official Docker repository for the latest version: ```bash # Add Docker's official GPG key curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg # Add Docker repository echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine sudo apt update sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin ``` For other distributions, check the [official installation guide](https://docs.docker.com/engine/install/). Download and install **Docker Desktop for Windows** from [docker.com](https://www.docker.com/products/docker-desktop/). Docker Desktop includes both Docker Engine and Docker Compose. After installation, you'll need to restart your computer. Make sure WSL2 is enabled if you're on Windows 10/11. Docker Desktop will prompt you to install it if needed. Download and install **Docker Desktop for Mac** from [docker.com](https://www.docker.com/products/docker-desktop/). Docker Desktop includes both Docker Engine and Docker Compose. Works on both Intel and Apple Silicon Macs. Verify everything is working: ```bash docker --version docker compose version ``` If both commands output version numbers, you're ready. ### Clone NodeLink Get the repository to your machine: ```bash git clone https://github.com/PerformanC/NodeLink.git cd NodeLink ``` You now have everything. The Dockerfile, the docker-compose.yml, all of it. ### Configure Your Instance Open `docker-compose.yml` in your editor. You'll see a massive file with every configuration option documented. Most of it you can leave as-is. Here's what you **must** change: ```yaml environment: NODELINK_SERVER_PASSWORD: "youshallnotpass" # ⚠️ CHANGE THIS NOW ``` Using the default password in production is like leaving your front door open with a sign that says "free stuff inside." Change it. Use something long and random. **Optional but recommended** configurations depend on what you need: If you want **Spotify support**, you'll need credentials from the Spotify Developer Dashboard: ```yaml NODELINK_SOURCES_SPOTIFY_ENABLED: "true" NODELINK_SOURCES_SPOTIFY_CLIENTID: "your_actual_spotify_client_id" NODELINK_SOURCES_SPOTIFY_CLIENTSECRET: "your_actual_spotify_client_secret" ``` If you want better **YouTube support** (help with age-restricted content and some geographically restricted content), the default remote server is `cipher.kikkia.dev`: ```yaml NODELINK_SOURCES_YOUTUBE_CIPHER_TOKEN: "your_cipher_token" #If you are using an internal or different server, you must provide the corresponding authorization for it. ``` If you want to **use all your CPU cores** for better performance under load, enable cluster mode: ```yaml NODELINK_CLUSTER_ENABLED: "true" NODELINK_CLUSTER_WORKERS: "0" # 0 means use all available CPU cores ``` Everything else has sensible defaults. You can explore the 200+ other options in the file when you need them. ### Launch It One command: ```bash docker compose up -d ``` Docker will build the NodeLink image from the Dockerfile, compile native modules, install dependencies, and start the server. The `-d` flag runs it in detached mode (background). This takes a few minutes the first time while it builds the image. Subsequent starts are instant. ### Verify It's Alive Check if NodeLink is responding: ```bash curl http://localhost:2333/version ``` You should see version information. If you do, NodeLink is running and ready to connect to your Discord bot. If something went wrong, check the logs: ```bash docker compose logs -f nodelink ``` The `-f` flag follows the logs in real-time. Press Ctrl+C to exit. *** ## Understanding the Configuration The `docker-compose.yml` file is massive because it exposes every single configuration option NodeLink supports as environment variables. Most of them you'll never touch. Here's what's actually in there: These control where NodeLink listens and how clients authenticate: ```yaml NODELINK_SERVER_HOST: "0.0.0.0" # Listen on all interfaces NODELINK_SERVER_PORT: "2333" # Default Lavalink port NODELINK_SERVER_PASSWORD: "youshallnotpass" # Change this ``` The host `0.0.0.0` means NodeLink accepts connections from anywhere. If you only want localhost access, use `127.0.0.1`. The port `2333` is the standard Lavalink port. Most clients expect this, but you can change it if needed. NodeLink uses a worker-based architecture where heavy tasks like playlist parsing happen in separate processes. This keeps your main audio loop smooth. Cluster mode takes this further by running multiple worker processes: ```yaml NODELINK_CLUSTER_ENABLED: "true" NODELINK_CLUSTER_WORKERS: "0" # 0 = use all CPU cores NODELINK_CLUSTER_MINWORKERS: "1" NODELINK_CLUSTER_MAXPLAYERSPERWORKER: "20" NODELINK_CLUSTER_SCALING_TARGETUTILIZATION: "0.7" NODELINK_CLUSTER_SCALING_SCALEUPTHRESHOLD: "0.75" NODELINK_CLUSTER_SCALING_SCALEDOWNTHRESHOLD: "0.3" ``` Why cluster mode? Better CPU utilization across cores. Auto-scaling where workers spawn and die based on player load. Reliability where one worker crashing doesn't take down the whole server. Set `WORKERS` to `0` and NodeLink automatically uses all your cores. Or specify a number like `4` for exactly 4 workers. Control the built-in audio mixing engine: ```yaml NODELINK_MIX_ENABLED: "true" NODELINK_MIX_DEFAULTVOLUME: "0.8" NODELINK_MIX_MAXLAYERSMIX: "5" NODELINK_MIX_AUTOCLEANUP: "true" ``` The mixer allows you to overlay tracks (like TTS or SFX) on top of the main audio. You can limit the number of simultaneous layers to manage resources. NodeLink supports multiple audio sources. Each can be enabled or disabled independently: **YouTube** (enabled by default, the backbone of most music bots): ```yaml NODELINK_SOURCES_YOUTUBE_ENABLED: "true" NODELINK_SOURCES_YOUTUBE_CIPHER_TOKEN: "" # Optional, helps with some restricted content ``` **Spotify** (requires API credentials): ```yaml NODELINK_SOURCES_SPOTIFY_ENABLED: "true" NODELINK_SOURCES_SPOTIFY_CLIENTID: "your_id" NODELINK_SOURCES_SPOTIFY_CLIENTSECRET: "your_secret" ``` **Local files** (play music from your server's filesystem): ```yaml NODELINK_SOURCES_LOCAL_ENABLED: "true" NODELINK_SOURCES_LOCAL_BASEPATH: "/app/music/" ``` **Others**: SoundCloud, Deezer, Apple Music, Tidal, Bandcamp, Twitch, Nico Nico, and more. All in the config file. All documented inline. Most work without additional setup. NodeLink includes audio filters for real-time audio modification. Standard filters like Equalizer, Karaoke, Timescale, Tremolo, Vibrato, Rotation, Distortion, and Channel Mix are all enabled by default. NodeLink-exclusive filters like Echo, Chorus, Compressor, High Pass, Phaser, and Spatial are also available. All filters are processed natively without external tools. Want to disable a specific filter? Just set it to false: ```yaml NODELINK_FILTERS_ENABLED_ECHO: "false" ``` You can combine multiple filters simultaneously. NodeLink processes them efficiently in the audio pipeline without significant overhead. NodeLink tries multiple lyric sources automatically in order until one succeeds: ```yaml NODELINK_LYRICS_YOUTUBE_ENABLED: "true" # Synced captions when available NODELINK_LYRICS_GENIUS_ENABLED: "true" # Plain text lyrics NODELINK_LYRICS_MUSIXMATCH_ENABLED: "true" # Synced lyrics (auto-obtains signature) NODELINK_LYRICS_LRCLIB_ENABLED: "true" # Open-source synced lyrics ``` NodeLink tries YouTube first for synced captions. If that fails, it tries Musixmatch. Then LRCLib. Finally Genius. First source that returns lyrics wins. This happens automatically when you request lyrics via the API. NodeLink can expose Prometheus metrics for production monitoring: ```yaml NODELINK_METRICS_ENABLED: "true" NODELINK_METRICS_AUTHORIZATION_TYPE: "Bearer" NODELINK_METRICS_AUTHORIZATION_PASSWORD: "" # Empty = use server password ``` Prometheus metrics require the `prom-client` package to be installed: ```bash npm install prom-client ``` Without this package, NodeLink will throw an error if metrics are enabled in your configuration. You can disable metrics by setting `metrics.enabled: false` in your config file. The metrics endpoint becomes available at `/v4/metrics`. Point your Prometheus scraper at it. Get insights into audio frame delivery, memory usage, event loop lag, player counts, and more. See the [Prometheus documentation](/docs/advenced/prometheus) for dashboard examples. NodeLink includes rate limiting and DoS protection out of the box: ```yaml NODELINK_RATELIMIT_ENABLED: "true" NODELINK_RATELIMIT_GLOBAL_MAXREQUESTS: "1000" NODELINK_RATELIMIT_GLOBAL_TIMEWINDOWMS: "60000" # 1 minute NODELINK_DOSPROTECTION_ENABLED: "true" NODELINK_DOSPROTECTION_THRESHOLDS_BURSTREQUESTS: "50" ``` These defaults protect against spam and abuse without affecting normal usage. Global rate limiting caps total requests per IP. DoS protection detects and blocks burst traffic patterns. Both are enabled by default and just work. Control how verbose NodeLink is and where logs go: ```yaml NODELINK_LOGGING_LEVEL: "info" # debug, info, warn, error NODELINK_LOGGING_FILE_ENABLED: "false" NODELINK_LOGGING_FILE_PATH: "logs" ``` The default `info` level shows important events without noise. Use `debug` when troubleshooting issues. If you enable file logging, mount a volume to persist logs: ```yaml volumes: - ./logs:/app/logs ``` Logs will appear in the `logs` directory on your host machine. You can configure plugins via environment variables, though complex setups are easier with a mounted `config.js`. ```yaml # Load plugins (JSON array string) NODELINK_PLUGINS: '[{"name": "my-plugin", "source": "npm"}]' # Configure specific plugins (JSON object string) NODELINK_PLUGINCONFIG: '{"my-plugin": {"token": "123"}}' ``` For local development or custom plugins, it's recommended to mount the `plugins/` directory: ```yaml volumes: - ./plugins:/app/plugins ``` *** ## Common Operations Commands you'll use regularly: ```bash # Start NodeLink in background docker compose up -d # Stop NodeLink docker compose down # Restart NodeLink (reload config changes) docker compose restart # Check if running docker compose ps ``` ```bash # View logs (follow mode) docker compose logs -f nodelink # View last 100 lines docker compose logs --tail=100 nodelink # View logs from specific time docker compose logs --since 30m nodelink ``` ```bash # Update to latest version cd NodeLink git pull docker compose up -d --build # Or using Docker Hub images docker compose pull docker compose up -d ``` ```bash # Access container shell docker compose exec nodelink sh # Run commands inside container docker compose exec nodelink npm run --version ``` *** ## Using Pre-built Images Don't want to build from source every time? Use the pre-built images from Docker Hub: ```yaml services: nodelink: image: performanc/nodelink:latest # ... rest of your config ... ``` Available tags: Most recent stable release. Updated with each new version. Safe for production. Specific version tags. Pin to a version for consistency. Useful when you want predictable behavior. Bleeding edge from main branch. Gets new features first. May break occasionally. Not for production. Using pre-built images means faster deployments. No compile time. Just pull and run. The tradeoff is slightly larger images since they're built for multiple architectures. *** ## Local Music Files Want to play music from your server's hard drive? NodeLink's local source lets you stream files directly from the filesystem. ### Create Music Directory Make a folder for your music files: ```bash mkdir local-music ``` Put your audio files there. NodeLink supports common formats like MP3, FLAC, WAV, OGG, M4A, and more. ### Mount Volume Edit your `docker-compose.yml` to mount the directory: ```yaml services: nodelink: volumes: - ./local-music:/app/music environment: NODELINK_SOURCES_LOCAL_ENABLED: "true" NODELINK_SOURCES_LOCAL_BASEPATH: "/app/music/" ``` The volume mount makes your `local-music` folder appear inside the container at `/app/music`. The basepath tells NodeLink where to look for files. ### Load Tracks Restart NodeLink to apply changes: ```bash docker compose restart ``` Now you can load tracks using the `file://` protocol: ``` file:///app/music/song.mp3 file:///app/music/albums/album1/track.flac ``` The path must start with the basepath you configured. NodeLink resolves the file and streams it like any other source. Local file access is powerful but requires careful configuration. NodeLink validates paths to prevent directory traversal attacks. Still, only enable this if you control the filesystem and know who can access the API. *** ## Production Deployment Running NodeLink in production requires a few extra considerations beyond just starting the container. ### Reverse Proxy Setup NodeLink should sit behind a reverse proxy in production for SSL termination and security. Here's an Nginx example: ```nginx server { listen 80; server_name audio.yourdomain.com; # Redirect HTTP to HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name audio.yourdomain.com; # SSL certificates (use certbot for Let's Encrypt) ssl_certificate /etc/letsencrypt/live/audio.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/audio.yourdomain.com/privkey.pem; location / { proxy_pass http://localhost:2333; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket support proxy_read_timeout 86400; } } ``` This configuration handles HTTPS, WebSocket upgrades, and proper header forwarding. Get SSL certificates from Let's Encrypt using certbot. It's free and automated. ### Resource Limits Prevent NodeLink from consuming all system resources if something goes wrong: ```yaml services: nodelink: deploy: resources: limits: cpus: '2.0' # Max 2 CPU cores memory: 2G # Max 2GB RAM reservations: cpus: '0.5' # Reserve half a core memory: 512M # Reserve 512MB RAM ``` These limits depend on your player count. More simultaneous players need more resources. Monitor actual usage and adjust accordingly. ### Health Checks Docker can monitor if NodeLink is actually responding, not just that the container is running: ```yaml healthcheck: test: ["CMD", "curl", "-f", "http://localhost:2333/version"] interval: 30s timeout: 10s retries: 3 start_period: 40s ``` If the health check fails three times, Docker marks the container as unhealthy. Combined with restart policies, this provides automatic recovery from crashes or hangs. ### Firewall Configuration If NodeLink runs on a remote server, allow incoming connections: ```bash # UFW (Ubuntu/Debian) sudo ufw allow 2333/tcp # firewalld (CentOS/RHEL) sudo firewall-cmd --add-port=2333/tcp --permanent sudo firewall-cmd --reload ``` Only open ports you actually use. If NodeLink is behind a reverse proxy, you might only need to open 80/443 for the proxy and keep 2333 local-only. ### Security Checklist Before going live, verify these points: * [ ] Changed default password to something strong and random * [ ] Server password is not committed to version control * [ ] Metrics endpoint uses separate authentication if exposed publicly * [ ] NodeLink is behind HTTPS reverse proxy * [ ] Firewall rules limit access to only necessary ports * [ ] Rate limiting is enabled * [ ] DoS protection is enabled * [ ] Health checks are configured * [ ] Restart policy is set to handle crashes * [ ] Logs are being collected somewhere * [ ] Resource limits prevent runaway usage * [ ] Backups exist for configuration *** ## Troubleshooting ### Container Won't Start Check the logs for specific errors: ```bash docker compose logs nodelink ``` Common problems and fixes: **Port already in use**: Something else is listening on 2333. Either stop that process or change NodeLink's port in `docker-compose.yml`: ```yaml ports: - "2334:2333" # Use 2334 externally, 2333 internally ``` **Permission denied**: Docker doesn't have permission to bind the port or access volumes. On Linux, add your user to the docker group: ```bash sudo usermod -aG docker $USER ``` Log out and back in for the change to take effect. **Invalid configuration**: Check for typos in `docker-compose.yml`. YAML is whitespace-sensitive. Use a validator if unsure. ### Can't Connect from Discord Bot First, verify NodeLink is accessible from outside the container: ```bash curl http://your-server-ip:2333/version ``` If this fails, check your firewall rules. If it succeeds but your bot still can't connect, verify the password matches between your bot config and `docker-compose.yml`. For remote servers, ensure the firewall allows incoming connections: ```bash sudo ufw status ``` The port (default 2333) should be listed as allowed. ### High CPU Usage Check how many players are active: ```bash curl http://localhost:2333/v4/stats -H "Authorization: yourPassword" ``` Look at the `players` and `playingPlayers` counts. High CPU with many players is normal. If CPU is high with few players, enable cluster mode: ```yaml NODELINK_CLUSTER_ENABLED: "true" NODELINK_CLUSTER_WORKERS: "0" # Use all cores ``` Restart after changing the config. ### Audio Cutting Out This usually indicates network issues or event loop blocking. Check frame statistics in the stats endpoint: ```bash curl http://localhost:2333/v4/stats -H "Authorization: yourPassword" ``` Look at `frameStats`. If `deficit` is positive or `nulled` is increasing, audio is being dropped. Try increasing the update interval: ```yaml NODELINK_PLAYERUPDATEINTERVAL: "1000" ``` Also check network latency between your server and Discord voice servers. High latency causes problems. ### Memory Growing Over Time External memory grows with active players. This is normal. Each player holds audio buffers for smooth playback. Memory should stabilize once player count is constant. If memory keeps growing even without new players, that might indicate a leak. Report it with logs and reproduction steps. *** ## The Real Talk Docker makes deploying NodeLink almost trivial. You don't need to be a DevOps expert. You don't need to understand multi-stage builds or image layers or any of that. The repository already has everything configured. A production-ready Dockerfile optimized for NodeLink's native modules and worker architecture. A comprehensive docker-compose.yml with every option documented. Health checks and restart policies. Security features enabled by default. Just clone it, change the password, and run `docker compose up -d`. That's literally the deployment process. Everything else in this document is optional. The defaults work. The configuration makes sense. The security is reasonable. You can deploy NodeLink to production with a handful of environment variables and be confident it'll run reliably. **You can go from zero to streaming audio in about 5 minutes.** *** ## What's Next? Now that NodeLink is containerized and running: Point your Discord bot at your NodeLink instance. Use the WebSocket API to manage players. Enable Spotify, Apple Music, or other sources. Configure API keys and credentials. Add Prometheus metrics and Grafana dashboards. Know exactly how your instance is performing. Try lyrics support, chapter markers, direct streaming, and NodeLink-exclusive filters. The code is open. The configs are documented. The community is active. Go build something cool with it. πŸ’™ # Plugin Development import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { TypeTable } from 'fumadocs-ui/components/type-table'; # Plugin Development Guide Writing plugins requires a solid understanding of JavaScript, Node.js, and asynchronous programming. You have full access to the server internals. **You can crash the server.** Proceed with caution and curiosity. NodeLink's plugin system is designed to be **native** and **low-overhead**. Plugins are just JavaScript modules that NodeLink loads at startup. They have access to the internal `nodelink` instance, allowing you to hook into the API, the audio pipeline, and the event system. *** ## 1. Architecture & Lifecycle NodeLink uses a **Cluster Architecture**. This means your plugin runs in multiple processes simultaneously, but with different roles.
**Context:** `master` The central brain. It handles the REST API, the WebSocket connection with Discord bots, and manages the worker processes. **Use this for:** * Custom API Endpoints (`/v4/my-route`) * Intercepting Player Commands (`play`, `volume`, etc.) * Intercepting WebSocket Packets * Modifying Track Metadata **Context:** `worker` The heavy lifters. Each worker handles a subset of players, decoding audio, applying filters, and sending UDP packets. **Use this for:** * Custom Audio Sources (Search & Resolve) * Custom Audio Filters (DSP) * Raw Audio Interceptors (PCM Manipulation)
### Directory Structure ```text NodeLink/ └── plugins/ └── my-plugin/ β”œβ”€β”€ package.json (Required) └── index.js (Entry point) ``` **`package.json`** is standard NPM format. `name`, `version`, and `main` are required. *** ## 2. Master Context API These methods are available when `context.type === 'master'`. ### Register Route Expose new REST endpoints. Useful for dashboards, webhooks, or custom controls. **Signature:** `nodelink.registerRoute(method, path, handler)` void' } }} /> ```javascript nodelink.registerRoute('POST', '/v4/my-plugin/trigger', async (nodelink, req, res, sendResponse) => { const body = req.body; // JSON body is already parsed if content-type is application/json nodelink.logger('info', 'MyPlugin', `Triggered with: ${body.action}`); sendResponse(req, res, { success: true }, 200); }); ``` ### Player Interceptor Intercept commands sent to players **before** they are processed or sent to workers. This is the "Gatekeeper". **Signature:** `nodelink.registerPlayerInterceptor(callback)` result | null' } }} /> **Actions:** `'play'`, `'stop'`, `'pause'`, `'seek'`, `'volume'`, `'filters'`, `'updateVoice'`, `'destroy'`. ```javascript nodelink.registerPlayerInterceptor(async (action, guildId, args) => { // Example: Prevent volume > 100 if (action === 'volume') { const vol = args[0]; if (vol > 100) { // Modify the argument directly to cap it args[0] = 100; } } // Example: Block a specific song if (action === 'play') { const track = args[0]; if (track.info.title.includes('Baby Shark')) { // Return an object to block execution and return error to client return { error: 'Safety hazard detected.' }; } } // Return null to let the command proceed return null; }); ``` ### WebSocket Interceptor Intercept raw WebSocket messages coming from clients (bots). **Signature:** `nodelink.registerWebSocketInterceptor(callback)` boolean' } }} /> ```javascript nodelink.registerWebSocketInterceptor(async (nodelink, socket, packet, clientInfo) => { if (packet.op === 'my-custom-op') { socket.send(JSON.stringify({ op: 'my-reply', payload: 'Pong!' })); return true; // Return true to STOP NodeLink from processing this packet further } return false; // Continue normal processing }); ``` ### Track Modifier Modify track objects right before they are sent to the client (e.g., in `loadTracks` response). **Signature:** `nodelink.registerTrackModifier(callback)` ```javascript nodelink.registerTrackModifier((track) => { // Add custom user data track.userData = { ...track.userData, source: 'modified' }; // You can even modify the info if (track.info.sourceName === 'youtube') { track.info.title = `[YT] ${track.info.title}`; } }); ``` *** ## 3. Worker Context API These methods are available when `context.type === 'worker'`. ### Register Source Add a new platform support. **Signature:** `nodelink.registerSource(name, sourceObject)` **Source Class Interface:** ```javascript class MySource { constructor(nodelink) { this.nodelink = nodelink; this.sourceName = 'mysource'; this.searchTerms = ['mysearch']; // Enables mysearch:query } // Handle 'mysearch:query' async search(query) { // Return { loadType: 'search', data: [tracks] } } // Handle direct URLs async resolve(url) { // Return { loadType: 'track', data: track } } // Get playback URL/stream for a track async getTrackUrl(trackInfo) { return { url: "https://stream.example.com/audio.mp3", protocol: "https", // 'https', 'hls', 'http' format: "mp3" // 'mp3', 'opus', 'aac', 'flac' }; } } ``` ### Register Filter Implement custom DSP filters. **Warning: This runs on the audio thread loop.** **Signature:** `nodelink.registerFilter(name, filterObject)` ```javascript class ClipperFilter { constructor() { this.threshold = 1.0; } update(config) { // Config comes from the 'filters' payload in Update Player if (config.clipper) this.threshold = config.clipper.threshold; } process(chunk) { // chunk is a Buffer containing 16-bit signed integer PCM // Modify it in place or return a new buffer return chunk; } } nodelink.registerFilter('clipper', new ClipperFilter()); ``` ### Audio Interceptor Inject a `Transform` stream into the pipeline. Use this for things like recording audio, silence detection, or complex buffering. **Signature:** `nodelink.registerAudioInterceptor(factory)` ```javascript const { Transform } = await import('node:stream'); nodelink.registerAudioInterceptor(() => { return new Transform({ transform(chunk, encoding, callback) { // 'chunk' is raw PCM. // Do whatever you want, then push it. this.push(chunk); callback(); } }); }); ``` *** ## 4. Communication (IPC) Sometimes Master needs to talk to Workers or vice-versa. ### Master to Worker Use `nodelink.workerManager.execute(worker, type, payload)`. ```javascript // Master const worker = nodelink.workerManager.getBestWorker(); const result = await nodelink.workerManager.execute(worker, 'myPluginCommand', { foo: 'bar' }); ``` ### Worker Handling Register a **Worker Interceptor** to handle the custom command. ```javascript // Worker nodelink.registerWorkerInterceptor(async (type, payload) => { if (type === 'myPluginCommand') { // Do logic return true; // Block default handler, although for custom types it doesn't matter } }); ``` *(Note: Currently, direct custom IPC responses from worker to master require hooking into the internal command queue or using side-channels, but interceptors allow you to execute logic).* *** ## 5. Development Checklist 1. **Check Context:** Always wrap logic in `if (context.type === '...')`. 2. **Log Smart:** Use `nodelink.logger(level, category, message)` for consistent logs. 3. **Handle Errors:** A crash in a plugin crashes the server. Use `try/catch`. 4. **No Blocking:** Never block the event loop in `worker` context. Audio will stutter immediately. Check out the built-in `nodelink-sample-plugin` in the `plugins/` directory of the repository for a fully working reference implementation. # Prometheus import { Callout } from 'fumadocs-ui/components/callout'; import { Steps, Step } from 'fumadocs-ui/components/steps'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; # Prometheus NodeLink exposes Prometheus metrics out of the box. No plugins, no extra setup, just detailed observability into your audio streaming performance. ## What you get Production metrics that matter: ## Quick setup Prometheus metrics require the `prom-client` package to be installed: ```bash npm install prom-client ``` Without this package, NodeLink will throw an error if metrics are enabled in your configuration. You can disable metrics by setting `metrics.enabled: false` in your config file. ### Configure metrics Metrics are enabled by default. Set your authorization in config.js: ```js export default { options: { metrics: { enabled: true, authorization: { type: 'Bearer', password: 'your_secret_here' } } } } ``` If you leave the password empty, NodeLink uses your server password instead. ### Test the endpoint ```bash curl -H "Authorization: Bearer your_secret_here" \ http://localhost:2333/v4/metrics ``` You should see: ``` # HELP nodelink_playing_players Number of active audio players # TYPE nodelink_playing_players gauge nodelink_playing_players 5 # HELP nodelink_frames_sent Audio frames sent to Discord # TYPE nodelink_frames_sent counter nodelink_frames_sent 150234 ``` ### Add to Prometheus Edit your `prometheus.yml`: ```yaml scrape_configs: - job_name: 'nodelink' scrape_interval: 15s static_configs: - targets: ['localhost:2333'] metrics_path: '/metrics' authorization: type: Bearer credentials: 'your_secret_here' ``` Reload Prometheus: ```bash curl -X POST http://localhost:9090/-/reload ``` ## Grafana dashboard We built a complete dashboard template for quick deployment. NodeLink Grafana Dashboard ### What's included Real-time audio health with active players, frame rates, and CPU load. Memory anatomy showing RSS vs heap vs external buffers. V8 heap space breakdown for new, old, code, and large objects. Event loop lag percentiles and GC pause heatmap. Frame delivery comparison and quality loss tracking. ### Import to Grafana ```json { "title": "NodeLink Ultimate \u2022 Complete Observability Dashboard πŸš€", "description": "Comprehensive monitoring for NodeLink Audio Streamer with optimized panel types for each metric category. Features real-time audio health, worker cluster monitoring, API analytics, and deep Node.js runtime insights.", "editable": true, "gnetId": null, "graphTooltip": 1, "id": null, "links": [], "panels": [ { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 100, "panels": [], "title": "🎯 System Overview & Health", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 3, "x": 0, "y": 1 }, "id": 101, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_players{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ‘₯ Total Players", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 3, "x": 3, "y": 1 }, "id": 102, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_playing_players{job=~\"$job\"}", "refId": "A" } ], "title": "🎧 Playing Now", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 1 }, { "color": "red", "value": 5 } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 3, "x": 6, "y": 1 }, "id": 103, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_voice_connections{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ”Š Voice Connections", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 10 } ] }, "unit": "short" } }, "gridPos": { "h": 5, "w": 3, "x": 9, "y": 1 }, "id": 104, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_websocket_connections{job=~\"$job\"}", "refId": "A" } ], "title": "🌐 WebSocket Clients", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 50 }, { "color": "red", "value": 80 } ] }, "unit": "percent" } }, "gridPos": { "h": 5, "w": 3, "x": 12, "y": 1 }, "id": 105, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_cpu_nodelink_load{job=~\"$job\"} * 100", "refId": "A" } ], "title": "πŸ”₯ CPU Load", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 0.015 }, { "color": "red", "value": 0.05 } ] }, "unit": "s" } }, "gridPos": { "h": 5, "w": 3, "x": 15, "y": 1 }, "id": 106, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_eventloop_lag_p99_seconds{job=~\"$job\"}", "refId": "A" } ], "title": "🐌 Loop Lag P99", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 0.5 }, { "color": "red", "value": 0.8 } ] }, "unit": "percentunit" } }, "gridPos": { "h": 5, "w": 3, "x": 18, "y": 1 }, "id": 107, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "process_open_fds{job=~\"$job\"} / process_max_fds{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ“‚ File Descriptors", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "ms" } }, "gridPos": { "h": 5, "w": 3, "x": 21, "y": 1 }, "id": 108, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(time() - process_start_time_seconds{job=~\"$job\"}) * 1000", "refId": "A" } ], "title": "\u23f1\ufe0f Uptime", "type": "stat" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 }, "id": 200, "panels": [], "title": "🎡 Audio Stream Health & Quality", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "Frames/sec", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 201, "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodelink_frames_sent{job=~\"$job\"}[$__rate_interval])", "legendFormat": "πŸ“€ Sent", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodelink_frames_expected{job=~\"$job\"}[$__rate_interval])", "legendFormat": "🎯 Expected", "refId": "B" } ], "title": "🌊 Audio Frame Flow Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "bars", "fillOpacity": 70, "gradientMode": "none", "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 202, "options": { "legend": { "calcs": [ "sum", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodelink_frames_nulled{job=~\"$job\"}[$__rate_interval])", "legendFormat": "🚫 Nulled (Empty)", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_frames_deficit{job=~\"$job\"}", "legendFormat": "\u26a0\ufe0f Deficit (Lag)", "refId": "B" } ], "title": "πŸ“‰ Audio Quality Issues (Lower = Better)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 15 }, "id": 203, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_player_stuck_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ”’ Stuck Players (Total)", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 15 }, "id": 204, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_stream_errors_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "\u274c Stream Errors (Total)", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 15 }, "id": 205, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_voice_connection_errors_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ“ž Voice Connection Errors", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ms" } }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 15 }, "id": 206, "options": { "legend": { "calcs": [ "mean", "lastNotNull" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_track_load_duration_ms{job=~\"$job\"} OR vector(0)", "legendFormat": "Track Load Time", "refId": "A" } ], "title": "🎡 Track Load Duration", "type": "timeseries" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 19 }, "id": 300, "panels": [], "title": "πŸ§‘\u200dπŸ’Ό Worker Cluster Monitoring", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 }, "id": 301, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_worker_players{job=~\"$job\"} * on(worker_id, worker_pid) group_left() (nodelink_worker_health{job=~\"$job\"} == 1)", "legendFormat": "Worker {{worker_id}} (PID {{worker_pid}})", "refId": "A" } ], "title": "πŸ‘· Players per Worker", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 30, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line+area" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "percent" } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 }, "id": 302, "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(nodelink_worker_cpu_load{job=~\"$job\"} * 100) * on(worker_id, worker_pid) group_left() (nodelink_worker_health{job=~\"$job\"} == 1)", "legendFormat": "Worker {{worker_id}} CPU", "refId": "A" } ], "title": "πŸ”₯ Worker CPU Load %", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 28 }, "id": 303, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_worker_memory_used_bytes{job=~\"$job\"} * on(worker_id, worker_pid) group_left() (nodelink_worker_health{job=~\"$job\"} == 1)", "legendFormat": "Worker {{worker_id}} Used", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_worker_memory_allocated_bytes{job=~\"$job\"} * on(worker_id, worker_pid) group_left() (nodelink_worker_health{job=~\"$job\"} == 1)", "legendFormat": "Worker {{worker_id}} Allocated", "refId": "B" } ], "title": "πŸ’Ύ Worker Memory Usage", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "0": { "color": "red", "text": "Unhealthy" }, "1": { "color": "green", "text": "Healthy" } }, "type": "value" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 6, "x": 12, "y": 28 }, "id": 304, "options": { "displayMode": "gradient", "minVizHeight": 10, "minVizWidth": 0, "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showUnfilled": true }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_worker_health{job=~\"$job\"} == 1", "legendFormat": "Worker {{worker_id}}", "refId": "A" } ], "title": "\u2764\ufe0f Worker Health Status", "type": "bargauge" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" } }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 28 }, "id": 305, "options": { "legend": { "calcs": [ "lastNotNull" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_worker_uptime_seconds{job=~\"$job\"} * on(worker_id, worker_pid) group_left() (nodelink_worker_health{job=~\"$job\"} == 1)", "legendFormat": "Worker {{worker_id}}", "refId": "A" } ], "title": "\u23f0 Worker Uptime", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 4, "x": 0, "y": 36 }, "id": 306, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_worker_restarts_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ”„ Worker Restarts", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 4, "x": 4, "y": 36 }, "id": 307, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_worker_failures_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ’₯ Worker Failures", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 10 }, { "color": "red", "value": 50 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 4, "x": 8, "y": 36 }, "id": 308, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_command_queue_size{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ“‹ Command Queue Size", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ms" } }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 36 }, "id": 309, "options": { "legend": { "calcs": [ "mean", "max" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_command_execution_time_ms{job=~\"$job\"}", "legendFormat": "Execution Time", "refId": "A" } ], "title": "\u26a1 Command Exec Time", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 3, "x": 18, "y": 36 }, "id": 310, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_command_timeouts_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "\u23f1\ufe0f Cmd Timeouts", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 3, "x": 21, "y": 36 }, "id": 311, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_command_retries_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ” Cmd Retries", "type": "stat" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 40 }, "id": 400, "panels": [], "title": "πŸ’Ύ Memory Anatomy (RSS vs Heap vs External)", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" } }, "gridPos": { "h": 10, "w": 14, "x": 0, "y": 41 }, "id": 401, "options": { "legend": { "calcs": [ "lastNotNull", "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "process_resident_memory_bytes{job=~\"$job\"}", "legendFormat": "πŸ’Ύ RSS (Physical RAM)", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_heap_size_used_bytes{job=~\"$job\"}", "legendFormat": "πŸ“¦ JS Heap Used", "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_external_memory_bytes{job=~\"$job\"}", "legendFormat": "πŸ”Œ External (C++ Buffers)", "refId": "C" } ], "title": "🧠 Memory Composition Overview", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "opacity", "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" } }, "gridPos": { "h": 10, "w": 10, "x": 14, "y": 41 }, "id": 402, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" }, "textMode": "name", "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "values": false, "fields": "", "calcs": [ "lastNotNull" ] } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_version_info{job=~\"$job\"}", "legendFormat": "{{version}}", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_heap_space_size_used_bytes{space=\"old\", job=~\"$job\"}", "legendFormat": "πŸ‘΄ Old Space", "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_heap_space_size_used_bytes{space=\"code\", job=~\"$job\"}", "legendFormat": "πŸ“œ Code Space", "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_heap_space_size_used_bytes{space=\"large_object\", job=~\"$job\"}", "legendFormat": "🐘 Large Objects", "refId": "D" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_heap_space_size_used_bytes{space=\"trusted\", job=~\"$job\"}", "legendFormat": "πŸ” Trusted", "refId": "E" } ], "title": "πŸ“¦ V8 Heap Space Breakdown (Stacked)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "red", "value": 90 } ] }, "unit": "percent" } }, "gridPos": { "h": 6, "w": 6, "x": 0, "y": 51 }, "id": 403, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(nodejs_heap_size_used_bytes{job=~\"$job\"} / nodejs_heap_size_total_bytes{job=~\"$job\"}) * 100", "refId": "A" } ], "title": "πŸ“Š Heap Usage %", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "bytes" } }, "gridPos": { "h": 6, "w": 4, "x": 6, "y": 51 }, "id": 404, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_memory_free_bytes{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ’š Free System Memory", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "bytes" } }, "gridPos": { "h": 6, "w": 4, "x": 10, "y": 51 }, "id": 405, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_memory_reservable_bytes{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ”΅ Reservable Memory", "type": "stat" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 57 }, "id": 500, "panels": [], "title": "\u26a1 Event Loop & Garbage Collection", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line+area" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 0.05 } ] }, "unit": "s" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 58 }, "id": 501, "options": { "legend": { "calcs": [ "mean", "max", "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_eventloop_lag_p99_seconds{job=~\"$job\"}", "legendFormat": "πŸ”΄ P99 (Critical)", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_eventloop_lag_p90_seconds{job=~\"$job\"}", "legendFormat": "🟠 P90", "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_eventloop_lag_p50_seconds{job=~\"$job\"}", "legendFormat": "πŸ”΅ P50 (Median)", "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_eventloop_lag_mean_seconds{job=~\"$job\"}", "legendFormat": "\u26aa Mean", "refId": "D" } ], "title": "🐌 Event Loop Lag (Percentiles)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "custom": { "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "scaleDistribution": { "type": "linear" } } } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 58 }, "id": 502, "options": { "calculate": true, "cellGap": 2, "cellRadius": 0, "color": { "exponent": 0.5, "fill": "dark-red", "mode": "scheme", "reverse": false, "scale": "exponential", "scheme": "Spectral", "steps": 64 }, "exemplars": { "color": "rgba(255,0,255,0.7)" }, "filterValues": { "le": 1e-100 }, "legend": { "show": true }, "rowsFrame": { "layout": "auto" }, "tooltip": { "show": true, "yHistogram": true }, "yAxis": { "axisPlacement": "left", "reverse": false, "unit": "s" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "sum(rate(nodejs_gc_duration_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (le)", "format": "heatmap", "legendFormat": "{{le}}", "refId": "A" } ], "title": "\u267b\ufe0f GC Pause Duration Distribution (Heatmap)", "type": "heatmap" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ops" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 66 }, "id": 503, "options": { "legend": { "calcs": [ "sum", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodejs_gc_duration_seconds_count{kind=\"minor\", job=~\"$job\"}[$__rate_interval])", "legendFormat": "🟒 Minor GC", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodejs_gc_duration_seconds_count{kind=\"major\", job=~\"$job\"}[$__rate_interval])", "legendFormat": "πŸ”΄ Major GC", "refId": "B" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodejs_gc_duration_seconds_count{kind=\"incremental\", job=~\"$job\"}[$__rate_interval])", "legendFormat": "πŸ”΅ Incremental GC", "refId": "C" } ], "title": "\u267b\ufe0f Garbage Collection Frequency", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 66 }, "id": 504, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_active_resources{job=~\"$job\"}", "legendFormat": "{{type}}", "refId": "A" } ], "title": "πŸ”Œ Active Resources (Event Loop)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 12, "x": 0, "y": 74 }, "id": 505, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_active_handles{job=~\"$job\"}", "legendFormat": "{{type}}", "refId": "A" } ], "title": "πŸ“‘ Active Handles (Sockets/Streams/Pipes)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 10 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 6, "x": 12, "y": 74 }, "id": 506, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_active_handles_total{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ“Š Total Active Handles", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 10 }, { "color": "red", "value": 20 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 6, "x": 18, "y": 74 }, "id": 507, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_active_resources_total{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ”‹ Total Active Resources", "type": "stat" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 80 }, "id": 600, "panels": [], "title": "🌐 API & Network Traffic", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "Requests/sec", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "reqps" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 81 }, "id": 601, "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(nodelink_api_requests_total{job=~\"$job\"}[$__rate_interval])", "legendFormat": "{{endpoint}}", "refId": "A" } ], "title": "πŸ“‘ API Requests Rate (by Endpoint)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "Errors/sec", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "bars", "fillOpacity": 70, "gradientMode": "none", "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 0.1 } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 81 }, "id": 602, "options": { "legend": { "calcs": [ "sum", "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_api_errors_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "API Errors", "refId": "A" } ], "title": "\u274c API Errors Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ms" } }, "gridPos": { "h": 6, "w": 8, "x": 0, "y": 89 }, "id": 603, "options": { "legend": { "calcs": [ "mean", "max", "lastNotNull" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_http_request_duration_ms{job=~\"$job\"}", "legendFormat": "HTTP Request Duration", "refId": "A" } ], "title": "\u23f1\ufe0f HTTP Request Duration", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 8, "x": 8, "y": 89 }, "id": 604, "options": { "legend": { "calcs": [ "mean", "lastNotNull" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_websocket_messages_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "WS Messages/sec", "refId": "A" } ], "title": "πŸ’¬ WebSocket Message Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 16, "y": 89 }, "id": 605, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_rate_limit_hits_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "🚦 Rate Limit Hits", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 20, "y": 89 }, "id": 606, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_dos_protection_blocks_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ›‘\ufe0f DoS Blocks", "type": "stat" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 95 }, "id": 700, "panels": [], "title": "🎢 Player & Track Management", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 96 }, "id": 701, "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_players{job=~\"$job\"}", "legendFormat": "Total Players", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_playing_players{job=~\"$job\"}", "legendFormat": "Playing Players", "refId": "B" } ], "title": "πŸ‘₯ Player Activity Over Time", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ops" } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 96 }, "id": 702, "options": { "legend": { "calcs": [ "sum", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_player_restorations_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "πŸ”„ Restorations", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_player_destructions_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "πŸ’₯ Destructions", "refId": "B" } ], "title": "πŸ”„ Player Lifecycle Events", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ops" } }, "gridPos": { "h": 6, "w": 8, "x": 0, "y": 104 }, "id": 703, "options": { "legend": { "calcs": [ "sum", "mean" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_track_loads_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "Track Loads/sec", "refId": "A" } ], "title": "🎡 Track Load Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ops" } }, "gridPos": { "h": 6, "w": 8, "x": 8, "y": 104 }, "id": 704, "options": { "legend": { "calcs": [ "sum", "mean" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_source_requests_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "Source Requests/sec", "refId": "A" } ], "title": "πŸ“₯ Source Requests Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ops" } }, "gridPos": { "h": 6, "w": 8, "x": 16, "y": 104 }, "id": 705, "options": { "legend": { "calcs": [ "sum" ], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(rate(nodelink_playback_events_total{job=~\"$job\"}[$__rate_interval])) OR vector(0)", "legendFormat": "Playback Events/sec", "refId": "A" } ], "title": "\u25b6\ufe0f Playback Events Rate", "type": "timeseries" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 110 }, "id": 800, "panels": [], "title": "πŸ–₯\ufe0f Process & System Metrics", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "percentunit" } }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 111 }, "id": 801, "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(process_cpu_user_seconds_total{job=~\"$job\"}[$__rate_interval])", "legendFormat": "πŸ‘€ User CPU", "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "rate(process_cpu_system_seconds_total{job=~\"$job\"}[$__rate_interval])", "legendFormat": "\u2699\ufe0f System CPU", "refId": "B" } ], "title": "\u26a1 Process CPU Usage (User vs System)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "line" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "transparent", "value": null }, { "color": "red", "value": 2 } ] }, "unit": "short" } }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 111 }, "id": 802, "options": { "legend": { "calcs": [ "mean", "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_cpu_system_load{job=~\"$job\"}", "legendFormat": "System Load Avg ({{nodelink_cpu_cores}} cores)", "refId": "A" } ], "title": "πŸ’» System CPU Load Average", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 4, "x": 0, "y": 119 }, "id": 803, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_cpu_cores{job=~\"$job\"}", "refId": "A" } ], "title": "πŸ–₯\ufe0f CPU Cores", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "ms" } }, "gridPos": { "h": 4, "w": 4, "x": 4, "y": 119 }, "id": 804, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(time() - process_start_time_seconds{job=~\"$job\"}) * 1000", "refId": "A" } ], "title": "\u23f0 System Uptime", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 4, "w": 4, "x": 8, "y": 119 }, "id": 805, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "values": false, "fields": "", "calcs": [ "lastNotNull" ] }, "textMode": "name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodejs_version_info{job=~\"$job\"}", "legendFormat": "{{version}}", "refId": "A" } ], "title": "πŸ“¦ Node.js Version", "type": "stat" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 123 }, "id": 900, "panels": [], "title": "🎯 Advanced Features & Extras", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 0, "y": 124 }, "id": 901, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_lyrics_requests_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "🎀 Lyrics Requests", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 100 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 4, "y": 124 }, "id": 902, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_filter_usage_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸŽ›\ufe0f Filter Usage", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "yellow", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 8, "y": 124 }, "id": 903, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "sum" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "(increase(nodelink_session_resumes_total{job=~\"$job\"}[$__range])) OR vector(0)", "refId": "A" } ], "title": "πŸ”„ Session Resumes", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 12, "y": 124 }, "id": 904, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_route_planner_ips{job=~\"$job\"}", "refId": "A" } ], "title": "🌍 Route Planner IPs", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 16, "y": 124 }, "id": 905, "options": { "colorMode": "background", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_route_planner_banned_ips{job=~\"$job\"}", "refId": "A" } ], "title": "🚫 Banned IPs", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "$datasource" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "blue", "value": null } ] }, "unit": "short" } }, "gridPos": { "h": 6, "w": 4, "x": 20, "y": 124 }, "id": 906, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "10.0.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "$datasource" }, "expr": "nodelink_worker_players{job=~\"$job\"} * on(worker_id, worker_pid) group_left() (nodelink_worker_health{job=~\"$job\"} == 1)", "refId": "A" } ], "title": "πŸ‘· Total Workers", "type": "stat" } ], "refresh": "5s", "schemaVersion": 38, "style": "dark", "tags": [ "nodelink", "nodejs", "audio", "complete", "monitoring" ], "templating": { "list": [ { "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, "hide": 0, "includeAll": false, "label": "Datasource", "multi": false, "name": "datasource", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "current": { "selected": false, "text": "All", "value": "$__all" }, "datasource": { "type": "prometheus", "uid": "$datasource" }, "definition": "label_values(up, job)", "hide": 0, "includeAll": true, "label": "Job", "multi": true, "name": "job", "options": [], "query": "label_values(up, job)", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query" } ] }, "time": { "from": "now-30m", "to": "now" }, "timepicker": {}, "timezone": "", "weekStart": "" } ``` Import it in Grafana: 1. Go to **Dashboards** β†’ **Import** 2. Paste the JSON or upload the file 3. Select your Prometheus datasource 4. Done The dashboard uses two template variables: * `$datasource` - Your Prometheus instance * `$job` - Job name filter (supports multi-select) *** **`nodelink_players`**\ Total number of players. **`nodelink_playing_players`**\ Number of currently playing players. **`nodelink_voice_connections`**\ Number of active voice connections. **`nodelink_voice_connection_errors_total`**\ Total number of voice connection errors. **`nodelink_frames_sent`**\ Total number of audio frames sent. **`nodelink_frames_nulled`**\ Total number of nulled audio frames. **`nodelink_frames_deficit`**\ Audio frame deficit. **`nodelink_frames_expected`**\ Total number of expected audio frames. **`nodelink_track_loads_total`**\ Total number of track loads. **`nodelink_track_load_duration_ms`**\ Track load duration in milliseconds. **`nodelink_stream_errors_total`**\ Total number of stream errors. **`nodelink_player_stuck_total`**\ Total number of stuck players. **`nodelink_player_restorations_total`**\ Total number of player restorations. **`nodelink_player_destructions_total`**\ Total number of player destructions. **`nodelink_filter_usage_total`**\ Total number of filter usage. **`nodelink_uptime_ms`**\ Server uptime in milliseconds. **`nodelink_cpu_cores`**\ Number of CPU cores. **`nodelink_cpu_system_load`**\ System CPU load average. **`nodelink_cpu_nodelink_load`**\ NodeLink CPU load. **`nodelink_memory_free_bytes`**\ Free system memory in bytes. **`nodelink_memory_used_bytes`**\ Used memory in bytes. **`nodelink_memory_allocated_bytes`**\ Allocated memory in bytes. **`nodelink_memory_reservable_bytes`**\ Reservable memory in bytes. **`nodelink_total_workers`**\ Total number of active workers. **`nodelink_worker_players`**\ Number of players per worker. **`nodelink_worker_playing_players`**\ Number of playing players per worker. **`nodelink_worker_memory_used_bytes`**\ Worker memory used in bytes. **`nodelink_worker_memory_allocated_bytes`**\ Worker memory allocated in bytes. **`nodelink_worker_cpu_load`**\ Worker CPU load. **`nodelink_worker_command_queue_length`**\ Worker command queue length. **`nodelink_worker_frames_sent`**\ Audio frames sent by worker. **`nodelink_worker_frames_nulled`**\ Audio frames nulled by worker. **`nodelink_worker_frames_deficit`**\ Audio frame deficit by worker. **`nodelink_worker_frames_expected`**\ Audio frames expected by worker. **`nodelink_worker_uptime_seconds`**\ Worker uptime in seconds. **`nodelink_worker_health`**\ Worker health status (1 = healthy, 0 = unhealthy). **`nodelink_worker_restarts_total`**\ Total number of worker restarts. **`nodelink_worker_failures_total`**\ Total number of worker failures. **`process_*` / `nodejs_*`**\ Standard Node.js and process metrics (CPU, memory, event loop, GC, handles). **`nodelink_api_requests_total`**\ Total number of API requests. **`nodelink_api_errors_total`**\ Total number of API errors. **`nodelink_source_requests_total`**\ Total number of source requests. **`nodelink_websocket_connections`**\ Number of active WebSocket connections. **`nodelink_websocket_messages_total`**\ Total number of WebSocket messages. **`nodelink_session_resumes_total`**\ Total number of session resumes. **`nodelink_command_queue_size`**\ Total size of command queue across all workers. **`nodelink_command_execution_time_ms`**\ Command execution time in milliseconds. **`nodelink_command_timeouts_total`**\ Total number of command timeouts. **`nodelink_command_retries_total`**\ Total number of command retries. **`nodelink_route_planner_ips`**\ Number of available IPs in route planner. **`nodelink_route_planner_banned_ips`**\ Number of banned IPs in route planner. **`nodelink_lyrics_requests_total`**\ Total number of lyrics requests. **`nodelink_rate_limit_hits_total`**\ Total number of rate limit hits. **`nodelink_dos_protection_blocks_total`**\ Total number of DoS protection blocks. **`nodelink_http_request_duration_ms`**\ HTTP request duration in milliseconds. ## Example Queries ### Audio Quality | Metric | Type | Description | | -------------------------- | ------- | ------------------------------------- | | `nodelink_playing_players` | Gauge | Active players streaming audio | | `nodelink_frames_sent` | Counter | Successfully delivered audio frames | | `nodelink_frames_expected` | Counter | Frames expected (3000/min per player) | | `nodelink_frames_nulled` | Counter | Empty frames sent | | `nodelink_frames_deficit` | Gauge | Difference between expected and sent | Keep deficit near 0 for smooth audio. Spikes indicate stuttering. *** ## Alerting Rules Example rules for production monitoring. ### Audio Degradation ```yaml groups: - name: nodelink_audio interval: 30s rules: - alert: HighFrameDeficit expr: nodelink_frames_deficit > 100 for: 1m labels: severity: warning annotations: summary: "Audio quality degraded" - alert: FrequentNulledFrames expr: rate(nodelink_frames_nulled[5m]) > 5 for: 2m labels: severity: critical annotations: summary: "High nulled frame rate" ``` ### Performance Issues ```yaml - alert: HighEventLoopLag expr: nodejs_eventloop_lag_p99_seconds > 0.1 for: 2m labels: severity: warning - alert: MemoryLeakSuspected expr: | (nodejs_heap_size_used_bytes - nodejs_heap_size_used_bytes offset 1h) / nodejs_heap_size_used_bytes offset 1h > 0.5 for: 30m labels: severity: warning ``` Event loop lag above 100ms causes audio stuttering and API slowdowns. *** ## Pro Tips Frame deficit should stay within Β±50. Large spikes need immediate investigation. External memory grows with active players. Each player holds decoded audio buffers, this is normal behavior. Watch for GC pauses over 100ms. Frequent long pauses mean you should increase heap size or reduce player count. Event loop lag spikes indicate blocking code. Use Node.js profiler to find synchronous bottlenecks. Use recording rules to pre-aggregate expensive queries: ```yaml - record: nodelink:frame_delivery_rate expr: rate(nodelink_frames_sent[5m]) ``` For retention beyond 2 weeks, consider pairing Prometheus with Thanos or Cortex. # Voice Receive (Relay) import { Callout } from 'fumadocs-ui/components/callout'; import { ApiCard, ApiSection, ApiSchema } from '@/components/api-ui'; # Voice Receive (Relay) NodeLink supports an experimental feature that allows you to receive and relay audio frames from Discord voice channels to your application via a binary WebSocket. This feature is currently experimental and may undergo breaking changes in the protocol. Use with caution in production. ## Configuration To enable voice receive, you must configure it in your `config.js` or via environment variables. ```js // config.js voiceReceive: { enabled: true, format: "opus" // "opus" or "pcm_s16le" (48kHz, Stereo) } ``` * **format**: `opus` sends raw opus frames (low bandwidth). `pcm_s16le` decodes audio on the server and sends 16-bit Little Endian PCM (high bandwidth). ## Connecting Voice receive uses a separate WebSocket endpoint: `WS /v4/websocket/voice/{guildId}` You must provide the same `Authorization` and `User-Id` headers as the main WebSocket. ## Binary Protocol The Voice Relay uses a custom **binary framing** to minimize overhead. Unlike the main WebSocket, frames are sent as raw binary data, not JSON. ### Frame Structure Each packet consists of a header followed by the audio payload. | Offset | Length | Type | Description | | :------- | :----- | :------- | :---------------------------------------- | | 0 | 1 | UInt8 | **Opcode**: 1 (Start), 2 (Stop), 3 (Data) | | 1 | 1 | UInt8 | **Format**: 0 (Opus), 2 (PCM) | | 2 | 1 | UInt8 | **Guild ID Length** (L1) | | 3 | L1 | String | **Guild ID** | | 3+L1 | 1 | UInt8 | **User ID Length** (L2) | | 4+L1 | L2 | String | **User ID** | | 4+L1+L2 | 4 | UInt32BE | **SSRC** (Discord Sync Source) | | 8+L1+L2 | 4 | UInt32BE | **Timestamp** | | 12+L1+L2 | Var | Binary | **Payload** (Audio Data) | ### Opcodes * **1 (Start)**: Sent when a user starts speaking. Payload is empty. * **2 (Stop)**: Sent when a user stops speaking. Payload is empty. * **3 (Data)**: Sent for every audio frame received. ## Implementation Example (Node.js) ```javascript const WebSocket = require('ws'); const ws = new WebSocket('ws://localhost:3000/v4/websocket/voice/123456789', { headers: { 'Authorization': 'youshallnotpass', 'User-Id': '987654321', 'Client-Name': 'MyBot/1.0.0' } }); ws.on('message', (data) => { if (!(data instanceof Buffer)) return; const op = data.readUInt8(0); const format = data.readUInt8(1); let offset = 2; const guildLen = data.readUInt8(offset++); const guildId = data.toString('utf8', offset, offset + guildLen); offset += guildLen; const userLen = data.readUInt8(offset++); const userId = data.toString('utf8', offset, offset + userLen); offset += userLen; const ssrc = data.readUInt32BE(offset); offset += 4; const timestamp = data.readUInt32BE(offset); offset += 4; const payload = data.slice(offset); if (op === 3) { // Process audio payload console.log(`Received ${payload.length} bytes from ${userId}`); } }); ``` # YouTube Live Chat import { Callout } from 'fumadocs-ui/components/callout'; import { ApiCard, ApiSection, ApiSchema } from '@/components/api-ui'; # YouTube Live Chat NodeLink supports receiving real-time chat messages from YouTube live streams via a WebSocket connection. This feature works by scraping the YouTube live chat feed. It does not require a Google Cloud API Key if your node is correctly configured with YouTube cookies or OAuth. ## Connecting Connect to the live chat WebSocket using the video identifier: `WS /v4/websocket/youtube/live/{videoId}` You can provide: * **videoId**: The 11-character YouTube video ID (e.g., `dQw4w9WgXcQ`). * **guildId**: If a player in that guild is currently playing a YouTube video, NodeLink will automatically resolve the video ID. * **encodedTrack**: A base64 encoded YouTube track string. ## Event Format Messages are sent as JSON strings. ### Chat Message ```json { "type": "message", "author": { "name": "Lucas", "id": "UC...", "avatar": "https://...", "isOwner": false, "isModerator": true, "isVerified": true }, "content": "Hello world!", "timestamp": 1733612345678 } ``` ### Action Event (SuperChats, Memberships) ```json { "type": "action", "action": "superchat", "author": { ... }, "amount": "$5.00", "content": "Great stream!", "timestamp": 1733612345678 } ``` ## Implementation Notes * **Clustering**: If cluster mode is enabled, NodeLink will automatically delegate the live chat fetcher to a specialized **Source Worker** to prevent blocking the main process event loop. * **Rate Limits**: YouTube may temporarily block chat scraping if too many concurrent connections are made from the same IP.