尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
musicrain is a self-hosted music player for the web. A SvelteKit app built to static files, it plays from a local file library, YouTube, Spotify and Deezer, keeps a queue, holds playlists and accounts, and can put several listeners in a room on the same track at the same position — inside about 50 ms of each other. The same build runs as an ordinary site, as an installable PWA, or embedded in a Discord voice channel as an Activity.
Currently running at.查看完整 README ↗musicrain is a self-hosted music player for the web. A SvelteKit app built to static files, it plays from a local file library, YouTube, Spotify and Deezer, keeps a queue, holds playlists and accounts, and can put several listeners in a room on the same track at the same position — inside about 50 ms of each other. The same build runs as an ordinary site, as an installable PWA, or embedded in a Discord voice channel as an Activity.
Currently running at https://music.gergov.bg/.
Behind it is Gaida, the self-hosted backend in this repository: one public API in front of several interchangeable "platform pods", one per music service. Every pod speaks the same small HTTP contract, so adding a service means adding a container, not touching the API. Two of the four pods are Python; the rest is .NET 10. The endpoint contract is in Backend/API.md, the room protocol in Backend/MULTIPLAYER_API.md, and the whole stack comes up from Backend/compose.yaml.
Prerequisites: Node.js 20 or newer for musicrain, and Docker with Compose v2 for the backend. Building the .NET services outside a container also wants the .NET 10 SDK.
Run musicrain:
cd Frontend
npm install
npm run dev
Vite serves on http://localhost:5173, bound to every interface so a phone on the same network can open it. Frontend/README.md has the full script list, the Discord Activity setup and the frontend's own structure.
[!IMPORTANT]
Frontend/.envships placeholders, so point musicrain at your own API before it can play anything:echo 'PUBLIC_API_URL=http://localhost:5340/Audio' >> Frontend/.env.local— the backend on compose's defaults. The value is inlined at build time, so changing it needs a rebuild rather than a restart.
Then the backend:
cd Backend
docker compose up --build
The compose defaults are what a fresh checkout runs on: no secrets, no accounts, every volume under Backend/data/, and the API on http://localhost:5340. Put music in Backend/data/music — or point MUSIC_LIBRARY_PATH at where it already lives — and the library pod picks it up on its next start. Spotify and Deezer search work with no credentials; Deezer audio is the one thing that needs a cookie.
Host-specific values and secrets live in Backend/.env, which compose reads on its own and git ignores. Backend/README.md lists them, and Backend/nginx.example.conf shows the path routing that goes in front of the stack.
timeupdate reports using AudioContext.currentTime, then subtracts baseLatency and outputLatency to get the position actually reaching the ear..svelte.ts files outside components, which is what lets the sync clock and the queue be unit-tested with no DOM@theme tokensIAsyncEnumerable<T> responses as the streaming primitive, and Serilog throughout.
├── Frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── lib/
│ │ ├── requests/
│ │ ├── routes/
│ │ └── state/
│ ├── static/
│ └── tools/
└── Backend/
├── data/
│ ├── deezer-audio/
│ ├── music/
│ ├── youtube-audio/
│ └── youtube-cache/
├── Gaida Library/
│ ├── Gaida.Admin/
│ ├── Gaida.CLI/
│ ├── Gaida.Core/
│ └── Gaida.Sqlite/
├── Platforms/
│ ├── Gaida.Platforms.MusicDatabase/
│ ├── Gaida.Platforms.YouTube/
│ ├── Gaida.Pods.Deezer/
│ ├── Gaida.Pods.MusicDatabase/
│ ├── Gaida.Pods.Spotify/
│ └── Gaida.Pods.YouTube/
├── scripts/
├── Services/
│ ├── Dom/
│ ├── Dunav/
│ ├── Gaida.API/
│ ├── Gaida.Bot/
│ ├── Moliv/
│ ├── Oko/
│ ├── Selo/
│ ├── Snimka/
│ └── Stih/
└── Tests/
├── Gaida.Tests/
└── Pods.Tests/
Frontend is musicrain itself, with its own README covering the scripts, the Discord Activity setup and the per-directory layout. Frontend/src/state holds the rune-based state classes — audio, queue, room session, account, playlists. Frontend/src/lib is the logic with no UI attached: the two clocks, the back stack, the JSON stream scanner, the Discord URL rewriting. Frontend/static is served at the site root: favicons, the PWA icons and the web app manifest.
Backend/Platforms holds the pods. A Gaida.Platforms.* project is library code — search providers and content getters against one service. A Gaida.Pods.* project is the container that wraps one of them in HTTP, or, for Spotify and Deezer, a standalone Python app. Each has its own README.
Backend/Services holds everything that is not a pod, named in Bulgarian: Gaida.API is the public front door and the transcoder, Dunav the fan-out download cache, Selo the room server, Dom accounts and playlists, Moliv the listening history, Stih the lyrics, Snimka every track image, Oko the admin panel, and Gaida.Bot a Discord bot on the same library.
Backend/data is where the compose defaults mount their volumes — the music library and the YouTube and Deezer audio caches. All of it is gitignored, and none of it is served by nginx directly. Album art lives in Snimka's own volume and reaches the browser through /Audio/Cover/Get.
Everything in this repository is released into the public domain under the Unlicense, with one exception: Backend/Platforms/Gaida.Pods.Deezer is under GPL-3.0-only, because its download and decrypt logic is vendored from streamrip. That pod is a container of its own and talks to the rest over HTTP, so nothing else here inherits the copyleft.
micro variant as @media (max-height: 320px), which covers Discord picture-in-picture, a popped-out window and a phone on its side in one rule, with no SDK layout-mode event.StreamSpreader lets an in-progress download serve every client that asked for it, using FileShare.ReadWrite | FileShare.Delete so the file can be evicted while readers still hold it — the bytes live until the last descriptor closes.ConcurrentDictionary<string, Lazy<Task<…>>>, so a thousand clients racing for a cold track cause exactly one encode.sqlite3wvunpack for hybrid tracks FFmpeg decodes lossyVoice extension takes Opus straight from the API without decoding it