Chat about this codebase

AI-powered code exploration

Online

Project Overview

Lampa is a free, self-hosted multimedia hub for browsing and streaming movies, TV shows and live channels. It unifies public links, torrents and IPTV sources into a single, customizable interface that runs on TVs, desktops and mobile devices.

What Is Lampa?

Lampa initializes a client-side HTML5 application (public/index.html → src/app.js)
• Loads global settings, language packs and UI modules
• Supports plugin architecture for custom sources and scrapers
• Provides a responsive skin with TV-friendly navigation and remote-control support

Supported Devices

• Smart TVs: WebOS, Tizen, Orsay, Netcast, Apple TV
• Android TV / Android Smartphones
• Desktop: NW.js, Electron, modern browsers
• Windows apps via AndroidJS
• MSX-based set-top boxes (manual setup)
• Dockerized deployments

Key Features

• Streaming: integrate HTTP/HTTPS public links and HLS streams
• IPTV: load M3U playlists and EPG data
• Torrents: play via WebTorrent or native client fallback
• Plugins: add new sources with JavaScript modules
• Skinning: switch between “TV” and “Mobile” layouts
• Offline mode: cache metadata and images in IndexedDB

Quick Start (Local Development)

Clone, install dependencies and launch the dev server with live reload:

git clone https://github.com/yumata/lampa-source.git
cd lampa-source
npm install
npm run dev        # starts local server on http://localhost:8000

Build production assets:

npm run build      # outputs to build/ directory

License

All code in the root and index/github/ is licensed under GNU GPL-2.0-or-later (see LICENSE).
The webos/ folder uses Apache 2.0 (see webos/LICENSE-2.0.txt).
Contributions must preserve existing license headers and include “Changed by” notices for GPL files.

When to Choose Lampa

• You need a self-hosted, privacy-focused media center
• You want to aggregate diverse free sources (IPTV, torrents, public links)
• You require cross-platform support with remote/navigation optimization
• You plan to extend functionality via custom plugins or skins
• You prefer an open-source solution under GPL/Apache licensing

Getting Started

Get Lampa running locally in minutes—no prior configuration required.

Prerequisites

  • Node.js ≥14 and npm (or Yarn)
  • Git
  • (Optional) Docker for containerized web serving

1. Clone & Install

git clone https://github.com/your-org/lampa-source.git
cd lampa-source
npm install

2. Run in Development Mode

Starts a live-reload server with inline source maps.

npm start
# or, equivalently
npx gulp debug

Open http://localhost:3000 (or the port logged in console).

3. Build for Production

Bundles JS/CSS, minifies assets, and outputs to dest/.

npm run build
# or
npx gulp pack

dest/app.min.js – your minified application bundle
dest/assets/ – compiled CSS and static files

4. Run Tests

Execute unit and integration checks.

npm test
# or
npx gulp test

5. Generate API & User Docs

Produces Markdown/HTML under docs/.

npm run docs
# or
npx gulp docs

6. Serve the Static Bundle

If you just need to serve dest/ over HTTP:

npx http-server dest/ -p 8080

Browse http://localhost:8080 to verify production assets.

7. (Optional) Docker: GitHub Index Site

Builds an Apache‐based container serving the static index with your domain/prefix.

  1. Navigate into the Docker context:
    cd index/github
    
  2. Build with your domain and URL prefix:
    docker build \
      --tag lampa-index \
      --build-arg domain=example.com \
      --build-arg prefix="https://" \
      .
    
  3. Verify start.json:
    docker run --rm lampa-index cat /usr/local/apache2/htdocs/msx/start.json
    
  4. Launch container on port 8080:
    docker run -d \
      --name lampa-index \
      -p 8080:80 \
      lampa-index
    
    Visit http://localhost:8080.

With these steps, you can clone, develop, build, test, document and even dockerize Lampa in minutes. Proceed to the next section to explore plugin development and advanced workflows.

3 Architecture

This section describes how the core bootstrap, rendering, and interaction layers fit together in the Lampa source. Understanding this flow helps you navigate and extend the codebase.

3.1 Application Bootstrap (src/app.js)

Overview
The bootstrap module initializes global classes, configuration, event handlers, then loads language packs, plugins, and UI components in a prioritized sequence.

Startup Sequence

  1. initClass() – attaches core utilities to window (e.g. Utils, Storage, Task).
  2. initConfig() – reads window.lampa_settings and applies defaults.
  3. loadTask() – enqueues primary (Task.queue) and secondary (Task.secondary) startup jobs.
  4. Task.start() – runs all tasks in order, then calls startApp().
  5. startApp() – invokes the UI render (render.app()), sets up network checks, and emits the 'app:ready' event.

Key Hooks & Events

  • window.addEventListener('app:init', fn) – before any tasks run.
  • window.addEventListener('app:ready', fn) – after UI is mounted and plugins loaded.

Example: Simple Startup

// src/index.js
import app from './app.js';

// Listen for full readiness
window.addEventListener('app:ready', () => {
  console.log('Lampa is ready');
});

// Kick off the bootstrap
app.initClass();
app.initConfig();
app.loadTask();
app.start(); // alias for Task.start()

Customizing

  • Insert tasks via Task.queue(fn) / Task.secondary(fn) before app.start().
  • Override window.lampa_settings fields before initConfig() for feature flags or custom endpoints.

3.2 UI Rendering (src/interaction/render.js)

Overview
The render module composes and injects UI components into the <div id="app"> container. It orchestrates header, menu, activity stack, background, settings, search, and birthday widgets.

Usage

import renderApp from './interaction/render.js';

// Direct invocation mounts the full layout
renderApp();

Extension Points

  • After mounting, you can inject new components into the DOM:

    // add a custom banner above the menu
    const appRoot = document.getElementById('app');
    const banner = document.createElement('div');
    banner.id = 'custom-banner';
    banner.innerHTML = '<h2>Welcome to My Lampa Mod</h2>';
    appRoot.insertBefore(banner, appRoot.firstChild);
    
  • To override a built-in panel, replace its container before calling renderApp():

    document.getElementById('app').innerHTML = '<div id="my-panel"></div>';
    renderApp();
    // now "#my-panel" will host your UI instead of the default layout
    

3.3 Interaction Controller (src/interaction/controller.js)

Overview
Controller provides focus management, key event binding, navigation commands, and collection handling for all UI screens (“activities”).

Core Methods

  • Controller.toggle(name, params)
    Show or hide a screen registered under name, passing params to its logic.

  • Controller.move(direction)
    Move focus in one of ['up','down','left','right'].

  • Controller.enter() / Controller.back()
    Trigger the active element’s enter or back handler.

  • Controller.collection.add({ name, items })
    Register a named collection of focusable elements.

  • Controller.call(fn)
    Intercept the next back event with a one-time callback.

Basic Navigation Example

import Controller from './interaction/controller.js';

// Open the “search” screen
Controller.toggle('search', { query: 'Lampa' });

// Move focus to the right, then confirm selection
Controller.move('right');
Controller.enter();

// Go back to the previous screen
Controller.back();

Managing Custom Collections

// Define a custom list of buttons for focus navigation
Controller.collection.add({
  name: 'my_buttons',
  items: [
    document.getElementById('btn-1'),
    document.getElementById('btn-2'),
    document.getElementById('btn-3')
  ]
});

// Switch Controller to your collection
Controller.toggle('my_buttons');

// Now arrow keys will navigate between btn-1 → btn-2 → btn-3

Advanced Back Interception

// Prompt user before navigating back
Controller.call(() => {
  if (confirm('Discard changes?')) {
    Controller.back(); // proceed with default behavior
  }
});
// Next back press will trigger the prompt instead of immediate navigation

How It All Fits

  1. Bootstrap (app.js) sets up global utilities and orchestrates loading.
  2. Render (render.js) builds and injects the static UI structure.
  3. Controller (controller.js) activates dynamic focus, input handling, and screen transitions.

