Import the MetaMesh app family from Worph/AppStore
23 apps (MetaCore, the library/network/client tiers and 13 MetaFeeder*) move out of the general-purpose Worph/AppStore into their own store so the MetaMesh family releases on its own cadence. - asset URLs repointed to cdn.jsdelivr.net/gh/Worph/MetaAppStore@main - store scaffolding added (category/recommend/featured lists, licence, contributing, gitattributes) - psd-source/meta-icons-v2 icon generator follows the apps MetaMCP stays in Worph/AppStore: it is third-party metatool-ai software, not part of MetaMesh.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# MetaWatch
|
||||
|
||||
Stremio-style, **video-only** client for the decentralized MetaMesh network.
|
||||
Browses curated rows + filtered search, hides anything without a poster +
|
||||
description, and plays back in the browser — the server does zero media work
|
||||
(flat Range/206 byte proxy, no transcode).
|
||||
|
||||
## What's in the stack
|
||||
|
||||
This is a **self-contained** stack (mirrors `packages/meta-watch/dev/`). It does
|
||||
**not** depend on the standalone MetaCore app — it ships its own dedicated
|
||||
infra so it can run alongside MetaGateway and the other Meta* apps with no
|
||||
collisions:
|
||||
|
||||
| Container | Role |
|
||||
|-----------|------|
|
||||
| `metawatch-app` | UI + back-end the browser talks to (behind OIDC) |
|
||||
| `metawatch-search` | Discovery: Kamilata federated search, identity/sign chain, prefs, User Data Layer |
|
||||
| `metawatch-share` | Pure content transport: byte Range, bitswap, BitTorrent tier |
|
||||
| `metawatch-core` | Dedicated meta-core: identity keystore + ingested records |
|
||||
| `metawatch` | nginx-hash-lock OIDC perimeter for the dashboard |
|
||||
|
||||
Identity rides `meta-watch → meta-search → meta-core` (meta-watch holds only
|
||||
verify keys). My List + Continue Watching persist in
|
||||
`/DATA/AppData/metawatch/data` via the User Data Layer op-log.
|
||||
|
||||
## Data layout
|
||||
|
||||
```
|
||||
/DATA/AppData/metawatch/
|
||||
├── metacore/ # shared by core + search + share (leader info, services, redis)
|
||||
└── data/ # library.json + oplog.cbor (My List / Continue Watching)
|
||||
```
|
||||
|
||||
## Federation
|
||||
|
||||
`metawatch-search` and `metawatch-share` join the **public** libp2p swarm
|
||||
(kad-DHT, cohort `metamesh-share-default`) and host-publish their libp2p
|
||||
listeners on TCP **4004** / **4003**. mDNS on the `pcs` network lets them find a
|
||||
same-host MetaGateway without any open ports. To scope a private cohort, set a
|
||||
matching `KAD_NAMESPACE` on every node.
|
||||
|
||||
## You need a content producer
|
||||
|
||||
MetaWatch is a *consumer*. To see anything, at least one **MetaGateway** (or
|
||||
another MetaShare archivist) must be reachable on the swarm. Install
|
||||
**MetaGateway** for the torznab/Prowlarr + Tribler feed.
|
||||
|
||||
## Images
|
||||
|
||||
| Image | Tag |
|
||||
|-------|-----|
|
||||
| `ghcr.io/worph/meta-watch` | `1.0.34` |
|
||||
| `ghcr.io/worph/meta-search` | `1.1.0` |
|
||||
| `ghcr.io/worph/meta-share` | `1.0.26` |
|
||||
| `ghcr.io/worph/meta-core` | `1.0.12` |
|
||||
| `ghcr.io/yundera/appshield` | `2.0.9` |
|
||||
|
||||
Publish the `worph/*` tags before listing the app — `:latest` tracks the newest
|
||||
`v*` git tag, not `main`, so an untagged fix is a no-op for a `docker compose pull`.
|
||||
@@ -0,0 +1,514 @@
|
||||
name: metawatch
|
||||
|
||||
# =============================================================================
|
||||
# MetaWatch — Stremio-style, video-only client for the MetaMesh network.
|
||||
#
|
||||
# Self-contained stack (mirrors packages/meta-watch/dev/): the meta-watch app
|
||||
# plus its OWN dedicated discovery (meta-search), transport (meta-share) and
|
||||
# meta-core. It does NOT share the standalone MetaCore app — its core holds
|
||||
# only this client's identity keystore + ingested records, so MetaWatch can be
|
||||
# installed alongside MetaGateway and the standalone Meta* apps with zero
|
||||
# container-name / data-path collisions.
|
||||
#
|
||||
# metawatch-core → /DATA/AppData/metawatch/metacore (per-stack meta-core)
|
||||
# metawatch-search → discovery (Kamilata federated search, identity proxy)
|
||||
# metawatch-share → pure content transport (byte Range, bitswap, BT)
|
||||
# metawatch-app → the UI/back-end the user browses (behind OIDC)
|
||||
#
|
||||
# Federation: meta-search/meta-share join the PUBLIC libp2p swarm (kad-DHT,
|
||||
# default cohort) on distinct host ports (4003/4004) + mDNS on `pcs`, so they
|
||||
# discover MetaGateway's records both same-host (mDNS) and across nodes (kad).
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# metawatch-core — dedicated meta-core for this client (identity keystore +
|
||||
# ingested records). File watcher OFF: MetaWatch is a consumer, not a library
|
||||
# indexer. Data lives under /DATA/AppData/metawatch/ so CasaOS archives it.
|
||||
# ---------------------------------------------------------------------------
|
||||
metawatch-core:
|
||||
# 1.0.1 carries the identity API (commit 52f5b5c) required for MetaWatch
|
||||
# sign-in / keypair generation; 1.0.0 predates it.
|
||||
image: ghcr.io/worph/meta-core:1.0.12
|
||||
container_name: metawatch-core
|
||||
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
|
||||
# instead of swap. Mirrors the dev sub-stack limits.
|
||||
mem_limit: 768m
|
||||
memswap_limit: 768m
|
||||
hostname: metawatch-core
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
user: "0:0"
|
||||
cpu_shares: 30
|
||||
expose:
|
||||
- 9000
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/metacore:/meta-core:rw
|
||||
# Subtitle store: player-uploaded subtitles land in /files/plugin/subtitles
|
||||
# (WebDAV PUT + POST /api/files/register, needs meta-core with that route).
|
||||
# Plugin output only — no /files/watch mount, watcher stays off.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/files/plugin:/files/plugin:rw
|
||||
environment:
|
||||
- BASE_URL=http://metawatch-core:9000
|
||||
- ENABLE_FILE_WATCHER=false
|
||||
- TZ=$TZ
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
networks:
|
||||
- metawatch
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# metawatch-search — DISCOVERY peer. Federated Kamilata search, record
|
||||
# metadata, poster/thumbnail recovery, shared prefs, User Data Layer and the
|
||||
# identity/sign chain (meta-watch → meta-search → meta-core). Public libp2p on
|
||||
# host port 4004; mDNS on pcs for same-host peer discovery.
|
||||
# ---------------------------------------------------------------------------
|
||||
metawatch-search:
|
||||
# 1.0.2 = rsa-enabled libp2p — required to join the public IPFS DHT (the
|
||||
# RSA-keyed bootstrap.libp2p.io nodes) and therefore to DISCOVER remote
|
||||
# gateways via the kad `metamesh-gateway` provider namespace. Without it
|
||||
# this peer only ever sees same-host mDNS peers.
|
||||
image: ghcr.io/worph/meta-search:1.1.0
|
||||
container_name: metawatch-search
|
||||
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
|
||||
# instead of swap. Mirrors the dev sub-stack limits.
|
||||
mem_limit: 1g
|
||||
memswap_limit: 1g
|
||||
hostname: metawatch-search
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
cpu_shares: 30
|
||||
expose:
|
||||
- 3000
|
||||
ports:
|
||||
# libp2p TCP — host-published so the swarm can dial this peer.
|
||||
- "4004:4001"
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/metacore:/meta-core:rw
|
||||
# META_SHARE_DATA (default /data/meta-share): prefs.json (language prefs)
|
||||
# + the redb blockstore holding poster/thumbnail blocks recovered off the
|
||||
# swarm. Without this mount both are on the container's writable layer and
|
||||
# every image bump loses the prefs and re-fetches every poster.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/search-data:/data:rw
|
||||
environment:
|
||||
P2P_LISTEN: "/ip4/0.0.0.0/tcp/4001"
|
||||
# Public IPFS DHT bootstraps by explicit /dns4/.../tcp/4001 (the nested
|
||||
# dnsaddr default resolves to wss-only in some container resolvers).
|
||||
# NOTE: no PUBLIC_ADDR here ON PURPOSE. meta-search supports it, but this
|
||||
# peer only needs to WALK the DHT: with the bootstraps below it discovers
|
||||
# remote gateways (kad `metamesh-gateway` provider records) and dials them
|
||||
# outbound. Announcing would publish this client's index to the swarm for
|
||||
# no benefit. The gateway is the peer that must be dialable — see
|
||||
# MetaGateway's META_GATEWAY_PUBLIC_ADDR.
|
||||
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
|
||||
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
|
||||
# It is here for resilience: the four IPFS-Foundation bootstrappers can
|
||||
# refuse an individual IP, and when they do a peer cannot join the DHT AT
|
||||
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
|
||||
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
|
||||
# reachable fifth was added. Costs nothing when the four work.
|
||||
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
|
||||
# Was LOAD-BEARING up to meta-search:1.0.2, which defaulted this to 8s — too
|
||||
# short for the gateway fan-out (Prowlarr -> public indexers), so search
|
||||
# returned empty. 300 is the default on the 1.1.0 pin; kept explicit so a
|
||||
# future default change can't silently shorten it again.
|
||||
SEARCH_TIMEOUT_SECS: "300"
|
||||
META_CORE_URL: "http://metawatch-core:9000"
|
||||
META_CORE_PATH: "/meta-core"
|
||||
BASE_URL: "http://metawatch-search:3000"
|
||||
# Renamed at the 1.1.0 bump (2026-08-26), as the old note here said to do:
|
||||
# META_SHARE_PEER_API_URL was the pre-1.0.8 spelling and now only logs a
|
||||
# deprecation warning. Do NOT set both.
|
||||
INTERNAL_BASE_URL: "http://metawatch-search:3000"
|
||||
# NOTE: KAD_NAMESPACE was dropped — it merely restated the default cohort
|
||||
# (`metamesh-share-default`). Set it to a non-default value on EVERY peer
|
||||
# to scope a private cohort; they must all agree or search returns zero.
|
||||
TZ: "$TZ"
|
||||
depends_on:
|
||||
metawatch-core:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
networks:
|
||||
- metawatch
|
||||
- pcs
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# metawatch-share — pure content TRANSPORT peer. Byte Range proxy for
|
||||
# playback/probe/warm/playback-focus over bitswap + a BitTorrent tier; seeds
|
||||
# whatever it fetches. Public libp2p on host port 4003.
|
||||
# ---------------------------------------------------------------------------
|
||||
metawatch-share:
|
||||
# 1.0.13 = nocopy library seeding + core/cache source surfacing, on top of
|
||||
# the 1.0.8 floor: rsa-enabled libp2p (joins the public IPFS DHT) +
|
||||
# P2P_ANNOUNCE so remote peers can dial this transport peer to fetch the
|
||||
# bytes it seeds (fetch-implies-seed). Without rsa it never reaches the DHT.
|
||||
image: ghcr.io/worph/meta-share:1.0.26
|
||||
container_name: metawatch-share
|
||||
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
|
||||
# instead of swap. Mirrors the dev sub-stack limits.
|
||||
mem_limit: 3g
|
||||
memswap_limit: 3g
|
||||
hostname: metawatch-share
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
cpu_shares: 30
|
||||
expose:
|
||||
- 3000
|
||||
ports:
|
||||
- "4003:4001"
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/metacore:/meta-core:rw
|
||||
# Operator settings written by the dashboard Settings panel (settings.json,
|
||||
# e.g. optional direct-NNTP/Usenet credentials). Persisted on disk so
|
||||
# dashboard edits survive container recreation — this stack has no separate
|
||||
# credentialed gateway peer to forward to, so config lives here.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/metashare-config:/config:rw
|
||||
# Seeded/fetched bytes (META_SHARE_DATA, default /data/meta-share): redb
|
||||
# blockstore, seed manifest, librqbit sparse files, NZB cache. This is the
|
||||
# playback cache — without the mount every recreate re-downloads the lot
|
||||
# from a cold swarm, and the peer seeds nothing back.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/share-data:/data:rw
|
||||
environment:
|
||||
P2P_LISTEN: "/ip4/0.0.0.0/tcp/4001"
|
||||
# Advertise the routable address (published host port 4003) so remote
|
||||
# peers can dial this transport peer for the bytes it seeds. Behind
|
||||
# Docker NAT the swarm otherwise only sees container-internal addresses.
|
||||
# Use the IPV4 spelling, NOT the bare APP_PUBLIC_IP: on an IPv6-primary
|
||||
# PCS that variable holds the v6 address, and `/ip4/<v6>/tcp/...` is not a
|
||||
# parseable multiaddr — meta-share REFUSES TO START rather than announce
|
||||
# a lie (config.rs: "PUBLIC_ADDR is not a valid multiaddr").
|
||||
PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4003"
|
||||
# Public IPFS DHT bootstraps by explicit /dns4/.../tcp/4001 (the nested
|
||||
# dnsaddr default resolves to wss-only in some container resolvers).
|
||||
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
|
||||
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
|
||||
# It is here for resilience: the four IPFS-Foundation bootstrappers can
|
||||
# refuse an individual IP, and when they do a peer cannot join the DHT AT
|
||||
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
|
||||
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
|
||||
# reachable fifth was added. Costs nothing when the four work.
|
||||
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
|
||||
META_CORE_URL: "http://metawatch-core:9000"
|
||||
META_CORE_PATH: "/meta-core"
|
||||
BASE_URL: "http://metawatch-share:3000"
|
||||
INTERNAL_BASE_URL: "http://metawatch-share:3000"
|
||||
TZ: "$TZ"
|
||||
depends_on:
|
||||
metawatch-core:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
networks:
|
||||
- metawatch
|
||||
- pcs
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# metawatch-app — the back-end the browser talks to. Zero media work: flat
|
||||
# Range/206 byte proxy to metawatch-share; search/identity via metawatch-search.
|
||||
# My List + Continue Watching persist in /DATA/AppData/metawatch/data.
|
||||
# ---------------------------------------------------------------------------
|
||||
metawatch-app:
|
||||
image: ghcr.io/worph/meta-watch:1.0.34
|
||||
container_name: metawatch-app
|
||||
hostname: metawatch-app
|
||||
restart: unless-stopped
|
||||
cpu_shares: 30
|
||||
# Hard RAM cap with swap disabled (memswap_limit == mem_limit → zero swap
|
||||
# budget on cgroup v2) so the container is OOM-killed instead of pushing
|
||||
# the host into swap. Same value the dev stack pins (METAWATCH_MEM=512m).
|
||||
mem_limit: 512m
|
||||
memswap_limit: 512m
|
||||
expose:
|
||||
- 3000
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metawatch/data:/app/data
|
||||
environment:
|
||||
META_SHARE_URL: "http://metawatch-share:3000"
|
||||
META_SEARCH_URL: "http://metawatch-search:3000"
|
||||
# Browser-reachable topbar chips — the AppShield gates below expose the
|
||||
# co-located meta-share transport and meta-search discovery peers behind
|
||||
# Yundera OIDC, and these URLs are what the topbar chip clicks open.
|
||||
META_SHARE_PUBLIC_URL: "https://metawatchshare-${APP_DOMAIN}"
|
||||
META_SEARCH_PUBLIC_URL: "https://metawatchsearch-${APP_DOMAIN}"
|
||||
META_WATCH_SYNC_PEERS: "http://metawatch-search:3000"
|
||||
# This box's own core — subtitle store only (uploads + curated matches).
|
||||
META_CORE_URL: "http://metawatch-core:9000"
|
||||
TZ: "$TZ"
|
||||
depends_on:
|
||||
metawatch-search:
|
||||
condition: service_healthy
|
||||
metawatch-share:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
networks:
|
||||
- metawatch
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OIDC perimeter for the MetaWatch UI. Self-registers as an OIDC client with
|
||||
# the platform's auth-registrar on first login. container_name MUST equal the
|
||||
# URL prefix (`metawatch`) so the registrar's redirect-URI allowlist matches
|
||||
# `metawatch-<user>.<domain>`.
|
||||
# ---------------------------------------------------------------------------
|
||||
metawatch:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
cpu_shares: 80
|
||||
container_name: metawatch
|
||||
hostname: metawatch
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
expose:
|
||||
- 80
|
||||
environment:
|
||||
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
||||
BACKEND_HOST: "metawatch-app"
|
||||
BACKEND_PORT: "3000"
|
||||
LISTEN_PORT: "80"
|
||||
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
|
||||
depends_on:
|
||||
- metawatch-app
|
||||
labels:
|
||||
# ---- metawatch-${APP_DOMAIN} (gateway TLS) ----
|
||||
caddy_0: metawatch-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.0_handle: "/health"
|
||||
caddy_0.0_handle.reverse_proxy: "metawatch-app:3000"
|
||||
caddy_0.1_handle: ""
|
||||
caddy_0.1_handle.reverse_proxy: "{{upstreams 80}}"
|
||||
|
||||
# ---- metawatch-IP.nip.io (gateway TLS) ----
|
||||
caddy_1: metawatch-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.0_handle: "/health"
|
||||
caddy_1.0_handle.reverse_proxy: "metawatch-app:3000"
|
||||
caddy_1.1_handle: ""
|
||||
caddy_1.1_handle.reverse_proxy: "{{upstreams 80}}"
|
||||
|
||||
# ---- metawatch-IP.sslip.io (Let's Encrypt) ----
|
||||
caddy_2: metawatch-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.0_handle: "/health"
|
||||
caddy_2.0_handle.reverse_proxy: "metawatch-app:3000"
|
||||
caddy_2.1_handle: ""
|
||||
caddy_2.1_handle.reverse_proxy: "{{upstreams 80}}"
|
||||
networks:
|
||||
- metawatch
|
||||
- pcs
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AppShield gates — expose metawatch-share and metawatch-search dashboards
|
||||
# behind Yundera OIDC SSO so the meta-watch topbar chips (🔎 meta-search /
|
||||
# 🔗 meta-share) can open them. container_name MUST equal the subdomain
|
||||
# prefix (auth-registrar derives the OIDC client_id from it), so these use
|
||||
# collision-free prefixes — the co-located backends are `metawatch-*` and
|
||||
# keep their internal ports. Same pattern MetaGateway uses for its own
|
||||
# metagatewaycore / metagatewayshare gates.
|
||||
# ---------------------------------------------------------------------------
|
||||
metawatchshare:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
cpu_shares: 80
|
||||
container_name: metawatchshare
|
||||
hostname: metawatchshare
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
expose: ["80"]
|
||||
environment:
|
||||
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
||||
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
|
||||
BACKEND_HOST: "metawatch-share"
|
||||
BACKEND_PORT: "3000"
|
||||
LISTEN_PORT: "80"
|
||||
labels:
|
||||
caddy_0: metawatchshare-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_1: metawatchshare-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_2: metawatchshare-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.reverse_proxy: "{{upstreams 80}}"
|
||||
depends_on:
|
||||
- metawatch-share
|
||||
networks:
|
||||
- metawatch
|
||||
- pcs
|
||||
|
||||
metawatchsearch:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
cpu_shares: 80
|
||||
container_name: metawatchsearch
|
||||
hostname: metawatchsearch
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
expose: ["80"]
|
||||
environment:
|
||||
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
||||
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
|
||||
BACKEND_HOST: "metawatch-search"
|
||||
BACKEND_PORT: "3000"
|
||||
LISTEN_PORT: "80"
|
||||
labels:
|
||||
caddy_0: metawatchsearch-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_1: metawatchsearch-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_2: metawatchsearch-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.reverse_proxy: "{{upstreams 80}}"
|
||||
depends_on:
|
||||
- metawatch-search
|
||||
networks:
|
||||
- metawatch
|
||||
- pcs
|
||||
|
||||
networks:
|
||||
metawatch:
|
||||
name: metawatch
|
||||
driver: bridge
|
||||
pcs:
|
||||
name: ${APP_NET:-pcs}
|
||||
external: true
|
||||
|
||||
x-casaos:
|
||||
architectures:
|
||||
- amd64
|
||||
- arm64
|
||||
author: Worph Team
|
||||
category: Media
|
||||
# `needs-public-ip` is the reserved functional tag: this app publishes a raw
|
||||
# libp2p TCP port that remote peers must dial inbound. Behind CGNAT or the
|
||||
# mesh-router tunnel it still installs and starts, but peers cannot reach it.
|
||||
tags: ["media", "needs-public-ip"]
|
||||
developer: Aptero Team
|
||||
|
||||
description:
|
||||
en_us: |
|
||||
**A Stremio-style, video-only player for the decentralized MetaMesh network.**
|
||||
|
||||
MetaWatch browses curated rows and filtered search across the MetaMesh
|
||||
federation and plays back in your browser — the server does zero media
|
||||
work (flat byte-range streaming, no transcode). Anything without a poster
|
||||
and a description is hidden, so the wall is always clean.
|
||||
|
||||
This is a **self-contained** stack: it ships its own discovery
|
||||
(meta-search), content transport (meta-share) and a dedicated meta-core
|
||||
for identity and state. Install it on its own — it does not depend on the
|
||||
standalone MetaCore app, and coexists with MetaGateway without conflicts.
|
||||
|
||||
**Key Features:**
|
||||
- **Curated Rows + Search** - Popular / trending / top-rated / anime rows, plus federated search
|
||||
- **Browser-Side Playback** - SegmentPlayer streams via Range/206; the server never transcodes
|
||||
- **Poster-Gated Wall** - Only titles with poster + description are shown
|
||||
- **Show Aggregation** - Per-episode records grouped into show pages by tmdbid/imdbid/title
|
||||
- **My List + Continue Watching** - Persistent, backed by the User Data Layer op-log
|
||||
- **TV Remote Navigation** - D-pad spatial navigation for the 10-foot UI
|
||||
- **Federated Discovery** - Joins the public libp2p swarm to find MetaGateway records
|
||||
|
||||
fr_fr: |
|
||||
**Un lecteur vidéo façon Stremio pour le réseau décentralisé MetaMesh.**
|
||||
|
||||
MetaWatch parcourt des rangées sélectionnées et une recherche filtrée à
|
||||
travers la fédération MetaMesh et lit la vidéo dans votre navigateur — le
|
||||
serveur ne fait aucun travail média (streaming par plages d'octets, sans
|
||||
transcodage). Tout ce qui n'a ni affiche ni description est masqué.
|
||||
|
||||
C'est une stack **autonome** : elle embarque sa propre découverte
|
||||
(meta-search), son transport de contenu (meta-share) et un meta-core dédié
|
||||
pour l'identité et l'état. Installez-la seule — elle ne dépend pas de
|
||||
l'app MetaCore et cohabite avec MetaGateway sans conflit.
|
||||
|
||||
**Fonctionnalités Clés:**
|
||||
- **Rangées + Recherche** - Populaire / tendances / mieux notés / anime, plus recherche fédérée
|
||||
- **Lecture Côté Navigateur** - SegmentPlayer en Range/206 ; le serveur ne transcode jamais
|
||||
- **Mur à Affiches** - Seuls les titres avec affiche + description sont montrés
|
||||
- **Agrégation de Séries** - Épisodes regroupés en pages de série par tmdbid/imdbid/titre
|
||||
- **Ma Liste + Reprendre** - Persistant, via l'op-log de la couche de données utilisateur
|
||||
- **Navigation Télécommande** - Navigation spatiale D-pad pour l'interface salon
|
||||
|
||||
tagline:
|
||||
en_us: Stremio-style video player for the decentralized MetaMesh network
|
||||
fr_fr: Lecteur vidéo façon Stremio pour le réseau décentralisé MetaMesh
|
||||
|
||||
title:
|
||||
en_us: MetaWatch
|
||||
|
||||
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaWatch/icon.png
|
||||
screenshot_link:
|
||||
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaWatch/screenshot-1.png
|
||||
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaWatch/screenshot-2.png
|
||||
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaWatch/screenshot-3.png
|
||||
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaWatch/thumbnail.png
|
||||
|
||||
index: /
|
||||
store_app_id: metawatch
|
||||
main: metawatch
|
||||
is_uncontrolled: false
|
||||
|
||||
tips:
|
||||
before_install:
|
||||
en_us: |
|
||||
**Self-contained — no MetaCore required.** MetaWatch ships its own
|
||||
discovery, transport and meta-core. Just install and open the dashboard.
|
||||
|
||||
**Authentication:** The UI is protected by Yundera's built-in Authelia
|
||||
via OIDC — sign in with your existing Yundera account on first open.
|
||||
|
||||
**Federation ports (optional).** meta-search and meta-share host-publish
|
||||
their libp2p listeners on TCP **4004** and **4003** so the public IPFS
|
||||
kad-DHT and other nodes can dial in. Open them inbound if you want this
|
||||
client to be reachable across networks; same-host discovery (e.g. a
|
||||
MetaGateway on the same box) works over mDNS without them.
|
||||
|
||||
**Content comes from gateways.** To see anything, at least one
|
||||
MetaGateway (or another MetaShare archivist) must be reachable on the
|
||||
swarm. Install **MetaGateway** for the torznab/Prowlarr + Tribler feed.
|
||||
|
||||
**After installation:**
|
||||
1. Open `https://metawatch-$APP_DOMAIN/`
|
||||
2. Sign in via the Authelia portal
|
||||
3. Browse the rows or search; results stream in as gateways respond
|
||||
|
||||
# Maison (Yundera dashboard) configuration. Read in preference to x-casaos.
|
||||
# schema_version 2 is required for `folders`: an older Maison refuses the app
|
||||
# outright rather than starting it without its directories.
|
||||
x-compose-app:
|
||||
schema_version: 2
|
||||
# Created and chowned by Maison before any image is pulled, before any hook
|
||||
# runs and before the containers start — on the first boot and every one after.
|
||||
# Compose would otherwise create a missing bind source as a root-owned dir.
|
||||
folders:
|
||||
- path: /DATA/AppData/metawatch/metacore
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metawatch/search-data
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metawatch/metashare-config
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metawatch/share-data
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metawatch/data
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
# Same string as the caddy_0 label, so the tile link and the route agree.
|
||||
webui-host: metawatch-${domain}
|
||||
webui-path: "/"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,59 @@
|
||||
# MetaWatch — Rationale
|
||||
|
||||
## What deviation / exception is being requested
|
||||
|
||||
1. `metawatch-core`, `metawatch-search` and `metawatch-share` run `user: 0:0`, and
|
||||
`metawatch-core` runs `privileged: true`.
|
||||
2. The stack publishes two host ports: `4003/tcp` and `4004/tcp` (libp2p).
|
||||
3. The app exposes **three** public hostnames — `metawatch-`, `metawatchshare-` and
|
||||
`metawatchsearch-` — each with its own AppShield sidecar, so two sidecar
|
||||
`container_name`s differ from the compose top-level `name:`.
|
||||
|
||||
## Why it is necessary
|
||||
|
||||
1. `metawatch-core` is a bundled MetaCore: rclone remote mounts need a mount namespace
|
||||
(`SYS_ADMIN` + `/dev/fuse` + an unconfined AppArmor profile), plus nginx and Redis.
|
||||
`metawatch-search` and `metawatch-share` write service registrations into the root-owned
|
||||
`/meta-core/services/`.
|
||||
2. libp2p is **not HTTP** — Caddy cannot proxy it, and remote peers resolve this node's
|
||||
public multiaddr from the kad-DHT and dial it directly. Without inbound reachability the
|
||||
node never serves bytes to the swarm. The app therefore carries the reserved
|
||||
`needs-public-ip` tag.
|
||||
3. Each hostname is a distinct UI, and `auth-registrar` derives an app's OIDC `client_id`
|
||||
from the sidecar's container name via PTR lookup, so three routes need three stable,
|
||||
distinct names. `x-casaos.main` points at `metawatch`, the sidecar carrying the primary
|
||||
route.
|
||||
|
||||
## Security mitigations in place
|
||||
|
||||
- Every publicly-routed hostname terminates at its own AppShield sidecar
|
||||
(`ghcr.io/yundera/appshield:2.0.9`) behind the PCS's Authelia SSO. Each sidecar sets
|
||||
`hostname:` equal to its `container_name` so `auth-registrar` can attest it via PTR and
|
||||
accept its redirect URIs; the names are app-prefixed, so they cannot collide with another
|
||||
app's services in the shared `pcs` DNS namespace.
|
||||
- The backends carry no Caddy labels and publish **no HTTP port** to the host. The only
|
||||
published ports are raw libp2p TCP.
|
||||
- libp2p transport is authenticated and encrypted (Noise) and peer identity is a public
|
||||
key, so an unauthenticated dial cannot read anything the node has not chosen to publish
|
||||
to the swarm.
|
||||
- No user directories are mounted — no `/DATA/Documents`, `/DATA/Downloads`, `/DATA/Media`
|
||||
or `/DATA/Gallery`.
|
||||
- `cpu_shares` on every service; memory-heavy services carry a hard `mem_limit` with
|
||||
`memswap_limit` equal to it. All image tags pinned.
|
||||
|
||||
## Alternatives considered and rejected
|
||||
|
||||
- **Proxy libp2p through Caddy.** Not possible; the DHT advertises a raw TCP multiaddr.
|
||||
- **One sidecar with path-based routing.** The three backends generate absolute URLs and
|
||||
set cookies on their own hostnames; path-mounting them breaks both.
|
||||
- **Drop `privileged` from `metawatch-core` for a `cap_add` list.** Same container as
|
||||
MetaCore; the reduced form was tried and still failed to bring up the supervised daemons.
|
||||
- **Use the shared-backend variant instead.** That is `MetaWatchShared`, for a box that
|
||||
already runs MetaCore and MetaShare. This app is the self-contained one.
|
||||
|
||||
## Data protection
|
||||
|
||||
All state lives under `/DATA/AppData/metawatch/`, declared in `x-compose-app.folders` and
|
||||
archived by Maison on uninstall, so an uninstall/reinstall with "keep user data" returns My
|
||||
List and Continue Watching intact. No user media directory is mounted into any service;
|
||||
bytes are served through the app's own transport peer.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Reference in New Issue
Block a user