3 Commits
Author SHA1 Message Date
Worph 3852d3375c update workflow
Build and Publish Docker Image / build-and-push (push) Failing after 22s
2026-07-01 22:22:36 +02:00
Worph 2ce28c1cbd readme 2026-07-01 22:20:50 +02:00
Worph f70bb52aab config fix feeder
Publish image / publish (push) Failing after 14s
2026-07-01 18:54:12 +02:00
4 changed files with 187 additions and 73 deletions
@@ -1,59 +1,77 @@
name: Publish image name: Build and Publish Docker Image
# Build and push the feeder image to the GitHub Container Registry (ghcr.io):
# * every push to main -> the moving `main` tag
# * every `v*` tag -> semver tags (e.g. v1.0.0 -> 1.0.0, 1.0, 1)
# The build context is the repo root (the workspace) so the vendored
# `meta-feeder-sdk` path dependency resolves; the Dockerfile lives under
# feeder-plugin/torrent-feeder/.
on: on:
push: push:
branches: [main] branches:
tags: ['v*'] - main
tags:
- 'v*'
pull_request:
branches:
- main
workflow_dispatch:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
publish: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Free disk space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry - name: Log in to Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract image metadata - name: Extract metadata (tags, labels)
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=sha,format=short type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./feeder-plugin/torrent-feeder/Dockerfile platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
provenance: false
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
+127
View File
@@ -1 +1,128 @@
# meta-feeder-tribler # meta-feeder-tribler
**Tribler feeder sidecar** for
[MetaMesh](https://github.com/worph/meta-gateway) — bridges the
**decentralized [Tribler](https://www.tribler.org/) network** (its IPv8
peer-to-peer overlay search) into a meta-gateway as a search source.
Unlike a Torznab/Prowlarr feeder that talks to centralized indexer sites,
Tribler has **no central index**: it runs its own anonymous overlay and each
peer contributes the torrents it has seen. This feeder does not reimplement
that overlay — it runs a **headless Tribler core as a sidecar container**
(`tribler-instance`) and queries its REST API, exactly the way a Torznab feeder
treats an external indexer. It bridges Tribler's *content corpus* into the
mesh so those torrents become discoverable and playable via meta-share.
This feeder was **split out of** [`meta-feeder-torrent`](../meta-feeder-torrent)
so that Tribler is a **single-plugin** feeder. A feeder exposes one `/config`
surface; a multi-plugin feeder (Torznab + Tribler in one binary) can only route
that surface to one plugin, so its dashboard config panel could not resolve to
the Tribler schema. Splitting gives Tribler its own clean config card.
## Metadata-only
This is the **light** feeder. It resolves a Tribler search hit's infohash
directly into a `btih-v1-file` CID and **advertises** the record — it does
**not** download or seed the bytes. There is no `librqbit` here: file lists
come from the Tribler core's own `torrentinfo` metainfo response, not from a
BitTorrent fetch. So the cold Docker build is much lighter than the torrent
feeder's, and full-file fetch/seed is left to meta-share's on-demand fetcher
(the feeder synthesizes magnets with a set of well-known public trackers so
that fetcher can find peers quickly, since Tribler torrents are often DHT-only).
It carries a **copy** of the source-agnostic "torrent-core" TMDB
catalog-discovery + enrichment stack (`tmdb`, `tmdb_budget`, `discovery`,
`enrich`, `title`, `consts`, `filename_meta`). That stack cannot live in a
container plugin — a container plugin enriches a *known* CID; it cannot answer
"what's popular" for the home-row browse — so the browse/catalog primitive is
inherently in-process and shared verbatim by every torrent source. Like every
feeder it stays **meta-core-free at the transport layer and blockstore-free**:
it finds records and enriches them; publishing resolved records + TMDB posters
goes to the meta-core configured in the dashboard.
## Role in MetaMesh
A feeder is a stateless HTTP sidecar. The gateway registers it as a remote
feeder plugin pointing at its `/` and drives the SDK contract
([`meta_feeder_sdk::serve_feeders`](crates/meta-feeder-sdk/src/serve.rs)):
| Endpoint | Purpose |
|----------|---------|
| `GET /manifest` | feeder identity + served types (`*` / `*` — Tribler is free-text, any query can return anything) |
| `GET /health` | liveness (`Degraded` until `configure()` runs) |
| `POST /query`, `POST /query_stream` | structured / streaming search over the Tribler local + remote (IPv8) overlay |
| `POST /compute` | outcome compute + TMDB enrichment |
| `GET /fetch/:upstream_id/:record_id`, `GET /blob/:upstream_id/:cid` | SDK byte routes (metadata-only here — no local bytes to serve) |
| `POST /enrich/callback` | async enrichment callback sink |
| `GET /config`, `GET /config/schema`, `GET\|PUT /config/values` | schema-driven config UI + API |
## Configuration
All Tribler config is **dashboard config only** — it lives in the persisted
`config.json` (written via the gateway dashboard) and has **no env var**. This
is the MetaMesh rule: a field in the web-UI config schema must not also be an
env seed (config from JSON / web UI is the single source of truth).
Configured on the plugin's card in the gateway dashboard:
| Field | Notes |
|-------|-------|
| `meta_core_url` | meta-core this feeder publishes resolved records + TMDB posters to (e.g. `http://metacore-app:9000`). Blank → publish soft-skips. |
| `sidecar_url` | Tribler core REST base (default `http://tribler-instance:8085`). |
| `api_key` | Tribler REST `X-Api-Key` (secret); the dev sidecar uses `changeme`. Blank → no auth header. |
| `tmdb_api_key` | TMDB v3 key / v4 bearer (secret) for poster / overview / tmdbid enrichment. Blank → TMDB enrichment soft-skips. |
| `tmdb_language` | TMDB metadata language tag (e.g. `en-US`, `fr-FR`, `ja-JP`). Blank → `en-US`. |
Config changes take effect on the next feeder restart (no hot reload). Open the
Tribler web UI via the "↗ Tribler UI" link on the plugin's card — it opens the
gated `/ui/` behind single sign-on (put `?key=` **after** the `#`; the UI is a
HashRouter).
Env vars are **infra only**:
| Env var | Default | Notes |
|---------|---------|-------|
| `META_FEEDER_HTTP_LISTEN` | `0.0.0.0:8080` | HTTP listen address |
| `META_FEEDER_STATE_DIR` | `/data/meta-feeder` | redb midhash cache + state |
| `RUST_LOG` | `info` | tracing filter |
## Image
```
ghcr.io/worph/meta-feeder-tribler
```
Exposes `8080`. Built and pushed by CI on every push to `main` (the moving
`main` tag) and on `v*` tags (semver tags). Metadata-only (no `librqbit`), so
the cold build is lighter than `meta-feeder-torrent`.
## Build locally
The build context is the **repo root** (the Cargo workspace) so the vendored
`meta-feeder-sdk` path dependency resolves:
```bash
docker build -f feeder-plugin/tribler-feeder/Dockerfile -t ghcr.io/worph/meta-feeder-tribler:dev .
```
## Repo layout
This repo is a self-contained Cargo workspace vendored out of the
`meta-gateway` monorepo:
```
Cargo.toml # workspace: members = crates/*, feeder-plugin/*
# (keeps the core2 [patch.crates-io] safety net)
crates/meta-feeder-sdk/ # vendored shared feeder SDK
feeder-plugin/tribler-feeder/ # this feeder's crate + Dockerfile
src/tribler/ # the Tribler source (IPv8-overlay REST client)
src/{tmdb,discovery,enrich,title,filename_meta,...}.rs
# copied source-agnostic "torrent-core" catalog stack
```
> The workspace keeps the `core2` `[patch.crates-io]` override (a yanked
> transitive dep) as a safety net, matching the `meta-gateway` workspace.
Upstream source of truth for the SDK and the Tribler source is
[`worph/meta-gateway`](https://github.com/worph/meta-gateway); changes there are
vendored back into this repo.
+15 -12
View File
@@ -7,12 +7,16 @@
//! torrent-core TMDB catalog-discovery stack. Split out of `meta-feeder-torrent` //! torrent-core TMDB catalog-discovery stack. Split out of `meta-feeder-torrent`
//! so its dashboard config panel resolves to the tribler schema. //! so its dashboard config panel resolves to the tribler schema.
//! //!
//! Env (shared): `META_FEEDER_HTTP_LISTEN` (default `0.0.0.0:8080`), //! ALL tribler config — sidecar URL, api key, tmdb token/language, meta-core
//! `META_FEEDER_STATE_DIR` (default `/data/meta-feeder`), `META_CORE_URL` (D4 //! URL — is read from the persisted `config.json` (dashboard-written) ONLY.
//! self-publish), `RUST_LOG`. //! There is NO env seed for these: a field in the dashboard config schema must
//! Env (tribler source): `TRIBLER_SIDECAR_URL`, `TRIBLER_API_KEY`, //! not have an env var (config from JSON file / web UI is the single source of
//! `TMDB_TOKEN`/`TMDB_LANGUAGE`, plus the enrichment-plugin URLs //! truth).
//! (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `META_FEEDER_CALLBACK_URL`). //!
//! Env (infra only): `META_FEEDER_HTTP_LISTEN` (default `0.0.0.0:8080`),
//! `META_FEEDER_STATE_DIR` (default `/data/meta-feeder`), `META_GATEWAY_PEER_ID`,
//! the enrichment-plugin URLs (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`,
//! `META_FEEDER_CALLBACK_URL`), `RUST_LOG`.
use std::net::SocketAddr; use std::net::SocketAddr;
@@ -21,7 +25,7 @@ use meta_feeder_sdk::serve_feeders;
use tracing::info; use tracing::info;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use tribler_feeder::tribler::{TriblerConfigFile, TriblerPlugin}; use tribler_feeder::tribler::TriblerPlugin;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
@@ -35,11 +39,10 @@ async fn main() -> anyhow::Result<()> {
let state_dir = let state_dir =
std::env::var("META_FEEDER_STATE_DIR").unwrap_or_else(|_| "/data/meta-feeder".to_string()); std::env::var("META_FEEDER_STATE_DIR").unwrap_or_else(|_| "/data/meta-feeder".to_string());
// Env is the first-boot seed; the persisted config.json (under the // No env seed: the plugin reads config.json in configure() (sidecar URL,
// per-source cache dir) wins once saved through the dashboard. The tribler // api key, tmdb, meta-core URL) and builds its enrichment/self-publish
// source builds its enrichment driver in configure(). // driver from config.meta_core_url there.
let mut tr = TriblerPlugin::new(); let tr = TriblerPlugin::new();
tr.set_seed_config(TriblerConfigFile::from_env());
let plugins: Vec<Box<dyn FeederPlugin>> = vec![Box::new(tr)]; let plugins: Vec<Box<dyn FeederPlugin>> = vec![Box::new(tr)];
+10 -44
View File
@@ -92,39 +92,11 @@ pub struct TriblerConfigFile {
} }
impl TriblerConfigFile { impl TriblerConfigFile {
/// Parse the config from the env (first-boot seed). Tribler-connection from // NOTE: `TriblerConfigFile::from_env()` was deliberately removed. Tribler
/// `TRIBLER_*`; the enrichment TMDB key/language from `TMDB_TOKEN` // config (sidecar URL, api key, tmdb token/language, meta-core URL) is
/// (or `PROWLARR_TMDB_TOKEN`) / `TMDB_LANGUAGE` — so an operator can still seed // dashboard config — it lives ONLY in the persisted config.json, never in
/// them from env, while the dashboard form overrides at runtime. // env. (Was: TRIBLER_SIDECAR_URL / TRIBLER_API_KEY / TMDB_TOKEN /
pub fn from_env() -> Self { // TMDB_LANGUAGE / META_CORE_URL — all now config-schema fields.)
let mut c = Self::default();
if let Ok(u) = std::env::var("META_CORE_URL") {
if !u.trim().is_empty() {
c.meta_core_url = Some(u.trim().to_string());
}
}
if let Ok(url) = std::env::var("TRIBLER_SIDECAR_URL") {
if !url.trim().is_empty() {
c.sidecar_url = Some(url);
}
}
if let Ok(key) = std::env::var("TRIBLER_API_KEY") {
if !key.trim().is_empty() {
c.api_key = Some(key);
}
}
if let Ok(t) = std::env::var("TMDB_TOKEN").or_else(|_| std::env::var("PROWLARR_TMDB_TOKEN")) {
if !t.trim().is_empty() {
c.tmdb_api_key = Some(t);
}
}
if let Ok(l) = std::env::var("TMDB_LANGUAGE") {
if !l.trim().is_empty() {
c.tmdb_language = Some(l);
}
}
c
}
} }
/// HTTP timeout for the *synchronous* calls (local search, remote-search /// HTTP timeout for the *synchronous* calls (local search, remote-search
@@ -223,9 +195,6 @@ pub struct TriblerPlugin {
sidecar_url: String, sidecar_url: String,
api_key: Option<String>, api_key: Option<String>,
cache: Option<MidhashCache>, cache: Option<MidhashCache>,
/// Env-derived config seed (set by `main` via [`set_seed_config`]).
/// `configure()` applies it when no persisted `config.json` exists.
seed_config: TriblerConfigFile,
/// Effective config snapshot, exposed (redacted) through the SDK config /// Effective config snapshot, exposed (redacted) through the SDK config
/// plane for the dashboard's per-plugin config form. /// plane for the dashboard's per-plugin config form.
cfg: TriblerConfigFile, cfg: TriblerConfigFile,
@@ -261,7 +230,6 @@ impl TriblerPlugin {
sidecar_url, sidecar_url,
api_key: None, api_key: None,
cache: None, cache: None,
seed_config: TriblerConfigFile::default(),
cfg: TriblerConfigFile::default(), cfg: TriblerConfigFile::default(),
enricher: None, enricher: None,
inline_tmdb: crate::enrich::Enricher::new(), inline_tmdb: crate::enrich::Enricher::new(),
@@ -283,11 +251,8 @@ impl TriblerPlugin {
self.api_key = Some(key); self.api_key = Some(key);
} }
/// Install the env-derived seed config (applied by `configure()` when no // set_seed_config was removed: there is no env seed. config.json is the only
/// persisted `config.json` exists). Mirrors the torznab feeder. // config source (dashboard-written); apply_config takes it in configure().
pub fn set_seed_config(&mut self, cfg: TriblerConfigFile) {
self.seed_config = cfg;
}
/// Apply an effective config: override sidecar URL / API key when present, /// Apply an effective config: override sidecar URL / API key when present,
/// otherwise keep the constructor defaults. Records the effective snapshot /// otherwise keep the constructor defaults. Records the effective snapshot
@@ -829,10 +794,12 @@ impl FeederPlugin for TriblerPlugin {
// plane when the operator saves through the dashboard) wins over the env // plane when the operator saves through the dashboard) wins over the env
// seed. Both are optional — tribler has working defaults, so a fully // seed. Both are optional — tribler has working defaults, so a fully
// unconfigured feeder still serves (DEFAULT_SIDECAR_URL, no auth key). // unconfigured feeder still serves (DEFAULT_SIDECAR_URL, no auth key).
// config.json (dashboard-written) is the ONLY config source — no env
// seed. Absent → tribler's built-in defaults (DEFAULT_SIDECAR_URL, no key).
let effective = std::fs::read(cache_dir.join("config.json")) let effective = std::fs::read(cache_dir.join("config.json"))
.ok() .ok()
.and_then(|b| serde_json::from_slice::<TriblerConfigFile>(&b).ok()) .and_then(|b| serde_json::from_slice::<TriblerConfigFile>(&b).ok())
.unwrap_or_else(|| self.seed_config.clone()); .unwrap_or_default();
self.apply_config(effective); self.apply_config(effective);
self.cache = Some(common::open_midhash_cache(cache_dir, "tribler")?); self.cache = Some(common::open_midhash_cache(cache_dir, "tribler")?);
// Build the enrichment driver: infra (META_CORE_URL + plugin URLs + peer // Build the enrichment driver: infra (META_CORE_URL + plugin URLs + peer
@@ -1448,7 +1415,6 @@ fn build_enricher(http: &reqwest::Client, cfg: &TriblerConfigFile) -> Option<Enr
.as_ref() .as_ref()
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.or_else(|| EnrichmentConfig::from_env().map(|c| c.meta_core_url))
.unwrap_or_else(|| meta_feeder_sdk::DEFAULT_META_CORE_URL.to_string()); .unwrap_or_else(|| meta_feeder_sdk::DEFAULT_META_CORE_URL.to_string());
let mut ecfg = EnrichmentConfig::from_meta_core(meta_core_url); let mut ecfg = EnrichmentConfig::from_meta_core(meta_core_url);
if let Some(k) = cfg.tmdb_api_key.as_ref().filter(|s| !s.trim().is_empty()) { if let Some(k) = cfg.tmdb_api_key.as_ref().filter(|s| !s.trim().is_empty()) {