Together, these layers deliver a modular, event-driven architecture that you can customize at startup, extend by swapping UI panels, or programmatically navigate via the Controller API.

4 Plugin Ecosystem

Lampa’s plugin ecosystem lets you extend the app via built-in or third-party JavaScript modules. It provides:

  • A core API for loading, adding, removing and injecting plugins
  • A scrollable, searchable UI for end-users
  • A cache with blacklist support to persist plugin state

4.1 Core Plugin API

Import the manager from src/utils/plugins.js and call init() on startup. All other methods become available once initialization completes.

Initialization

import Plugins from './src/utils/plugins.js';

await Plugins.init();
// At this point Plugins.cache (object) and Plugins._awaits (array) are populated

Loading Plugins

Load plugins from one of:

  • Built-in definitions
  • A remote store JSON (array of manifest objects)
  • The local cache
// Load built-ins
const builtIns = await Plugins.load({ source: 'builtin' });

// Load from your custom store URL
const storeList = await Plugins.load({ source: 'https://cdn.example.com/store.json' });

// Load installed (cached) plugins
const installed = await Plugins.load({ source: 'cache' });

Adding, Removing & Saving

// Define a plugin manifest
const myPlugin = {
  id:          'com.example.weather',
  title:       'Weather Overlay',
  description: 'Shows current weather',
  url:         'https://cdn.example.com/weather.js',
  icon:        'https://cdn.example.com/icons/weather.svg',
  version:     '1.0.0',
  author:      'Example Inc.'
};

// Add to in-memory list
Plugins.add(myPlugin);

// Remove by ID (also adds to blacklist)
Plugins.remove('com.example.extraPlugin');

// Persist changes to localStorage
Plugins.save();

Injecting Plugins

After loading or adding, inject a plugin into the page:

// Push a single manifest (will call addPluginParams + script injection)
Plugins.push(myPlugin);

// Or bulk-push all awaiting plugins
Plugins.push();

4.2 Built-In Plugins

Lampa bundles core features (DLNA client, Subtitles, Favorites) as built-ins under src/plugins. The system:

  • Auto-loads their manifests on Plugins.init()
  • Injects them via Plugins.push() at app startup

To override or update a built-in plugin, re-add its manifest with the same id before calling Plugins.push().

4.3 Creating Third-Party Plugins

Manifest Structure

Your plugin manifest must include:

  • id (string, unique)
  • title, description, version, author
  • url (string, JS entry point)
  • icon (string, SVG/PNG URL)

Optionally add priority (number) to control load order.

Hosting & Publishing

  1. Host a JSON array of manifest objects at a stable URL.
  2. Instruct users to add your store URL via the UI (see 4.4).

Example store format (store.json):

[
  {
    "id": "com.example.weather",
    "title": "Weather Overlay",
    "description": "Shows current weather",
    "url": "https://cdn.example.com/weather.js",
    "icon": "https://cdn.example.com/icons/weather.svg",
    "version": "1.0.0",
    "author": "Example Inc."
  },
  // …more plugins
]

Programmatic Installation

import Plugins from './src/utils/plugins.js';

await Plugins.init();

// Load your store
const remote = await Plugins.load({ source: 'https://cdn.example.com/store.json' });

// Find and install one plugin
const weather = remote.find(p => p.id === 'com.example.weather');
Plugins.add(weather);
Plugins.save();
Plugins.push(weather);

4.4 Managing Plugins via UI

Lampa provides a built-in interface (src/interaction/extensions/main.js) to browse, install and remove plugins.

Initialization

import ExtensionsUI from './src/interaction/extensions/main.js';

// Bind to your container and default stores
ExtensionsUI.init({
  rootSelector: '.extensions-root',
  stores: [
    'https://cdn.example.com/store.json'
  ]
});

Opening the Manager

// Show the modal or panel
ExtensionsUI.open();

User Actions

  • Add Store: Prompt for a new store URL
  • Install: Hover + Enter on a plugin item
  • Remove: Context menu or Remove button adds it to blacklist
  • Refresh: Reloads all stores and updates the list

