Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6a1d8090e | ||
|
|
3852d3375c | ||
|
|
2ce28c1cbd | ||
|
|
f70bb52aab |
@@ -5,7 +5,7 @@ name: Publish image
|
||||
# * 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/.
|
||||
# feeder-plugin/tribler-feeder/.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./feeder-plugin/torrent-feeder/Dockerfile
|
||||
file: ./feeder-plugin/tribler-feeder/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -1 +1,128 @@
|
||||
# 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.
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
//! torrent-core TMDB catalog-discovery stack. Split out of `meta-feeder-torrent`
|
||||
//! so its dashboard config panel resolves to the tribler schema.
|
||||
//!
|
||||
//! Env (shared): `META_FEEDER_HTTP_LISTEN` (default `0.0.0.0:8080`),
|
||||
//! `META_FEEDER_STATE_DIR` (default `/data/meta-feeder`), `META_CORE_URL` (D4
|
||||
//! self-publish), `RUST_LOG`.
|
||||
//! Env (tribler source): `TRIBLER_SIDECAR_URL`, `TRIBLER_API_KEY`,
|
||||
//! `TMDB_TOKEN`/`TMDB_LANGUAGE`, plus the enrichment-plugin URLs
|
||||
//! (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `META_FEEDER_CALLBACK_URL`).
|
||||
//! ALL tribler config — sidecar URL, api key, tmdb token/language, meta-core
|
||||
//! URL — is read from the persisted `config.json` (dashboard-written) ONLY.
|
||||
//! There is NO env seed for these: a field in the dashboard config schema must
|
||||
//! not have an env var (config from JSON file / web UI is the single source of
|
||||
//! truth).
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
@@ -21,7 +25,7 @@ use meta_feeder_sdk::serve_feeders;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use tribler_feeder::tribler::{TriblerConfigFile, TriblerPlugin};
|
||||
use tribler_feeder::tribler::TriblerPlugin;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
@@ -35,11 +39,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
let state_dir =
|
||||
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
|
||||
// per-source cache dir) wins once saved through the dashboard. The tribler
|
||||
// source builds its enrichment driver in configure().
|
||||
let mut tr = TriblerPlugin::new();
|
||||
tr.set_seed_config(TriblerConfigFile::from_env());
|
||||
// No env seed: the plugin reads config.json in configure() (sidecar URL,
|
||||
// api key, tmdb, meta-core URL) and builds its enrichment/self-publish
|
||||
// driver from config.meta_core_url there.
|
||||
let tr = TriblerPlugin::new();
|
||||
|
||||
let plugins: Vec<Box<dyn FeederPlugin>> = vec![Box::new(tr)];
|
||||
|
||||
|
||||
@@ -92,39 +92,11 @@ pub struct TriblerConfigFile {
|
||||
}
|
||||
|
||||
impl TriblerConfigFile {
|
||||
/// Parse the config from the env (first-boot seed). Tribler-connection from
|
||||
/// `TRIBLER_*`; the enrichment TMDB key/language from `TMDB_TOKEN`
|
||||
/// (or `PROWLARR_TMDB_TOKEN`) / `TMDB_LANGUAGE` — so an operator can still seed
|
||||
/// them from env, while the dashboard form overrides at runtime.
|
||||
pub fn from_env() -> Self {
|
||||
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
|
||||
}
|
||||
// NOTE: `TriblerConfigFile::from_env()` was deliberately removed. Tribler
|
||||
// config (sidecar URL, api key, tmdb token/language, meta-core URL) is
|
||||
// dashboard config — it lives ONLY in the persisted config.json, never in
|
||||
// env. (Was: TRIBLER_SIDECAR_URL / TRIBLER_API_KEY / TMDB_TOKEN /
|
||||
// TMDB_LANGUAGE / META_CORE_URL — all now config-schema fields.)
|
||||
}
|
||||
|
||||
/// HTTP timeout for the *synchronous* calls (local search, remote-search
|
||||
@@ -223,9 +195,6 @@ pub struct TriblerPlugin {
|
||||
sidecar_url: String,
|
||||
api_key: Option<String>,
|
||||
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
|
||||
/// plane for the dashboard's per-plugin config form.
|
||||
cfg: TriblerConfigFile,
|
||||
@@ -261,7 +230,6 @@ impl TriblerPlugin {
|
||||
sidecar_url,
|
||||
api_key: None,
|
||||
cache: None,
|
||||
seed_config: TriblerConfigFile::default(),
|
||||
cfg: TriblerConfigFile::default(),
|
||||
enricher: None,
|
||||
inline_tmdb: crate::enrich::Enricher::new(),
|
||||
@@ -283,11 +251,8 @@ impl TriblerPlugin {
|
||||
self.api_key = Some(key);
|
||||
}
|
||||
|
||||
/// Install the env-derived seed config (applied by `configure()` when no
|
||||
/// persisted `config.json` exists). Mirrors the torznab feeder.
|
||||
pub fn set_seed_config(&mut self, cfg: TriblerConfigFile) {
|
||||
self.seed_config = cfg;
|
||||
}
|
||||
// set_seed_config was removed: there is no env seed. config.json is the only
|
||||
// config source (dashboard-written); apply_config takes it in configure().
|
||||
|
||||
/// Apply an effective config: override sidecar URL / API key when present,
|
||||
/// 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
|
||||
// seed. Both are optional — tribler has working defaults, so a fully
|
||||
// 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"))
|
||||
.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.cache = Some(common::open_midhash_cache(cache_dir, "tribler")?);
|
||||
// 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()
|
||||
.map(|s| s.trim().to_string())
|
||||
.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());
|
||||
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()) {
|
||||
|
||||
Reference in New Issue
Block a user