Last Music Bot

A self-hosted Discord music bot with a React dashboard. Slash commands drive the same player the web UI does, with Lavalink handling audio and Spotify supplying autoplay recommendations.

Open Source Self-hosted TypeScript / Node 24 discord.js / Lavalink AGPL-3.0

Overview

Last Music Bot is two halves of one product: a Discord bot and a React dashboard that control the same player. Whether you type /play in a channel or click a track in the browser, the request lands in the same player service. The dashboard is not a read-only mirror of the bot; it is a second front end to it.

Audio is delegated to Lavalink, so the bot process never touches an audio stream; it sends the node a track and receives events back. That is what lets a single instance serve several guilds without turning into an encoding server.

It is designed to be self-hosted. You run it, you own the data, and the whole stack comes up under Docker.

Key Features

  • Two Front Ends Slash commands and a React dashboard drive the same player service, so the two never disagree about what is playing.
  • Lavalink Audio Playback is handled by a Lavalink node over the standard client, keeping the bot process free of audio work.
  • Spotify Autoplay When the queue runs dry the bot keeps going, choosing related tracks rather than repeating the last one.
  • Playlist Import Pull existing playlists in from Spotify and YouTube instead of rebuilding them by hand.
  • Persistent Library Playlists, likes, history, and per-guild settings live in SQLite and survive restarts.
  • Drag-and-Drop Queue Reorder the queue from the dashboard, on desktop or touch.
  • Audited Actions Actions are written to an append-only JSONL log, so who-did-what is answerable after the fact.
  • Scheduled Backups A backup service snapshots the databases on a loop, with a restore path that does not involve improvising.
  • One-Command Stack Bot, dashboard, Lavalink, and the cipher helper all come up together under Docker Compose.
  • Tested Unit, smoke, and end-to-end suites cover the player, the services, and the command handlers.

Slash Commands

The command surface is deliberately small. Anything beyond transport control belongs in the dashboard, where there is room for it.

Command What it does
/play Queue a track, playlist, or search. Joins your voice channel if the bot is not already in one.
/pause · /resume Hold and release playback without losing the queue.
/skip Advance to the next track. With autoplay on, an empty queue is refilled rather than stopped.
/connect · /disconnect Move the bot into or out of a voice channel explicitly.
/control Bring up the interactive player controls in-channel.
/admin Owner-side maintenance. Not for general use.

Architecture & Stack

Discord and the dashboard are two doors into one player service. Below it, the work fans out: Lavalink for audio, the recommendation engine for autoplay, and SQLite for anything that must survive a restart. Click a node to jump to the section that documents it.

Discord

Slash commands · voice

React Dashboard

Vite · queue · library

discord.js Client

Command handlers

REST API

Express · JWT · zod

PlayerService

Queue · state · events

Lavalink

Audio node · LavaSrc

Recommendation

Autoplay · ANN index

SQLite Store

Library · player state

yt-cipher

Signature helper

Spotify Catalog

Related tracks · import

Audit & Backup

JSONL log · snapshots

The Containers

Four services come up together. Splitting them apart is what allows a redeploy of the bot to leave audio playback untouched.

Service Role
bot discord.js client, the REST API, and the player service.
ui The React dashboard, built by Vite.
lavalink The audio node. Does the actual streaming and encoding.
yt-cipher Signature helper Lavalink needs to keep resolving YouTube sources.
Repo layout
.
├── apps/bot/          # discord.js client, REST API, player, services, tests
│   └── src/
│       ├── api/           # Express route registration
│       ├── discord/       # Slash commands
│       ├── player/        # PlayerService - the one source of truth
│       ├── lavalink/      # Audio node client
│       ├── recommendation/# Autoplay + ANN index
│       ├── spotify/       # Auth, catalog, playlist import
│       ├── youtube/       # Playlist import
│       ├── store/         # SQLite stores + repositories
│       ├── audit/         # Append-only JSONL logger
│       └── backup/        # Scheduled snapshots
├── src/               # React dashboard (production UI)
├── lavalink/          # Lavalink node config
└── docker-compose.local.yml

Lavalink & the Audio Pipeline

The bot never touches an audio stream. It sends a Lavalink node a track and gets events back, and the node does the streaming, decoding, and mixing. That separation is the reason a redeploy of the bot does not interrupt playback: the audio container keeps running.

Almost every built-in source is switched off

Lavalink ships with source managers for YouTube, Bandcamp, Twitch, Vimeo and more. Nearly all of them are disabled here, and only http is left on. That looks like a mistake until you see the plugin list: the built-ins are turned off because the plugins replace them, and running both would mean two code paths competing to resolve the same URL.

Plugin What it is for
youtube-plugin Handles YouTube resolution, in place of the built-in source manager.
LavaSrc Adds Spotify as a catalog. See below: it does not do what most people assume.

Spotify is a catalog, not a stream

This is the part worth understanding. Spotify audio is never played. Spotify is used to identify the music; the audio is then sourced from YouTube. LavaSrc resolves each Spotify track by trying, in order:

LavaSrc resolution order
providers:
  - "ytsearch:%ISRC%"     # 1. match the exact recording
  - "ytsearch:%QUERY%"    # 2. fall back to artist + title text

The ISRC is the recording's global identifier, so searching by it finds that recording, not a cover, a live take, or a remaster that happens to share the title. Only when a track has no usable ISRC does it fall back to a plain text search, which is the path that occasionally lands on the wrong version. That two-step order is the whole reason Spotify-sourced playback usually gets the right take.

Surviving YouTube

YouTube is a moving target, and two pieces of the config exist purely to cope with that.

The YouTube plugin is configured to rotate through several client identities: MUSIC, ANDROID_VR, WEB, WEBEMBEDDED, and TV. They break at different times and under different conditions, so if one client stops resolving, another is likely to still work. It is redundancy, not cleverness.

The second piece is yt-cipher, which is why there is a fourth container in the stack that otherwise looks unnecessary. YouTube protects stream URLs with a signature computed by JavaScript that changes regularly. Rather than making the audio node run that JavaScript itself, the node delegates signature work to a small dedicated service over the local network. Keeping it separate means the thing that breaks most often is also the thing you can update or restart on its own.

Tuning

Setting Value Why
Frame buffer 10 s, non-allocating Absorbs network jitter without churning memory per player.
Buffer duration 1 s Keeps the gap between a command and hearing it short.
Playlist load limit 250 tracks A ceiling on how much one /play of a huge playlist can queue.
Spotify page limit 6 pages Bounds how far an album or playlist import will page through the API.

Dashboard & REST API

The dashboard is a Vite + React 18 app. It holds no player logic of its own; it calls the same API the bot exposes, which is why the queue you drag in the browser is the queue Discord is playing.

The API is Express, authenticated with JWT, and validates request bodies with zod so a malformed payload is rejected at the edge rather than halfway through a state change. Routes are grouped by concern:

Route group Covers
Auth Sign-in and session issuing for the dashboard.
Playback Play, pause, skip, seek, volume, queue order.
Library Playlists, likes, and listening history.
Guild Per-server settings and state.
Spotify Account linking, catalog lookups, playlist import.
Admin Owner-only maintenance operations.

Library, Storage & Backups

State is split across two SQLite databases, and the split is deliberate: library.db holds what you want to keep (playlists, likes, history, settings, linked accounts), while player.db holds what is merely true right now. Losing the second is an inconvenience; losing the first is a loss.

Each concern gets its own repository rather than sharing one god-object over the connection, and schema changes run through an explicit migration path instead of being applied by hand.

Two safety nets sit underneath. Actions are appended to a JSONL audit log, which is append-only and therefore still trustworthy after something has gone wrong. And a backup service snapshots the databases on a schedule, with a restore command as a first-class script rather than an improvised copy.

Autoplay & Recommendations

An empty queue is where most music bots stop. This one keeps going: when the last track ends, the recommendation service picks what follows.

Rather than asking an API for "related tracks" every time, the bot maintains its own approximate nearest-neighbour index over a track index it builds up. Candidates are drawn from that index and enriched through the Spotify catalog, which keeps suggestions relevant to what has actually been playing instead of drifting into whatever is globally popular.

Playlists can be imported wholesale from Spotify (private, public, or embedded) and from YouTube, so an existing collection does not have to be rebuilt track by track.

Self-Hosting

The whole stack is Docker Compose. Node 24 or newer is required if you intend to run the bot outside a container.

Bring the stack up
# Fill in your own configuration first
cp .env.example .env

npm run dev:up:build     # first run: builds the bot and UI images
npm run dev:up           # thereafter
npm run dev:logs         # tail the bot
npm run dev:down         # tear it down

Configuration is entirely environment-driven: the same image runs locally and in production, with no code branching between them. .env.example documents every key the stack reads. Your credentials stay yours: they are gitignored, and the deploy path is built so that they are never uploaded from a developer machine.

You will need your own credentials. A Discord application for the bot, and Spotify API credentials if you want autoplay and playlist import. There is no shared or hosted instance to borrow. That is what self-hosted means, and it is why the data stays on your machine.

Terms & License

Last Music Bot is open source under the GNU Affero General Public License v3.0. The Affero clause matters here: if you run a modified version as a network service that other people use, you must offer those users the source of your modifications. Running it unmodified for your own server carries no such obligation.

Lavalink, discord.js, and the other dependencies remain under their own licenses. Discord, Spotify, and YouTube are trademarks of their respective owners; this project is not affiliated with any of them, and you are responsible for complying with their terms of service.

Last Download Manager

A modern, feature-rich download manager built with C++ and wxWidgets. Accelerated multi-segment downloads, video site support via yt-dlp, browser extension integration, and robust crash recovery - all powered by native Windows APIs.

Stable Release Windows 10/11 C++ / wxWidgets GPL-3.0
Last Download Manager interface: category sidebar, download table showing a 7.2 GB ISO at 51% and 47 MB/s, and the live speed graph

Overview

Last Download Manager is a native Windows download manager built on C++ and wxWidgets. It splits a file into segments and pulls them in parallel over WinINet, which is where the speed comes from, and it treats interruption as normal rather than exceptional: chunks retry, downloads resume, and progress is checkpointed to disk so a crash costs you seconds instead of a whole file.

Video sites are handled by shipping the problem out to yt-dlp, with ffmpeg and Deno fetched and updated automatically. A browser extension intercepts downloads and hands them to the running app over a local HTTP server.

Known issue. The UI can freeze or stall for a couple of seconds when starting a yt-dlp video download. The work is being done off the UI thread incorrectly; a fix is planned. Ordinary HTTP downloads are unaffected.

Key Features

  • Multi-Segment Engine Splits each file across parallel WinINet connections, which is where the throughput gain comes from.
  • Resume & Auto-Retry Two-level retry with exponential backoff; part files survive failure, so an interrupted download continues rather than restarting.
  • Crash Recovery Progress is written to the database periodically, so an unexpected shutdown does not cost the transfer.
  • Checksum Verification MD5 and SHA256 hashes verified against the finished file.
  • Video Sites yt-dlp integration covering YouTube, Vimeo, and 1000+ sites, with resolution and format chosen before the download starts.
  • Managed Toolchain yt-dlp, ffmpeg, and the Deno runtime are downloaded and kept current automatically. Nothing to install by hand.
  • Browser Extension Chrome, Edge, Brave, and Firefox: auto-intercept, right-click download, page video scan, and batch grabbing.
  • Speed Graph Live throughput plotted over a rolling window, so a stalling connection is visible rather than inferred.
  • Categories & Scheduler Automatic sorting into Documents, Video, Music, Images, Programs, and Compressed, plus start and stop times for queues.
  • Tray & Dark Mode Minimise to tray with notifications, and a dark theme throughout.

Download Engine

The engine assumes networks fail. Retries are layered so that a brief blip is absorbed at the chunk level without disturbing the download, while a genuine outage backs off instead of hammering the server.

Level Attempts Backoff
Chunk: one segment fails 3 500 ms base delay
Download: the whole transfer fails 5 Exponential, 2 s → 32 s

Resume

Segments are written as .part0, .part1, and so on, and are kept when a download fails, and that is what makes resume possible after the automatic retries are exhausted. On restart the app checks the existing size on disk, and validates Content-Range responses before appending, so a server that ignores the range request cannot silently corrupt the file.

Thread Safety

Status, sizes, and speed are atomics; metadata, chunk lists, and database writes are mutex-guarded. WinINet handles are owned by RAII session objects, and async tasks are tracked so they cannot outlive the download that spawned them.

Video & Tool Management

Video extraction is delegated to yt-dlp rather than reimplemented. The app fetches the toolchain itself on first use and keeps it updated, so there is no manual setup step.

Tool Why it is there
yt-dlp Extracts and downloads from YouTube, Vimeo, and 1000+ other sites.
ffmpeg Merges the separate audio and video streams these sites serve.
deno JavaScript runtime some extractors need, YouTube in particular.

All three live in %APPDATA%\LDM\tools\. Delete that folder and the app will simply fetch them again, which is the quickest fix when an extractor starts misbehaving after a site change.