Customizing the UI

Override or extend templates and styles:

  • Templates live in src/interaction/extensions/templates/
  • CSS classes:
    • .ext-main__list – scrollable container
    • .ext-item – each plugin row
    • .ext-item__icon, .ext-item__title, .ext-item__action

You can inject your own templates/styles before calling ExtensionsUI.init():

Lampa.Template.add('ext_custom_style', `
  <style>
    .ext-item { background: #222; }
  </style>
`);
document.body.append(Lampa.Template.get('ext_custom_style', {}, true));

By leveraging the core API and the built-in UI, you can offer a seamless plugin experience for both developers and end-users.

Configuration & State Management

This section describes how Lampa persists configuration and application state on the client and keeps it synchronized across sessions and devices.

Persistent Storage Utilities (src/utils/storage.js)

Provides a simple API over localStorage with change events, in-memory caching, and server‐sync support.

Core Methods

  • get(key, defaultValue?)
    Retrieve a value or return defaultValue if not set.
  • set(key, value)
    Store a JSON‐serializable value under key and emit a change:key event.
  • remove(key)
    Delete one entry and emit remove:key.
  • clearAll()
    Wipe all managed keys and emit clearAll.

Caching & Server Sync

  • cache(key, fetcher)
    Call fetcher() once, store its promise result under key, then return it on subsequent calls.
  • sync(key, url, { method?, interval? })
    • Push local value to url on change.
    • Poll or debounce as configured.

Events

  • Storage.listener.follow(event, callback)
    change:myKey(newValue)
    remove:myKey()
    clearAll()

Examples

Basic get/set and change listener:

import Storage from 'src/utils/storage.js';

// Initialize theme
const theme = Storage.get('ui_theme', 'light');
applyTheme(theme);

// React to changes
Storage.listener.follow('change:ui_theme', newVal => {
  applyTheme(newVal);
});

// Update on user action
document.getElementById('themeToggle').onclick = () => {
  const next = Storage.get('ui_theme') === 'light' ? 'dark' : 'light';
  Storage.set('ui_theme', next);
};

Cache remote data locally:

// First call fetches; next calls return cached array
Storage.cache('user.playlists', () =>
  fetch('/api/playlists').then(res => res.json())
).then(playlists => render(playlists));

Sync a config object to server on each change:

// Pushes to PUT /api/user/settings after debouncing 300ms
Storage.sync('user.settings', '/api/user/settings', {
  method: 'PUT',
  interval: 300
});

Storage Workers (src/utils/storage_workers.js)

High-level classes that manage typed collections in localStorage, integrate with REST APIs and real-time sockets, and emit granular events.

ArrayWorker

Manages an array of items (e.g., bookmarks, messages) with add/remove/update operations.

Constructor options:

  • key (string): storage key
  • apiUrl (string): REST endpoint (GET initial, POST add, DELETE remove)
  • socket? (SocketIO): subscribe to server push events

Methods:

  • init()
    • Load from localStorage or apiUrl → store and emit a sync event.
    • Bind socket events if provided.
  • add(item) → returns added item
  • remove(id)
  • update(item)
  • getAll() → current array

Events via listener.follow(event, cb):

  • add{ item }
  • remove{ id }
  • update{ item }
  • sync{ items }

Example—bookmark list:

import { ArrayWorker } from 'src/utils/storage_workers.js';
import socket from 'src/utils/socket.js';

const bookmarks = new ArrayWorker({
  key: 'bookmarks',
  apiUrl: '/api/bookmarks',
  socket
});

bookmarks.listener.follow('add', ({ item }) => {
  renderBookmark(item);
});
bookmarks.listener.follow('remove', ({ id }) => {
  removeBookmarkUI(id);
});

// Initialize (loads local or fetches remote)
bookmarks.init();

// Add a new bookmark
document.getElementById('saveBtn').onclick = () => {
  const card = { id: Date.now(), title: 'New Card' };
  bookmarks.add(card);
};

ObjectWorker

Manages a key→value map (e.g., user preferences).

