owotify watches the Spotify track currently playing on your account, finds synced lyrics from
LRCLIB, and posts those lyrics to Discord. It can send normal Discord messages, update your
visible Discord custom status, or do both at the same time.
The app is a TypeScript long-running worker. Discord delivery is handled with direct REST and Gateway calls, without a selfbot framework dependency.
- Polls Spotify for the current playback state.
- Fetches synced or plain lyrics from LRCLIB.
- Sends timed lyric lines to a Discord channel or DM.
- Updates the account's visible Discord custom status in status mode.
- Supports runtime control commands from Discord.
- Supports channel targets, DM targets, and runtime target overrides.
- Avoids accidental pings with
allowed_mentions: { parse: [] }. - Retries Discord rate limits using the returned retry delay.
- Ships Docker, Docker Compose, Render, Railway, Nixpacks, and Procfile metadata.
This project uses a Discord user token, not a bot token.
- Account automation can violate Discord rules and may trigger account restrictions.
- A leaked
DISCORD_TOKENgrants access to the account. Keep it in.envor host secret storage only. - Never commit
.env, paste tokens into chat, or print tokens in logs. - Discord client or API changes can break user-token automation.
- Lyric posting can send messages quickly. Tune
OWOTIFY_MIN_MESSAGE_INTERVAL_MS,OWOTIFY_MAX_LINES_PER_TICK, and output mode for the target channel.
- Node.js
24or newer - Yarn
1.22or newer - A Spotify app with authorization code flow credentials
- A Spotify refresh token for the account to monitor
- A Discord user token for the account that will send messages or update status
- A Discord channel ID, DM channel ID, or DM recipient user ID
Install dependencies:
yarn installCreate local config:
cp .env.example .envFill Spotify app credentials in .env:
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REDIRECT_URI=http://127.0.0.1:4377/callbackGenerate a Spotify refresh token:
yarn spotify:authCopy the printed SPOTIFY_REFRESH_TOKEN into .env, then fill Discord settings:
DISCORD_TOKEN=
DISCORD_CHANNEL_ID=
DISCORD_DM_RECIPIENT_ID=
SPOTIFY_REFRESH_TOKEN=Run locally:
yarn devBy default, lyric posting starts disabled. Send this command from the same Discord account in the configured channel or DM:
owo start
Use DISCORD_CHANNEL_ID when you already know the target channel or DM channel ID:
DISCORD_CHANNEL_ID=1521022683905523834
DISCORD_DM_RECIPIENT_ID=Discord channel URLs are also accepted:
DISCORD_CHANNEL_ID=https://discord.com/channels/@me/1521022683905523834
DISCORD_DM_RECIPIENT_ID=Use DISCORD_DM_RECIPIENT_ID when owotify should create or reuse a DM with a user:
DISCORD_CHANNEL_ID=
DISCORD_DM_RECIPIENT_ID=123456789012345678DISCORD_DM_RECIPIENT_ID must be a user ID, not an existing DM channel ID. If you already have
a DM channel ID, put it in DISCORD_CHANNEL_ID.
OWOTIFY_OUTPUT_MODE controls where lyrics go.
| Mode | Behavior |
|---|---|
message |
Send lyric output as Discord messages only. |
status |
Update Discord custom status only. |
both |
Send Discord messages and update custom status. |
Status mode updates the account's visible custom status through Discord user settings and also
sends a Gateway presence update for the active session. If Discord rejects the custom status
request, owotify logs the API error and reports it in the owo mode status command reply.
Runtime output mode changes are not written back to .env. Set this when you want status mode
after every restart:
OWOTIFY_OUTPUT_MODE=statusor:
OWOTIFY_OUTPUT_MODE=bothCommands are sent from the same Discord account that owns DISCORD_TOKEN. The default command
prefix is owo.
Commands require DISCORD_GATEWAY_ENABLED=true, because owotify receives them through Gateway
MESSAGE_CREATE events.
| Command | Description |
|---|---|
owo start |
Enable lyric posting. Lyrics begin when Spotify reports active playback. |
owo resume |
Alias for start. |
owo stop |
Stop lyric posting while keeping the process online. |
owo pause |
Alias for stop. |
owo status |
Show posting state, output mode, target, current track, and lyric index. |
owo mode |
Show the current runtime output mode. |
owo mode message |
Send lyrics as Discord messages. |
owo mode status |
Update Discord custom status instead of sending lyric messages. |
owo mode both |
Send Discord messages and update Discord custom status. |
owo target show |
Show the current output target. |
owo target here |
Send output to the channel or DM where the command was typed. |
owo target channel <id/url> |
Override output to a channel ID or Discord channel URL. |
owo target dm <user_id> |
Override output by creating or reusing a DM with a user ID. |
owo target reset |
Reset the target to .env. |
owo skip |
Clear the current track session and reload lyrics on the next poll. |
owo reload |
Alias for skip. |
owo help |
Show command help. |
owo shutdown |
Clear custom status, stop lyric posting, and shut down the process. |
Target and mode commands are runtime-only. Restarting owotify restores the values from .env.
All runtime configuration is environment-based.
| Variable | Required | Default | Description |
|---|---|---|---|
DISCORD_TOKEN |
Yes | none | Discord user token used for REST and Gateway auth. |
DISCORD_CHANNEL_ID |
Conditional | none | Target guild, text, or DM channel ID. Discord channel URLs are accepted. |
DISCORD_DM_RECIPIENT_ID |
Conditional | none | User ID to DM when DISCORD_CHANNEL_ID is empty. |
DISCORD_API_BASE_URL |
No | https://discord.com/api/v10 |
Discord REST API base URL. |
DISCORD_GATEWAY_URL |
No | wss://gateway.discord.gg/?v=10&encoding=json |
Discord Gateway URL. |
DISCORD_GATEWAY_ENABLED |
No | true |
Enables Gateway connection for commands and presence. |
DISCORD_GATEWAY_INTENTS |
No | 37377 |
Gateway intents used for selfbot command messages. |
SPOTIFY_CLIENT_ID |
Yes | none | Spotify app client ID. |
SPOTIFY_CLIENT_SECRET |
Yes | none | Spotify app client secret. |
SPOTIFY_REFRESH_TOKEN |
Yes | none | Refresh token from yarn spotify:auth. |
SPOTIFY_MARKET |
No | US |
Spotify market for playback metadata. |
SPOTIFY_REDIRECT_URI |
No | http://127.0.0.1:4377/callback |
Redirect URI used by the auth helper. |
LRCLIB_BASE_URL |
No | https://lrclib.net |
Lyrics provider base URL. |
OWOTIFY_LOG_LEVEL |
No | info |
Pino log level. |
OWOTIFY_POLL_INTERVAL_MS |
No | 2000 |
Spotify polling interval. |
OWOTIFY_LYRIC_LOOKAHEAD_MS |
No | 350 |
Sends lines slightly ahead of playback time. |
OWOTIFY_REWIND_RESET_THRESHOLD_MS |
No | 3000 |
Rewind threshold before lyric index resets. |
OWOTIFY_MIN_MESSAGE_INTERVAL_MS |
No | 1100 |
Minimum delay between Discord messages. |
OWOTIFY_MAX_LINES_PER_TICK |
No | 4 |
Maximum lyric lines sent per poll tick. |
OWOTIFY_MAX_MESSAGE_LENGTH |
No | 1900 |
Maximum Discord message chunk length. |
OWOTIFY_AUTO_START |
No | false |
Starts lyric posting automatically when true. |
OWOTIFY_OUTPUT_MODE |
No | message |
message, status, or both. |
OWOTIFY_PRESENCE_STATUS |
No | online |
Presence state for status mode: online, idle, dnd, or invisible. |
OWOTIFY_STATUS_TEMPLATE |
No | {line} |
Custom status template for lyric lines. |
OWOTIFY_STATUS_IDLE_TEMPLATE |
No | owotify idle |
Custom status shown when stopped or no playback is active. |
OWOTIFY_COMMANDS_ENABLED |
No | true |
Enables Discord control commands. |
OWOTIFY_COMMAND_PREFIX |
No | owo |
Prefix used for Discord control commands. |
OWOTIFY_SEND_TRACK_HEADER |
No | true |
Sends or applies a header when a new track starts. |
OWOTIFY_TRACK_HEADER_TEMPLATE |
No | Now playing: {track} - {artist} |
New-track template. |
OWOTIFY_LYRIC_LINE_TEMPLATE |
No | {line} |
Discord message template for synced lyric lines. |
OWOTIFY_NO_LYRICS_TEMPLATE |
No | No synced lyrics found for {track} - {artist}. |
Missing-lyrics message and status template. |
OWOTIFY_PLAIN_LYRICS_MODE |
No | off |
Set to once to send plain lyrics when synced lyrics are unavailable. |
OWOTIFY_PLAIN_LYRICS_TEMPLATE |
No | {plainLyrics} |
Plain lyrics fallback template. |
OWOTIFY_SEND_NO_LYRICS_MESSAGE |
No | true |
Enables missing-lyrics output. |
At least one of DISCORD_CHANNEL_ID or DISCORD_DM_RECIPIENT_ID is required.
Templates can use these placeholders:
| Variable | Meaning |
|---|---|
{track} |
Spotify track name. |
{artist} |
Comma-separated artist names. |
{artists} |
Alias for {artist}. |
{album} |
Spotify album name. |
{line} |
Current synced lyric line. |
{plainLyrics} |
Plain lyrics text when available. |
{spotifyUrl} |
Spotify track URL when available. |
{progressMs} |
Track progress reported by Spotify. |
{durationMs} |
Track duration in milliseconds. |
{timeMs} |
Synced lyric timestamp in milliseconds. |
| Command | Description |
|---|---|
yarn dev |
Run the TypeScript app with tsx watch. |
yarn build |
Clean and compile TypeScript to dist/. |
yarn start |
Run compiled output from dist/. |
yarn test |
Run unit tests with Vitest. |
yarn lint |
Run ESLint. |
yarn typecheck |
Type-check without emitting files. |
yarn spotify:auth |
Generate a Spotify refresh token. |
src/
config.ts Environment parsing and validation
index.ts Application entry point
owotifyRunner.ts Playback loop, commands, and output routing
discord/
commandParser.ts Discord command parsing
discordMessenger.ts Discord REST, Gateway, messages, status, and targets
discordTarget.ts Channel and DM target normalization
lyrics/
lrcParser.ts Synced LRC parsing
lrclibClient.ts LRCLIB lookup client
spotify/
spotifyClient.ts Spotify token refresh and playback polling
utils/ Shared helpers
tests/ Unit tests
scripts/spotifyAuth.ts Spotify OAuth helper
owotify is a worker process. It does not expose an HTTP port.
| Platform | Files | Notes |
|---|---|---|
| Docker | Dockerfile, .dockerignore |
Multi-stage production image. |
| Docker Compose | compose.yaml |
Reads local .env. |
| Render | render.yaml |
Docker-backed worker service. |
| Railway | railway.json |
Dockerfile builder and worker command. |
| Nixpacks | nixpacks.toml |
Buildpack-style install, build, and start phases. |
| Procfile hosts | Procfile |
Defines a worker process. |
Build and run with Docker:
docker build -t owotify .
docker run --env-file .env --name owotify owotifyRun with Docker Compose:
docker compose up --buildFor hosted deployments, add every required secret from .env to the platform's secret or
variable manager. Do not commit production secrets.
Check the runtime state:
owo status
If lyric posting is stopped, send:
owo start
Confirm status mode is active:
owo mode
Switch to status mode:
owo mode status
Make status mode persistent across restarts:
OWOTIFY_OUTPUT_MODE=statusIf Discord rejects the custom status request, owotify logs Discord custom status update failed
with the returned status and error details.
Commands need Gateway support:
DISCORD_GATEWAY_ENABLED=true
OWOTIFY_COMMANDS_ENABLED=trueCommands must be typed by the same Discord account that owns DISCORD_TOKEN.
Increase:
OWOTIFY_MIN_MESSAGE_INTERVAL_MS=1500
OWOTIFY_MAX_LINES_PER_TICK=2LRCLIB may not have synced lyrics for the current track. You can send plain lyrics once when available:
OWOTIFY_PLAIN_LYRICS_MODE=onceApache-2.0. See LICENSE.