Streams behind a CDN that checks where the request came from are handled by propagating the Referer header from the originating page.

Installation & Setup

Click the Download button above for the latest release. Requires Windows 10 or 11. Nothing else needs installing: the video toolchain fetches itself on first use.

Building from Source

Requires Visual Studio 2022 with the Desktop development with C++ workload and wxWidgets 3.2 or newer. Networking uses the native WinINet APIs, so there is no libcurl dependency to satisfy.

  1. Build wxWidgets from wxwidgets.org.
  2. Set WXWIN to your wxWidgets directory; the project expects libraries at $(WXWIN)\lib\vc_x64_lib.
  3. Open LDM.sln, select Release and x64, and build.
Debug console output
# Either pass the flag...
LDM.exe --debug

# ...or drop an empty debug.txt next to the executable
type nul > debug.txt

Browser Extension & Local API

The extension does not download anything itself. It intercepts the browser's download, cancels it, and hands the URL to the running app, which means the app must already be running, since the extension will not start it for you.

  1. Start LDM.
  2. Open chrome://extensions, edge://extensions, brave://extensions, or about:debugging#/runtime/this-firefox.
  3. Enable Developer mode.
  4. Click Load unpacked (Chrome, Edge, Brave) or Load Temporary Add-on (Firefox).
  5. Select the BrowserExtension folder from your LDM installation.

Beyond auto-intercept it adds a right-click Download with LDM entry, a video tab that scans the page for media and offers quality selection, batch Download All Links and Download All Media actions, a manual URL box, a live connection indicator, configurable hotkeys, and filters for file type, domain, and minimum size.

Local HTTP API

The two halves talk over a small HTTP server bound to loopback. It is worth understanding if you are debugging a connection problem, or building against it.

Endpoint Method Purpose
/token GET Fetch the session auth token.
/ping GET Liveness check behind the extension's connection indicator.
/download POST Queue a URL, optionally with the page it came from.
POST http://127.0.0.1:45678/download
Content-Type: application/json
X-Auth-Token: <token from GET /token>

{
  "url": "https://example.com/Win11_25H2_English_x64.iso",
  "referer": "https://example.com/downloads"
}

referer is optional and only sent when the page supplied one; it is what allows downloads from CDNs that reject requests arriving without an origin.

The server is not a hole in your machine. It binds to 127.0.0.1 only, so nothing off the machine can reach it; every request must carry the token from /token, the origin is validated, and connections are capped at 16. The token exists so that a random web page cannot inject downloads into your queue.

Architecture & Component Layout

Two front doors, the wxWidgets window and the extension's HTTP server, feed one queue. DownloadManager owns that queue and dispatches to either the segmented HTTP engine or yt-dlp, depending on what the URL turns out to be. Click a node to jump to the section that documents it.

Browser Extension

Intercept · grab · batch

wxWidgets UI

MainWindow · table · graph

HttpServer

127.0.0.1:45678 · token

DownloadManager

Queue · dispatch · categories

DownloadEngine

WinINet · segments · retry

YtDlpManager

Video sites · quality

DatabaseManager

XML store · crash recovery

Tool Cache

yt-dlp · ffmpeg · deno

Source layout
Last-Download-Manager/
├── LDM.sln
├── BrowserExtension/         # MV3 extension: background, content, popup, options
└── LDM/
    ├── main.cpp
    ├── core/                 # Download · DownloadEngine · DownloadManager · YtDlpManager
    ├── ui/                   # MainWindow · DownloadsTable · CategoriesPanel · SpeedGraphPanel
    ├── database/             # DatabaseManager - XML persistence
    ├── utils/                # HttpServer · Settings · ThemeManager · HashUtils
    └── resources/            # Icons, manifests, assets

Troubleshooting & FAQ

The issues that actually come up, and what is really going on underneath each.

The app freezes for a few seconds on video downloads
This is a known bug, not a misconfiguration on your side. Starting a yt-dlp download can stall the UI for a second or two. A fix is planned. Regular HTTP downloads are not affected, and the download itself still completes; it is the window that goes unresponsive, not the transfer.
The extension says it cannot connect
The extension talks to LDM over a local HTTP server, so LDM has to be running; it will not be launched for you. If the app is running and the indicator is still red, something else may hold port 45678, or the extension may be holding a token from a previous session; reload the unpacked extension to make it re-fetch from /token.
A download failed and I do not want to start over
You do not have to. Part files are deliberately kept when a download fails, so hitting resume picks up from the bytes already on disk, even after the automatic retries have run out. The app also checks existing file size at startup, so resuming works across restarts.
A video site suddenly stopped working
Sites change their players, and extractors break until yt-dlp catches up. The toolchain updates itself, but you can force the issue by deleting %APPDATA%\LDM\tools\. The app re-downloads yt-dlp, ffmpeg, and Deno on next use.
A download works in the browser but fails in LDM
Usually the CDN is checking where the request came from. Downloads started through the extension carry the page's Referer along with the URL; a URL you pasted by hand has no such context, so a link that works in the tab can 403 when fetched cold. Use the extension for those.
The build cannot find wxWidgets
The project resolves libraries through WXWIN, and expects them under $(WXWIN)\lib\vc_x64_lib, that is, a 64-bit static build. Setting the variable after Visual Studio is already open will not take; restart it so the new environment is picked up.
How do I see what it is actually doing?
Run with --debug, or drop an empty debug.txt beside the executable, and the app opens a console with live output. The second form is the useful one when the app is being started by something other than your shell.

Terms & License

Last Download Manager is open source under the GNU General Public License v3.0. You may use, study, modify, and redistribute it, provided derivative works stay under the same license and the source remains available.

yt-dlp, ffmpeg, and Deno are separate projects under their own licenses; LDM downloads and invokes them but does not bundle or modify them. You are responsible for respecting the terms of service of whatever site you download from.

Last Rich Presence

Windows app for accurate, polished Discord Rich Presence for media, creative, and productivity workflows. Built with C++20 and WinUI 3.

Stable Release Windows 10/11 C++20 / WinUI 3 GPL-3.0
Last Rich Presence home view: connected to Discord, media activity detected from YouTube Music via a browser hint, and creativity activity detected from Adobe Photoshop

Overview

Last Rich Presence keeps Discord aligned with what you are actually doing on Windows, across three independent activity lanes: media playback read from Windows media sessions, creative work detected from Adobe-family desktop apps, and productivity work detected from Office-family apps. Each lane owns a separate Discord application, so a track, a Photoshop session, and a Word document can appear as distinct cards rather than fighting over one status.