Constructor options like ArrayWorker.

Methods:

  • init()
  • set(prop, value)
  • get(prop)
  • getAll()
  • remove(prop)

Events:

  • set{ prop, value }
  • remove{ prop }
  • sync{ obj }

Example—user profile settings:

import { ObjectWorker } from 'src/utils/storage_workers.js';

const profile = new ObjectWorker({
  key: 'user.profile',
  apiUrl: '/api/profile'
});

profile.listener.follow('set', ({ prop, value }) => {
  document.querySelector(`[data-field=${prop}]`).textContent = value;
});

profile.init().then(() => {
  // pre-fill form
  const data = profile.getAll();
  form.name.value = data.name;
});

// Update on form change
form.name.onchange = e => profile.set('name', e.target.value);

ID‐Filtered Worker

Use when you need filtered views of an array by a property (e.g., messages by chatId).

import { IdWorker } from 'src/utils/storage_workers.js';

const chatWorker = new IdWorker({
  key: 'messages',
  apiUrl: '/api/messages',
  socket,
  idField: 'chatId'
});

// Listen to new messages for chatId=42
const room42 = chatWorker.filter('42');
room42.listener.follow('add', ({ item }) => {
  appendMessageUI(item);
});
room42.init();

These storage utilities and workers decouple state management from UI logic, provide real‐time updates, and ensure persistence across sessions and devices. Use them to build reactive, sync-aware features with minimal boilerplate.

6 Platform Deployment

Package Lampa for each target: Web (Docker), Tizen, and WebOS. Follow these steps to build, configure, and deploy your app in each environment.

6.1 Building the Web Frontend Docker Image

Overview
Use the Apache HTTP Server base image to serve your compiled web assets. Supply domain and optional prefix build arguments to customize endpoints inside msx/start.json.

Key Dockerfile Snippet (index/github/Dockerfile)

FROM httpd:alpine3.15

ARG domain
ARG prefix="http://"

RUN test -n "$domain" \
  || (echo "ERROR: domain is not set" >&2 && exit 1)

COPY ./dist/ /usr/local/apache2/htdocs/
# Replace tokens in start.json
RUN sed -i \
      -e "s|{PREFIX}|${prefix}|g" \
      -e "s|{domain}|${domain}|g" \
      /usr/local/apache2/htdocs/msx/start.json

Build Commands

# HTTPS prefix
docker build \
  -f index/github/Dockerfile \
  -t lampa-web:latest \
  --build-arg domain=api.example.com \
  --build-arg prefix="https://" .

# HTTP (default) prefix
docker build \
  -f index/github/Dockerfile \
  -t lampa-web:latest \
  --build-arg domain=api.example.com .

Verification

docker run --rm lampa-web:latest \
  cat /usr/local/apache2/htdocs/msx/start.json
# Confirm all {domain} and {PREFIX} tokens are replaced

CI/CD Snippet (GitHub Actions)

- name: Build Docker frontend
  run: |
    docker build \
      -f index/github/Dockerfile \
      --build-arg domain=${{ secrets.FRONTEND_DOMAIN }} \
      --build-arg prefix="https://" \
      -t lampa-web:${{ github.sha }} .

6.2 Packaging for Tizen

Overview
src/utils/tizen.js exports helpers to launch Tizen app controls, prepare pick data, convert content cards to TV tiles, build UI sections, and handle deep links.

Import and Initialize

import {
  launchAppControl,
  setPickData,
  cardsToTiles,
  buildSection,
  handleDeepLink
} from './utils/tizen.js';

// On app start
document.addEventListener('DOMContentLoaded', () => {
  handleDeepLink();              // Process deep links (e.g. tizen://...)
  initializeUI();                // Your UI setup function
});

Launching an External App

// Launch gallery picker for images
const pickData = { type: 'image/*' };
setPickData(pickData);

launchAppControl({
  operation: 'http://tizen.org/appcontrol/operation/pick',
  uri: 'gallery',
  mime: 'image/*'
}, () => {
  console.log('Gallery opened');
}, err => {
  console.error('Pick failed', err);
});

