# Louis! A Yoto Make Your Own (MYO) client backed by YouTube ![Louis](docs/images/louis-readme-banner.webp) Louis turns YouTube into audio playlists for your [Yoto](https://yotoplay.com/). Search or paste a link, arrange tracks, then Update — Louis downloads and transcodes. Linking a physical **Make Your Own (MYO)** card happens in the Yoto app, not here. Self-hosted **Nuxt** server app. Yoto OAuth token exchange and YouTube audio download (via yt-dlp) need a long-running server process, so a static export (Netlify/Vercel static, GitHub Pages, etc.) cannot power those flows. **Website:** [louis.romanek.us](https://louis.romanek.us/) · install guides, FAQ, and desktop downloads for end users. Demo: [https://github.com/user-attachments/assets/717ae10e-c22e-40ae-8175-acfed4fce100](https://github.com/user-attachments/assets/717ae10e-c22e-40ae-8175-acfed4fce100) [Website](https://louis.romanek.us/) · [Download](#download-desktop) · [Docker](#docker) · [Home Assistant](#home-assistant) · [Hosting](docs/HOSTING.md) · [Desktop docs](docs/DESKTOP.md) · [Contributing](CONTRIBUTING.md) · [Releases](docs/RELEASE.md) **Personal use only.** You are responsible for complying with [YouTube’s Terms of Service](https://www.youtube.com/t/terms) and applicable law when downloading audio. ## Features - Search YouTube and preview audio — **YouTube Data API** when configured (faster typed search, optional content filtering), otherwise bundled **yt-dlp**. Paste a video, Shorts, playlist, or channel URL in Search to load it; check rows to add them together - Browse your Yoto playlists. **New** names a playlist and creates it on Yoto right away (empty, or with tracks already picked in Search) - Drag-and-drop playlist editing (desktop); phone Search / Library flow with Add to playlist; **Add to Home**; phone Menu can Update every pending playlist at once - Playlist covers: generated art on create; **Artwork** to generate, upload, or crop a cover to Yoto’s default size (638×1011). Rename or delete from the playlist menu - Auto-split long YouTube sources (>55 min) into Part tracks; trim intros/outros before save - Save / Update to Yoto with download and transcode progress; optional normalize for new YouTube extracts - Per-track 16×16 art (Yoto icon library, [yotoicons.com](https://yotoicons.com/), upload, or draw) — reconnect Yoto once if icon upload asks for `user:icons:manage` - Same app as **desktop** (macOS / Windows), **Docker**, or **Home Assistant** — installers are **unsigned** (Gatekeeper / SmartScreen may warn) ## Choose how to run | You want… | Start here | | ---------------------------- | ---------------------------------------------- | | Mac / Windows app, no Docker | [Download (desktop)](#download-desktop) | | Docker on a NAS / homelab | [Docker](#docker) · [Hosting](docs/HOSTING.md) | | Home Assistant on your LAN | [Home Assistant](#home-assistant) | | Local Node development | [Native development](#native-development) | ## Download (desktop) Installers ship as **Assets** on each GitHub Release (same `vX.Y.Z` as Docker): **[Latest release](https://github.com/stuartromanek/louis/releases/latest)** | Platform | Asset | | ------------------- | --------------------------- | | macOS Apple Silicon | `Louis--arm64.dmg` | | macOS Intel | `Louis--x64.dmg` | | Windows | `Louis-Setup-.exe` | After install, the setup wizard asks for a Yoto client ID, then a **recommended** YouTube Data API key (Skip uses bundled yt-dlp). Prefer **Use default client** for Yoto, or bring your own from [yoto.dev](https://yoto.dev/get-started/start-here/) and paste `http://127.0.0.1:4010/api/yoto/auth/callback` into **Allowed Callback URLs**. Change keys later in **Settings → Advanced**. Details: [docs/DESKTOP.md](docs/DESKTOP.md). Installers are currently **unsigned** (Gatekeeper / SmartScreen may warn). Signing notes: [docs/DESKTOP_SIGNING.md](docs/DESKTOP_SIGNING.md). End-user install walkthroughs: [louis.romanek.us](https://louis.romanek.us/#install). ## Docker Docker includes Node, yt-dlp, and ffmpeg — you only need Docker and a Yoto client ID. ```bash git clone https://github.com/stuartromanek/louis.git cd louis cp .env.example .env # Paste Louis's bundled LOUIS_YOTO_CLIENT_ID or your own (see Self-host). # Recommended: LOUIS_YOUTUBE_API_KEY docker compose up -d --build ``` Open Louis at the same origin other devices will use (this machine: [http://localhost:4000](http://localhost:4000); phones/tablets: `http://:4000`). Health: `GET /api/health`. Prebuilt images (multi-arch `linux/amd64` + `linux/arm64` on each `v*` release): ```bash docker pull stuartromanek/louis:latest # or pin: stuartromanek/louis:vX.Y.Z ``` Equivalent on GHCR: `ghcr.io/stuartromanek/louis:latest` (and `:vX.Y.Z`). > Docker Hub anonymous pull rate limits can apply on busy hosts; GHCR is an equivalent fallback with the same tags. **Homelab one-click:** Portainer app template, Coolify, and CasaOS notes — [docs/HOSTING.md](docs/HOSTING.md). Cut releases: [docs/RELEASE.md](docs/RELEASE.md). ## Home Assistant Install Louis from Supervisor as a custom add-on (wraps the same GHCR image; options map to `LOUIS_*`; audio under `/data/audio`; UI on host port **4000**, not ingress). 1. **Settings → Add-ons → Add-on store → ⋮ → Repositories** → add `https://github.com/stuartromanek/louis` 2. Install **Louis**. Defaults use Louis’s bundled Yoto client and `http://homeassistant.local:4000/api/yoto/auth/callback` — no yoto.dev app needed if you keep both. Change the redirect off that URI only with your **own** public client (see [DOCS.md](homeassistant/louis/DOCS.md)). **youtube_api_key** is recommended (Data API search); leave empty to search with bundled yt-dlp. **youtube_safe_search** (`none` / `moderate` / `strict`) applies to typed search only when a key is set. 3. Open `http://homeassistant.local:4000` (or your host:port) Full options, redirect URI, and `cookie_secure` notes: [homeassistant/louis/DOCS.md](homeassistant/louis/DOCS.md). Sources live under `homeassistant/`; root `repository.yaml` + `louis/` symlinks are for Supervisor discovery. ## Self-host ### 1. Yoto client ID Louis ships a **public** PKCE client ID (not a secret): `PK00MDKCVwWvOG8o3px3qSl57FhfUZxm`. Paste it into `LOUIS_YOTO_CLIENT_ID` when you open Louis at a redirect already registered on Louis’s Yoto app — same value as desktop **Use default client** / the HA add-on default. There is **no** silent fallback if the env var is empty. | Redirect (exact) | Client | | -------------------------------------------------------- | ----------------------------------------------------------- | | `http://127.0.0.1:4010/api/yoto/auth/callback` | Louis bundled (desktop) — see [DESKTOP.md](docs/DESKTOP.md) | | `http://homeassistant.local:4000/api/yoto/auth/callback` | Louis bundled (HA default) | For any **other** origin (NAS IP, custom hostname, HTTPS domain), create your **own** public client at [yoto.dev](https://yoto.dev/get-started/start-here/) so you can register that exact `/api/yoto/auth/callback`, then use that client ID instead: | Setting | Value | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Allowed Callback URLs** (yoto.dev field name) | `http://:4000/api/yoto/auth/callback` or `https://your-domain/api/yoto/auth/callback` — same origin you open Louis | | Scopes | `offline_access user:content:view user:content:manage user:icons:manage` | Ports: desktop OAuth is **4010** (`127.0.0.1` only). Docker / Home Assistant / reverse proxy (Nginx Proxy Manager, etc.) use **4000**, or your HTTPS hostname with no port when TLS terminates at the proxy. Paste the callback into **Allowed Callback URLs** — not Login URI / Logout URI. ### 2. YouTube API (recommended) A YouTube Data API v3 key is **recommended** for faster search and `safeSearch=moderate` on typed search. In [Google Cloud Console](https://console.cloud.google.com/): create or pick a project, [enable YouTube Data API v3](https://console.cloud.google.com/apis/library/youtube.googleapis.com), then [create an API key](https://console.cloud.google.com/apis/credentials). If Google asks **What data will you be accessing?**, choose **Public data** (not User data) — Louis only needs an API key for public video search, not Google user OAuth. Restricting the key to YouTube Data API v3 is optional but smart. Walkthrough: [YouTube Data API getting started](https://developers.google.com/youtube/v3/getting-started). Search still works without a key (bundled yt-dlp): slower, no safeSearch, and search can break on a different week than download. Leave `LOUIS_YOUTUBE_API_KEY` unset to use that path. ### 3. Environment Copy `[.env.example](.env.example)`. Use `LOUIS_*` **names** so the same file works for local dev, `docker compose`, and `docker run --env-file .env` without rebuilding the image. Legacy `NUXT_`* / `NUXT_PUBLIC_*` names still work as a deprecated fallback (`LOUIS_*` wins when both are set). #### Required | Variable | Notes | | ---------------------- | ------------------------------------------------------------------------------------------------------- | | `LOUIS_YOTO_CLIENT_ID` | Public PKCE client ID — Louis bundled (`PK00…`) for pre-registered redirects, or your own from yoto.dev | #### Yoto | Variable | Notes | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `LOUIS_YOTO_REDIRECT_URI` | Optional pin; must match the portal. Unset: Louis uses the Host the browser actually used. Prefer a hostname over a DHCP IP. Other devices cannot use the Docker host’s localhost | | `LOUIS_COOKIE_SECURE` | OAuth cookie `Secure` flag. Docker image defaults `false` (LAN HTTP). Node-without-Docker: when unset, secure iff `NODE_ENV=production`. Set `true` behind HTTPS | #### YouTube / audio | Variable | Notes | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `LOUIS_YOUTUBE_API_KEY` | Recommended. YouTube Data API v3 (faster typed search). Unset: search without a key | | `LOUIS_YOUTUBE_SAFE_SEARCH` | Typed-search content filtering when a Data API key is set: `none`, `moderate` (default), or `strict`. Desktop: **Settings → Advanced**; HA: **youtube_safe_search** option | | `LOUIS_AUDIO_WORK_DIR` | Default `/data/audio` in Docker. Preview/save cache plus `logs/pipeline.jsonl` (YouTube extract diagnostics). For a long local capture, point this at a stable path rather than OS tmp. | | `LOUIS_AUDIO_JOB_MAX_AGE_MS` | Stale `jobs/` cleanup (default 1h) | | `LOUIS_AUDIO_CACHE_MAX_AGE_MS` | Cache file TTL (default 14d) | | `LOUIS_AUDIO_CACHE_MAX_BYTES` | Combined preview + save cache cap (default 5 GiB). Does not sweep `logs/`. | | `LOUIS_YTDLP_PATH` | Optional pin. Docker ships yt-dlp on `PATH`; Settings → Advanced can install a newer nightly into the audio volume / desktop app data (preferred when its version is newer) | | `LOUIS_YTDLP_COOKIES_FILE` | Optional Netscape `cookies.txt`. Downloads try anonymously first; cookies are used only if YouTube blocks with bot check, hard 403, or age-gate. Prefer a throwaway Google account; never commit the file | #### Advanced / debug | Variable | Notes | | --------------------------- | ------------------------------- | | `LOUIS_ENABLE_DEBUG_ROUTES` | `true` enables debug API routes | | `LOUIS_PIPELINE_LOG` | Pipeline journal on by default (`logs/pipeline.jsonl` under the audio work dir). Set `0` to disable | | `LOUIS_PIPELINE_LOG_VERBOSE` | `1` adds yt-dlp `-v` and larger stderr excerpts for a short capture window | After capturing, copy `logs/` off the volume if needed and run: ```bash npm run diagnose:pipeline -- /data/audio npm run diagnose:pipeline -- --video dQw4w9wgXcQ --since 7d ``` ```bash docker run -p 4000:4000 --env-file .env louis:local ``` ### 4. Deploy constraints - **Single instance** — save-job progress is in memory - **HTTPS in production** — Docker image defaults OAuth cookies to `LOUIS_COOKIE_SECURE=false` (LAN HTTP). Node-without-Docker defaults to `secure` when `NODE_ENV=production`. Set `true` behind TLS / reverse proxy; keep `false` for plain HTTP (Portainer LAN, Home Assistant) - **Persistent disk** — recommended for the audio cache under `LOUIS_AUDIO_WORK_DIR` (`cache/preview/`, `cache/save/`, `logs/`). Stale `jobs/` dirs and old cache files are swept on startup and after downloads; pipeline logs rotate by size and are not swept with the cache. Compose uses the named volume `louis-audio` ## Native development For local Node (without Docker), install these first: - Node.js 22+ (also used as yt-dlp’s JS runtime for YouTube signing) - [yt-dlp](https://github.com/yt-dlp/yt-dlp) — required for search (without a Data API key) and for save; keep it current - [ffmpeg](https://ffmpeg.org/) — required for save - Optional: `LOUIS_YOUTUBE_API_KEY` (faster search + `LOUIS_YOUTUBE_SAFE_SEARCH`); `LOUIS_YTDLP_COOKIES_FILE` as above Self-host web UI has no YouTube key field — set `LOUIS_YOUTUBE_API_KEY` and `LOUIS_YOUTUBE_SAFE_SEARCH` in `.env` (desktop app: **Settings → Advanced**). ```bash npm install cp .env.example .env npm run dev ``` Dev server: port **4000**. Tests: `npm test` (see [CONTRIBUTING.md](CONTRIBUTING.md)). Production without Docker: ```bash npm run build npm run start ``` ## License & notices MIT — see [LICENSE](LICENSE). Fonts (LT Saeada, self-hosted), OpenMoji icons, and [SND](https://snd.dev/) UI sounds are used; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Security reports: [SECURITY.md](SECURITY.md).