Imported from benkaiser/stretto (
AGENTS.md). Install upstream withnpx skills add benkaiser/stretto. Copyright stays with the author.
IMPORTANT: before you do anything else, run the beans prime command and heed its output.
IMPORTANT: Read files in their entirety, do not read portions of files unless the contents are longer than 2000 lines.
AGENTS.md — Stretto Music Player
Project Overview
Stretto is an open-source, web-based music player (v2.0.0, MIT license) that streams music from YouTube, SoundCloud, and Audius. It runs as a Progressive Web App with offline support, cloud sync, and multi-device remote control.
Live: https://next.kaiserapps.com/
Architecture
Monolithic full-stack application — a single Node.js/Express server serves both a REST API and a server-rendered Pug shell, which boots a React 16 SPA. Not a monorepo, not microservices, not GraphQL.
| Layer | Technology |
|---|---|
| Backend | Express.js, Socket.IO, Mongoose |
| Frontend | React 16, React Router v4, SCSS, Bootstrap 3 (Bootswatch) |
| Database | MongoDB (per-user document blobs) |
| Real-time | Socket.IO (multi-device remote control) |
| PWA | Service Worker + Cache API + BroadcastChannel |
| Deployment | Docker + Dokku PaaS via GitHub Actions |
Directory Structure
stretto/
├── index.js # Express server entry point
├── controllers/
│ ├── index.js # All REST API routes (single Express Router)
│ └── socketio.js # Socket.IO remote control handlers
├── models/
│ ├── data_mapper.js # Facade wrapping all model operations
│ ├── user.js # User model (email, hash, salt, version, googleObject)
│ ├── song.js # Song model (stores ALL songs for a user as one blob)
│ ├── playlist.js # Playlist model (stores ALL playlists as one blob)
│ ├── artist.js # Followed artists model
│ └── shared_playlist.js # Shared playlist model (keyed by UUID)
├── services/
│ ├── google.js # Google OAuth2 token verification
│ └── itunes.js # iTunes Search API (artist lookup, song feed, cover art)
├── src/
│ ├── js/
│ │ ├── index.js # Frontend entry point (Loader class, initializes subsystems)
│ │ ├── serviceworker.js # Service worker (offline caching, audio storage)
│ │ ├── models/ # Client-side data models (Song, Playlist, DataLayer)
│ │ ├── services/ # Player engine, streaming, auth, sync, integrations
│ │ └── views/ # React components (routes, layout, pages)
│ └── scss/
│ └── main.scss # All styles (~850 lines)
├── views/ # Pug server templates (layout, index, redirects)
├── static/
│ ├── assets/ # Icons, manifest.json, fonts (committed)
│ └── js/ # Webpack output (gitignored)
├── bin/ # Docker helper scripts (go, build, run, restart)
├── stubs/
│ └── jquery.js # jQuery stub (returns null, avoids bootstrap-slider dep)
├── webpack.config.js # Two entry points: main + serviceworker
├── docker-compose.yml # MongoDB + web services
├── Dockerfile.local # Node 16 dev container
├── .env.example # Required env vars template
├── .github/workflows/deploy.yml # CI/CD → Dokku
└── package.json # Dependencies and scripts
Backend
Server Entry (index.js)
- Loads
.envviadotenv - Creates Express + HTTP server + Socket.IO
- Middleware:
body-parser,cookie-parser,express-session(MongoDB-backed viaconnect-mongodb-session) - Proxies
/scapi→https://api-v2.soundcloud.com(SoundCloud API proxy) - Pug view engine, serves static files from
/static - Routes from
controllers/index.js, Socket.IO fromcontrollers/socketio.js
Key API Routes (controllers/index.js)
| Route | Method | Auth | Purpose |
|---|---|---|---|
/googleLogin |
POST | No | Google OAuth login + session |
/login |
POST | No | Email/password login |
/createaccount |
POST | No | Account registration |
/checklogin |
POST | No | Check session status |
/logout |
POST | No | Destroy session |
/forgotpassword |
POST | No | Send password reset email (Zoho SMTP) |
/completereset |
POST | No | Complete password reset with token |
/latestversion |
GET | Yes | User's data version number |
/latestdata |
GET | Yes | Full user library (songs + playlists) |
/uploaddata |
POST | Yes | Sync library data (optimistic concurrency) |
/addsong |
POST | Yes | Add a single song to a playlist |
/suggest/artists |
GET | Yes | Artist suggestions via iTunes |
/artists/followed |
GET | Yes | Followed artists + songs |
/artists/follow |
POST | Yes | Follow an artist |
/artists/unfollow |
POST | Yes | Unfollow an artist |
/share |
POST | Yes | Share playlist (UUID link) |
/shared/:guid |
GET | No | View shared playlist |
/spotify_callback |
GET | No | Spotify OAuth callback |
* |
GET | No | Catch-all → serves React SPA |
Authentication: Session-based via req.session.loggedIn. A simple loggedIn middleware function guards protected routes.
Socket.IO (controllers/socketio.js)
Enables remote control across devices. Devices join rooms by user email and can emit playpause, previous, next to control playback on all connected devices.
Database Models (models/)
All models use Mongoose. The DataMapper class (data_mapper.js) is a static facade that wraps all model CRUD.
Critical design note: Songs and playlists are stored as entire blobs per user (one document per user, not per song). Every sync operation uploads/downloads the entire library. A version timestamp on the User model provides optimistic concurrency control.
| Model | Key Fields |
|---|---|
| User | email, hash, salt, resetToken, publicJsonLibrary, version, googleObject |
| Song | email, songBlob (Mixed — all songs as one blob) |
| Playlist | email, playlistBlob (Mixed — all playlists as one blob) |
| Artist | email, artists (Mixed array) |
| SharedPlaylist | guid, email, playlistBlob |
Password hashing uses PBKDF2 with SHA-512 (10,000 iterations, 512-byte key).
Frontend
Initialization (src/js/index.js)
The Loader class initializes subsystems in order:
TitleUpdater→ updates browser tab title with current songFirstRunExperience→ onboarding flowAccountManager→ auth stateKeyboard→ keyboard shortcuts (Space = play/pause, arrows = next/prev)Theme→ loads Bootswatch theme from CDNModelInitialiser→ loads songs/playlists from localStorageRootView→ mounts React Router into#reactdivLyrics→ auto-fetches lyrics via AZLyricsServiceWorkerClient→ registers service workerSoundcloudOAuth→ SoundCloud auth initialization
Routing (src/js/views/root.jsx)
React Router v4 (BrowserRouter + Switch):
| Path | Component | Description |
|---|---|---|
/ |
Intro |
Welcome/landing page |
/home |
Home |
Playlist tiles grid |
/playlist/:playlist |
Playlist |
Song list view |
/player |
PlayerView |
Full-screen mobile player |
/add |
Add |
Add songs by URL |
/edit/:id |
Edit |
Edit song metadata |
/search/:search |
Search |
Search library |
/spotify |
Spotify |
Spotify import |
/discover |
Discover |
Top charts (iTunes/Spotify) |
/artists/feed |
ArtistsFeed |
Followed artists' new music |
/artists/add |
ArtistSuggestions |
Discover & follow artists |
/artists/manage |
ArtistsManage |
Manage followed artists |
/soundcloud |
Soundcloud |
SoundCloud integration |
/sync |
Sync |
Cloud sync page |
/backup |
BackupRestore |
Backup/restore library |
/settings |
Settings |
App settings |
/shared/:guid |
SharedPlaylist |
View shared playlist |
/mix/:playlist |
YoutubeMix |
YouTube radio mix |
/remote |
Remote |
Remote control (uses separate layout) |
/explicitscan |
ExplicitScan |
Scan for explicit tracks |
Layout (src/js/views/layout.jsx)
Responsive with a 700px breakpoint:
- Desktop: Fixed left
Sidebar(logo, search, navigation, player controls, album art) + content area - Mobile:
MobileHeader(top navbar) +MobileFooter(bottom player controls) + content area - Global overlays:
Bootbox(modals),AlerterContainer(toasts),ContextMenu(right-click menus)
State Management
No Redux or MobX. Uses a custom observer pattern:
SongandPlaylistare static classes holding in-memory arrays- They expose
addOnChangeListener(callback)andchange()(notify) methods - Components subscribe in constructors and call
setState({})to re-render on changes - Persistence:
DataLayerwrapslocalStorage;SyncManagersyncs to server
Player Architecture (Strategy Pattern)
The Player singleton (src/js/services/player.js) orchestrates playback, delegating to stream-specific implementations:
Player (orchestrator)
├── HTML5AudioPlayer → offline cached files
├── YoutubeStreamPlayer → YouTube via redirect endpoint (library songs)
├── YoutubeIframePlayer → YouTube IFrame API (preview/non-library)
├── SoundcloudStreamPlayer → SoundCloud HLS streams
└── AudiusStreamPlayer → Audius API streams
All extend AbstractHTML5AudioPlayer which provides a common interface: dispose(), toggle(), setVolume(), getPosition(), setCurrentTime().
Song IDs are prefixed by source: y_ (YouTube), s_ (SoundCloud), a_ (Audius).
Key Frontend Services (src/js/services/)
| Service | File | Purpose |
|---|---|---|
| Player | player.js |
Singleton — playback orchestration, queue, shuffle, repeat, MediaSession API |
| YouTube | youtube.js |
Search (scrapes results page), video info extraction, playlist parsing |
| SoundCloud | soundcloud.js |
Search & track resolution |
| Audius | audius.js |
Resolve & stream via Audius API |
| AccountManager | account_manager.js |
Singleton — auth state, Google OAuth + email/password login |
| SyncManager | sync_manager.jsx |
Singleton — bidirectional sync with version-based conflict resolution |
| SpotifyAPI | spotify_api.js |
Spotify Web API — OAuth, playlists, albums, charts |
| SpotifyImporter | spotify_importer.js |
Imports Spotify playlists by finding YouTube matches |
| SocketManager | socket_manager.js |
Singleton — Socket.IO client for remote control |
| Lyrics | lyrics.jsx |
Auto-fetches lyrics from AZLyrics on song change |
| ServiceWorkerClient | service_worker_client.jsx |
Communicates with SW via BroadcastChannel for offline audio |
| Alerter | alerter.jsx |
Toast notification system (react-alert) |
| Bootbox | bootbox.jsx |
Modal dialog system (alert, confirm, prompt) |
Service Worker (src/js/serviceworker.js)
Offline-first PWA strategy:
- App shell caching:
/,/static/js/main.js, Google API, Font Awesome - Navigation preload: Returns cached shell for all SPA routes, background-refetches
- Third-party caching: Bootswatch themes, YouTube thumbnails, album art from various CDNs
- Audio offline storage: Separate
stretto-cache-audiocache for downloaded songs - BroadcastChannel API: Receives
DOWNLOAD,OFFLINE_RAW_FILE,REMOVE_OFFLINE,EMIT_OFFLINEDmessages
Styling
- SCSS — single file
src/scss/main.scss - Bootstrap 3 via Bootswatch CDN themes (15 themes, switchable at runtime in Settings)
- react-bootstrap v0.32 for UI components
- Font Awesome 4.7 for icons
- Custom font: WisdomScript (logo)
- Responsive: 700px breakpoint for mobile vs desktop layout
Build & Dev
NPM Scripts
| Script | Command | Purpose |
|---|---|---|
dev |
nodemon & webpack --watch |
Development (auto-restart server + hot rebuild) |
start |
webpack && node --harmony index.js |
Production |
webpack |
webpack |
Build only |
Webpack (webpack.config.js)
- Two entry points:
main→src/js/index.js,serviceworker→src/js/serviceworker.js - Output:
static/js/[name].js - Babel:
@babel/preset-env(last 2 Chrome versions), decorators (legacy), JSX, object rest spread - Loaders:
babel-loader,style-loader+css-loader,sass-loader - jQuery: Aliased to
stubs/jquery.js(returnsnull) to avoid pulling in full jQuery
Docker
bin/go # docker-compose up -d
bin/build # docker-compose build
bin/run <cmd> # docker-compose run --rm web /bin/bash -c "<cmd>"
bin/restart # stop → build → start
bin/softrestart # stop → start (no rebuild)
docker-compose.yml defines two services: mongo (MongoDB) and web (Node.js app on port 3000).
Environment Variables (.env)
Required variables (see .env.example):
GOOGLE_CLIENT_ID— Google OAuth client IDSPOTIFY_CLIENT_ID— Spotify API client IDSOUNDCLOUD_CLIENT_ID— SoundCloud API client IDSMTP_PASSWORD— Zoho SMTP password for password reset emailsYOUTUBE_REDIRECT_ENDPOINT— YouTube stream redirect URLSOUNDCLOUD_REDIRECT_ENDPOINT— SoundCloud stream redirect URL
CI/CD (.github/workflows/deploy.yml)
- Trigger: Push to
master - Action: Deploys to Dokku PaaS at
ssh://dokku@kaiserapps.com/stretto-next
External Integrations
| Integration | Purpose | Auth Method |
|---|---|---|
| Google OAuth | User authentication | OAuth2 ID token (server-side verification) |
| YouTube | Music search & streaming | Scraping (no API key) + redirect endpoint |
| SoundCloud | Music search & streaming | Client ID + HLS |
| Audius | Decentralized music streaming | Public API (no auth) |
| Spotify | Playlist import, chart data | OAuth2 implicit flow (client-side) |
| iTunes | Charts, artist discovery | Public Search API |
| AZLyrics | Lyrics fetching | Scraping via js-azlyrics |
| MusicBrainz | Cover art lookup | Public API |
| Zoho SMTP | Password reset emails | SMTP credentials |
Testing
There are no tests. No test framework, test files, or test scripts exist in the project.
Key Patterns & Conventions
- Singletons:
Player,AccountManager,SocketManager,SyncManagerare all instantiated at module level and exported as instances (export default new ClassName()) - Observer pattern: Models use
addOnChangeListener/change()for reactive updates instead of Redux - Per-user blobs: Database stores entire song/playlist collections as single documents per user — not normalized
- Source-prefixed IDs: Song IDs use
y_,s_,a_prefixes to identify their streaming source - JSX in services: Some service files use
.jsxextension and contain React elements (e.g.,alerter.jsx,sync_manager.jsx) - No TypeScript: Entire codebase is plain JavaScript with JSX
- Class components: All React components are class-based (no hooks)
- Global imports: Services and models are imported directly and used as global singletons throughout the codebase