diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..a188e069
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+docs/*
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
new file mode 100644
index 00000000..ccd3327e
--- /dev/null
+++ b/.github/workflows/deploy-docs.yml
@@ -0,0 +1,57 @@
+name: Deploy VitePress site to Pages
+
+on:
+ push:
+ paths:
+ - "docs/**"
+ - ".github/workflows/deploy-docs.yml"
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v1
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ working-directory: docs
+ run: bun install
+
+ - name: Build with VitePress
+ working-directory: docs
+ run: bun run docs:build
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/.vitepress/dist
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ needs: build
+ runs-on: ubuntu-latest
+ name: Deploy
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 5f79cc2e..56941648 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,9 @@ extra/*
config.toml
local.config.toml
.env
+
+# VitePress Docs
+docs/node_modules/
+docs/.vitepress/dist/
+docs/.vitepress/cache/
+
diff --git a/config.example.toml b/config.example.toml
index ad4a17e0..6e0beffb 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -19,6 +19,32 @@ tape = { tape_stop = true, tape_stop_duration_ms = 500, curve = "sinusoidal" } #
# List of mirror provider patterns. %ISRC% or %QUERY%
providers = ["ytsearch:%ISRC%", "ytsearch:%QUERY%", "scsearch:%QUERY%"]
+[player.mirrors.best_match]
+# Enable weighted scoring to find the best-matching candidate.
+# When false, the first resolvable result from each provider is accepted immediately.
+# Default: true
+scoring = true
+
+# Prefixes treated as throttled (rate-sensitive) — tried sequentially after all parallel free providers.
+# Default: ["ytmsearch:", "ytsearch:"]
+throttled_prefixes = ["ytmsearch:", "ytsearch:"]
+
+# Confidence thresholds (only used when scoring = true). All values in [0.0, 1.0].
+# immediate_use — accept instantly, cancel remaining parallel searches.
+# high_confidence — try up to 2 candidates instead of 3.
+# min_similarity — discard anything below this score entirely.
+immediate_use = 0.88
+high_confidence = 0.75
+min_similarity = 0.50
+
+# Scoring weights (should sum to 1.0).
+weight_title = 0.50
+weight_artist = 0.30
+weight_duration = 0.20
+
+# Milliseconds within which two track durations are considered identical.
+duration_tolerance_ms = 3000
+
[logging]
level = "info" # trace | debug | info | warn | error | off
filters = "rustalink=debug" # e.g. "rustalink=debug,davey=off"
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000..c9daea57
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+.vitepress/dist
+.vitepress/cache
diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js
new file mode 100644
index 00000000..5312a884
--- /dev/null
+++ b/docs/.vitepress/config.js
@@ -0,0 +1,35 @@
+import { defineConfig } from 'vitepress'
+
+export default defineConfig({
+ title: "Rustalink",
+ description: "High-performance Rust audio server documentation",
+ base: '/Rustalink/',
+ cleanUrls: true,
+ themeConfig: {
+ logo: '/logo.svg',
+ nav: [
+ { text: 'Docs', link: '/' }
+ ],
+ sidebar: [
+ {
+ text: 'Guide',
+ items: [
+ { text: 'Introduction', link: '/' },
+ { text: 'Installation', link: '/guide/installation' },
+ { text: 'Docker', link: '/guide/docker' },
+ { text: 'Configuration', link: '/guide/configuration' },
+ { text: 'Architecture', link: '/guide/architecture' },
+ { text: 'Filters', link: '/guide/filters' },
+ { text: 'REST API', link: '/guide/api' },
+ { text: 'Pterodactyl', link: '/guide/pterodactyl' }
+ ]
+ }
+ ],
+ socialLinks: [
+ { icon: 'github', link: 'https://github.com/bongodevs/Rustalink' }
+ ],
+ search: {
+ provider: 'local'
+ }
+ }
+})
diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js
new file mode 100644
index 00000000..b408de0b
--- /dev/null
+++ b/docs/.vitepress/theme/index.js
@@ -0,0 +1,9 @@
+import DefaultTheme from 'vitepress/theme'
+import './style.css'
+
+export default {
+ extends: DefaultTheme,
+ enhanceApp({ app, router, siteData }) {
+ // Custom enhancements can go here
+ }
+}
diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css
new file mode 100644
index 00000000..52f0bdbe
--- /dev/null
+++ b/docs/.vitepress/theme/style.css
@@ -0,0 +1,31 @@
+:root {
+ --vp-c-brand-1: #FF6A00;
+ --vp-c-brand-2: #D84315;
+ --vp-c-brand-3: #FF8A33;
+ --vp-home-hero-name-color: transparent;
+ --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #FF6A00 30%, #D84315);
+}
+
+.dark {
+ --vp-c-bg: #0f0f0f;
+ --vp-c-bg-alt: #1a1a1a;
+ --vp-c-bg-elv: #1a1a1a;
+ --vp-c-bg-soft: #1a1a1a;
+
+ --vp-c-text-1: #ffffff;
+ --vp-c-text-2: rgba(255, 255, 255, 0.7);
+
+ --vp-button-brand-bg: #FF6A00;
+ --vp-button-brand-hover-bg: #D84315;
+ --vp-button-brand-active-bg: #D84315;
+}
+
+html {
+ color-scheme: dark !important;
+}
+
+/* Force dark mode */
+body {
+ background-color: var(--vp-c-bg);
+ color: var(--vp-c-text-1);
+}
diff --git a/docs/bun.lock b/docs/bun.lock
new file mode 100644
index 00000000..d3c3aba4
--- /dev/null
+++ b/docs/bun.lock
@@ -0,0 +1,360 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "rustalink-docs",
+ "dependencies": {
+ "vitepress": "^1.6.4",
+ "vue": "^3.5.30",
+ },
+ },
+ },
+ "packages": {
+ "@algolia/abtesting": ["@algolia/abtesting@1.15.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw=="],
+
+ "@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.7", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
+
+ "@algolia/autocomplete-plugin-algolia-insights": ["@algolia/autocomplete-plugin-algolia-insights@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A=="],
+
+ "@algolia/autocomplete-preset-algolia": ["@algolia/autocomplete-preset-algolia@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA=="],
+
+ "@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.17.7", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg=="],
+
+ "@algolia/client-abtesting": ["@algolia/client-abtesting@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw=="],
+
+ "@algolia/client-analytics": ["@algolia/client-analytics@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g=="],
+
+ "@algolia/client-common": ["@algolia/client-common@5.49.1", "", {}, "sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg=="],
+
+ "@algolia/client-insights": ["@algolia/client-insights@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g=="],
+
+ "@algolia/client-personalization": ["@algolia/client-personalization@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w=="],
+
+ "@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg=="],
+
+ "@algolia/client-search": ["@algolia/client-search@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA=="],
+
+ "@algolia/ingestion": ["@algolia/ingestion@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ=="],
+
+ "@algolia/monitoring": ["@algolia/monitoring@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw=="],
+
+ "@algolia/recommend": ["@algolia/recommend@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw=="],
+
+ "@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA=="],
+
+ "@algolia/requester-fetch": ["@algolia/requester-fetch@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw=="],
+
+ "@algolia/requester-node-http": ["@algolia/requester-node-http@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@docsearch/css": ["@docsearch/css@3.8.2", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="],
+
+ "@docsearch/js": ["@docsearch/js@3.8.2", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="],
+
+ "@docsearch/react": ["@docsearch/react@3.8.2", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
+
+ "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.72", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-wkcixntHvaCoqPqerGrNFcHQ3Yx1ux4ZkhscCDK0DEHpP62XCH+cxq1HTsRjbUiQl/M9K8bj03HF6Wgn5iE2rQ=="],
+
+ "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@shikijs/core": ["@shikijs/core@2.5.0", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="],
+
+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^3.1.0" } }, "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w=="],
+
+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw=="],
+
+ "@shikijs/langs": ["@shikijs/langs@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w=="],
+
+ "@shikijs/themes": ["@shikijs/themes@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw=="],
+
+ "@shikijs/transformers": ["@shikijs/transformers@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/types": "2.5.0" } }, "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg=="],
+
+ "@shikijs/types": ["@shikijs/types@2.5.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw=="],
+
+ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
+
+ "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+
+ "@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
+
+ "@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
+
+ "@vue/compiler-dom": ["@vue/compiler-dom@3.5.30", "", { "dependencies": { "@vue/compiler-core": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g=="],
+
+ "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.30", "@vue/compiler-dom": "3.5.30", "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A=="],
+
+ "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
+
+ "@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
+
+ "@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
+
+ "@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
+
+ "@vue/reactivity": ["@vue/reactivity@3.5.30", "", { "dependencies": { "@vue/shared": "3.5.30" } }, "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q=="],
+
+ "@vue/runtime-core": ["@vue/runtime-core@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg=="],
+
+ "@vue/runtime-dom": ["@vue/runtime-dom@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/runtime-core": "3.5.30", "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw=="],
+
+ "@vue/server-renderer": ["@vue/server-renderer@3.5.30", "", { "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "vue": "3.5.30" } }, "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ=="],
+
+ "@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
+
+ "@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
+
+ "@vueuse/integrations": ["@vueuse/integrations@12.8.2", "", { "dependencies": { "@vueuse/core": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g=="],
+
+ "@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
+
+ "@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
+
+ "algoliasearch": ["algoliasearch@5.49.1", "", { "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", "@algolia/client-analytics": "5.49.1", "@algolia/client-common": "5.49.1", "@algolia/client-insights": "5.49.1", "@algolia/client-personalization": "5.49.1", "@algolia/client-query-suggestions": "5.49.1", "@algolia/client-search": "5.49.1", "@algolia/ingestion": "1.49.1", "@algolia/monitoring": "1.49.1", "@algolia/recommend": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ=="],
+
+ "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
+
+ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
+
+ "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
+
+ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+ "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
+
+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
+
+ "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="],
+
+ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "oniguruma-to-es": ["oniguruma-to-es@3.1.1", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ=="],
+
+ "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "preact": ["preact@10.28.4", "", {}, "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ=="],
+
+ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
+
+ "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
+
+ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+
+ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
+
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+
+ "search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
+
+ "shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+ "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
+
+ "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
+
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+ "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
+
+ "vitepress": ["vitepress@1.6.4", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="],
+
+ "vue": ["vue@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", "@vue/runtime-dom": "3.5.30", "@vue/server-renderer": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ }
+}
diff --git a/docs/guide/api.md b/docs/guide/api.md
new file mode 100644
index 00000000..f00d51f5
--- /dev/null
+++ b/docs/guide/api.md
@@ -0,0 +1,535 @@
+# REST API
+
+Rustalink implements the Lavalink v4 API. Below are the supported endpoints and their raw responses.
+
+## GET /v4/info
+
+Get basic server information.
+
+**Example request:**
+```bash
+curl -X GET -H "Authorization: youshallnotpass" "http://localhost:2333/v4/info"
+```
+
+
+Raw API response (Expand)
+
+```json
+{
+ "version": {
+ "semver": "1.0.5",
+ "major": 1,
+ "minor": 0,
+ "patch": 5,
+ "preRelease": null
+ },
+ "buildTime": 1773050806122,
+ "git": {
+ "branch": "dev",
+ "commit": "802.....",
+ "commitTime": 1773050774000
+ },
+ "jvm": "Rust",
+ "lavaplayer": "symphonia",
+ "sourceManagers": [
+ "youtube",
+ "spotify",
+ "jiosaavn",
+ "gaana",
+ "tidal",
+ "audiomack",
+ "shazam",
+ "mixcloud",
+ "bandcamp",
+ "reddit",
+ "audius",
+ "netease",
+ "http",
+ "local"
+ ],
+ "filters": [
+ "volume",
+ "equalizer",
+ "karaoke",
+ "timescale",
+ "tremolo",
+ "vibrato",
+ "distortion",
+ "rotation",
+ "channelMix",
+ "lowPass",
+ "echo",
+ "highPass",
+ "normalization",
+ "chorus",
+ "compressor",
+ "flanger",
+ "phaser",
+ "phonograph",
+ "reverb",
+ "spatial",
+ "pluginFilters"
+ ],
+ "plugins": []
+}
+```
+
+
+## GET /v4/stats
+
+Get server statistics.
+
+**Example request:**
+```bash
+curl -X GET -H "Authorization: youshallnotpass" "http://localhost:2333/v4/stats"
+```
+
+
+Raw API response (Expand)
+
+```json
+{
+ "players": 0,
+ "playingPlayers": 0,
+ "uptime": 298962,
+ "memory": {
+ "free": 19219042304,
+ "used": 49045504,
+ "allocated": 49045504,
+ "reservable": 32953487360
+ },
+ "cpu": {
+ "cores": 8,
+ "systemLoad": 0.31881649017333985,
+ "lavalinkLoad": 0.00006302547175437213
+ }
+}
+```
+
+
+## GET /v4/routeplanner/status
+
+Get routeplanner status.
+
+**Example request:**
+```bash
+curl -X GET -H "Authorization: youshallnotpass" "http://localhost:2333/v4/routeplanner/status"
+```
+
+
+Raw API response (Expand)
+
+```json
+
+```
+
+
+## GET /v4/loadtracks?identifier=ytsearch:never+gonna+give+you+up
+
+Load a track.
+
+**Example request:**
+```bash
+curl -X GET -H "Authorization: youshallnotpass" "http://localhost:2333/v4/loadtracks?identifier=ytsearch:never+gonna+give+you+up"
+```
+
+
+Raw API response (Expand)
+
+```json
+{
+ "loadType": "search",
+ "data": [
+ {
+ "encoded": "QAAA4wMARFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIFZpZGVvKSAoNEsgUmVtYXN0ZXIpAAtSaWNrIEFzdGxleQAAAAAAA0PwAAtkUXc0dzlXZ1hjUQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PWRRdzR3OVdnWGNRAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL2RRdzR3OVdnWGNRL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA",
+ "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": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAwQMAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADkFtYXppbmcgTHlyaWNzAAAAAAADQ/AACzdGd0RQMTdYUGxrAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9N0Z3RFAxN1hQbGsBADBodHRwczovL2kueXRpbWcuY29tL3ZpLzdGd0RQMTdYUGxrL3NkZGVmYXVsdC5qcGcAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "7FwDP17XPlk",
+ "isSeekable": true,
+ "author": "Amazing Lyrics",
+ "length": 214000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - Never Gonna Give You Up",
+ "uri": "https://www.youtube.com/watch?v=7FwDP17XPlk",
+ "artworkUrl": "https://i.ytimg.com/vi/7FwDP17XPlk/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA4gMAMUluc3VyQUFBbmNlICYgUmljayBBc3RsZXkgTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAAI0NTQUEgSW5zdXJhbmNlIEdyb3VwLCBhIEFBQSBJbnN1cmVyAAAAAAAA/egAC0d0TDFodWluOUVFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9R3RMMWh1aW45RUUBADBodHRwczovL2kueXRpbWcuY29tL3ZpL0d0TDFodWluOUVFL3NkZGVmYXVsdC5qcGcAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "GtL1huin9EE",
+ "isSeekable": true,
+ "author": "CSAA Insurance Group, a AAA Insurer",
+ "length": 65000,
+ "isStream": false,
+ "position": 0,
+ "title": "InsurAAAnce & Rick Astley Never Gonna Give You Up",
+ "uri": "https://www.youtube.com/watch?v=GtL1huin9EE",
+ "artworkUrl": "https://i.ytimg.com/vi/GtL1huin9EE/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA0QMAQE5ldmVyIEdvbm5hIEdpdmUgWW91IFVwIHwgUmljayBBc3RsZXkgUm9ja3MgTmV3IFllYXIncyBFdmUgLSBCQkMAA0JCQwAAAAAAA5IQAAtYR3hJRTFocjB3NAABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PVhHeElFMWhyMHc0AQAwaHR0cHM6Ly9pLnl0aW1nLmNvbS92aS9YR3hJRTFocjB3NC9zZGRlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA",
+ "info": {
+ "identifier": "XGxIE1hr0w4",
+ "isSeekable": true,
+ "author": "BBC",
+ "length": 234000,
+ "isStream": false,
+ "position": 0,
+ "title": "Never Gonna Give You Up | Rick Astley Rocks New Year's Eve - BBC",
+ "uri": "https://www.youtube.com/watch?v=XGxIE1hr0w4",
+ "artworkUrl": "https://i.ytimg.com/vi/XGxIE1hr0w4/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAwQMAJEZhbWlseSBHdXkgLSBOZXZlciBHb25uYSBHaXZlIFlvdSBVcAAPQXJyaWYgSmFsYWx1ZGluAAAAAAABpeAAC0RzQzhqUVhSYlFFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9RHNDOGpRWFJiUUUBADBodHRwczovL2kueXRpbWcuY29tL3ZpL0RzQzhqUVhSYlFFL2hxZGVmYXVsdC5qcGcAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "DsC8jQXRbQE",
+ "isSeekable": true,
+ "author": "Arrif Jalaludin",
+ "length": 108000,
+ "isStream": false,
+ "position": 0,
+ "title": "Family Guy - Never Gonna Give You Up",
+ "uri": "https://www.youtube.com/watch?v=DsC8jQXRbQE",
+ "artworkUrl": "https://i.ytimg.com/vi/DsC8jQXRbQE/hqdefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAxgMAJ05ldmVyIEdvbm5hIEdpdmUgWW91IFVwICgyMDIyIFJlbWFzdGVyKQALUmljayBBc3RsZXkAAAAAAAND8AALM0JGVGlvNTI5NncAAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj0zQkZUaW81Mjk2dwEANmh0dHBzOi8vaS55dGltZy5jb20vdmlfd2VicC8zQkZUaW81Mjk2dy9zZGRlZmF1bHQud2VicAAAB3lvdXR1YmUAAAAAAAAAAA==",
+ "info": {
+ "identifier": "3BFTio5296w",
+ "isSeekable": true,
+ "author": "Rick Astley",
+ "length": 214000,
+ "isStream": false,
+ "position": 0,
+ "title": "Never Gonna Give You Up (2022 Remaster)",
+ "uri": "https://www.youtube.com/watch?v=3BFTio5296w",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/3BFTio5296w/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA2QMAOuOAkOaXpeacrOiqnuWtl+W5leOAkVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAAC1JpY2sgQXN0bGV5AAAAAAADQ/AAC1JyRVN2U1JOcGVvAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9UnJFU3ZTUk5wZW8BADZodHRwczovL2kueXRpbWcuY29tL3ZpX3dlYnAvUnJFU3ZTUk5wZW8vc2RkZWZhdWx0LndlYnAAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "RrESvSRNpeo",
+ "isSeekable": true,
+ "author": "Rick Astley",
+ "length": 214000,
+ "isStream": false,
+ "position": 0,
+ "title": "【日本語字幕】Rick Astley - Never Gonna Give You Up",
+ "uri": "https://www.youtube.com/watch?v=RrESvSRNpeo",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/RrESvSRNpeo/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAywMALk5ldmVyIEdvbm5hIEdpdmUgWW91IFVwIC0gUmljayBBc3RsZXkgKEx5cmljcykAD0ludml0ZWQgS2luZ2RvbQAAAAAAA0PwAAtNT2NRYVI3X1dybwABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PU1PY1FhUjdfV3JvAQAwaHR0cHM6Ly9pLnl0aW1nLmNvbS92aS9NT2NRYVI3X1dyby9zZGRlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA",
+ "info": {
+ "identifier": "MOcQaR7_Wro",
+ "isSeekable": true,
+ "author": "Invited Kingdom",
+ "length": 214000,
+ "isStream": false,
+ "position": 0,
+ "title": "Never Gonna Give You Up - Rick Astley (Lyrics)",
+ "uri": "https://www.youtube.com/watch?v=MOcQaR7_Wro",
+ "artworkUrl": "https://i.ytimg.com/vi/MOcQaR7_Wro/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAyQMANUJhcnJ5IFdoaXRlIC0gTmV2ZXIgTmV2ZXIgR29ubmEgR2l2ZSBZYSBVcCDigKIgVG9wUG9wAAZUb3BQb3AAAAAAAAOhsAALSzV6UDdlUWx0REUAAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1LNXpQN2VRbHRERQEAMGh0dHBzOi8vaS55dGltZy5jb20vdmkvSzV6UDdlUWx0REUvc2RkZWZhdWx0LmpwZwAAB3lvdXR1YmUAAAAAAAAAAA==",
+ "info": {
+ "identifier": "K5zP7eQltDE",
+ "isSeekable": true,
+ "author": "TopPop",
+ "length": 238000,
+ "isStream": false,
+ "position": 0,
+ "title": "Barry White - Never Never Gonna Give Ya Up • TopPop",
+ "uri": "https://www.youtube.com/watch?v=K5zP7eQltDE",
+ "artworkUrl": "https://i.ytimg.com/vi/K5zP7eQltDE/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAvAMAHU5ldmVyLCBOZXZlciBHb25uYSBHaXZlIFlhIFVwAAtCYXJyeSBXaGl0ZQAAAAAAB0dIAAtRcGJoU2xjZV9lawABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PVFwYmhTbGNlX2VrAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL1FwYmhTbGNlX2VrL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA",
+ "info": {
+ "identifier": "QpbhSlce_ek",
+ "isSeekable": true,
+ "author": "Barry White",
+ "length": 477000,
+ "isStream": false,
+ "position": 0,
+ "title": "Never, Never Gonna Give Ya Up",
+ "uri": "https://www.youtube.com/watch?v=QpbhSlce_ek",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/QpbhSlce_ek/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA4wMARFJpY2sgQXN0bGV5ICAtIE5ldmVyIEdvbm5hIEdpdmUgWW91IFVwIChQaWFub2ZvcnRlKSAoT2ZmaWNpYWwgQXVkaW8pAAtSaWNrIEFzdGxleQAAAAAAAzRQAAtHSE1qRDBMcDVEWQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PUdITWpEMExwNURZAQA2aHR0cHM6Ly9pLnl0aW1nLmNvbS92aV93ZWJwL0dITWpEMExwNURZL3NkZGVmYXVsdC53ZWJwAAAHeW91dHViZQAAAAAAAAAA",
+ "info": {
+ "identifier": "GHMjD0Lp5DY",
+ "isSeekable": true,
+ "author": "Rick Astley",
+ "length": 210000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - Never Gonna Give You Up (Pianoforte) (Official Audio)",
+ "uri": "https://www.youtube.com/watch?v=GHMjD0Lp5DY",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/GHMjD0Lp5DY/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA+gMAWVLEq8SLYSDEkmFzdGzEk2FoIC0gTmV2ZXIgZ29ubmEgZ2l2ZSB5b3UgdXAgQ292ZXIgSW4gT2xkIEVuZ2xpc2guIEJhcmRjb3JlL01lZGlldmFsIHN0eWxlABN0aGVfbWlyYWNsZV9hbGlnbmVyAAAAAAACfLgAC2NFcmdNSlNncHYwAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9Y0VyZ01KU2dwdjABADBodHRwczovL2kueXRpbWcuY29tL3ZpL2NFcmdNSlNncHYwL3NkZGVmYXVsdC5qcGcAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "cErgMJSgpv0",
+ "isSeekable": true,
+ "author": "the_miracle_aligner",
+ "length": 163000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rīċa Ēastlēah - Never gonna give you up Cover In Old English. Bardcore/Medieval style",
+ "uri": "https://www.youtube.com/watch?v=cErgMJSgpv0",
+ "artworkUrl": "https://i.ytimg.com/vi/cErgMJSgpv0/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA3gMAP1JpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKE9mZmljaWFsIEFuaW1hdGVkIFZpZGVvKQALUmljayBBc3RsZXkAAAAAAANACAALTExGaEthcW5Xd2sAAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1MTEZoS2Fxbld3awEANmh0dHBzOi8vaS55dGltZy5jb20vdmlfd2VicC9MTEZoS2Fxbld3ay9zZGRlZmF1bHQud2VicAAAB3lvdXR1YmUAAAAAAAAAAA==",
+ "info": {
+ "identifier": "LLFhKaqnWwk",
+ "isSeekable": true,
+ "author": "Rick Astley",
+ "length": 213000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - Never Gonna Give You Up (Official Animated Video)",
+ "uri": "https://www.youtube.com/watch?v=LLFhKaqnWwk",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/LLFhKaqnWwk/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA1QMALlJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKEx5cmljcykAE1lvdW5nIFBpbGdyaW0gTXVzaWMAAAAAAAN6oAALNlBMYXRQTW94R3cAAQAraHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj02UExhdFBNb3hHdwEANmh0dHBzOi8vaS55dGltZy5jb20vdmlfd2VicC82UExhdFBNb3hHdy9zZGRlZmF1bHQud2VicAAAB3lvdXR1YmUAAAAAAAAAAA==",
+ "info": {
+ "identifier": "6PLatPMoxGw",
+ "isSeekable": true,
+ "author": "Young Pilgrim Music",
+ "length": 228000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - Never Gonna Give You Up (Lyrics)",
+ "uri": "https://www.youtube.com/watch?v=6PLatPMoxGw",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/6PLatPMoxGw/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA1gMAOExpc2EgU3RhbnNmaWVsZCAtIE5ldmVyLCBOZXZlciBHb25uYSBHaXZlIFlvdSBVcCAoVmlkZW8pABBMaXNhU3RhbnNmaWVsZHR2AAAAAAAEFuAAC3B6WXBIWEN1bUlJAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9cHpZcEhYQ3VtSUkBADBodHRwczovL2kueXRpbWcuY29tL3ZpL3B6WXBIWEN1bUlJL3NkZGVmYXVsdC5qcGcAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "pzYpHXCumII",
+ "isSeekable": true,
+ "author": "LisaStansfieldtv",
+ "length": 268000,
+ "isStream": false,
+ "position": 0,
+ "title": "Lisa Stansfield - Never, Never Gonna Give You Up (Video)",
+ "uri": "https://www.youtube.com/watch?v=pzYpHXCumII",
+ "artworkUrl": "https://i.ytimg.com/vi/pzYpHXCumII/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA6QMAT1JpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgW1JlbWFzdGVyZWQgSW4gNEtdIChPZmZpY2lhbCBNdXNpYyBWaWRlbykADEVuam95IGl08J+kjQAAAAAAAzwgAAtMUTR3OXhpSGtyWQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PUxRNHc5eGlIa3JZAQAwaHR0cHM6Ly9pLnl0aW1nLmNvbS92aS9MUTR3OXhpSGtyWS9zZGRlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA",
+ "info": {
+ "identifier": "LQ4w9xiHkrY",
+ "isSeekable": true,
+ "author": "Enjoy it🤍",
+ "length": 212000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - Never Gonna Give You Up [Remastered In 4K] (Official Music Video)",
+ "uri": "https://www.youtube.com/watch?v=LQ4w9xiHkrY",
+ "artworkUrl": "https://i.ytimg.com/vi/LQ4w9xiHkrY/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAAywMALk5ldmVyIEdvbm5hIEdpdmUgWW91IFVwIChMeXJpY3MpIC0gUmljayBBc3RsZXkAD0dpb3Zhbm5hIExvemFubwAAAAAAA0AIAAtTYllYa09Bb1pwSQABACtodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PVNiWVhrT0FvWnBJAQAwaHR0cHM6Ly9pLnl0aW1nLmNvbS92aS9TYllYa09Bb1pwSS9zZGRlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA",
+ "info": {
+ "identifier": "SbYXkOAoZpI",
+ "isSeekable": true,
+ "author": "Giovanna Lozano",
+ "length": 213000,
+ "isStream": false,
+ "position": 0,
+ "title": "Never Gonna Give You Up (Lyrics) - Rick Astley",
+ "uri": "https://www.youtube.com/watch?v=SbYXkOAoZpI",
+ "artworkUrl": "https://i.ytimg.com/vi/SbYXkOAoZpI/sddefault.jpg",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA4gMAO1JpY2sgQXN0bGV5IC0gTkVWRVIgR09OTkEgR0lWRSBZT1UgVVAgKFN1bmcgYnkgMTY5IE1vdmllcyEpABNUaGUgVW51c3VhbCBTdXNwZWN0AAAAAAACm/gAC2VweVJVcDBCaHJBAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZXB5UlVwMEJockEBADZodHRwczovL2kueXRpbWcuY29tL3ZpX3dlYnAvZXB5UlVwMEJockEvc2RkZWZhdWx0LndlYnAAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "epyRUp0BhrA",
+ "isSeekable": true,
+ "author": "The Unusual Suspect",
+ "length": 171000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - NEVER GONNA GIVE YOU UP (Sung by 169 Movies!)",
+ "uri": "https://www.youtube.com/watch?v=epyRUp0BhrA",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/epyRUp0BhrA/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ },
+ {
+ "encoded": "QAAA3wMAQFJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAgKFBpYW5vZm9ydGUpIChQZXJmb3JtYW5jZSkAC1JpY2sgQXN0bGV5AAAAAAADX0gAC3JUZ2E0MXIzYTRzAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9clRnYTQxcjNhNHMBADZodHRwczovL2kueXRpbWcuY29tL3ZpX3dlYnAvclRnYTQxcjNhNHMvc2RkZWZhdWx0LndlYnAAAAd5b3V0dWJlAAAAAAAAAAA=",
+ "info": {
+ "identifier": "rTga41r3a4s",
+ "isSeekable": true,
+ "author": "Rick Astley",
+ "length": 221000,
+ "isStream": false,
+ "position": 0,
+ "title": "Rick Astley - Never Gonna Give You Up (Pianoforte) (Performance)",
+ "uri": "https://www.youtube.com/watch?v=rTga41r3a4s",
+ "artworkUrl": "https://i.ytimg.com/vi_webp/rTga41r3a4s/sddefault.webp",
+ "isrc": null,
+ "sourceName": "youtube"
+ },
+ "pluginInfo": {},
+ "userData": {}
+ }
+ ]
+}
+```
+
+
+## GET /v4/sessions/xyz
+
+Get session information (returns 404/Error if invalid).
+
+**Example request:**
+```bash
+curl -X GET -H "Authorization: youshallnotpass" "http://localhost:2333/v4/sessions/xyz"
+```
+
+
+Raw API response (Expand)
+
+```json
+{
+ "timestamp": 1773058695473,
+ "status": 404,
+ "error": "Not Found",
+ "message": "Session not found: xyz",
+ "path": "/v4/sessions/xyz"
+}
+```
+
+
+## PATCH /v4/sessions/xyz
+
+Update session properties.
+
+**Example request:**
+```bash
+curl -X PATCH -H "Authorization: youshallnotpass" -H "Content-Type: application/json" -d '{"resuming": true, "timeout": 60}' "http://localhost:2333/v4/sessions/xyz"
+```
+
+
+Raw API response (Expand)
+
+```json
+{
+ "timestamp": 1773058695481,
+ "status": 404,
+ "error": "Not Found",
+ "message": "Session not found",
+ "path": "/v4/sessions/xyz"
+}
+```
+
+
diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md
new file mode 100644
index 00000000..a5d4ef9d
--- /dev/null
+++ b/docs/guide/architecture.md
@@ -0,0 +1,20 @@
+# Architecture
+
+Rustalink is built on a modern, asynchronous foundation using Tokio, ensuring low latency and high concurrency.
+
+## Core Components
+
+- **REST API:** Powered by Axum for lightning-fast JSON processing.
+- **WebSocket Manager:** Real-time state synchronization and command routing.
+- **Audio Pipeline:** Efficient raw packet decoding, routing, and encoding.
+- **Filter Engine:** 32-bit floating-point DSP engine for studio-grade effects.
+
+## Data Flow
+
+```mermaid
+graph LR
+ Client --> API[REST / WS]
+ API --> Pipeline
+ Pipeline --> Filters
+ Filters --> Discord[Discord Voice]
+```
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md
new file mode 100644
index 00000000..f4b0a689
--- /dev/null
+++ b/docs/guide/configuration.md
@@ -0,0 +1,18 @@
+# Configuration
+
+Rustalink relies on a `config.toml` file for predictable, readable configuration.
+
+## Example
+
+```toml
+[server]
+port = 2333
+address = "0.0.0.0"
+password = "youshallnotpass"
+
+[audio]
+item_timeout_ms = 3000
+
+[plugins]
+# Plugin configurations go here
+```
diff --git a/docs/guide/docker.md b/docs/guide/docker.md
new file mode 100644
index 00000000..0455748e
--- /dev/null
+++ b/docs/guide/docker.md
@@ -0,0 +1,26 @@
+# Docker
+
+The recommended, zero-hassle way to deploy Rustalink.
+
+## `docker-compose.yml`
+
+```yaml
+
+services:
+ rustalink:
+ image: ghcr.io/appujet/rustalink:latest
+ container_name: rustalink
+ restart: unless-stopped
+ ports:
+ - "2333:2333"
+ environment:
+ - RUST_LOG=info
+ volumes:
+ - ./config.toml:/app/config.toml
+```
+
+## Start the Container
+
+```bash
+docker-compose up -d
+```
diff --git a/docs/guide/filters.md b/docs/guide/filters.md
new file mode 100644
index 00000000..182d94e0
--- /dev/null
+++ b/docs/guide/filters.md
@@ -0,0 +1,15 @@
+# Filters
+
+Rustalink features a suite of built-in, `f32`-precision audio filters, eliminating integer quantization noise for crystal-clear playback.
+
+## Available Filters
+
+- **Volume:** Gain adjustments.
+- **Equalizer:** 15-band EQ.
+- **Timescale:** Modify speed, pitch, and rate seamlessly.
+- **Tremolo:** Periodic volume fluctuation.
+- **Vibrato:** Periodic pitch fluctuation.
+- **Rotation:** 8D spatial panning.
+- **Distortion:** Saturation and clipping effects.
+- **ChannelMix:** Custom audio matrix layouts.
+- **LowPass:** High-frequency softening.
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
new file mode 100644
index 00000000..d584f83e
--- /dev/null
+++ b/docs/guide/installation.md
@@ -0,0 +1,55 @@
+# Installation
+
+Get Rustalink up and running in minutes. Whether you prefer building from source or using our pre-packaged binaries, we've got you covered.
+
+## Prerequisites
+
+Before building from source, ensure you have the following installed on your system:
+
+- **[Rust](https://rustup.rs/)** (latest stable toolchain)
+- **CMake** (required for building certain C dependencies)
+- **OpenSSL** (required for secure connections)
+
+## Install from Releases
+
+The fastest way to get started is by downloading a pre-compiled binary from our GitHub Releases page.
+
+1. Navigate to the [Rustalink Releases](https://github.com/appujet/Rustalink/releases) page.
+2. Download the latest archive for your operating system (Windows, macOS, or Linux).
+3. Extract the binary and place it in your desired folder.
+4. Download the `config.example.toml`, rename it to `config.toml`, and place it in the same directory.
+5. Execute the binary: `./rustalink` (or `rustalink.exe` on Windows).
+
+## Build from Source
+
+Compiling Rustalink from source guarantees you get the very latest performance optimizations specific to your machine architecture.
+
+1. **Clone the repository:**
+
+ ```bash
+ git clone https://github.com/appujet/Rustalink.git
+ cd Rustalink
+ ```
+
+2. **Build the project:**
+ This step will download dependencies and compile the server. It might take a few minutes on the first run.
+
+ ```bash
+ cargo build --release
+ ```
+
+## Run the Server
+
+Before starting, ensure your `config.toml` is correctly configured in your working directory. You can copy the example configuration:
+
+```bash
+cp config.example.toml config.toml
+```
+
+Start the compiled binary:
+
+```bash
+./target/release/rustalink
+```
+
+Once running, you should see logs indicating that the server is listening for connections. You're now ready to connect your Lavalink clients!
diff --git a/docs/guide/pterodactyl.md b/docs/guide/pterodactyl.md
new file mode 100644
index 00000000..355f6aaf
--- /dev/null
+++ b/docs/guide/pterodactyl.md
@@ -0,0 +1,115 @@
+# Pterodactyl Hosting
+
+This guide explains how to host your own Rustalink node on a Pterodactyl panel.
+
+## Prerequisites
+
+- A Pterodactyl panel with administrative access (to import the egg).
+- A node with Docker support.
+
+## Importing the Egg
+
+1. Download the `egg-rustalink.json` file from the button below, or copy the content from the configuration block.
+
+ Download egg-rustalink.json
+
+ ::: details Click to expand egg configuration
+
+ ```json
+ {
+ "_comment": "do not edit this file, use the pterodactyl panel to edit the egg",
+ "meta": {
+ "version": "PTDL_v2",
+ "update_url": null
+ },
+ "exported_at": "2026-03-09T13:15:00Z",
+ "name": "Rustalink",
+ "author": "sdipedit@gmail.com",
+ "description": "Rustalink is a high-performance, standalone Discord audio sending node written in Rust.",
+ "features": null,
+ "docker_images": {
+ "ghcr.io/bongodevs/rustalink:latest": "ghcr.io/bongodevs/rustalink:latest"
+ },
+ "file_denylist": [],
+ "startup": "/app/rustalink",
+ "config": {
+ "files": {
+ "config.toml": {
+ "parser": "toml",
+ "find": {
+ "server.address": "0.0.0.0",
+ "server.port": "{{server.build.default.port}}",
+ "server.authorization": "{{env.SERVER_AUTH}}"
+ }
+ }
+ },
+ "startup": {
+ "done": "Listening on"
+ },
+ "logs": {
+ "custom": true,
+ "location": "logs/rustalink.log"
+ },
+ "stop": "^C"
+ },
+ "scripts": {
+ "installation": {
+ "container": "debian:bookworm-slim",
+ "entrypoint": "bash",
+ "script": "#!/bin/bash\n# Installation script for Rustalink\n\napt update\napt install -y curl\n\nif [ ! -f config.toml ]; then\n echo \"Downloading default config...\"\n curl -sSL https://raw.githubusercontent.com/bongodevs/Rustalink/HEAD/config.example.toml -o config.toml\nfi"
+ }
+ },
+ "variables": [
+ {
+ "name": "Server Password",
+ "description": "The password required to connect to this Rustalink node.",
+ "env_variable": "SERVER_AUTH",
+ "default_value": "youshallnotpass",
+ "user_viewable": true,
+ "user_editable": true,
+ "rules": "required|string|min:1"
+ }
+ ]
+ }
+ ```
+
+ :::
+
+2. Log in to your Pterodactyl panel as an administrator.
+3. Go to **Nests** in the admin sidebar.
+4. Select a nest (e.g., "Generic") or create a new one.
+5. Click **Import Egg** in the top right.
+6. Select the `egg-rustalink.json` file you downloaded earlier.
+7. Click **Import**.
+
+## Creating the Server
+
+1. Go to the **Servers** section in the admin panel.
+2. Click **Create New**.
+3. Fill in the server details (Name, Owner, etc.).
+4. In the **Nest Configuration** section, select the nest where you imported the egg and choose **Rustalink** as the egg.
+5. Set the **Docker Image** to `ghcr.io/bongodevs/rustalink:latest`.
+6. Configure the resource limits as needed.
+7. Click **Create Server**.
+
+## Configuration
+
+Once the server is created, Pterodactyl will automatically generate a `config.toml` file if it doesn't exist (using the installation script).
+
+### Environment Variables
+
+You can configure basic settings through the **Startup** tab in the server console:
+
+- **Server Password**: Set the `SERVER_AUTH` variable to your desired authorization token.
+- **Server Port**: This is automatically linked to the server's primary allocation.
+
+### Advanced Configuration
+
+For more advanced settings, you can edit the `config.toml` file directly via the **File Manager**.
+
+> [!TIP]
+> Make sure the `server.address` is set to `0.0.0.0` inside `config.toml` to allow external connections (the egg handles this by default).
+
+## Support
+
+If you encounter any issues, feel free to open an issue on our [GitHub repository](https://github.com/bongodevs/Rustalink/issues).
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..88eba12e
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,4 @@
+---
+layout: doc
+---
+# WIP
\ No newline at end of file
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 00000000..074a81ed
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "docs",
+ "version": "1.0.0",
+ "description": "Rustalink Documentation",
+ "type": "module",
+ "scripts": {
+ "docs:dev": "vitepress dev",
+ "docs:build": "vitepress build",
+ "docs:preview": "vitepress preview"
+ },
+ "dependencies": {
+ "vitepress": "^1.6.4",
+ "vue": "^3.5.30"
+ }
+}
\ No newline at end of file
diff --git a/docs/public/egg-rustalink.json b/docs/public/egg-rustalink.json
new file mode 100644
index 00000000..b644da71
--- /dev/null
+++ b/docs/public/egg-rustalink.json
@@ -0,0 +1,55 @@
+{
+ "_comment": "do not edit this file, use the pterodactyl panel to edit the egg",
+ "meta": {
+ "version": "PTDL_v2",
+ "update_url": null
+ },
+ "exported_at": "2026-03-09T13:15:00Z",
+ "name": "Rustalink",
+ "author": "sdipedit@gmail.com",
+ "description": "Rustalink is a high-performance, standalone Discord audio sending node written in Rust.",
+ "features": null,
+ "docker_images": {
+ "ghcr.io/bongodevs/rustalink:latest": "ghcr.io/bongodevs/rustalink:latest"
+ },
+ "file_denylist": [],
+ "startup": "/app/rustalink",
+ "config": {
+ "files": {
+ "config.toml": {
+ "parser": "toml",
+ "find": {
+ "server.address": "0.0.0.0",
+ "server.port": "{{server.build.default.port}}",
+ "server.authorization": "{{env.SERVER_AUTH}}"
+ }
+ }
+ },
+ "startup": {
+ "done": "Listening on"
+ },
+ "logs": {
+ "custom": true,
+ "location": "logs/rustalink.log"
+ },
+ "stop": "^C"
+ },
+ "scripts": {
+ "installation": {
+ "container": "debian:bookworm-slim",
+ "entrypoint": "bash",
+ "script": "#!/bin/bash\n# Installation script for Rustalink\n\napt update\napt install -y curl\n\nif [ ! -f config.toml ]; then\n echo \"Downloading default config...\"\n curl -sSL https://raw.githubusercontent.com/bongodevs/Rustalink/HEAD/config.example.toml -o config.toml\nfi"
+ }
+ },
+ "variables": [
+ {
+ "name": "Server Password",
+ "description": "The password required to connect to this Rustalink node.",
+ "env_variable": "SERVER_AUTH",
+ "default_value": "youshallnotpass",
+ "user_viewable": true,
+ "user_editable": true,
+ "rules": "required|string|min:1"
+ }
+ ]
+}
diff --git a/docs/public/logo.svg b/docs/public/logo.svg
new file mode 100644
index 00000000..4a5fd183
--- /dev/null
+++ b/docs/public/logo.svg
@@ -0,0 +1,430 @@
+
+
+
+
diff --git a/src/audio/constants.rs b/src/audio/constants.rs
index 0af33469..80f849d8 100644
--- a/src/audio/constants.rs
+++ b/src/audio/constants.rs
@@ -30,13 +30,13 @@ pub const LAYER_BUFFER_SIZE: usize = 1_024 * 1_024;
// ── Segmented remote reader ──────────────────────────────────────────────────
-pub const CHUNK_SIZE: usize = 128 * 1_024;
-pub const PREFETCH_CHUNKS: usize = 2;
+pub const CHUNK_SIZE: usize = 256 * 1_024;
+pub const PREFETCH_CHUNKS: usize = 4;
pub const MAX_CONCURRENT_FETCHES: usize = 2;
pub const HTTP_CLIENT_TIMEOUT_SECS: u64 = 15;
pub const MAX_FETCH_RETRIES: u32 = 5;
pub const WORKER_IDLE_MS: u64 = 50;
-pub const FETCH_WAIT_MS: u64 = 500;
+pub const FETCH_WAIT_MS: u64 = 250;
pub const PROBE_TIMEOUT_SECS: u64 = 10;
// ── HttpSource ───────────────────────────────────────────────────────────────
diff --git a/src/audio/demux/format.rs b/src/audio/demux/format.rs
index 6d2c62df..b6b84d17 100644
--- a/src/audio/demux/format.rs
+++ b/src/audio/demux/format.rs
@@ -1,5 +1,3 @@
-//! Audio format detection via header byte sniffing.
-
use crate::common::types::AudioFormat;
/// Sniff the container format from the first bytes of arbitrary data.
diff --git a/src/audio/demux/mod.rs b/src/audio/demux/mod.rs
index 98c9d2ea..6a550e88 100644
--- a/src/audio/demux/mod.rs
+++ b/src/audio/demux/mod.rs
@@ -1,17 +1,3 @@
-//! Demux layer — format detection and container parsing.
-//!
-//! # Usage
-//!
-//! ```rust
-//! use crate::audio::demux::{detect_format, AudioFormat};
-//!
-//! let header = &bytes[..12]; // first bytes of the stream
-//! match detect_format(header) {
-//! AudioFormat::WebmOpus => { /* use WebmOpusDemuxer */ }
-//! _ => { /* use AudioProcessor transcode path */ }
-//! }
-//! ```
-
pub mod format;
pub mod webm_opus;
@@ -29,9 +15,7 @@ pub use webm_opus::WebmOpusDemuxer;
use crate::audio::constants::{MIXER_CHANNELS, TARGET_SAMPLE_RATE};
pub use crate::common::types::AudioFormat;
-/// Resolved demux result returned by `open_format`.
pub enum DemuxResult {
- /// Symphonia-probed format reader + selected track id + codec decoder.
Transcode {
format: Box,
track_id: u32,
@@ -41,9 +25,6 @@ pub enum DemuxResult {
},
}
-/// Open a media source and detect its format.
-///
-/// Returns a `DemuxResult` describing the decode path, or a symphonia `Error`.
pub fn open_format(
source: Box,
kind: Option,
diff --git a/src/audio/filters/timescale.rs b/src/audio/filters/timescale.rs
index a0068075..973170c6 100644
--- a/src/audio/filters/timescale.rs
+++ b/src/audio/filters/timescale.rs
@@ -49,7 +49,7 @@ impl TimescaleFilter {
let num_input_samples = self.input_buffer.len();
let num_input_frames = num_input_samples / 2;
-
+
if num_input_frames < 4 {
return Vec::new();
}
@@ -87,7 +87,7 @@ impl TimescaleFilter {
let consumed_frames = self.position.floor() as usize;
let keep_from_frame = consumed_frames.saturating_sub(1);
-
+
if keep_from_frame > 0 {
let samples_to_drain = keep_from_frame * 2;
if samples_to_drain < self.input_buffer.len() {
diff --git a/src/audio/source/client.rs b/src/audio/source/client.rs
index e2cd1307..daefc2bc 100644
--- a/src/audio/source/client.rs
+++ b/src/audio/source/client.rs
@@ -14,7 +14,7 @@ pub fn create_client(
let mut builder = Client::builder()
.user_agent(user_agent)
.connect_timeout(Duration::from_secs(5))
- .read_timeout(Duration::from_secs(30))
+ .read_timeout(Duration::from_secs(8))
.tcp_nodelay(true)
.tcp_keepalive(Duration::from_secs(25))
.pool_max_idle_per_host(64)
diff --git a/src/audio/source/segmented.rs b/src/audio/source/segmented.rs
index 931e763d..878828f9 100644
--- a/src/audio/source/segmented.rs
+++ b/src/audio/source/segmented.rs
@@ -274,6 +274,10 @@ async fn fetch_worker(
claimed
}
.map(|(idx, retries)| {
+ debug!(
+ "Worker {}: claiming chunk {} (retry={})",
+ worker_id, idx, retries
+ );
state.chunks.insert(idx, ChunkState::Downloading);
(idx, retries, total_len)
})
@@ -297,7 +301,23 @@ async fn fetch_worker(
match fetch_chunk(&client, &url, offset, size).await {
Ok(bytes) => {
- let actual = bytes.len();
+ let actual = bytes.len() as u64;
+ if actual != size {
+ warn!(
+ "Worker {}: partial fetch for chunk {} (got {}/{} bytes)",
+ worker_id, idx, actual, size
+ );
+ requeue_or_fatal(
+ lock,
+ cvar,
+ idx,
+ prior_retries,
+ &format!("partial fetch: {}/{} bytes", actual, size),
+ );
+ tokio::time::sleep(Duration::from_millis(FETCH_WAIT_MS)).await;
+ continue;
+ }
+
let mut state = lock.lock();
state.chunks.insert(idx, ChunkState::Ready(bytes));
trace!(
@@ -340,10 +360,12 @@ fn requeue_or_fatal(
) {
let mut state = lock.lock();
if prior_retries >= MAX_FETCH_RETRIES {
- state.fatal_error = Some(format!(
+ let msg = format!(
"Chunk {}: permanently failed after {} retries: {}",
idx, prior_retries, error
- ));
+ );
+ warn!("SegmentedSource: fatal error - {}", msg);
+ state.fatal_error = Some(msg);
} else {
state
.chunks
diff --git a/src/config/server.rs b/src/config/server.rs
index d9cee819..b34673b2 100644
--- a/src/config/server.rs
+++ b/src/config/server.rs
@@ -81,6 +81,37 @@ pub struct RoutePlannerConfig {
#[serde(default)]
pub struct MirrorsConfig {
pub providers: Vec,
+ pub best_match: BestMatchConfig,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+#[serde(default)]
+pub struct BestMatchConfig {
+ pub scoring: bool,
+ pub throttled_prefixes: Vec,
+ pub min_similarity: f64,
+ pub high_confidence: f64,
+ pub immediate_use: f64,
+ pub weight_title: f64,
+ pub weight_artist: f64,
+ pub weight_duration: f64,
+ pub duration_tolerance_ms: u64,
+}
+
+impl Default for BestMatchConfig {
+ fn default() -> Self {
+ Self {
+ scoring: true,
+ throttled_prefixes: vec!["ytmsearch:".into(), "ytsearch:".into()],
+ min_similarity: 0.50,
+ high_confidence: 0.75,
+ immediate_use: 0.88,
+ weight_title: 0.50,
+ weight_artist: 0.30,
+ weight_duration: 0.20,
+ duration_tolerance_ms: 3_000,
+ }
+ }
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
diff --git a/src/gateway/encryption.rs b/src/gateway/encryption.rs
index d93670d1..cd12bdf5 100644
--- a/src/gateway/encryption.rs
+++ b/src/gateway/encryption.rs
@@ -28,10 +28,13 @@ pub struct DaveHandler {
external_sender_set: bool,
pending_proposals: Vec>,
was_ready: bool,
+ recognized_users: HashSet,
}
impl DaveHandler {
pub fn new(user_id: UserId, channel_id: ChannelId) -> Self {
+ let mut recognized_users = HashSet::new();
+ recognized_users.insert(user_id);
Self {
session: None,
user_id,
@@ -41,10 +44,32 @@ impl DaveHandler {
external_sender_set: false,
pending_proposals: Vec::new(),
was_ready: false,
+ recognized_users,
}
}
+ pub fn add_users(&mut self, uids: &[u64]) {
+ for &uid in uids {
+ self.recognized_users.insert(UserId(uid));
+ }
+ debug!("DAVE adding users: {:?}", uids);
+ }
+
+ pub fn remove_user(&mut self, uid: u64) {
+ self.recognized_users.remove(&UserId(uid));
+ debug!("DAVE removing user: {}", uid);
+ }
+
+ pub fn set_protocol_version(&mut self, version: u16) {
+ self.protocol_version = version;
+ }
+
pub fn setup_session(&mut self, version: u16) -> AnyResult> {
+ if version == 0 {
+ self.reset();
+ return Ok(Vec::new());
+ }
+
let nz_version = NonZeroU16::new(version).unwrap_or(DAVE_MIN_VERSION);
let session = if let Some(s) = &mut self.session {
@@ -98,19 +123,17 @@ impl DaveHandler {
}
}
- pub fn prepare_epoch(&mut self, epoch: u64, protocol_version: u16) {
- if epoch == 1
- && let Err(e) = self.setup_session(protocol_version)
- {
- warn!("DAVE prepare_epoch setup failed: {e}");
+ pub fn prepare_epoch(&mut self, epoch: u64, protocol_version: u16) -> Option> {
+ if epoch == 1 {
+ match self.setup_session(protocol_version) {
+ Ok(kp) => return Some(kp),
+ Err(e) => warn!("DAVE prepare_epoch setup failed: {e}"),
+ }
}
+ None
}
- pub fn process_external_sender(
- &mut self,
- data: &[u8],
- connected_users: &HashSet,
- ) -> AnyResult>> {
+ pub fn process_external_sender(&mut self, data: &[u8]) -> AnyResult>> {
let mut responses = Vec::new();
if let Some(session) = &mut self.session {
@@ -122,9 +145,10 @@ impl DaveHandler {
"DAVE processing {} buffered proposals",
self.pending_proposals.len()
);
+ let user_ids: Vec = self.recognized_users.iter().map(|u| u.0).collect();
for prop_data in std::mem::take(&mut self.pending_proposals) {
if let Ok(Some(res)) =
- Self::do_process_proposals(session, &prop_data, connected_users)
+ Self::do_process_proposals(session, &prop_data, &user_ids)
{
responses.push(res);
}
@@ -165,11 +189,7 @@ impl DaveHandler {
Ok(transition_id)
}
- pub fn process_proposals(
- &mut self,
- data: &[u8],
- connected_users: &HashSet,
- ) -> AnyResult