Middleware-плагины

Middleware-плагины

0.4 NEWtarget: middleware оборачивает существующие балансеры и фильтрует/мерджит их выдачу.

Middleware-плагин не заменяет балансер — он садится поверх одного или нескольких балансеров и:

  • получает уже собранные ответы upstream’ов,
  • решает, что показать пользователю,
  • может убирать дубли, фильтровать по качеству, тегу, голосу, языку,
  • может объединять данные с нескольких источников в один список.

Когда использовать

Задача Решение
Скрыть всё ниже 720p Middleware с фильтром quality.minHeight >= 720
Слить collaps + filmix в один список Middleware с upstream’ами ["collaps", "filmix"]
Убрать рекламные дубли в озвучках Middleware дедуплицирует voice[] по нормализованному имени
Принудительно проксировать всё через /proxy/ Middleware прогоняет каждый data[].url через host_proxy_url

Manifest

{
  "id": "quality_filter",
  "name": "Quality filter",
  "version": "0.1.0",
  "target": "middleware",
  "entry_server": "plugin.wasm",
  "language": "tinygo",
  "abi_version": 1,
  "upstreams": ["collaps", "filmix"],
  "permissions": ["log"],
  "config_schema": [
    {"key": "minQuality", "type": "enum", "options": ["480","720","1080","2160"], "default": "720"}
  ]
}

upstreams — обязательное поле для middleware. Допустимы:

  • любой ID, который зарегистрирован в DynamicRouteRegistry (built-in балансеры, JS-модули, другие WASM-плагины),
  • "*" — wildcard, используется только в тестах (резолвер не разворачивает его автоматически).

Как это работает

Lampa  ──GET /lite/quality_filter──►  lampac-go
                                         │
                            ┌────────────┴────────────┐
                            │ MiddlewareHandler       │
                            └────────────┬────────────┘
                                         │ resolveUpstream("collaps")
                                         │ resolveUpstream("filmix")
                                         ▼
                            ┌─────────────────────────┐
                            │ /lite/collaps           │ → JSON
                            │ /lite/filmix            │ → JSON
                            └────────────┬────────────┘
                                         │ inject __upstreams в config
                                         ▼
                            ┌─────────────────────────┐
                            │ wazero.handle(ptr, len) │
                            └────────────┬────────────┘
                                         │
                                         ▼
                                    ответ Lampa

Upstream’ы вызываются последовательно — это сделано осознанно: cookies / DDoS-Guard / CDN cooldown’ы плохо переносят параллельные запросы (см. заметки про Mirage CDN). 99% middleware кейсов — ≤3 upstream’а, так что заметной потери latency нет.

TinyGo пример

package main

import (
    "encoding/json"
    "strconv"

    "lampac.cc/sdk/lampac"
)

func main() {}

type item struct {
    Name    string                 `json:"name"`
    Quality map[string]interface{} `json:"quality,omitempty"`
}

type list struct {
    Type string `json:"type"`
    Data []item `json:"data"`
}

//export handle
func handle(ptr, length uint32) uint64 {
    inv := lampac.ReadInvocation(ptr, length)

    var cfg struct{ MinQuality string `json:"minQuality"` }
    _ = json.Unmarshal(inv.Config, &cfg)
    minH, _ := strconv.Atoi(cfg.MinQuality)
    if minH == 0 { minH = 720 }

    out := list{Type: "movie"}
    for _, up := range lampac.Upstreams(inv) {
        if up.Error != "" {
            lampac.Warn("upstream " + up.Balancer + " failed: " + up.Error)
            continue
        }
        var env list
        if err := json.Unmarshal(up.Body, &env); err != nil { continue }
        for _, it := range env.Data {
            if hasHigherQuality(it, minH) {
                out.Data = append(out.Data, it)
            }
        }
    }
    body, _ := json.Marshal(out)
    return lampac.WriteResponse(body)
}

func hasHigherQuality(it item, min int) bool {
    for label := range it.Quality {
        n, _ := strconv.Atoi(strings.TrimRight(label, "p"))
        if n >= min { return true }
    }
    return false
}

Полный исходник: wasm_modules/quality_filter.

Как хост передаёт upstream’ы плагину

Чтобы не ломать ABI, upstream-данные кладутся в inv.Config под зарезервированным ключом __upstreams:

{
  "minQuality": "720",
  "__upstreams": [
    {"balancer": "collaps", "status": 200, "body": {"type":"movie","data":[...]}},
    {"balancer": "filmix",  "status": 503, "error": "upstream timeout"}
  ]
}

В TinyGo SDK:

ups := lampac.Upstreams(inv) // []UpstreamResult

В Rust SDK:

let ups: Vec<UpstreamResult> = lampac_sdk::upstreams(&inv);

Цепочки middleware

Один middleware может звать другого через upstreams. Это работает, потому что middleware-плагин зарегистрирован в DynamicRouteRegistry ровно так же, как балансер:

quality_filter --upstreams: ["voice_dedup"]──►  voice_dedup --upstreams: ["collaps","filmix"]──►  collaps + filmix

Только не зацикливай.

Permissions

Middleware не нуждается в http:* (если только не делает свои HTTP-запросы помимо upstream’ов). Минимальный набор:

"permissions": ["log"]

Если плагин нормализует URL’ы через host_proxy_url — добавь "proxy".

Production tips

  • Cap upstream timeout — каждый вызов upstream’а ограничен 30 секундами. Если у вас балансер с долгим resolve (зетфлекс, мираж), ставьте таймаут пользователя в Lampa выше.
  • Ошибки одного upstream’а не валят middleware — он получает up.Error и решает сам.
  • Кеширование — middleware-плагин может кешировать через host_cache_* ровно так же, как server-плагин.