A minimal IPTV streaming desktop app built with Electrobun, React, Tailwind CSS, and Effect.
- Add and manage M3U playlists (persisted in SQLite)
- Browse channels grouped by category
- HLS & native stream playback
- Rename playlists inline (double-click the name)
- Collapsible channel sidebar while watching
# Install dependencies
bun install
# Start dev (Vite builds assets, Electrobun runs the app)
bun run dev
# Start with HMR (Vite dev server + Electrobun in watch mode)
bun run dev:hmr# Build a canary release for the current platform
bun run build:canaryOutput: build/canary-macos-arm64/tv-canary.app (macOS arm64)
macOS blocks apps that aren't signed by an Apple-registered developer. To open a local build:
Remove quarantine (one-time, per build)
xattr -cr build/canary-macos-arm64/tv-canary.app
open build/canary-macos-arm64/tv-canary.appOr via Finder
- Navigate to
build/canary-macos-arm64/ - Right-click
tv-canary.app→ Open - Click Open in the security dialog
You only need to do this once per build. After that, double-clicking works.
The .github/workflows/release.yml workflow automatically builds and uploads release artifacts when you publish a GitHub release.
What gets built:
tv-canary-macos-arm64.dmg— macOS Apple Silicontv-canary-macos-x64.dmg— macOS Inteltv-canary-windows-x64.exe— Windows
To create a release:
- Push your changes
- Go to GitHub → Releases → Draft a new release
- Set a tag (e.g.
v1.0.0) and publish - GitHub Actions builds all platforms and uploads
.dmg/.exeto the release automatically
Optional: code signing
Add these secrets in Settings → Secrets → Actions to sign the builds:
| Secret | Description |
|---|---|
APPLE_CERTIFICATE |
Base64-encoded .p12 export |
APPLE_CERTIFICATE_PASSWORD |
Password for the .p12 |
APPLE_TEAM_ID |
10-character Team ID |
APPLE_SIGNING_IDENTITY |
Developer ID Application: Name (TEAMID) |
APPLE_ID |
Apple ID email (for notarization) |
APPLE_APP_PASSWORD |
App-specific password (for notarization) |
WIN_CERTIFICATE |
Base64-encoded .pfx |
WIN_CERTIFICATE_PASSWORD |
Password for the .pfx |
Without these secrets the build runs unsigned — fine for personal use and testing.
src/
├── bun/
│ ├── index.ts # Main process: window, menu, RPC handlers
│ └── db.ts # SQLite database (bun:sqlite)
├── mainview/
│ ├── App.tsx # Root component
│ ├── main.tsx # React entry point
│ ├── rpc.ts # Electrobun RPC bridge (renderer side)
│ ├── components/
│ │ ├── Player.tsx # HLS video player
│ │ ├── Sidebar.tsx # Channel list with categories
│ │ └── PlaylistGrid.tsx # Playlist management grid
│ ├── hooks/
│ │ ├── usePlaylist.ts # M3U fetch & parse
│ │ └── useSavedPlaylists.ts # RPC-backed playlist state
│ └── services/
│ └── PlaylistService.ts # Effect-based M3U parser
└── shared/
└── rpcSchema.ts # Shared RPC type definitions
electrobun.config.ts # App name, identifier, icons, copy rules
vite.config.ts # Vite build config
| Layer | Library |
|---|---|
| Runtime | Bun |
| Desktop framework | Electrobun |
| UI | React 18 + Tailwind CSS |
| Async/effects | Effect |
| Video | hls.js + native WKWebView HLS |
| Virtualization | @tanstack/react-virtual |
| Database | bun:sqlite (SQLite) |