The app is single-instance and tray-first: it can start minimized, close to tray, and redirect a second launch back to the running instance. An optional Chromium extension improves timeline accuracy for web players that Windows reports poorly.

Key Features

  • Media Lane Reads active Windows media sessions (GSMTC) and builds presence with title, artist, album, playback state, and timeline.
  • Creativity Lane Detects 23 Adobe-family desktop apps, from Photoshop and Premiere Pro through the Substance 3D suite, with project-name and window-title display.
  • Productivity Lane Detects Word, Excel, PowerPoint, OneNote, Access, Publisher, Visio, Project, and Codex, with project or file-name display.
  • Three Discord Apps Each lane connects under its own application ID, so lanes coexist as separate Discord cards instead of overwriting one another.
  • Browser Companion Optional MV3 extension supplies high-confidence hints for 14 web players over Chromium native messaging.
  • Hint Arbitration Browser hints are scored on playback state, tab visibility, confidence, and age, so a stale background tab cannot hijack the timeline.
  • Privacy Modes Per-lane Normal, App Only, and Private modes, plus a sensitive-keyword filter and a blocked app and site term list.
  • Detection Modes Choose whether a lane follows the foreground window, any visible window, or foreground with visible fallback.
  • Tray-First Workflow Single instance, close to tray, launch on startup, start minimized, and a global Rich Presence master toggle.
  • Diagnostics Export and import settings, inspect detection state, and enable source debug mode when a lane misbehaves.

Activity Lanes

The three lanes run independently. Each has its own detector, its own presence manager, and its own Discord application ID, and each can be disabled without affecting the others.

Lane Source Discord Card
Media Windows media sessions (GSMTC), optionally refined by browser hints Dedicated application
Creativity Adobe-family desktop windows Dedicated application
Productivity Office-family desktop windows and Codex Dedicated application

Detection Modes

The Creativity and Productivity lanes decide which app to report using one of three strategies.

Mode Behaviour
ForegroundPreferredVisibleFallback Default. Reports the focused app; falls back to any visible supported window when focus moves elsewhere.
ForegroundOnly Reports only the app you are actively focused on. Presence clears the moment you alt-tab away.
VisibleWindowOnly Reports any open supported window, whether or not it holds focus.

Privacy Modes

Mode What Discord sees
Normal App name plus the project or document name.
AppOnly App name only. File and project names are withheld.
Private A generic card. Neither the app nor the document is named.

Conflict & Idle Behaviour

When media and creative activity are live at once, CreativePriorityMode decides which wins: Auto, PreferMedia, or PreferCreative. When an app closes, CreativeIdleBehavior either holds the last card for five seconds (HoldLast5Seconds) or clears it at once (ClearImmediately), which avoids flicker when you are switching between documents.

Installation & Setup

Click the Download button above for the latest installer. Requires Windows 10 or 11 and the Discord desktop client, running. The web client does not expose the local IPC socket that Rich Presence needs.

Building from Source

Requires Visual Studio 2026 with the Desktop development for C++ workload, the MSVC v145 toolset, Node.js on PATH for the extension syntax checks, and Inno Setup 6 if you intend to produce the unpackaged installer.

Configuration Purpose
Debug / Release Standard development builds.
Release-Inno Unpackaged, self-contained build for Inno installer releases.
Release-MSIX Packaged MSIX build path.

The verification script restores NuGet packages, builds the solution, runs the extension's JavaScript syntax checks, executes the native guard tests, validates release artifacts, and performs a launch smoke test.

Verify a build
.\scripts\verify.ps1 -Configuration Debug -Platform x64

# Non-interactive runners have no desktop session for the launch smoke test
.\scripts\verify.ps1 -Configuration Debug -Platform x64 -SkipLaunchSmoke

# The MSIX path installs the package, launches it by identity, then uninstalls
.\scripts\verify.ps1 -Configuration Release-MSIX -Platform x64

Browser Extension & Hint Contract

Windows reports browser playback poorly: the timeline is often missing or stale. The optional MV3 extension fills that gap by reading the page directly and sending the desktop app a structured hint.

  1. Open chrome://extensions or edge://extensions and enable Developer mode.
  2. Click Load unpacked and select the browser-extension/ folder.
  3. Open the extension popup to choose whether hints are enabled and which sites may send them.
  4. Launch the desktop app once afterwards so it can refresh the per-user native-host registration.
Keep the extension key stable. Native messaging is bound to the fixed extension ID hodkjclfknpkaockiingkiijbbjekebj. Changing the committed key without also updating the app-side allow-list will silently break every hint.

Transport

The extension never talks to the app directly. It posts to a Chromium native-messaging host, which the app itself provides when launched with --browser-native-host; that host then forwards the payload into the running instance over a local named pipe. The app verifies the calling origin and the parent process before accepting anything, and it will not auto-launch if it is closed.

Channel Identifier
Native messaging host com.lastprojects.lastrichpresence
Named pipe \\.\pipe\LastRichPresence.BrowserHints
Accepted parents chrome.exe, msedge.exe
Max message size 64 KB

Hint Payload

Every tab reports its own hint. The background worker scores them and forwards only the winner, so a paused background tab cannot override the video you are actually watching. Playback state is worth the most, then tab visibility, then the scraper's own confidence, with older hints decaying.

Outbound hint · schemaVersion 1
{
  "schemaVersion": 1,
  "kind": "hint",
  "source": "lrp-browser-extension",
  "timestamp": 1749650412233,
  "data": {
    "service": "YouTube Music",
    "siteKey": "youtube_music",
    "mediaKind": "music",
    "title": "Time",
    "artist": "Hans Zimmer",
    "album": "Inception OST",
    "isPlaying": true,
    "positionSeconds": 191,
    "durationSeconds": 348,
    "pageHost": "music.youtube.com",
    "pageVisible": true,
    "tabId": 42,
    "sequence": 17,
    "confidence": 92,
    "rule": "media-session"
  }
}

When no tab qualifies, the extension sends { "kind": "clear" } instead, and the app falls back to whatever GSMTC reports on its own.

Supported Web Sources

Fourteen players ship with high-confidence scrapers: YouTube, YouTube Music, Spotify Web, SoundCloud, Apple Music, Amazon Music, Deezer, TIDAL, JioSaavn, Gaana, Wynk Music, Bandcamp, Mixcloud, and Twitch. Each can be toggled individually from the extension popup.

Architecture & Component Layout

Detection, presence building, and Discord IPC live in src/core/; the WinUI 3 shell, pages, and tray controller live in src/ui/. Each lane is a vertical slice (detector, presence manager, Discord application), and the browser chain feeds only the media lane. Click a node to jump to the section that documents it.

Browser Extension

MV3 · 14 web players

WinUI 3 Shell

MainWindow · pages · tray

Native Host

Named pipe bridge

MediaDetector

GSMTC + hint arbitration

CreativeDetector

Adobe-family windows

ProductiveDetector

Office-family windows

PresenceManager

Media card builder

CreativePresenceManager

Creativity card builder

ProductivePresenceManager

Productivity card builder

DiscordRPC

Local IPC · 3 application IDs

Source layout
Last Rich Presence/
├── src/
│   ├── core/                 # Detection, presence building, settings, Discord IPC
│   └── ui/                   # WinUI shell, pages, settings, tray, animations
├── browser-extension/        # Optional MV3 companion extension
├── installer/                # Inno Setup script and installer guide
├── scripts/                  # Build and verification scripts
├── tests/                    # Native guard tests
└── Assets/                   # Branding and curated fallback app logos

Settings & Persistence

Settings are stored in the WinRT ApplicationData local settings container, not a config file you edit by hand. Use the Settings page, or the diagnostics export and import if you want to move a configuration between machines.

Group Controls
General Rich Presence master toggle, close to tray, launch on startup, start minimized, tray left-click behaviour.
Media Timestamps, source label, paused state, album art, idle card, activity type override.
Privacy Sensitive keyword filter, strict browser privacy, browser album-art suppression, blocked app and site terms.
Creativity / Productivity Per-lane enable, detection mode, privacy mode, project-name display, per-app filters, activity type override.
Appearance Theme: FollowSystem, Light, or Dark.

Privacy & Network

The app is local-first, but two features reach the network and are worth knowing about before you enable them. Album-art fallback may query itunes.apple.com; if no direct artwork exists and only a thumbnail is available, the app can upload those image bytes to Imgur to obtain a hostable asset URL. Both are governed by the browser privacy and album-art settings, and turning album art off stops them entirely.

Troubleshooting & FAQ

The most common issues seen during install, build, and daily use.

Rich Presence is not updating at all
Confirm the Discord desktop client is running. Rich Presence is delivered over a local IPC socket that only the desktop client exposes, so it cannot work with Discord in a browser tab. Then check that the global Rich Presence toggle is on, and that the specific lane you expect is enabled.
Launching the app appears to do nothing
This is expected. The app is single-instance: a second launch redirects to the instance already running, which is most likely sitting in the tray. Check the notification area rather than launching again.
Browser hints never arrive
Native messaging binds to a fixed extension ID, so it is easy to break. Launch the desktop app at least once after installing or updating the extension so it can refresh the per-user host registration. Confirm hints are enabled in the extension popup and that the site itself is ticked. If you rebuilt the extension, make sure the committed key is unchanged: a new key means a new ID, and the app's allow-list will reject it. Note also that the extension will not launch the app for you; it must already be running.
The wrong tab is showing on Discord
Hints from every tab compete, and the winner is scored on playback state, tab visibility, scraper confidence, and age. A tab that is actually playing should always outrank a paused one. If a stale tab is winning anyway, close it or untick that site in the popup. Note also that hints older than the staleness window are discarded outright.
My Adobe or Office app is not detected
Check the lane's per-app filter first: individual apps can be switched off. Then check the detection mode: under ForegroundOnly the card clears as soon as the app loses focus, which is often mistaken for a detection failure. ForegroundPreferredVisibleFallback is the forgiving default.
Build fails with LNK1201 or LNK1104
The linker cannot overwrite the executable because a copy is still running, and because the app is tray-first, it is easy to miss. Exit it from the tray, then rebuild. Duplicate WindowsAppRuntimeAutoInitializer warnings during the build are known and harmless.
Launch-on-startup does not stick
For unpackaged installs the setting is a registry entry. Verify that HKCU\Software\Microsoft\Windows\CurrentVersion\Run\LastRichPresence exists and points at the installed executable.

Terms & License

Last Rich Presence is open source under the GNU General Public License v3.0. You may use, study, modify, and redistribute it, provided derivative works remain under the same license and the source stays available.

Adobe, Microsoft Office, Discord, and the streaming services named above are trademarks of their respective owners. This project is not affiliated with, endorsed by, or sponsored by any of them; app detection relies only on publicly visible window metadata.

Last Music Player

A fast native Windows music player for local libraries, with optional remote playback through your own Music API. Offline playback comes first. Remote search, radio, lyrics, and imports stay behind a small provider contract you control.

Stable Release Windows 10/11 WinUI 3 / WASDK C++/WinRT SQLite 3 Last Projects License 1.0
Last Music Player home screen showing music lists, custom local mixes, playback control, and library views.

Overview

Last Music Player is a high-performance, native Windows music player built specifically for local music enthusiasts. It features native integration with the Windows Shell, seamless playback management, SQLite-backed library indexing, and a contract-driven structure allowing users to hook up external audio streaming servers (Music APIs) via plain HTTP endpoints.

Key Features

  • Local-First Play standard local formats (MP3, FLAC, M4A, WAV, Opus) with a blazing-fast startup time.
  • Gapless Engine Multi-source native queue manager fetches the next song block in advance for zero gap clicks.
  • Library Browser Browse by songs, albums, artists, and genres with SQLite-backed indexing.
  • Playlists & Mixes Build manual playlists and use auto-generated mixes.
  • Synced Lyrics Time-synced lyrics display when a Music API provider is configured.
  • Windows Shell Sync Integrates natively with SMTC (System Media Transport Controls), media keys, and system flyouts.
  • Google Cast Optional Cast output engine for streaming to Chromecast and Google Nest speakers.
  • Discord Rich Presence Optional now-playing status on Discord.
  • Music API Provider User-supplied remote catalog for search, streaming, radio, lyrics, and link imports via a simple HTTP contract.

Installation & Setup

Click the Download button above to get the latest release installer. Run the installer to configure the environment. Requires Windows 10 (2004+) or Windows 11.

Building from Visual Studio

To compile and build the client application locally:

  1. Clone the repository recursively to fetch submodules: git clone --recursive
  2. Open Last Music Player.slnx in Visual Studio 2022.
  3. NuGet packages should restore automatically. If not, right-click the solution and choose Restore NuGet Packages.
  4. Set the active architecture configuration to x64 (Debug or Release).
  5. Build and run. The output binaries will write to the build directories.

Building from Command Line

Use the MSBuild toolchain from the Visual Studio Developer Command Prompt to compile the project headless:

MSBuild Command
MSBuild.exe "Last Music Player.vcxproj" /t:Build /p:Configuration=Release /p:Platform=x64

