Skip to content
ternilabsPublic

About

A modern, lightweight, browser-based streaming platform integrated with third-party APIs.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

TerniLabs Stream

A lightweight, educational streaming metadata browser for movies and TV series.

TypeScript Preact Vite License Version

Overview • Features • Getting started • Usage • Architecture • Configuration • Disclaimer

Overview

TerniLabs Stream is a single-page web app built with Preact and Vite that browses movie and TV metadata and routes playback to one of 18 third-party embed providers. It is a client for a separate stream-api service: this repository contains the UI only.

The app is designed as a fast, dependency-light reference implementation for a catalog browser. It uses Preact hooks for state, preact-iso for client-side routing, and a 24-hour localStorage cache layer that keeps the upstream API inside its free-tier rate limit.

Important

This project is intended for educational and private use only. The developer does not condone or encourage copyright infringement. TerniLabs does not store or host any media. All streams are served by third-party providers that are not affiliated with, endorsed by, or connected to this project.

Features

  • Three routes — Home (/), Search (/search), Watch (/watch/:id).
  • Home page — four paged sections (Trending Movies, Trending TV, Top Rated Movies, Top Rated TV) with skeleton loading and viewport-aware card counts (6 / 4 / 2 by breakpoint).
  • Footer — a site-wide disclaimer stating the project is not affiliated with any streaming platform.
  • Search — debounced quick-search panel with up to 6 results, recent searches (max 5, deduplicated, individually removable), and a dedicated /search route with All / TV / Movie filter and first/prev/next/last pagination.
  • Watch page — server selector, TV season and episode pickers populated from API metadata, detail card, trailer link, up to 12 recommendations, and expandable description and cast lists.
  • Settings dialog — live source health indicators merged from the API snapshot, with a confirmed local-storage clear action.
  • Daily cache — 24-hour localStorage cache that transparently reuses API responses.
  • Rate-limit friendly — daily-resetting Ko-fi donation prompt shown when the API returns 429, with no extra request cost.
  • Stable loading — skeletons are built from the same classes as the content they replace, so pages do not reflow when data arrives.
  • Accessibility — aria-expanded, aria-current, listbox / option semantics on all custom dropdowns, and prefers-reduced-motion respected on shimmer and spinners.
  • Responsive — 6 / 4 / 2 column media grids and a mobile-only search overlay with scrim and Escape-to-close.

Tech stack

Concern Choice
UI runtime Preact 10.x
Routing preact-iso
Icons preact-feather
Fonts @fontsource-variable/red-hat-* (Display, Text, Mono)
Styling Tailwind CSS v4 via @tailwindcss/vite
Build tool Vite 8.x
Language TypeScript 6.x (strict)
Unit tests Vitest + Testing Library + jsdom
End-to-end tests Playwright

Getting started

Prerequisites

Install

npm install

Run the dev server

npm run dev

The app starts on http://localhost:5173 (Vite default) and binds to 0.0.0.0 for LAN access.

Build for production

npm run build