Building a Content Section

// Convert JSON cards into focusable tiles
const cards = await fetch('/api/cards').then(r => r.json());
const tiles = cardsToTiles(cards);
// Render into a container
const section = buildSection('Recommended', tiles);
document.getElementById('content').appendChild(section);

Practical Tips

  • Define "tizen:application id" and "tizen:icon" in config.xml to match your package
  • Call tizen.application.getCurrentApplication().exit() on exit events
  • Test deep links with tizen://launch?appId=...&data=... in the Tizen emulator

6.3 Packaging for WebOS

Overview
src/utils/webos_launcher.js initializes Luna service requests and UI controls for adding/removing devices in WebOS.

Import and Initialize

import initWebOSLauncher from './utils/webos_launcher.js';

document.addEventListener('DOMContentLoaded', () => {
  initWebOSLauncher();  // Attaches event handlers and Luna bridge
});

UI Integration Example

<!-- In your app’s main HTML -->
<button id="add-device">Add Device</button>
<button id="remove-device">Remove Device</button>
<script src="bundle.js"></script>

Handling Add/Remove Requests

// initWebOSLauncher binds these internally, but you can override
document.getElementById('add-device').addEventListener('click', () => {
  window.WebOS.service.request({
    service: 'luna://com.webos.service.update/devices/add',
    parameters: { id: 'device123', name: 'Living Room' },
    onSuccess: res => console.log('Device added', res),
    onFailure: err => console.error('Add failed', err)
  });
});

document.getElementById('remove-device').addEventListener('click', () => {
  window.WebOS.service.request({
    service: 'luna://com.webos.service.update/devices/remove',
    parameters: { id: 'device123' },
    onSuccess: res => console.log('Device removed'),
    onFailure: err => console.error('Remove failed', err)
  });
});

Practical Tips

  • Ensure luna-service2 permissions in appinfo.json
  • Test launcher calls in the WebOS emulator or powered-off device via ares-setup-device
  • Monitor returned payloads in the DevTools Console under the “Network” tab for Luna calls

With these deployment steps, Lampa runs seamlessly in containerized web environments, Tizen TVs, and WebOS devices, leveraging each platform’s native controls and packaging workflows.

\n```\n\nHandling Add/Remove Requests \n```javascript\n// initWebOSLauncher binds these internally, but you can override\ndocument.getElementById('add-device').addEventListener('click', () => {\n window.WebOS.service.request({\n service: 'luna://com.webos.service.update/devices/add',\n parameters: { id: 'device123', name: 'Living Room' },\n onSuccess: res => console.log('Device added', res),\n onFailure: err => console.error('Add failed', err)\n });\n});\n\ndocument.getElementById('remove-device').addEventListener('click', () => {\n window.WebOS.service.request({\n service: 'luna://com.webos.service.update/devices/remove',\n parameters: { id: 'device123' },\n onSuccess: res => console.log('Device removed'),\n onFailure: err => console.error('Remove failed', err)\n });\n});\n```\n\nPractical Tips \n- Ensure `luna-service2` permissions in `appinfo.json` \n- Test launcher calls in the WebOS emulator or powered-off device via `ares-setup-device` \n- Monitor returned payloads in the DevTools Console under the “Network” tab for Luna calls\n\nWith these deployment steps, Lampa runs seamlessly in containerized web environments, Tizen TVs, and WebOS devices, leveraging each platform’s native controls and packaging workflows."; document.addEventListener('DOMContentLoaded', function() { const contentDiv = AppUtils.dom.query('#documentation-content'); const tocNav = AppUtils.dom.query('#toc-nav'); const tocNavMobile = AppUtils.dom.query('#toc-nav-mobile'); // Initialize mobile menu functionality MobileMenu.init(); // Apply content styling and initialize TOC if (contentDiv) { ContentStyling.applyProseStyles(contentDiv); TableOfContents.init(contentDiv, tocNav, tocNavMobile); } // Setup quick actions QuickActions.init(tocNav, contentDiv); });