The release binaries will write directly to: x64/Release/Last_Music_Player.exe

Custom Secrets (AppSecrets)

For optional features like Discord Rich Presence, the developer API secrets are excluded from Git to prevent leakage. You can configure them locally for custom builds.

Note: If AppSecrets.local.h is absent during compilation, the client falls back to AppSecrets.example.h and automatically disables Discord integrations.

Configuration Steps

  1. Navigate to the Backend/ directory.
  2. Copy the template file AppSecrets.example.h and name the new file AppSecrets.local.h.
  3. Open AppSecrets.local.h and insert your custom API client IDs.
AppSecrets.local.h
#pragma once

// Discord Application Client ID for Rich Presence integrations.
// Replace with your custom Discord Developer Portal Application ID.
#define LMP_DISCORD_CLIENT_ID "your-discord-app-id-here"

Architecture & Component Layout

Last Music Player separates components into distinct native layers. The Frontend manages the WinUI 3 presentation structure, while the Backend handles state, cache, hardware playback queues, and SQLite interactions.

WinUI 3 Frontend

MainWindow.xaml / Navigation

AudioPlayer

MediaPlayer / Gapless engine

DatabaseEngine

SQLite local storage & cache

ProviderClient

HTTP Music API Client

Windows SMTC

System Media Keys & Flyouts

User Provider Server

Remote Streams & Lyrics

Core Code Directory Structure

Directory Responsibility & Module Info
App.xaml* Main entry-point, WinUI life-cycle management, shell configuration, and application boot.
MainWindow/ Views, library browsers, playlists, now playing controls, queue visuals, and settings UI panels.
Backend/ Audio playback engine, SQLite integration, Google Cast client, Discord RPC client, settings, and lyrics services.
Frontend/ Navigation logic wrappers and converters, visual and layout helpers.
ThirdParty/sqlite/ Vendored SQLite database engine (amalgamated source).
installer/ Setup packaging configuration written for Inno Setup.

Gapless Playback Engine Flow

The AudioPlayer.cpp class manages smooth transitions by utilizing the native Windows.Media.Playback.MediaPlaybackList API. When gapless playback is enabled:

  • The currently playing track sits at index 0 of the playlist queue.
  • A lookahead worker pre-loads the next song, configuring the audio stream into index 1 of the list in advance.
  • When the current song terminates, the player transitions to the next track instantly without closing and reopening the audio channel.

SQLite Database Schema

Last Music Player manages the library metadata and user interactions locally using SQLite. The database stores local tracks scanned from disk, remote tracks resolving from the Provider API, local playlists, and configuration logs.

Table Name Table description.
Column Name Data Type Constraints & Metadata Description
Common Query Pattern
SQL Query
-- SQL query will display here

Database Versions & Schema Migration

The DB engine implements a version-gate check using PRAGMA user_version. The schema is currently at Version 6.

DB Version Migration Change Log
v2 Added column helpers to support remote media integrations (SourceKey, SourceKind, Provider, SourceUrl, etc.). Added playlist schema, and unique indices on source tracking components.
v4 Dropped columns supporting volume normalizations (ReplayGainDb) since that feature was deprecated, optimization clean-up.
v5 Rebranded default labels for remote imports: modified DateAddedText values matching remote source indicators to say "Music API" to prevent UI leakage.
v6 Schema version baseline optimization to prevent repetitive migration iterations, improving initialization speed.

Music API Provider Contract

If you configure a custom Music API Provider under settings, the player offloads catalog searching, album indexing, streaming, related radio, and synchronized lyrics requests to your provider base URL via HTTP.

GET /v1/endpoint Auth: Bearer Token

Endpoint description details.

URL Query Parameters
Parameter Type Status Description
Request & Response Models
-- Code snippet loader

Authentication Standard

The client app communicates the API key configured in settings in two ways:

  • JSON Endpoints: Rest query API calls from the client pass the key as an HTTP Authorization Bearer token header:
    Authorization: Bearer <api_key>
  • Media Streams & Artworks: Since Windows Media and image rendering pipelines perform native queries direct from the OS stack where custom headers cannot be attached, keys are passed via query strings:
    ?access_token=<api_key>

Troubleshooting & FAQ

Common issues encountered during development, installation, or configuration of Last Music Player client and provider stacks.

Why does the compiler throw access-violation exceptions on startup?
Make sure that the MediaPlayer instance inside AudioPlayer.cpp is not initialized during static/global variable setup. The constructor runs before init_apartment() has been called, causing ABI proxy failures. Ensure the client player instantiation is called lazily (e.g. on UI thread initialization).
Why are Equalizer controls disabled?
The user interface elements for the 10-band equalizer are visible in Settings but are currently disabled. The DSP audio effect is operational inside EqualizerEffect.cpp, but the UI is deactivated pending fine-tuning of the biquad audio filter coefficients to avoid crackling noise during playback adjustments.
Why isn't Google Cast detecting my casting devices?
Google Cast relies on network device discovery features. Ensure that the casting devices (like Chromecast or Google Nest speakers) are on the exact same local Wi-Fi/Ethernet subnet as the Windows desktop PC, and that network profiles on Windows are set to "Private" to allow device discoveries.
How are API requests handled if the provider times out?
JSON endpoints like search or lyrics checks are hard-capped with a 15-second timeout inside the client app. If the server does not respond within this window, the client aborts the request. Audio transcoding or file resolving operations should be handled under the streaming path (/v1/stream) which does not enforce this timeout constraint.
How does the local library scanner index track metadata?
The local library scanner (found under MainWindow.LibraryScan.cpp) runs background tasks that recursively crawl configured directories. It parses audio file headers for embedded tags (ID3, Vorbis, MP4 metadata) and saves them to the local SQLite catalog. Track cover artwork is extracted, written to the local app data folder, and referenced as the track's ArtworkUrl.
How is the local disk cache managed for remote music streams?
The client app integrates a local cache manager (StreamCache.cpp) which writes remote audio stream segments to temporary disk storage during active playback. If a track's sourceUrl is requested again, the client redirects the media pipeline to fetch bytes directly from local cache, saving bandwidth and preventing network latency gaps.
Why does the app lose connection with media keys and Windows Volume Flyout (SMTC)?
Windows manages media focus dynamically. If another application (such as Spotify or an active browser tab playing video) grabs exclusive SMTC focus, the OS stops routing media button presses to Last Music Player. Clicking play or pause inside the client app forces a re-registration of the player session to re-acquire focus.
How does the gapless playback engine pre-load the next track?
When a track is active in the native MediaPlaybackList, the AudioPlayer listens to item transitions. When the current track approaches its end, a lookahead thread resolves the stream URL of the next queue item, pre-loads it at index 1 of the playback list, and switches streams seamlessly when index 0 finishes.

