**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 `.env` via `dotenv` - Creates Express + HTTP server + Socket.IO - Middleware: `body-parser`, `cookie-parser`, `express-session` (MongoDB-backed via `connect-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 from `controllers/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: 1. `TitleUpdater` → updates browser tab title with current song 2. `FirstRunExperience` → onboarding flow 3. `AccountManager` → auth state 4. `Keyboard` → keyboard shortcuts (Space = play/pause, arrows = next/prev) 5. `Theme` → loads Bootswatch theme from CDN 6. `ModelInitialiser` → loads songs/playlists from localStorage 7. `RootView` → mounts React Router into `#react` div 8. `Lyrics` → auto-fetches lyrics via AZLyrics 9. `ServiceWorkerClient` → registers service worker 10. `SoundcloudOAuth` → 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: - `Song` and `Playlist` are static classes holding in-memory arrays - They expose `addOnChangeListener(callback)` and `change()` (notify) methods - Components subscribe in constructors and call `setState({})` to re-render on changes - **Persistence:** `DataLayer` wraps `localStorage`; `SyncManager` syncs 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-audio` cache for downloaded songs - **BroadcastChannel API:** Receives `DOWNLOAD`, `OFFLINE_RAW_FILE`, `REMOVE_OFFLINE`, `EMIT_OFFLINED` messages ### 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` (returns `null`) to avoid pulling in full jQuery ### Docker ```bash bin/go # docker-compose up -d bin/build # docker-compose build bin/run # docker-compose run --rm web /bin/bash -c "" 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 ID - `SPOTIFY_CLIENT_ID` — Spotify API client ID - `SOUNDCLOUD_CLIENT_ID` — SoundCloud API client ID - `SMTP_PASSWORD` — Zoho SMTP password for password reset emails - `YOUTUBE_REDIRECT_ENDPOINT` — YouTube stream redirect URL - `SOUNDCLOUD_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 1. **Singletons:** `Player`, `AccountManager`, `SocketManager`, `SyncManager` are all instantiated at module level and exported as instances (`export default new ClassName()`) 2. **Observer pattern:** Models use `addOnChangeListener`/`change()` for reactive updates instead of Redux 3. **Per-user blobs:** Database stores entire song/playlist collections as single documents per user — not normalized 4. **Source-prefixed IDs:** Song IDs use `y_`, `s_`, `a_` prefixes to identify their streaming source 5. **JSX in services:** Some service files use `.jsx` extension and contain React elements (e.g., `alerter.jsx`, `sync_manager.jsx`) 6. **No TypeScript:** Entire codebase is plain JavaScript with JSX 7. **Class components:** All React components are class-based (no hooks) 8. **Global imports:** Services and models are imported directly and used as global singletons throughout the codebase