The static bundle is emitted to dist/. Serve it with any static host — _redirects is already configured for SPA fallback (/* /index.html 200), so it works on hosts like Netlify and Cloudflare Pages out of the box.

Preview the production build

npm run preview

Type-check and test

npm run typecheck   # tsc -b, no emit
npm test            # vitest run
npm run test:watch  # vitest watch mode

End-to-end tests

npm run e2e         # Playwright, against captured API fixtures
npm run e2e:ui      # Playwright UI mode
npm run e2e:live    # same specs against the live stream-api

The suite runs against fixtures in e2e/fixtures/ by default, so it is deterministic offline and in CI. That also gives the layout-shift assertions stable data to measure. e2e:live points the same specs at the real API and skips the assertions that depend on exact fixture contents.

Browsers install once with npx playwright install chromium.

Usage

Routes

Path Component Purpose
/ HomePage Four paged catalog rails with skeleton loading.
/search SearchPage Full search with type filter, pagination, and URL-synced state.
/watch/:id WatchPage Player, server selector, TV picker, details, recommendations, cast, trailer; invalid watch routes render a centered invalid state.

Query parameters

Route Param Purpose
/search q Search query.
/search type multi (default), tv, or movie.
/search page 1-based page index.
/watch/:id id Must be a positive integer. Invalid IDs render the invalid watch state without calling the API.
/watch/:id type movie (default when omitted) or tv. Unsupported values render the invalid watch state without calling the API.
/watch/:id season, episode Used when type=tv. Invalid values are normalized to the first valid season and episode from the API.

Keyboard and pointer

  • Esc closes the mobile search overlay and the search panel.
  • Click outside the search panel closes it.
  • The search input debounces by 500 ms; stale responses from earlier requests are dropped.

Architecture

src/
├── app.tsx                # LocationProvider + ErrorBoundary + Router + Footer
├── main.tsx               # Preact render entry point
├── components/            # Nav, SearchBox, MediaCard, MediaSection, etc.
├── pages/                 # HomePage, SearchPage, WatchPage
├── hooks/                 # useVisibleCount, useSourceHealth
├── lib/
│   ├── api-client.ts      # Typed fetch wrapper, 4 s timeout, single 502 retry
│   ├── embed-resolver.ts  # URL template → embed URL for 18 sources
│   ├── local-store.ts     # Versioned localStorage helpers
│   ├── queries.ts         # 24 h cache + in-flight de-duplication over the API client
│   ├── source-health.ts   # Merges registry with API health snapshot
│   ├── source-registry.ts # 18 embed providers (movie + tv templates)
│   └── types.ts           # Shared domain and API types
├── styles/                # Tailwind v4 entry, tokens, and one file per area
└── test/                  # Vitest setup

e2e/
├── fixtures/              # API responses captured from stream-api
├── support/               # Route mocking and shared helpers
└── *.spec.ts              # Playwright specs

Data flow

  1. The api-client issues typed requests to stream-api with a 4 s timeout and a single retry on 502.
  2. queries.ts wraps every call in a versioned 24 h localStorage cache, so the second mount of a route or page is free. Callers that ask for the same key before the first request settles share that request rather than issuing their own.
  3. source-registry.ts lists 18 third-party embed providers; source-health.ts overlays the API's health snapshot to drive the status dots in the settings dialog.
  4. embed-resolver.ts turns (source, { type, id, season, episode }) into the final embed URL — no media is proxied through this app.
  5. The home, search, and watch pages all read through queries.ts, so caching is transparent.

Source registry

Sources are static metadata in src/lib/source-registry.ts. Each entry declares a movie and a TV embed template with {id}, {season}, and {episode} placeholders. The settings dialog renders the registry merged with the API health snapshot, so a failing provider is visible without leaving the app.

Configuration

The app reads a single environment variable at build time:

Variable Default Purpose
VITE_API_BASE_URL (empty — same origin) Base URL of the stream-api service.

Create a .env file at the project root:

# .env
VITE_API_BASE_URL=https://your-stream-api.example.com

Warning

Do not commit your .env file. It is already covered by .gitignore.

SPA routing

_redirects ships a single catch-all that maps every unknown path to index.html with a 200 response, which is what hosts like Netlify and Cloudflare Pages need for client-side routes like /watch/123 to refresh cleanly.

Disclaimer

Caution

TerniLabs Stream is a metadata browser and embed launcher. It does not host, store, mirror, or transcode any media. All playback is delegated to third-party providers listed in src/lib/source-registry.ts, which are not affiliated with, endorsed by, or connected to this project.

The project is intended for educational and private use only. The developer does not condone or encourage copyright infringement. You are responsible for ensuring that your use of the third-party providers complies with their terms and with the laws of your jurisdiction.

Support

If you find a bug or want to propose a change, please open an issue or pull request on the original GitHub repository.

Support performance improvements and independent servers through Ko-fi.

Developed with GPT 5.5 (medium intelligence) as the brain and DeepSeek V4 Flash (medium intelligence) as the executor.

About

A modern, lightweight, browser-based streaming platform integrated with third-party APIs.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages