JS-модули источников
JS-модули — современный способ подключить новый источник контента без пересборки сервера и без Go-тулчейна у пользователя. Каждый модуль — это одна JavaScript-функция в изолированной песочнице (Goja), которая принимает HTTP-запрос от Lampa и возвращает JSON в формате плейлиста.
Почему JS, а не Go
| Критерий | Go-subprocess (custom balancers) | JS-модуль (новый) |
|---|---|---|
| Размер артефакта | ~9 МБ бинарь на плагин | 5–20 КБ .js файл |
| Нужен Go на машине | Да | Нет |
| Hot-reload | После go build и restart subprocess |
Мгновенно, по кнопке в админке |
| Запуск | Отдельный процесс + TCP | Внутри основного процесса, in-process handler |
| Порог входа | Go, конкурентность, net/http | JavaScript + «fetch-like» API |
| Deploy | Скомпилировать 11 бинарей под каждую ОС | Скопировать папку |
| Редактирование | IDE + компилятор | Прямо в браузере через админку |
Архитектура
modules/
<module-id>/
manifest.json # метаданные + схема настроек
index.js # код модуля
config.json # (опционально) сохранённые настройки пользователя
<любые файлы> # иконки, README, тест-фикстуры…При старте lampac-go:
- Сканирует
modules/— ищет поддиректории сmanifest.json - Компилирует
index.jsчерез Goja (pure-Go реализация ECMAScript 5.1+) - Регистрирует HTTP-handler на
/lite/<id>(и всех саб-путях/lite/<id>/...) - Если в манифесте указан
quality— добавляет бейдж в выдачу (「FHD」,「4K」)
При каждом запросе /lite/<id>?...:
- Создаётся новый инстанс Goja VM (изолирован от других запросов)
- Вызывается глобальная функция
handle(inv)модуля - Возвращённое значение сериализуется в JSON и отдаётся клиенту
Приоритет маршрутов
JS-модули перехватывают встроенные Go-балансеры с таким же ID. Если у вас установлен и включён модуль starlight, то /lite/starlight будет идти в JS, а не в Go-код. Выключение модуля в админке возвращает поведение встроенного балансера.
modules/starlight/ → /lite/starlight. Использовать зарезервированные имена (events) нельзя — модуль не загрузится.manifest.json
Минимальный манифест:
{
"id": "mysource",
"name": "My Source",
"version": "1.0.0",
"entry": "index.js",
"content_types": ["movie", "serial"]
}Полная схема
| Поле | Тип | Описание |
|---|---|---|
id |
string, обязательно | ID модуля и имя маршрута ([a-z][a-z0-9_]{1,31}) |
name |
string | Отображаемое название (по умолчанию = id) |
version |
string | SemVer (по умолчанию 0.0.0) |
author |
string | Автор/команда |
description |
string | Описание для карточки в админке |
icon |
string | Путь к файлу иконки внутри папки модуля |
entry |
string | Имя JS-файла (по умолчанию index.js) |
content_types |
string[] | movie, serial, anime, tv |
quality |
string | Бейдж в выдаче: SD, HD, FHD, 4K |
languages |
string[] | Коды языков контента (uk, ru, en, ja, …) |
anime |
bool | Пометить как аниме-источник (влияет на фильтрацию по original_language) |
ukrainian |
bool | Маркер украинского источника (для фильтра в админке) |
tags |
string[] | Произвольные теги для фильтрации |
repository |
string | Ссылка на исходники (GitHub и т.п.) |
config_schema |
ConfigField[] |
Поля настроек, показываемые в админке |
ConfigField
{
"key": "host",
"label": "Хост API",
"type": "string",
"default": "https://example.com",
"description": "Базовый URL источника",
"placeholder": "https://...",
"secret": false
}| Поле | Значения |
|---|---|
type |
string, int, number, bool, secret, textarea |
secret |
true — поле шифруется и маскируется в UI (••••••) |
Значения, которые пользователь задал в админке, сохраняются в modules/<id>/config.json и приходят в JS как inv.config.
API песочницы
Внутри index.js доступны глобальные объекты:
http — HTTP-клиент
var res = http.get('https://example.com/api', {
headers: { 'Accept': 'application/json', 'X-Token': '...' },
userAgent: 'MyBot/1.0',
referer: 'https://example.com/'
});
if (res.ok) {
var data = res.json(); // распарсить как JSON (бросит исключение при ошибке)
// или res.text — тело как строка
// res.status — код
// res.headers — объект с первой value каждого заголовка (lowercase)
// res.cookies — [{name, value}, ...]
// res.duration_ms
}| Метод | Назначение |
|---|---|
http.get(url, opts?) |
GET |
http.post(url, opts?) |
POST |
http.put(url, opts?) |
PUT |
http.delete(url, opts?) |
DELETE |
http.head(url, opts?) |
HEAD |
http.request(method, url, opts?) |
произвольный метод |
opts могут содержать:
| Поле | Пример | Описание |
|---|---|---|
headers |
{Accept:'...', 'X-Foo':'bar'} |
HTTP-заголовки |
body |
'raw' или {json:true} |
Тело (строка или JSON-объект) |
form |
{q:'foo', page:1} |
Форма x-www-form-urlencoded |
userAgent |
'Mozilla/5.0 ...' |
User-Agent |
referer |
'https://example.com/' |
Referer |
Важно: http.get не бросает исключение при сетевой ошибке — он возвращает {ok:false, error:'...', status:0}. Всегда проверяйте res.ok.
Лимиты:
- Таймаут запроса — 30 секунд (совокупно для всех HTTP-вызовов в одном invoke)
- Макс. размер ответа — 8 МБ
- User-Agent по умолчанию —
Mozilla/5.0 ... Chrome/120.0(можно переопределить)
proxy — проксирование стримов
var safe = proxy.url(streamUrl, 'mysource');
// → http://your-server/proxy/<encrypted>.m3u8
// сервер проксирует трафик к источнику, пряча реальный URL
// и обходя Origin/Referer-ограничения CDN
Когда нужно пробросить специфические заголовки в CDN (например Origin, кастомный Authorization):
var safe = proxy.urlWithHeaders(streamUrl, 'mysource', {
'Origin': 'https://cdn-origin.example.com',
'Referer': 'https://player.example.com/'
});Прокси работает для любых HLS/MP4/DASH-стримов. Второй аргумент plugin используется для учёта трафика в статистике админки.
cache — per-module кэш
var cached = cache.get('search:' + query);
if (cached === undefined) {
cached = fetchSomething(query);
cache.set('search:' + query, cached, 600); // TTL 600 секунд
}| Метод | Описание |
|---|---|
cache.get(key) |
Получить (возвращает undefined при промахе) |
cache.set(key, value, ttlSeconds) |
Сохранить |
cache.delete(key) |
Удалить ключ |
cache.clear() |
Очистить весь кэш модуля |
Кэш живёт в RAM, не переживает перезагрузку сервера. Изолирован по модулям (ключи не конфликтуют).
util — утилиты
util.urlencode(s) // encodeURIComponent
util.urldecode(s) // decodeURIComponent
util.md5(s) // hex-строка
util.sha1(s)
util.sha256(s)
util.hexEncode(s) // string → hex
util.hexDecode(hex) // hex → string
util.trim(s)
util.sleep(ms) // поспать (с учётом глобального таймаута)
util.buildEpisodeIdent(base, s, e, t) // "base_s_e_t" для Lampa ident
btoa / atob — base64
atob('SGVsbG8=') // → "Hello"
btoa('Hello') // → "SGVsbG8="
atob понимает как стандартный base64, так и url-safe без padding.
console / log — логирование
console.log('fetched', count, 'items');
console.warn('unexpected format:', data);
console.error('CDN returned', res.status);Логи видны в админке на вкладке Модули → конкретный модуль → Логи, а также в общем логе lampac-go. Ring-buffer на 500 записей на модуль.
manifest — ваш manifest.json
console.log(manifest.version, manifest.quality);Глобальный readonly-объект с данными из manifest.json.
Контракт handle(inv)
Каждый модуль должен экспортировать функцию handle(inv). Это единственная точка входа.
inv — контекст вызова
function handle(inv) {
// inv.query — { title: 'Inception', year: '2010', ... } (HTTP query params)
// inv.headers — { accept: '...', ... } (lowercase)
// inv.host — 'http://lampac.example.com:888' (URL самого сервера)
// inv.requestIP — IP клиента
// inv.path — 'mysource/play' (без /lite/ префикса)
// inv.life — true для автоматической выдачи (`?life=true`)
// inv.checksearch — true для проверки доступности (`?checksearch=true`)
// inv.userAgent
// inv.config — объект с настройками из config_schema
}Ответ
Возвращайте обычный JS-объект/массив — runtime сериализует его в JSON и отдаёт клиенту. Поддерживаемые форматы описаны в разделе «Формат ответа» далее.
Формат ответа Lampa
Lampa ожидает один из нескольких «типов» ответа в зависимости от этапа:
checksearch — есть ли что-то для этого тайтла
if (inv.checksearch) {
var results = searchAPI(query);
return results.length > 0
? { rch: true, type: 'movie', quality: 'FHD' }
: { rch: false };
}Клиент дёргает этот эндпоинт, чтобы решить — показывать источник в выдаче или нет.
Список похожих (similar)
Когда одному поиску совпадает несколько тайтлов — пусть пользователь выберет:
return {
type: 'similar',
data: [
{
title: 'Interstellar',
year: 2014,
poster: 'https://.../poster.jpg',
url: inv.host + '/lite/mysource?href=' + util.urlencode(detailURL)
},
// ...
]
};Фильм (movie)
Один или несколько вариантов озвучки одного фильма:
return {
type: 'movie',
data: [{
method: 'play',
url: proxy.url(streamURL, 'mysource'),
stream: proxy.url(streamURL, 'mysource'), // дубль для совместимости
name: 'Украинская озвучка',
title: 'Interstellar (2014)',
quality: { '1080p': proxy.url(hd, 'mysource'), '720p': proxy.url(sd, 'mysource') },
qualitys: { /* то же самое — legacy */ }
}]
};Список сезонов (season)
return {
type: 'season',
data: [
{
method: 'link',
id: 1,
url: inv.host + '/lite/mysource?href=...&s=1',
name: '1 сезон'
},
// ...
]
};Список серий (episode)
return {
type: 'episode',
data: [
{
method: 'play',
url: proxy.url(streamURL, 'mysource'),
stream: proxy.url(streamURL, 'mysource'),
name: 'Эпизод 1',
title: 'Foundation S01E01',
s: 1,
e: 1
}
],
// опционально — переключатель озвучек/субтитров:
voice: [
{ name: 'Дубляж', active: true, url: inv.host + '/lite/mysource?...&t=dub' },
{ name: 'Субтитры', active: false, url: inv.host + '/lite/mysource?...&t=sub' }
]
};Поля записей
| Поле | Назначение |
|---|---|
method |
play — прямой стрим; link — переход на другой URL; call — запросить JSON по URL и продолжить |
url |
Поток (HLS/MP4/DASH) или URL для link/call |
stream |
Дубль url для старых клиентов |
name |
Короткое имя (отображается в списке) |
title |
Полное название (в плеере) |
s, e |
Номер сезона / эпизода |
quality / qualitys |
Мапа {quality: url} для переключения качеств |
poster |
URL постера |
Полный пример модуля
Минимальный, но функциональный модуль для API, возвращающего JSON:
modules/hello/manifest.json:
{
"id": "hello",
"name": "Hello Source",
"version": "1.0.0",
"description": "Demo JS module",
"content_types": ["movie"],
"quality": "FHD",
"config_schema": [
{"key": "host", "label": "API host", "default": "https://api.example.com"},
{"key": "token", "label": "Token", "secret": true}
]
}modules/hello/index.js:
function cfg(inv, k){ return (inv.config && inv.config[k]) || ''; }
function search(inv, title){
var url = cfg(inv, 'host').replace(/\/+$/,'') + '/search?q=' + util.urlencode(title);
var res = http.get(url, { headers: { 'X-Token': cfg(inv,'token') }});
if (!res.ok) { console.warn('hello: API', res.status); return []; }
return res.json().results || [];
}
function handle(inv){
if (inv.checksearch) {
var r = search(inv, inv.query.title || inv.query.original_title || '');
return r.length ? { rch: true, type: 'movie', quality: manifest.quality } : { rch: false };
}
var title = (inv.query.title || '').trim();
if (!title) return { type: 'movie', data: [] };
var results = search(inv, title);
return {
type: 'movie',
data: results.map(function(r){
return {
method: 'play',
url: proxy.url(r.stream_url, 'hello'),
stream: proxy.url(r.stream_url, 'hello'),
name: r.title,
title: r.title + ' (' + r.year + ')'
};
})
};
}Положите обе файла в modules/hello/, зайдите в админку → Модули → увидите карточку. Включите, откройте /lite/hello?title=Test&checksearch=true — должно вернуть {rch:true,...} если API отвечает.