Licensing & Contribution Terms

Last Music Player is distributed as a source-available project, not open-source. Using, modifying, or redistributing the application codebase is governed by specific licensing guidelines.

License Summary

The code is subject to the custom Last Projects License 1.0. Key tenets include:

  • Personal Use: Free for natural persons studying, building, or modifying the software for strictly personal and non-commercial purposes.
  • Modified Source: If you modify the codebase, you are required to publish the modified source code in a qualifying public Git repository within seven calendar days.
  • Organizational Restrictions: Non-profits, companies, government institutions, and schools require a separately negotiated commercial license.
  • Dataset Constraints: Using the source code or binary outputs for AI training datasets is strictly prohibited without explicit written consent.

Contributor Agreement (CLA)

By submitting code patches or pull requests to the project, you agree to grant ownership and redistribution rights to Last Projects. You can review the details in CLA.md.

For organization license requests or questions, reach out to: info@lastprojects.com.

RainFrames

Live weather, clear at a glance. Search any place on Earth for forecasts, radar, wind, temperature, air quality, and alerts in one calm view. Free to use, and an account is optional.

Live Free to use No account required Proprietary

Overview

RainFrames is a hosted weather app you use in the browser. There is nothing to install and nothing to configure: open rainframes.com, search for a place, and the forecast is there.

This page is a guide to using the service. RainFrames is a closed-source product, so it documents what the app does and how to read it, not how it is built.

You do not need an account. Signed-out visitors get the complete free weather experience: search, forecasts, maps, radar, air quality, and alerts. Signing in adds premium forecast sources and syncs your settings between devices. It unlocks nothing that was previously withheld.

The Home Dashboard

Everything about one place, on one screen. Search a city at the top, or press Use location and let the browser tell it where you are. The four icons down the left edge are the whole app: Home, Map, Favourites, and Settings at the bottom.

RainFrames home dashboard for Kolkata in light rain: a large current-conditions card, a grid of six metrics, and an hourly forecast strip
The home view. The large card is now; the grid beside it is the detail; the strip below is the next several hours.

The current-conditions card

The big card is the answer to "what is it like right now". Two parts of it are easy to miss and worth knowing.

Feels like is the number that actually matters. In the example above it is 31°C but feels like 40°, because humidity is at 78% and sweat cannot evaporate. Dress for the second number, not the first.

The small badge in the corner (Google, in that screenshot) names the service that produced this forecast. It changes depending on whether you are signed in and where you are asking about, which is explained in Weather Sources below.

The metric grid

Six numbers, each with the context needed to read it rather than just the raw figure.

Tile How to read it
Precipitation The chance of rain, and how much. "65% · 5.7 mm" means rain is likely, and enough of it to be worth an umbrella. A high percentage with a low millimetre figure means drizzle, not a downpour.
Wind Current speed. The direction and the wider pattern live on the wind map.
Humidity Labelled, not just numbered. "78% · Muggy" is why the feels-like temperature is nine degrees above the real one.
UV index Sunburn risk, with a word attached. "3 · moderate" is fine; a high reading is the app telling you to cover up.
Local time A live clock in that city's timezone. Useful when the place you are checking is not the place you are standing.
Air quality The AQI with a plain-language band. "142 · Moderate" is worse than it sounds if you have asthma.

Hourly and seven-day

The hourly strip scrolls sideways through the coming hours, each with a temperature and a chance of rain, and it marks sunset in place so you can see it coming rather than calculate it.

The seven-day list gives each day a condition, a precipitation chance, and a coloured bar spanning its low to its high. The bar is the quickest read on the page: a long bar is a day that swings, a short one is a day that does not.

Seven-day forecast with low-to-high range bars, an embedded weather map, and cards for sun position, moon phase, and detailed air quality
Further down: the week ahead, an embedded map, and the sun, moon, and air-quality detail.

Sun, moon, and air quality

The Sun card draws an arc with the sun's current position on it, so a glance tells you how much daylight is left, with sunrise, sunset, and total daylight spelled out. The Moon card shows the phase and illumination, plus moonrise and moonset.

The Air quality card is the one worth expanding. The headline AQI sits on a 0 to 300+ scale so you can see where it falls, and it breaks out the individual pollutants underneath: PM2.5, PM10, O₃, NO₂, and CO. The card also names the main pollutant, which is what turns "the air is bad" into "the air is bad because of ozone".

Conditions card showing visibility and pressure with plain-language interpretations, and a weather alerts card showing no active alerts
Conditions and alerts. Every number is interpreted, not just reported.

Conditions and alerts

Visibility and pressure come with a sentence rather than being left as raw figures. "998 hPa · Low pressure, unsettled weather likely" is a forecast in itself; falling pressure is what precedes bad weather, and the card says so instead of assuming you know.

The Weather alerts card lists official warnings for the place you are viewing. Quiet is the normal state, and it says 0 active rather than showing nothing, so you can tell the difference between "no alerts" and "not loaded".

Maps, Layers, and the Globe

The map is where the app stops answering "what is it like" and starts answering "what is coming". Pick a layer from the dropdown at the top left, and scrub the timeline at the bottom to move through time. The play button animates it.

The timeline runs from recent observations forward through the forecast, which is what makes it possible to see where a band of rain is heading, not merely where it is now. Hovering anywhere on the map reads the value out at that point.

Radar layer showing precipitation echoes in blue, green, and yellow across South and Southeast Asia, with a timeline scrubber at the bottom
Radar. Where precipitation is falling right now. Blue is light, green is moderate, yellow and beyond is heavy. Measured in dBZ, which the readout shows on hover.
Rain layer showing forecast precipitation intensity across the region
Rain. The forecast counterpart to radar. Radar shows what is falling; rain shows what is expected to.
Temperature layer, a warm orange field across India with a cool green and blue band over the Himalayas and Tibetan Plateau
Temperature. Warm to cool, and geography becomes obvious: that cool ribbon across the middle is the Himalayas and the Tibetan Plateau, drawn purely by their altitude.
Wind layer with speed shown as colour and direction as flowing white streamlines
Wind. Colour is speed, and the drifting white streaks are direction. The scale runs cool through green to warm and finally magenta, so calm air and a storm are distinguishable at a glance without stopping to read a legend.
Pressure layer showing a broad field of atmospheric pressure with a readout of 997 hPa at the selected location
Pressure. The least dramatic layer and the most predictive. Lows bring unsettled weather; highs bring calm. This is the layer that tells you what the others are about to do.

