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.
- Navigate into the Docker context:
cd index/github
- Build with your domain and URL prefix:
docker build \ --tag lampa-index \ --build-arg domain=example.com \ --build-arg prefix="https://" \ .
- Verify
start.json
:docker run --rm lampa-index cat /usr/local/apache2/htdocs/msx/start.json
- Launch container on port 8080:
Visit http://localhost:8080.docker run -d \ --name lampa-index \ -p 8080:80 \ lampa-index
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
initClass()
– attaches core utilities towindow
(e.g. Utils, Storage, Task).initConfig()
– readswindow.lampa_settings
and applies defaults.loadTask()
– enqueues primary (Task.queue
) and secondary (Task.secondary
) startup jobs.Task.start()
– runs all tasks in order, then callsstartApp()
.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)
beforeapp.start()
. - Override
window.lampa_settings
fields beforeinitConfig()
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 undername
, passingparams
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
- Bootstrap (
app.js
) sets up global utilities and orchestrates loading. - Render (
render.js
) builds and injects the static UI structure. - 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
- Host a JSON array of manifest objects at a stable URL.
- 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 returndefaultValue
if not set.set(key, value)
Store a JSON‐serializable value underkey
and emit achange:key
event.remove(key)
Delete one entry and emitremove:key
.clearAll()
Wipe all managed keys and emitclearAll
.
Caching & Server Sync
cache(key, fetcher)
Callfetcher()
once, store its promise result underkey
, then return it on subsequent calls.sync(key, url, { method?, interval? })
• Push local value tourl
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 keyapiUrl
(string): REST endpoint (GET
initial,POST
add,DELETE
remove)socket?
(SocketIO): subscribe to server push events
Methods:
init()
• Load from localStorage orapiUrl
→ store and emit async
event.
• Bind socket events if provided.add(item)
→ returns added itemremove(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
Overviewsrc/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"
inconfig.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
Overviewsrc/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 inappinfo.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.