Skip to content

mewbleh/owo

Repository files navigation

owotify

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.

What It Does

  • 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.

Important Notice

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_TOKEN grants access to the account. Keep it in .env or 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.

Requirements

  • Node.js 24 or newer
  • Yarn 1.22 or 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

Quick Start

Install dependencies:

yarn install

Create local config:

cp .env.example .env

Fill Spotify app credentials in .env:

SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REDIRECT_URI=http://127.0.0.1:4377/callback

Generate a Spotify refresh token:

yarn spotify:auth

Copy 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 dev

By default, lyric posting starts disabled. Send this command from the same Discord account in the configured channel or DM:

owo start

Discord Targets

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=123456789012345678

DISCORD_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.

Output Modes

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=status

or:

OWOTIFY_OUTPUT_MODE=both

Discord Commands

Commands 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.

Configuration

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.

Template Variables

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.

Scripts

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.

Project Layout

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

Deployment

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 owotify

Run with Docker Compose:

docker compose up --build

For hosted deployments, add every required secret from .env to the platform's secret or variable manager. Do not commit production secrets.

Troubleshooting

It detects tracks but does not send lyrics

Check the runtime state:

owo status

If lyric posting is stopped, send:

owo start

It detects tracks but does not update custom status

Confirm status mode is active:

owo mode

Switch to status mode:

owo mode status

Make status mode persistent across restarts:

OWOTIFY_OUTPUT_MODE=status

If Discord rejects the custom status request, owotify logs Discord custom status update failed with the returned status and error details.

Commands do not respond

Commands need Gateway support:

DISCORD_GATEWAY_ENABLED=true
OWOTIFY_COMMANDS_ENABLED=true

Commands must be typed by the same Discord account that owns DISCORD_TOKEN.

Messages are too fast

Increase:

OWOTIFY_MIN_MESSAGE_INTERVAL_MS=1500
OWOTIFY_MAX_LINES_PER_TICK=2

Lyrics are missing

LRCLIB may not have synced lyrics for the current track. You can send plain lyrics once when available:

OWOTIFY_PLAIN_LYRICS_MODE=once

License

Apache-2.0. See LICENSE.

Releases

Packages

Contributors

Languages