The globe

The button beside the layer dropdown swaps the flat map for an interactive globe. It is not decoration. A flat projection stretches everything near the poles and cuts the planet at an arbitrary line, so weather systems large enough to wrap around the Earth get distorted or split in half. On a globe they stay whole.

The interactive weather globe against a starfield, showing the wind layer wrapped around the Earth with a cyclone visible near Japan
The same wind layer on the globe. The spiral off the coast of Japan is a cyclone, and it reads as one turning system here rather than a smear at the edge of a rectangle.

Favourites and Settings

Press Save in the header to keep the place you are looking at. The Favourites tab then shows them all at once, each with its current temperature, condition, and the day's high and low, so you can compare a handful of cities without searching them one at a time. Open dashboard jumps any of them back to the full view.

Favourites view: a grid of saved city cards including Puchong, Cluj-Napoca, New York, Reykjavik, Nuuk, Phoenix, Myrtle Beach and Raiganj, each showing current temperature, condition, and the day's high and low
Saved cities, side by side. Useful for the places you actually care about: home, family, and wherever you are travelling next.
Settings page with controls for account, forecast provider, temperature unit, theme, performance, and saved cities
Settings. Five controls, and the two at the bottom are the ones people miss.
Setting What it does
Forecast provider Signed in only. Choose whether Google Weather or Apple Weather powers your forecasts.
Temperature unit Celsius or Fahrenheit, also togglable from the header.
Theme Auto follows the active weather and local time, so a stormy night does not look like a clear afternoon. Dark stays put if you would rather it did not move.
Performance Auto adapts to your device. Lite turns off the heavy effects for smoothness; High keeps everything. This is the fix if the maps feel sluggish.
Saved cities How many you have kept, and whether they are synced to your account. Clear them all from here.

Free Use and Signing In

The distinction is worth being precise about, because it is not the usual one. The free tier is not a trial or a crippled preview. Anonymous visitors get the whole weather app. What an account changes is the forecast source and whether your settings follow you between devices.

Signed out Signed in
Forecasts, maps, radar, air quality, alerts Yes Yes
Saved cities On this device Synced across devices
Units, theme, quality, last location On this device Synced across devices
Premium forecast sources No Yes, and selectable

Sign-in uses a MagnetoFX account. There is no separate RainFrames password to create or forget.

Where the Weather Comes From

RainFrames does not measure the weather; it presents it. The badge on the conditions card always names whichever service actually answered, and which one that is depends on whether you are signed in and where you are asking about.

You are Forecast source
Signed in, default Google Weather
Signed in, Apple selected Apple Weather
Signed out WeatherAPI
Signed out, in India, Pakistan, Bangladesh, or Indonesia Open-Meteo forecast, with air quality and alerts from WeatherAPI
Anyone, when a source is having a bad day Open-Meteo, as a fallback

That last row is the one people rarely think about. Weather APIs go down, and a weather app that goes down with them is not much use, so RainFrames falls back rather than showing you an error. If the badge is not the source you expected, this is usually why.

Maps, radar, and the weather layers are rendered from MapTiler with MapLibre GL, over basemaps built from OpenStreetMap data.

Privacy and Your Data

A weather app has to know where before it can tell you anything, and how it learns that is the whole privacy story.

If you grant location access, the browser tells RainFrames where you are. If you do not, it falls back to an approximate location derived from your IP address, which is enough for a city-level forecast and no more precise than that. Granting location access is always your choice, and declining it does not lock you out of anything: searching for a place works just as well.

Beyond location, the service processes what it needs to answer your request: the places you search, and, if you are signed in, your account identity as returned by the MagnetoFX account service. Cookies and browser storage hold your session and your preferences.

Signing in means your settings are stored server-side, which is precisely what allows them to follow you to another device. That is the trade: sync requires the service to keep saved cities, last used location, units, theme, forecast source, and quality settings against your account. Staying signed out keeps all of it on your own device.

FAQ

Is it really free? What is the catch?
Free for personal, noncommercial use, and that is the only catch worth knowing. There is no paywall in front of the weather itself: forecasts, maps, radar, air quality, and alerts are all available without an account. Using it at work, inside an organisation, or as part of a product is a different matter and needs a written license.
Why is "feels like" so different from the actual temperature?
Because your body does not measure air temperature, it measures how fast it can lose heat. High humidity stops sweat evaporating, so 31°C at 78% humidity feels like 40°. Wind does the reverse and makes cold feel colder. When the two numbers diverge, trust the feels-like one.
Why does it show a different weather provider than last time?
Three things decide the source: whether you are signed in, where you are asking about, and whether the usual source is reachable. If an upstream provider fails, RainFrames quietly falls back to Open-Meteo instead of showing you an error page. The badge on the conditions card always names whichever source actually answered.
What is the difference between the Radar and Rain layers?
Radar is observation: what is falling right now, as detected. Rain is prediction: what is expected to fall. Scrub the timeline backwards and radar is the honest record; scrub it forwards and you are looking at a forecast either way.
My location is wrong or too vague
Without location permission the app can only place you by IP address, which is approximate and can land you in the wrong part of a city, or another one entirely if you are on a VPN or a mobile network. Either press Use location and grant access, or just search for the place. Searching always beats guessing.
The map is sluggish on my device
Animated weather layers and the globe are genuinely demanding to render. In Settings, set Performance to Lite, which turns off the heavy effects. The forecast data is identical; only the visual richness changes.
My saved cities vanished
Signed out, saved cities live in your browser, so clearing site data, using private browsing, or switching browser or device loses them. Sign in and they are kept against your account instead, which is the point of sync. Settings tells you which of the two you are in.
Can I use RainFrames at work, or build on it?
Not under the free terms. Any use by or for an organisation, of any size and regardless of nonprofit or educational status, needs a separate written license, as does integrating RainFrames or its data into something else. Write to info@lastprojects.com and ask.
Can I see the source, or self-host it?
No. Unlike the other projects here, RainFrames is closed source and is not offered under an open-source or source-available license. It exists as a hosted service. Source access, if you have a reason to need it, is a licensing conversation.

Terms & Licensing

RainFrames is proprietary, closed-source software, provided as a service rather than sold. Using it grants no ownership of the service, its code, design, branding, or data.

The hosted app is free for a personal, noncommercial purpose. A separate written license is required for commercial or organisational use, for integrating the service or its data into another product, for source access, for use of the branding, and for AI-related uses such as training on the service or its content.

Weather, map, and location data come from third-party providers, each retaining its own licenses and terms. Basemaps include OpenStreetMap contributor data.

Licensing enquiries: info@lastprojects.com