A full-featured, self-hosted frontend for YouTube, SoundCloud, PeerTube and more, built on top of NewPipeExtractor.
Licensed under GPL-3.0 — the same license as NewPipeExtractor. No Google account required. No ads. No tracking.
- Features
- Tech Stack
- Prerequisites
- Quick Start
- Running in Development
- Running with Docker
- Desktop App (Tauri)
- User Guide
- Settings Reference
- SponsorBlock
- API Reference
- Project Structure
- Troubleshooting
| Feature | Description |
|---|---|
| 🔍 Search | Search YouTube, SoundCloud, PeerTube without an account |
| 📺 Watch | Play videos with quality selector (144p–2160p) |
| 🎵 Audio only | Switch to background audio mode — saves bandwidth |
| 🖼️ Picture in Picture | Float video in a small window while browsing |
| 💬 Comments | Read comments without signing in |
| 🔤 Subtitles | Select subtitle language; auto-generated subtitles supported |
| ⏩ Resume | Automatically resumes where you left off |
| 📡 Feed | Latest videos from subscribed channels |
| 🔔 Subscriptions | Subscribe to channels — stored locally, no Google account |
| 🔖 Watchlist | Save videos to watch later |
| 📚 Playlists | Create and manage local playlists |
| 🕐 History | Full watch history with resume positions |
| ⬇️ Downloads | Download video or audio to your server |
| ⏭️ SponsorBlock | Opt-in automatic sponsor segment skipping |
| 🌙 Dark / Light | Toggle between dark and light theme |
| 🐳 Docker | One command to run the full stack |
| 🖥️ Desktop | Wrap as a native desktop app via Tauri |
| Service | Search | Watch | Channel | Trending | Comments |
|---|---|---|---|---|---|
| YouTube | ✅ | ✅ | ✅ | ✅ | ✅ |
| SoundCloud | ✅ | ✅ | ✅ | ✅ | ❌ |
| PeerTube | ✅ | ✅ | ✅ | ✅ | ❌ |
| Bandcamp | ✅ | ✅ | ✅ | ❌ | ❌ |
| media.ccc.de | ✅ | ✅ | ✅ | ✅ | ❌ |
| Odysee | ✅ | ✅ | ✅ | ❌ | ❌ |
Switch services using the dropdown in the search bar. The service selector only shows services relevant to the current context (e.g. the trending tab only shows services with a trending feed).
| Layer | Technology |
|---|---|
| Extraction engine | NewPipeExtractor (Java/Kotlin) |
| Backend API | Ktor (Kotlin) |
| Database | SQLite via Exposed ORM |
| Frontend | React + Vite + Tailwind CSS |
| State management | Zustand |
| Data fetching | TanStack Query |
| Desktop wrapper | Tauri 2 |
| Container | Docker + Docker Compose |
Build tool & Language:
| Component | Version | Purpose |
|---|---|---|
| Kotlin | 2.0.0 | JVM language |
| JDK | 22 | Java runtime |
| Gradle | 8.14 | Build system |
Core libraries:
| Library | Version | Purpose |
|---|---|---|
| Ktor Server Core | 2.3.12 | HTTP server framework |
| Ktor Server Netty | 2.3.12 | Async network engine |
| Ktor Content Negotiation | 2.3.12 | JSON/serialization middleware |
| Ktor CORS | 2.3.12 | Cross-origin request handling |
| Ktor Call Logging | 2.3.12 | Request/response logging |
| Ktor Status Pages | 2.3.12 | Error handling |
| Ktor Client CIO | 2.3.12 | HTTP client for file streaming |
Extraction & Parsing:
| Library | Version | Purpose |
|---|---|---|
| NewPipeExtractor | 0.26.0 (JitPack) | YouTube/SoundCloud/PeerTube extractor |
| Kotlinx Serialization | 1.6.3 | JSON serialization |
Database:
| Library | Version | Purpose |
|---|---|---|
| Exposed Core | 0.50.1 | ORM framework |
| Exposed DAO | 0.50.1 | Data access objects |
| Exposed JDBC | 0.50.1 | JDBC driver integration |
| Exposed Java Time | 0.50.1 | DateTime support |
| SQLite JDBC | 3.45.3.0 | SQLite driver |
Async & Utilities:
| Library | Version | Purpose |
|---|---|---|
| Kotlinx Coroutines | 1.8.1 | Async/await support |
| Logback | 1.5.6 | Logging implementation |
| dotenv-kotlin | 6.4.1 | Environment variable loading |
Testing:
| Library | Version | Purpose |
|---|---|---|
| Ktor Server Tests | 2.3.12 | Integration testing |
| Kotlin Test | 2.0.0 | Unit testing framework |
Build & Runtime:
| Tool | Version | Purpose |
|---|---|---|
| Node.js | 24 LTS | JavaScript runtime |
| TypeScript | 5.4.5 | Type-safe JavaScript |
| Vite | 5.3.1 | Dev server & build tool |
| React Router | 6.23.1 | Client-side routing |
UI & Styling:
| Library | Version | Purpose |
|---|---|---|
| React | 18.3.1 | Component framework |
| React DOM | 18.3.1 | React rendering engine |
| Tailwind CSS | 3.4.4 | Utility-first CSS |
| PostCSS | 8.4.38 | CSS preprocessor |
| Autoprefixer | 10.4.19 | CSS vendor prefixes |
| clsx | 2.1.1 | Conditional CSS classes |
| Lucide React | 0.383.0 | Icon library |
State & Data:
| Library | Version | Purpose |
|---|---|---|
| TanStack Query | 5.40.0 | Server state management |
| Zustand | 4.5.2 | Client state (preferences, UI) |
| Axios | 1.7.2 | HTTP client |
Media:
| Library | Version | Purpose |
|---|---|---|
| React Player | 2.16.0 | Video/audio playback |
Dev Tools:
| Tool | Version | Purpose |
|---|---|---|
| Vite React Plugin | 4.3.1 | Vite React JSX support |
| ESLint | Latest | Code linting |
Frontend wrapper:
| Component | Version | Purpose |
|---|---|---|
| Tauri | 2.0.0 | Desktop app framework |
| Tauri CLI | 2.0.0 | Build & dev tool |
| Tauri API | 2.0.0 | Rust/JavaScript bridge |
| Tauri Shell Plugin | 2.0.0 | Shell command execution |
Serialization:
| Library | Version | Purpose |
|---|---|---|
| serde | 1.0 | Rust serialization |
| serde_json | 1.0 | JSON support |
Rust toolchain:
| Tool | Version | Purpose |
|---|---|---|
| Rust | Latest (via rustup) | Systems language |
| Cargo | Latest (via rustup) | Rust package manager |
| Tool | Version | Purpose |
|---|---|---|
| Docker | Latest | Container runtime |
| Docker Compose | Latest | Multi-container orchestration |
| nginx | Latest | Reverse proxy & static server |
| Alpine Linux | 3.x | Lightweight base image |
| Tool | Version | Download |
|---|---|---|
| JDK | 21 or 25 | adoptium.net |
| Node.js | 24 LTS | nodejs.org |
| Git | Any | git-scm.com |
| Tool | Download |
|---|---|
| Docker Desktop | docker.com/products/docker-desktop |
| Tool | Download |
|---|---|
| Rust toolchain | rustup.rs |
| C++ build tools | Windows: Visual C++ Build Tools |
| Xcode CLT | macOS: xcode-select --install |
| WebKit dev libs | Linux: see Desktop App section |
# 1. Clone or unzip the project
cd my-newpipe-web
# 2. Start everything with Docker
docker compose up --build
# 3. Open in browser
open http://localhost:3000That's it. The backend runs on port 8080, frontend on port 3000.
Running without Docker gives you hot-reload on both frontend and backend.
cd backend
# First run: Gradle will download all dependencies including NewPipeExtractor
./gradlew run
# On Windows:
gradlew.bat runThe backend starts on http://localhost:8080.
On first run Gradle downloads dependencies — this takes 1–3 minutes. Subsequent starts are much faster.
Open a new terminal:
cd frontend
npm install # first run only
npm run devThe frontend starts on http://localhost:5173.
The Vite dev server proxies all /api requests to the Ktor backend automatically,
so you don't need to configure CORS or URLs manually.
Navigate to http://localhost:5173 in your browser.
Docker runs the full stack (backend + frontend + nginx) in containers.
# Build and start
docker compose up --build
# Run in background
docker compose up --build -d
# Stop
docker compose down
# View logs
docker compose logs -f backend
docker compose logs -f frontend| Service | Port |
|---|---|
| Frontend (nginx) | http://localhost:3000 |
| Backend (Ktor) | http://localhost:8080 |
Docker mounts two local directories as volumes:
| Directory | Contents |
|---|---|
./data/ |
SQLite database (history, playlists, subscriptions, etc.) |
./downloads/ |
Downloaded video and audio files |
These persist across container restarts. Back them up to keep your data.
The desktop app wraps the web frontend in a native window using Tauri. It requires the Ktor backend to be running separately.
# Install rustup (the Rust toolchain manager)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Restart your terminal, then verify:
rustc --version
cargo --versionWindows: Install Visual C++ Build Tools and select "Desktop development with C++".
macOS:
xcode-select --installLinux (Ubuntu/Debian):
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev build-essential# Make sure the backend is already running:
cd backend && ./gradlew run
# In a new terminal, start the frontend dev server:
cd frontend && npm run dev
# In another terminal, start the desktop app:
cd desktop
npm install
npm run devA native window opens pointing at the frontend. Any changes to the frontend hot-reload in the window automatically.
cd desktop
npm run buildOutput is in desktop/src-tauri/target/release/bundle/.
- Windows:
.msiinstaller - macOS:
.dmg - Linux:
.AppImageor.deb
Home page shows trending videos from YouTube.
Search — type in the search bar at the top and press Enter or click the 🔍 icon. Results come from YouTube (and other supported services if the URL is recognized).
Recent searches are saved locally and shown as you type.
Click any video card to open the watch page.
Quality selector — below the player toolbar, click any resolution badge to switch. Your preferred quality is applied automatically (set in Settings).
Playback speed — use the browser's native controls (right-click the video). Your default speed is set in Settings.
Resume — if you've watched part of a video before, it automatically picks up from where you left off.
- Click the CC button in the player toolbar to enable subtitles.
- Language buttons appear below the quality selector.
- Click a language to switch.
- Set your preferred default language in Settings → Player → Preferred subtitle language.
Auto-generated subtitles are labeled (auto).
Click the PiP button in the player toolbar. The video pops out into a small floating window managed by your browser. You can browse other pages while the video keeps playing. Click the X on the PiP window or press PiP again to return to full mode.
PiP requires a browser that supports the Picture-in-Picture API (Chrome, Edge, Safari, Firefox 116+).
Click the Audio only button to switch the player to an audio-only stream. This is useful for music or podcasts — it uses significantly less data and CPU than video streams.
Click the button again to switch back to video.
On any Channel page, click Subscribe. The subscription is saved locally in the database — no Google account needed.
The Feed page shows the latest videos from all your subscribed channels, sorted by upload date.
The Subscriptions page lists all channels you've subscribed to. Click a channel name to go to its page, or click Unsubscribe to remove it.
On any video's watch page, click the Save (bookmark) button to add it to your watchlist. Click again to remove it.
View your watchlist from the left sidebar.
Creating a playlist:
- Go to Library in the sidebar.
- Click New Playlist.
- Enter a name and optional description.
- Click Create.
Adding a video to a playlist:
- Open the video's watch page.
- Click the Playlist button.
- Select an existing playlist or create a new one on the spot.
- A checkmark confirms the video was added.
Viewing a playlist:
- Go to Library.
- Click on any playlist card.
- Videos are listed in order. Click any to watch.
The History page shows all videos you've watched, most recent first. Each entry stores your watch position so you can resume later.
- Click any entry to resume watching.
- Click the trash icon to remove a single entry.
- Click Clear All to wipe the full history.
- You can also clear history from Settings → Privacy.
On any video's watch page, click Download. The download starts immediately in the background on the server.
Go to the Downloads page to:
- See download progress (with a progress bar for active downloads)
- Download the completed file to your device (click the ↓ icon)
- Delete downloads
Downloaded files are stored in the ./downloads/ directory on the server.
Note: The download uses the currently selected stream URL. If you want audio only, enable Background Audio Mode first, then click Download.
On the watch page, click Show Comments to load and display the comments section. Click again to collapse it.
Pinned comments are marked 📌. Comments hearted by the creator are marked ❤️.
Open Settings from the left sidebar (gear icon at the bottom).
| Setting | Description |
|---|---|
| Theme | Toggle between dark mode (default) and light mode |
| Setting | Default | Description |
|---|---|---|
| Preferred quality | 720p | Automatically selects this quality when opening a video |
| Default playback speed | 1x | Videos start at this speed |
| Show subtitles by default | Off | Auto-enable CC when available |
| Preferred subtitle language | en | ISO 639-1 code for your preferred subtitle language |
| Setting | Default | Description |
|---|---|---|
| Enable SponsorBlock | Off | Master toggle — must be enabled for any skipping to happen |
| Sponsor segments | ✅ | Paid promotions integrated into the video |
| Self-promotion | — | Creator's own unpaid promotions |
| Interaction reminders | — | Like/subscribe/comment calls to action |
| Intro / title card | — | Opening animation or title card |
| Outro / credits | — | Closing credits or endscreen |
| Preview of content | — | Teaser for what's coming up |
| Off-topic (music) | — | Non-music sections in music videos |
| Filler / tangent | — | Filler content or tangents unrelated to the topic |
See SponsorBlock categories for detailed descriptions.
| Action | Description |
|---|---|
| Clear history | Deletes all watch history from the database |
| Clear recent searches | Clears the local search history shown in the search bar |
SponsorBlock is a community-driven project that crowdsources timestamps for skippable segments in YouTube videos.
- Go to Settings and enable SponsorBlock.
- Choose which segment categories you want to skip.
- When you watch a video, the app fetches segment timestamps from
sponsor.ajay.appusing only the video ID. - When your playback position enters a segment, it automatically skips to the end of that segment.
- A brief toast notification shows what was skipped.
- A counter in the player toolbar shows how many segments were skipped.
- Only the video ID is sent to the SponsorBlock API.
- Your watch history, identity, and IP address are not associated with any user account in SponsorBlock.
- SponsorBlock is disabled by default. You must explicitly enable it in Settings.
- If the SponsorBlock API is unreachable, video playback continues normally.
If you notice incorrect segments or want to contribute timestamps, install the SponsorBlock browser extension or use the Android app.
All endpoints are served by the Ktor backend on port 8080.
In Docker, the frontend nginx config proxies /api/* to the backend.
| Method | Path | Description |
|---|---|---|
| GET | /search?q={query} |
Search for videos |
| GET | /video/{id} |
Get video metadata |
| GET | /stream/{id} |
Get playable stream URLs + subtitles |
| GET | /channel/{id} |
Get channel info + recent videos |
| GET | /trending |
Get trending videos |
| GET | /comments/{videoId} |
Get video comments |
| Method | Path | Description |
|---|---|---|
| GET | /history |
Get all watch history (newest first) |
| POST | /history |
Add or update a history entry |
| DELETE | /history |
Clear all history |
| DELETE | /history/{id} |
Remove a single entry |
POST /history body:
{
"videoId": "dQw4w9WgXcQ",
"title": "Video title",
"uploader": "Channel name",
"thumbnailUrl": "https://...",
"duration": 212,
"watchedSeconds": 95
}| Method | Path | Description |
|---|---|---|
| GET | /watchlist |
Get watchlist |
| POST | /watchlist |
Add a video |
| DELETE | /watchlist/{id} |
Remove a video |
| Method | Path | Description |
|---|---|---|
| GET | /playlists |
List all playlists |
| POST | /playlists |
Create a playlist |
| GET | /playlists/{id} |
Get playlist with all videos |
| DELETE | /playlists/{id} |
Delete a playlist |
| POST | /playlists/{id}/videos |
Add a video to a playlist |
| DELETE | /playlists/{id}/videos/{videoItemId} |
Remove a video |
| Method | Path | Description |
|---|---|---|
| GET | /subscriptions |
List all subscriptions |
| POST | /subscriptions |
Subscribe to a channel |
| DELETE | /subscriptions/{id} |
Unsubscribe |
| GET | /feed |
Latest videos from subscribed channels |
| Method | Path | Description |
|---|---|---|
| GET | /downloads |
List all downloads |
| POST | /downloads |
Start a new download |
| GET | /downloads/{id} |
Get download status |
| DELETE | /downloads/{id} |
Delete download record + file |
| GET | /downloads/{id}/file |
Download the completed file |
my-newpipe-web/
│
├── backend/ Kotlin + Ktor API server
│ ├── build.gradle.kts Dependencies (NewPipeExtractor, Exposed, etc.)
│ ├── Dockerfile
│ └── src/main/kotlin/com/newpipeweb/
│ ├── Application.kt Entry point
│ ├── NewPipeDownloader.kt HTTP adapter for NewPipeExtractor
│ ├── plugins/ Ktor plugin configs (CORS, routing, etc.)
│ ├── routes/ API route handlers
│ ├── services/
│ │ └── YouTubeService.kt All NewPipeExtractor calls
│ ├── database/
│ │ ├── tables/ SQLite table definitions
│ │ └── repositories/ Database CRUD operations
│ └── models/ Serializable data classes
│
├── frontend/ React + Vite + Tailwind
│ ├── src/
│ │ ├── App.tsx Router + layout
│ │ ├── main.tsx Entry point
│ │ ├── api/
│ │ │ ├── client.ts All backend API calls
│ │ │ └── sponsorblock.ts SponsorBlock API client
│ │ ├── components/
│ │ │ ├── layout/ Navbar, Sidebar
│ │ │ ├── video/ VideoCard, VideoGrid
│ │ │ ├── playlist/ AddToPlaylistModal
│ │ │ └── common/ LoadingSpinner, ErrorMessage, EmptyState
│ │ ├── hooks/
│ │ │ ├── index.ts All TanStack Query hooks
│ │ │ └── useSponsorBlock.ts SponsorBlock segment hook
│ │ ├── pages/ One file per route
│ │ ├── store/
│ │ │ └── useAppStore.ts Zustand global state + user preferences
│ │ └── types/
│ │ └── index.ts TypeScript interfaces
│ ├── Dockerfile
│ └── nginx.conf Serves frontend + proxies /api to backend
│
├── desktop/ Tauri 2 desktop wrapper
│ └── src-tauri/
│ ├── tauri.conf.json Window config, points at frontend
│ ├── Cargo.toml Rust dependencies
│ └── src/main.rs Minimal Tauri entry point
│
├── data/ SQLite database (auto-created, git-ignored)
├── downloads/ Downloaded files (auto-created, git-ignored)
├── docker-compose.yml
└── README.md
Could not find com.github.TeamNewPipe:NewPipeExtractor
- Make sure JitPack is in your repositories in
build.gradle.kts - Run
./gradlew dependenciesto force a dependency refresh - Check your internet connection — JitPack needs to build the library on first fetch
Port 8080 already in use
- Something else is running on 8080
- Change the port in
Application.kt:port = 8081 - Update
vite.config.tsproxy target to match
Stream URLs expire — NewPipeExtractor stream URLs are valid for roughly 6 hours. If you leave a video paused for a long time and come back, you may need to refresh the page to get fresh stream URLs.
No playable stream found — YouTube occasionally changes its internal API.
Check the NewPipeExtractor releases
for a newer version and update build.gradle.kts.
- Make sure it's enabled in Settings (it's off by default)
- Check at least one segment category is selected
- Open browser DevTools → Network tab and look for requests to
sponsor.ajay.app - If requests return 404, the video has no community-submitted segments yet
Backend container exits immediately
docker compose logs backendLook for the error. Common cause: JDK version mismatch in Dockerfile.
Change eclipse-temurin:21-jre-alpine to eclipse-temurin:25-jre-alpine if needed.
bind: address already in use
Change the host port in docker-compose.yml:
ports:
- "8081:8080" # host:containercargo not found — Install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
WebKit2GTK not found (Linux) — Install system dependencies:
sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-devWhite screen in desktop window — Make sure both the backend (./gradlew run)
and the frontend dev server (npm run dev) are running before launching Tauri.