add app for each plugin

This commit is contained in:
Worph
2026-09-13 19:47:17 +02:00
parent 39f82e1f96
commit cb067ee50f
84 changed files with 7072 additions and 3 deletions
@@ -0,0 +1,172 @@
name: metafeederinternetarchive
# =============================================================================
# MetaFeeder · Internet Archive — free audio from archive.org for MetaMesh.
#
# Bridges the Internet Archive's open audio collections (etree live recordings,
# netlabels, 78rpm transfers) into MetaMesh. Keyless and open: no account, no
# API key, nothing to configure. Covers are taken from the archive's own
# thumbnail service, which answers for every item — without a cover the
# consumer's quality gate would drop the whole free tier into the raw bucket.
#
# It needs, over the shared `pcs` network:
# - the **MetaGateway** app — which must list this feeder's URL
# (`http://metafeederinternetarchive-feeder:8080`) in its feeders config so it gets discovered.
# Discovery is startup-only, so restart the gateway afterwards.
#
# Everything is scoped under the `metafeederinternetarchive-*` prefix + the per-stack `metafeederinternetarchive` network +
# /DATA/AppData/metafeederinternetarchive/ data root, so it installs beside the other Meta* apps with
# zero collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metafeederinternetarchive-feeder — the feeder itself. The gateway reaches it directly on
# `pcs` for discovery/query/compute; the human sees it through the AppShield
# gate below.
# ---------------------------------------------------------------------------
metafeederinternetarchive-feeder:
image: ghcr.io/worph/meta-feeder-internetarchive:1.1.2
cpu_shares: 30
container_name: metafeederinternetarchive-feeder
# Bytes tier: a single archive item can list hundreds of files and one
# track is buffered whole before it is handed over, so 256m (the identity
# feeders' figure) is too tight. Swap disabled → OOM instead of thrash.
mem_limit: 512m
memswap_limit: 512m
hostname: metafeederinternetarchive-feeder
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
RUST_LOG: "info,meta_feeder=info,internetarchive_feeder=info"
META_FEEDER_STATE_DIR: "/data/meta-feeder"
TZ: "$TZ"
volumes:
# Persist feeder config.json + redb cache so it survives recreate.
- ${DATA_ROOT:-/DATA}/AppData/metafeederinternetarchive/state:/data/meta-feeder:rw
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
networks:
- metafeederinternetarchive
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the feeder's config page behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). The gateway still reaches the feeder directly on `pcs`; index → /config.
# ---------------------------------------------------------------------------
metafeederinternetarchive:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metafeederinternetarchive
hostname: metafeederinternetarchive
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: "metafeederinternetarchive-feeder"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metafeederinternetarchive-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metafeederinternetarchive-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metafeederinternetarchive-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metafeederinternetarchive-feeder
networks:
- metafeederinternetarchive
- pcs
networks:
metafeederinternetarchive:
name: metafeederinternetarchive
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Media
developer: Aptero Team
main: metafeederinternetarchive
webui_port: 80
index: /config
description:
en_us: |
**Free audio from the Internet Archive, on the MetaMesh network.**
MetaFeeder · Internet Archive bridges archive.org's open audio collections —
**etree** live recordings, **netlabels**, **78rpm** transfers — into MetaMesh.
Keyless and open: nothing to sign up for, nothing to configure.
It is a *bytes* tier: the audio it finds is fetched and seeded to the swarm,
so a track stays playable for the next peer that asks.
**Requires:** **MetaGateway** — its meta-core stores the resolved records and
announces them to the swarm. The gateway must list this feeder in its config.
fr_fr: |
**L'audio libre d'Internet Archive, sur le réseau MetaMesh.**
MetaFeeder · Internet Archive connecte les collections audio ouvertes
d'archive.org (**etree**, **netlabels**, **78rpm**) à MetaMesh. Sans clé et
sans configuration. C'est une couche *octets* : l'audio trouvé est récupéré
puis partagé au swarm. **Nécessite** l'app **MetaGateway**.
tagline:
en_us: Internet Archive free audio (etree, netlabels, 78rpm) for MetaMesh
fr_fr: L'audio libre d'Internet Archive (etree, netlabels, 78rpm) pour MetaMesh
title:
en_us: MetaFeeder · Internet Archive
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederInternetArchive/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederInternetArchive/thumbnail.png
store_app_id: metafeederinternetarchive
is_uncontrolled: false
tips:
before_install:
en_us: |
**Install order:** install **MetaGateway** first (its meta-core stores and
announces what this feeder finds).
**After installing this app:** in **MetaGateway**, add this feeder from the
dashboard's *Add a feeder* box — name `internetarchive-feeder`, URL `http://metafeederinternetarchive-feeder:8080`.
The gateway restarts itself: feeder discovery is startup-only.
No API key and no account: the feeder answers as soon as the gateway
discovers it. Pairs with **MetaListen** for a music client.
**Authentication:** the config UI is protected by Yundera's Authelia (OIDC).
# 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/metafeederinternetarchive/state
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metafeederinternetarchive-${domain}
webui-path: "/config"
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

@@ -0,0 +1,35 @@
# MetaFeeder · Internet Archive — Rationale
## What deviation / exception is being requested
`metafeederinternetarchive-feeder` runs `user: 0:0`.
## Why it is necessary
Carried over unchanged from the MetaFeeder listings this app is a sibling of, so the
feeder behaves exactly as they do. The MetaFeeder images write their state and redb
cache as the container user and some reach MetaCore's root-owned tree; a uid-1000
process is refused.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The feeder carries no
Caddy labels and publishes no host port, so it is reachable only from the `pcs` Docker
network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. Its only bind is its own state under
`/DATA/AppData/metafeederinternetarchive/`, declared in `x-compose-app.folders` and owned `$PUID:$PGID`.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for this image; the sibling MetaFeeder apps all
run `0:0`, and changing the runtime user here would make this listing the odd one out
for no measured benefit.
## Data protection
The feeder holds no user media. Its entire state lives under `/DATA/AppData/metafeederinternetarchive/`, which
is the unit Maison archives on uninstall and restores from backup.
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+179
View File
@@ -0,0 +1,179 @@
name: metafeederjamendo
# =============================================================================
# MetaFeeder · Jamendo — Creative-Commons music for MetaMesh.
#
# Opt-in: soft-skips (healthy, answers nothing) unless JAMENDO_CLIENT_ID is set.
#
# ⚠ Jamendo's non-commercial tier is a MONTHLY BUDGET (35,000 requests), not a
# per-second rate. Spend it and the upstream is gone until the calendar turns —
# a backoff does not recover it. The feeder tracks the budget itself; don't
# raise it here without reading its consts.rs.
#
# It needs, over the shared `pcs` network:
# - the **MetaGateway** app — which must list this feeder's URL
# (`http://metafeederjamendo-feeder:8080`) in its feeders config so it gets discovered.
# Discovery is startup-only, so restart the gateway afterwards.
#
# Everything is scoped under the `metafeederjamendo-*` prefix + the per-stack `metafeederjamendo` network +
# /DATA/AppData/metafeederjamendo/ data root, so it installs beside the other Meta* apps with
# zero collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metafeederjamendo-feeder — the feeder itself. The gateway reaches it directly on
# `pcs` for discovery/query/compute; the human sees it through the AppShield
# gate below.
# ---------------------------------------------------------------------------
metafeederjamendo-feeder:
image: ghcr.io/worph/meta-feeder-jamendo:1.1.2
cpu_shares: 30
container_name: metafeederjamendo-feeder
# Bytes tier: one mp32 derivative is buffered whole before hand-off.
# Swap disabled (memswap_limit == mem_limit) → OOM instead of thrash.
mem_limit: 512m
memswap_limit: 512m
hostname: metafeederjamendo-feeder
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
RUST_LOG: "info,meta_feeder=info,jamendo_feeder=info"
META_FEEDER_STATE_DIR: "/data/meta-feeder"
# Jamendo is opt-in. Blank → the feeder soft-skips. This image has no
# config-UI field for the client id yet, so it is read from the environment.
JAMENDO_CLIENT_ID: "${JAMENDO_CLIENT_ID:-}"
TZ: "$TZ"
volumes:
# Persist feeder config.json + redb cache so it survives recreate.
- ${DATA_ROOT:-/DATA}/AppData/metafeederjamendo/state:/data/meta-feeder:rw
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
networks:
- metafeederjamendo
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the feeder's config page behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). The gateway still reaches the feeder directly on `pcs`; index → /config.
# ---------------------------------------------------------------------------
metafeederjamendo:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metafeederjamendo
hostname: metafeederjamendo
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: "metafeederjamendo-feeder"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metafeederjamendo-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metafeederjamendo-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metafeederjamendo-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metafeederjamendo-feeder
networks:
- metafeederjamendo
- pcs
networks:
metafeederjamendo:
name: metafeederjamendo
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Media
developer: Aptero Team
main: metafeederjamendo
webui_port: 80
index: /config
description:
en_us: |
**Creative-Commons music for the MetaMesh network.**
MetaFeeder · Jamendo bridges **Jamendo**'s Creative-Commons catalogue into
MetaMesh as a *bytes* tier: tracks are fetched and seeded to the swarm.
**Opt-in.** It needs a free Jamendo client id; without one it stays healthy
and simply finds nothing. Jamendo's non-commercial tier is a **monthly
budget** (35,000 requests), not a rate limit — the feeder paces itself
against it.
**Requires:** **MetaGateway** — its meta-core stores the resolved records and
announces them to the swarm. The gateway must list this feeder in its config.
fr_fr: |
**La musique Creative Commons pour le réseau MetaMesh.**
MetaFeeder · Jamendo connecte le catalogue Creative Commons de **Jamendo**
à MetaMesh : les morceaux sont récupérés puis partagés au swarm.
**Sur activation** : nécessite un identifiant client Jamendo gratuit ; sans
lui, le feeder reste sain et ne trouve rien. Le quota Jamendo est **mensuel**
(35 000 requêtes). **Nécessite** l'app **MetaGateway**.
tagline:
en_us: Jamendo Creative-Commons music for MetaMesh (client id required)
fr_fr: Musique Creative Commons Jamendo pour MetaMesh (identifiant client requis)
title:
en_us: MetaFeeder · Jamendo
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederJamendo/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederJamendo/thumbnail.png
store_app_id: metafeederjamendo
is_uncontrolled: false
tips:
before_install:
en_us: |
**Install order:** install **MetaGateway** first (its meta-core stores and
announces what this feeder finds).
**After installing this app:** in **MetaGateway**, add this feeder from the
dashboard's *Add a feeder* box — name `jamendo-feeder`, URL `http://metafeederjamendo-feeder:8080`.
The gateway restarts itself: feeder discovery is startup-only.
**Client id:** register a free app at developer.jamendo.com, set
`JAMENDO_CLIENT_ID` in this app's environment, then restart the app.
Without it the feeder soft-skips. Pairs with **MetaListen**.
**Authentication:** the config UI is protected by Yundera's Authelia (OIDC).
# 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/metafeederjamendo/state
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metafeederjamendo-${domain}
webui-path: "/config"
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+35
View File
@@ -0,0 +1,35 @@
# MetaFeeder · Jamendo — Rationale
## What deviation / exception is being requested
`metafeederjamendo-feeder` runs `user: 0:0`.
## Why it is necessary
Carried over unchanged from the MetaFeeder listings this app is a sibling of, so the
feeder behaves exactly as they do. The MetaFeeder images write their state and redb
cache as the container user and some reach MetaCore's root-owned tree; a uid-1000
process is refused.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The feeder carries no
Caddy labels and publishes no host port, so it is reachable only from the `pcs` Docker
network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. Its only bind is its own state under
`/DATA/AppData/metafeederjamendo/`, declared in `x-compose-app.folders` and owned `$PUID:$PGID`.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for this image; the sibling MetaFeeder apps all
run `0:0`, and changing the runtime user here would make this listing the odd one out
for no measured benefit.
## Data protection
The feeder holds no user media. Its entire state lives under `/DATA/AppData/metafeederjamendo/`, which
is the unit Maison archives on uninstall and restores from backup.
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

@@ -0,0 +1,180 @@
name: metafeedermusicbrainz
# =============================================================================
# MetaFeeder · MusicBrainz — the MUSIC IDENTITY tier for MetaMesh.
#
# MusicBrainz names the *work*: album, artist, release group, cover art (via the
# Cover Art Archive) and the ids the audio tiers anchor on. Keyless, no config.
# It serves no audio and must not — pair it with a bytes tier
# (Internet Archive, Jamendo) or the delegated YouTube tier.
#
# ⚠ MusicBrainz publishes a hard 1 request/second ceiling, enforced per source
# IP — one misbehaving feeder blocks every peer behind the same NAT. The rate
# limiter is in the image; there is no knob here and there should not be.
#
# It needs, over the shared `pcs` network:
# - the **MetaGateway** app — which must list this feeder's URL
# (`http://metafeedermusicbrainz-feeder:8080`) in its feeders config so it gets discovered.
# Discovery is startup-only, so restart the gateway afterwards.
#
# Everything is scoped under the `metafeedermusicbrainz-*` prefix + the per-stack `metafeedermusicbrainz` network +
# /DATA/AppData/metafeedermusicbrainz/ data root, so it installs beside the other Meta* apps with
# zero collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metafeedermusicbrainz-feeder — the feeder itself. The gateway reaches it directly on
# `pcs` for discovery/query/compute; the human sees it through the AppShield
# gate below.
# ---------------------------------------------------------------------------
metafeedermusicbrainz-feeder:
image: ghcr.io/worph/meta-feeder-musicbrainz:1.1.2
cpu_shares: 30
container_name: metafeedermusicbrainz-feeder
# Identity tier: JSON in, cards out, nothing buffered. Swap disabled
# (memswap_limit == mem_limit) → OOM instead of thrash.
mem_limit: 256m
memswap_limit: 256m
hostname: metafeedermusicbrainz-feeder
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
RUST_LOG: "info,meta_feeder=info,musicbrainz_feeder=info"
META_FEEDER_STATE_DIR: "/data/meta-feeder"
TZ: "$TZ"
volumes:
# Persist feeder config.json + redb cache so it survives recreate.
- ${DATA_ROOT:-/DATA}/AppData/metafeedermusicbrainz/state:/data/meta-feeder:rw
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
networks:
- metafeedermusicbrainz
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the feeder's config page behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). The gateway still reaches the feeder directly on `pcs`; index → /config.
# ---------------------------------------------------------------------------
metafeedermusicbrainz:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metafeedermusicbrainz
hostname: metafeedermusicbrainz
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: "metafeedermusicbrainz-feeder"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metafeedermusicbrainz-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metafeedermusicbrainz-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metafeedermusicbrainz-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metafeedermusicbrainz-feeder
networks:
- metafeedermusicbrainz
- pcs
networks:
metafeedermusicbrainz:
name: metafeedermusicbrainz
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Media
developer: Aptero Team
main: metafeedermusicbrainz
webui_port: 80
index: /config
description:
en_us: |
**The music identity tier for MetaMesh.**
MetaFeeder · MusicBrainz names the *work* — album, artist, release group,
cover art from the Cover Art Archive, and the ids every audio tier anchors
on. Keyless and cheap.
It serves **no audio**. Pair it with a bytes tier — **MetaFeeder · Internet
Archive** or **MetaFeeder · Jamendo** — or with the delegated **MetaFeeder ·
YouTube**: recordings matched to one MusicBrainz work converge on a single
album page instead of fragmenting into one tile per spelling.
**Requires:** **MetaGateway** — its meta-core stores the resolved records and
announces them to the swarm. The gateway must list this feeder in its config.
fr_fr: |
**La couche d'identité musicale de MetaMesh.**
MetaFeeder · MusicBrainz nomme l'*œuvre* — album, artiste, groupe de sortie,
pochette (Cover Art Archive) et les identifiants sur lesquels s'ancrent les
couches audio. Sans clé.
Il ne sert **aucun audio** : à associer à **MetaFeeder · Internet Archive**,
**MetaFeeder · Jamendo** ou **MetaFeeder · YouTube**. **Nécessite** l'app
**MetaGateway**.
tagline:
en_us: MusicBrainz — the music identity tier for MetaMesh
fr_fr: MusicBrainz — la couche d'identité musicale de MetaMesh
title:
en_us: MetaFeeder · MusicBrainz
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederMusicBrainz/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederMusicBrainz/thumbnail.png
store_app_id: metafeedermusicbrainz
is_uncontrolled: false
tips:
before_install:
en_us: |
**Install order:** install **MetaGateway** first (its meta-core stores and
announces what this feeder finds).
**After installing this app:** in **MetaGateway**, add this feeder from the
dashboard's *Add a feeder* box — name `musicbrainz-feeder`, URL `http://metafeedermusicbrainz-feeder:8080`.
The gateway restarts itself: feeder discovery is startup-only.
No API key. For anything playable, also install a bytes tier
(**MetaFeeder · Internet Archive**, **MetaFeeder · Jamendo**) or
**MetaFeeder · YouTube**. Pairs with **MetaListen**.
**Authentication:** the config UI is protected by Yundera's Authelia (OIDC).
# 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/metafeedermusicbrainz/state
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metafeedermusicbrainz-${domain}
webui-path: "/config"
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+35
View File
@@ -0,0 +1,35 @@
# MetaFeeder · MusicBrainz — Rationale
## What deviation / exception is being requested
`metafeedermusicbrainz-feeder` runs `user: 0:0`.
## Why it is necessary
Carried over unchanged from the MetaFeeder listings this app is a sibling of, so the
feeder behaves exactly as they do. The MetaFeeder images write their state and redb
cache as the container user and some reach MetaCore's root-owned tree; a uid-1000
process is refused.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The feeder carries no
Caddy labels and publishes no host port, so it is reachable only from the `pcs` Docker
network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. Its only bind is its own state under
`/DATA/AppData/metafeedermusicbrainz/`, declared in `x-compose-app.folders` and owned `$PUID:$PGID`.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for this image; the sibling MetaFeeder apps all
run `0:0`, and changing the runtime user here would make this listing the odd one out
for no measured benefit.
## Data protection
The feeder holds no user media. Its entire state lives under `/DATA/AppData/metafeedermusicbrainz/`, which
is the unit Maison archives on uninstall and restores from backup.
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+179
View File
@@ -0,0 +1,179 @@
name: metafeederyoutube
# =============================================================================
# MetaFeeder · YouTube — the DELEGATED playback tier for MetaMesh.
#
# ⚠ No bytes are ever fetched, transported or seeded by this feeder. It resolves
# a reference — an 11-character video id — and playback happens on YouTube. That
# is the whole design, and it is why the feeder is cheap and why a dead upstream
# instance is a retry rather than a broken stream.
#
# Video ids are read through a POOL of public Piped instances, tried in order.
# Measured during the source study: of four instances, one 502'd, two
# 301-redirected away and one answered. The pool is the mitigation; do not pin
# it to a single hostname.
#
# It needs, over the shared `pcs` network:
# - the **MetaGateway** app — which must list this feeder's URL
# (`http://metafeederyoutube-feeder:8080`) in its feeders config so it gets discovered.
# Discovery is startup-only, so restart the gateway afterwards.
#
# Everything is scoped under the `metafeederyoutube-*` prefix + the per-stack `metafeederyoutube` network +
# /DATA/AppData/metafeederyoutube/ data root, so it installs beside the other Meta* apps with
# zero collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metafeederyoutube-feeder — the feeder itself. The gateway reaches it directly on
# `pcs` for discovery/query/compute; the human sees it through the AppShield
# gate below.
# ---------------------------------------------------------------------------
metafeederyoutube-feeder:
image: ghcr.io/worph/meta-feeder-youtube:1.1.1
cpu_shares: 30
container_name: metafeederyoutube-feeder
# Delegated tier: it moves an 11-character video id, never bytes.
# Swap disabled (memswap_limit == mem_limit) → OOM instead of thrash.
mem_limit: 256m
memswap_limit: 256m
hostname: metafeederyoutube-feeder
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
RUST_LOG: "info,meta_feeder=info,youtube_feeder=info"
META_FEEDER_STATE_DIR: "/data/meta-feeder"
TZ: "$TZ"
volumes:
# Persist feeder config.json + redb cache so it survives recreate.
- ${DATA_ROOT:-/DATA}/AppData/metafeederyoutube/state:/data/meta-feeder:rw
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
networks:
- metafeederyoutube
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the feeder's config page behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). The gateway still reaches the feeder directly on `pcs`; index → /config.
# ---------------------------------------------------------------------------
metafeederyoutube:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metafeederyoutube
hostname: metafeederyoutube
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: "metafeederyoutube-feeder"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metafeederyoutube-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metafeederyoutube-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metafeederyoutube-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metafeederyoutube-feeder
networks:
- metafeederyoutube
- pcs
networks:
metafeederyoutube:
name: metafeederyoutube
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Media
developer: Aptero Team
main: metafeederyoutube
webui_port: 80
index: /config
description:
en_us: |
**YouTube as a delegated playback tier for MetaMesh.**
MetaFeeder · YouTube resolves a *reference*, not a file: playback happens on
YouTube and **no bytes are ever transported or seeded**. That makes it the
cheapest way to make a catalogue playable — and the one tier that adds
nothing to the swarm.
Video ids are read through a pool of public **Piped** instances, rotating
past a dead one. Keyless, nothing to configure.
**Pairs with MetaFeeder · MusicBrainz**, which names the recording a
candidate has to match (±10 s runtime) before it is accepted.
**Requires:** **MetaGateway** — its meta-core stores the resolved records and
announces them to the swarm. The gateway must list this feeder in its config.
fr_fr: |
**YouTube comme couche de lecture déléguée pour MetaMesh.**
MetaFeeder · YouTube résout une *référence*, pas un fichier : la lecture a
lieu sur YouTube et **aucun octet n'est transporté ni partagé**. Les
identifiants vidéo sont lus via un pool d'instances **Piped** publiques.
Sans clé. **Va avec MetaFeeder · MusicBrainz**. **Nécessite** l'app
**MetaGateway**.
tagline:
en_us: YouTube references for MetaMesh — delegated playback, no bytes seeded
fr_fr: Références YouTube pour MetaMesh — lecture déléguée, aucun octet partagé
title:
en_us: MetaFeeder · YouTube
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederYouTube/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaFeederYouTube/thumbnail.png
store_app_id: metafeederyoutube
is_uncontrolled: false
tips:
before_install:
en_us: |
**Install order:** install **MetaGateway** first (its meta-core stores and
announces what this feeder finds).
**After installing this app:** in **MetaGateway**, add this feeder from the
dashboard's *Add a feeder* box — name `youtube-feeder`, URL `http://metafeederyoutube-feeder:8080`.
The gateway restarts itself: feeder discovery is startup-only.
No API key. For music, also install **MetaFeeder · MusicBrainz** — it
names the recording a YouTube candidate must match before it is accepted.
**Authentication:** the config UI is protected by Yundera's Authelia (OIDC).
# 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/metafeederyoutube/state
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metafeederyoutube-${domain}
webui-path: "/config"
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+35
View File
@@ -0,0 +1,35 @@
# MetaFeeder · YouTube — Rationale
## What deviation / exception is being requested
`metafeederyoutube-feeder` runs `user: 0:0`.
## Why it is necessary
Carried over unchanged from the MetaFeeder listings this app is a sibling of, so the
feeder behaves exactly as they do. The MetaFeeder images write their state and redb
cache as the container user and some reach MetaCore's root-owned tree; a uid-1000
process is refused.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The feeder carries no
Caddy labels and publishes no host port, so it is reachable only from the `pcs` Docker
network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. Its only bind is its own state under
`/DATA/AppData/metafeederyoutube/`, declared in `x-compose-app.folders` and owned `$PUID:$PGID`.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for this image; the sibling MetaFeeder apps all
run `0:0`, and changing the runtime user here would make this listing the odd one out
for no measured benefit.
## Data protection
The feeder holds no user media. Its entire state lives under `/DATA/AppData/metafeederyoutube/`, which
is the unit Maison archives on uninstall and restores from backup.
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

@@ -0,0 +1,174 @@
name: metapluginanimedetector
# =============================================================================
# MetaPlugin · Anime Detector — the `anime-detector` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `anime-detector` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginanimedetector-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginanimedetector-*` prefix + the per-stack `metapluginanimedetector`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginanimedetector-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginanimedetector-plugin:
image: ghcr.io/worph/metamesh-plugin-anime-detector:1.0.0
cpu_shares: 30
container_name: metapluginanimedetector-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginanimedetector-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Nothing to configure: this plugin takes its work from the /process
# payload and writes back through the caller. No key, no WebDAV.
TZ: "$TZ"
networks:
- metapluginanimedetector
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginanimedetector:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginanimedetector
hostname: metapluginanimedetector
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: "metapluginanimedetector-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginanimedetector-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginanimedetector-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginanimedetector-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginanimedetector-plugin
networks:
- metapluginanimedetector
- pcs
networks:
metapluginanimedetector:
name: metapluginanimedetector
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginanimedetector
webui_port: 80
index: /manifest
description:
en_us: |
**The `anime-detector` enrichment plugin for MetaMesh, as its own app.**
Flags anime content from release keywords, Japanese text in the title and the audio tracks present.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginanimedetector-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `anime-detector`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `anime-detector` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `anime-detector` de MetaMesh, en app autonome.**
Détecte les contenus anime à partir des mots-clés de release, du texte japonais dans le titre et des pistes audio présentes.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginanimedetector-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Detects anime content from keywords, Japanese text and audio
fr_fr: Détecte les contenus anime (mots-clés, texte japonais, audio)
title:
en_us: MetaPlugin · Anime Detector
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginAnimeDetector/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginAnimeDetector/thumbnail.png
store_app_id: metapluginanimedetector
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `anime-detector` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `anime-detector` running and
addressable at `http://metapluginanimedetector-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginanimedetector-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+42
View File
@@ -0,0 +1,42 @@
# MetaPlugin · Anime Detector — Rationale
## What deviation / exception is being requested
`metapluginanimedetector-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+194
View File
@@ -0,0 +1,194 @@
name: metapluginffmpeg
# =============================================================================
# MetaPlugin · FFmpeg — the `ffmpeg` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `ffmpeg` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginffmpeg-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginffmpeg-*` prefix + the per-stack `metapluginffmpeg`
# network + /DATA/AppData/metapluginffmpeg/ data root, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginffmpeg-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginffmpeg-plugin:
image: ghcr.io/worph/metamesh-plugin-ffmpeg:1.0.0
cpu_shares: 30
container_name: metapluginffmpeg-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 512m
memswap_limit: 512m
hostname: metapluginffmpeg-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
volumes:
# Plugin cache. Persisted so a recreate does not redo work that is
# expensive to redo; safe to delete, it rebuilds.
- ${DATA_ROOT:-/DATA}/AppData/metapluginffmpeg/cache:/cache:rw
networks:
- metapluginffmpeg
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginffmpeg:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginffmpeg
hostname: metapluginffmpeg
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: "metapluginffmpeg-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginffmpeg-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginffmpeg-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginffmpeg-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginffmpeg-plugin
networks:
- metapluginffmpeg
- pcs
networks:
metapluginffmpeg:
name: metapluginffmpeg
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginffmpeg
webui_port: 80
index: /manifest
description:
en_us: |
**The `ffmpeg` enrichment plugin for MetaMesh, as its own app.**
Probes video, audio and subtitle streams with FFprobe and writes the `stream/*` keys — codec, resolution, duration, per-track language.
Its probe is the *authoritative* source for anything a filename can only
guess at — `real-probe > title-guess`.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginffmpeg-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `ffmpeg`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `ffmpeg` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `ffmpeg` de MetaMesh, en app autonome.**
Sonde les flux vidéo, audio et sous-titres avec FFprobe et écrit les clés `stream/*` — codec, résolution, durée, langue par piste.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginffmpeg-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: FFprobe stream metadata for MetaMesh records
fr_fr: Métadonnées de flux FFprobe pour les enregistrements MetaMesh
title:
en_us: MetaPlugin · FFmpeg
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFFmpeg/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFFmpeg/thumbnail.png
store_app_id: metapluginffmpeg
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `ffmpeg` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `ffmpeg` running and
addressable at `http://metapluginffmpeg-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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/metapluginffmpeg/cache
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginffmpeg-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+44
View File
@@ -0,0 +1,44 @@
# MetaPlugin · FFmpeg — Rationale
## What deviation / exception is being requested
`metapluginffmpeg-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`.
Its only bind is its own cache under `/DATA/AppData/metapluginffmpeg/`, declared in
`x-compose-app.folders` and owned `$PUID:$PGID`. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. Its only state is a rebuildable cache under
`/DATA/AppData/metapluginffmpeg/`, which is the unit Maison archives on uninstall.
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+192
View File
@@ -0,0 +1,192 @@
name: metapluginfileinfo
# =============================================================================
# MetaPlugin · File Info — the `file-info` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `file-info` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginfileinfo-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginfileinfo-*` prefix + the per-stack `metapluginfileinfo`
# network + /DATA/AppData/metapluginfileinfo/ data root, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginfileinfo-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginfileinfo-plugin:
image: ghcr.io/worph/metamesh-plugin-file-info:1.0.0
cpu_shares: 30
container_name: metapluginfileinfo-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginfileinfo-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
volumes:
# Plugin cache. Persisted so a recreate does not redo work that is
# expensive to redo; safe to delete, it rebuilds.
- ${DATA_ROOT:-/DATA}/AppData/metapluginfileinfo/cache:/cache:rw
networks:
- metapluginfileinfo
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginfileinfo:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginfileinfo
hostname: metapluginfileinfo
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: "metapluginfileinfo-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginfileinfo-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginfileinfo-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginfileinfo-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginfileinfo-plugin
networks:
- metapluginfileinfo
- pcs
networks:
metapluginfileinfo:
name: metapluginfileinfo
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginfileinfo
webui_port: 80
index: /manifest
description:
en_us: |
**The `file-info` enrichment plugin for MetaMesh, as its own app.**
Extracts basic file information — type, MIME, size.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginfileinfo-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `file-info`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `file-info` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `file-info` de MetaMesh, en app autonome.**
Extrait les informations de base d'un fichier — type, MIME, taille.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginfileinfo-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: File type, MIME and size for MetaMesh records
fr_fr: Type, MIME et taille de fichier pour les enregistrements MetaMesh
title:
en_us: MetaPlugin · File Info
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFileInfo/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFileInfo/thumbnail.png
store_app_id: metapluginfileinfo
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `file-info` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `file-info` running and
addressable at `http://metapluginfileinfo-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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/metapluginfileinfo/cache
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginfileinfo-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+44
View File
@@ -0,0 +1,44 @@
# MetaPlugin · File Info — Rationale
## What deviation / exception is being requested
`metapluginfileinfo-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`.
Its only bind is its own cache under `/DATA/AppData/metapluginfileinfo/`, declared in
`x-compose-app.folders` and owned `$PUID:$PGID`. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. Its only state is a rebuildable cache under
`/DATA/AppData/metapluginfileinfo/`, which is the unit Maison archives on uninstall.
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@@ -0,0 +1,186 @@
name: metapluginfilenameparser
# =============================================================================
# MetaPlugin · Filename Parser — the `filename-parser` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ WHAT CONSUMES IT. A MetaFeeder's enrichment sidecar slot: set
# `FILENAME_PARSER_URL` on the feeder to `http://metapluginfilenameparser-plugin:8080` and it uses this
# shared instance instead of its own bundled copy. (The feeder SDK otherwise
# DERIVES the sidecar URL from its own hostname, so the explicit variable is
# what makes sharing possible at all.)
# **NOT MetaSort**: MetaSort starts its own plugin containers from its
# `plugins.yml` (`image:`) and has no URL slot, so installing this app does
# not add a plugin to MetaSort.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginfilenameparser-*` prefix + the per-stack `metapluginfilenameparser`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginfilenameparser-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginfilenameparser-plugin:
image: ghcr.io/worph/metamesh-plugin-filename-parser:1.0.3
cpu_shares: 30
container_name: metapluginfilenameparser-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginfilenameparser-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# ⚠ CONSUMER-SPECIFIC, and the one reason not to share a single instance
# between a gateway feeder and MetaSort. These are title *guesses*
# (quality / codec / languages). A gateway feeder has no real file and
# wants them (`true`); MetaSort probes the real stream and must NOT have
# them, or a guessed `languages/<iso>` member pollutes the probed set.
# Blank = off = the plugin's own default, which is MetaSort's semantics.
EMIT_TITLE_TAGS: "${EMIT_TITLE_TAGS:-}"
TZ: "$TZ"
networks:
- metapluginfilenameparser
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginfilenameparser:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginfilenameparser
hostname: metapluginfilenameparser
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: "metapluginfilenameparser-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginfilenameparser-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginfilenameparser-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginfilenameparser-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginfilenameparser-plugin
networks:
- metapluginfilenameparser
- pcs
networks:
metapluginfilenameparser:
name: metapluginfilenameparser
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginfilenameparser
webui_port: 80
index: /manifest
description:
en_us: |
**The `filename-parser` enrichment plugin for MetaMesh, as its own app.**
Parses a release filename into title, season, episode, year and quality.
⚠ **One instance cannot serve both a gateway feeder and MetaSort.**
`EMIT_TITLE_TAGS` is a process-level switch, and the two want opposite
values — see this app's environment. Install a second copy, or keep the
feeder's bundled sidecar, rather than flipping it back and forth.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginfilenameparser-plugin:8080`.
**Who can use it today:** a **MetaFeeder** app, through its
`FILENAME_PARSER_URL` setting — one shared copy instead of one bundled per feeder.
**Not a MetaSort plugin.** MetaSort starts its own plugin containers from
`plugins.yml` and has no slot for a plugin URL.
fr_fr: |
**Le plugin d'enrichissement `filename-parser` de MetaMesh, en app autonome.**
Analyse un nom de fichier de release en titre, saison, épisode, année et qualité.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginfilenameparser-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Filename → title, season, episode, year, quality
fr_fr: Nom de fichier → titre, saison, épisode, année, qualité
title:
en_us: MetaPlugin · Filename Parser
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFilenameParser/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFilenameParser/thumbnail.png
store_app_id: metapluginfilenameparser
is_uncontrolled: false
tips:
before_install:
en_us: |
**What this is for:** running the `filename-parser` plugin **once, shared**, instead of
one bundled copy inside every app that needs it.
**After installing:** set `FILENAME_PARSER_URL` to `http://metapluginfilenameparser-plugin:8080` in the
MetaFeeder app that should use it, then restart that feeder. Left unset,
the feeder keeps using its own bundled copy.
**It is not a MetaSort plugin.** MetaSort spawns its own plugin containers
from `plugins.yml` (`image:`), with no URL slot — installing this app does
not add `filename-parser` to MetaSort.
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginfilenameparser-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

@@ -0,0 +1,42 @@
# MetaPlugin · Filename Parser — Rationale
## What deviation / exception is being requested
`metapluginfilenameparser-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+189
View File
@@ -0,0 +1,189 @@
name: metapluginfullhash
# =============================================================================
# MetaPlugin · Full Hash — the `full-hash` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `full-hash` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginfullhash-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginfullhash-*` prefix + the per-stack `metapluginfullhash`
# network + /DATA/AppData/metapluginfullhash/ data root, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginfullhash-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: background.
# ---------------------------------------------------------------------------
metapluginfullhash-plugin:
image: ghcr.io/worph/metamesh-plugin-fullhash:1.0.0
cpu_shares: 30
container_name: metapluginfullhash-plugin
# 2 GB, swap disabled (memswap_limit == mem_limit) → OOM instead of
# swap. Sized for whole-file hashing of large media over WebDAV; this
# is the one plugin that genuinely needs it.
mem_limit: 2g
memswap_limit: 2g
hostname: metapluginfullhash-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Nothing to configure: this plugin takes its work from the /process
# payload and writes back through the caller. No key, no WebDAV.
TZ: "$TZ"
volumes:
# Plugin cache. Persisted so a recreate does not redo work that is
# expensive to redo; safe to delete, it rebuilds.
- ${DATA_ROOT:-/DATA}/AppData/metapluginfullhash/cache:/cache:rw
networks:
- metapluginfullhash
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginfullhash:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginfullhash
hostname: metapluginfullhash
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: "metapluginfullhash-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginfullhash-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginfullhash-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginfullhash-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginfullhash-plugin
networks:
- metapluginfullhash
- pcs
networks:
metapluginfullhash:
name: metapluginfullhash
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginfullhash
webui_port: 80
index: /manifest
description:
en_us: |
**The `full-hash` enrichment plugin for MetaMesh, as its own app.**
Hashes a whole file — high-performance parallel hashing with optimised I/O — and writes the SHA-256 CID that duplicate detection keys on.
⚠ **The expensive one.** It reads every byte of every file it is given,
over WebDAV, on the background queue. The 2 GB limit and the 5-minute
timeout are sized for large media files, not padding.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginfullhash-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `full-hash`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `full-hash` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `full-hash` de MetaMesh, en app autonome.**
Hache un fichier entier — hachage parallèle haute performance — et écrit le CID SHA-256 sur lequel la détection de doublons s'appuie.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginfullhash-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Whole-file SHA-256 hashing for MetaMesh (duplicate detection)
fr_fr: Hachage SHA-256 de fichiers entiers pour MetaMesh (détection de doublons)
title:
en_us: MetaPlugin · Full Hash
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFullHash/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginFullHash/thumbnail.png
store_app_id: metapluginfullhash
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `full-hash` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `full-hash` running and
addressable at `http://metapluginfullhash-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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/metapluginfullhash/cache
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginfullhash-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+44
View File
@@ -0,0 +1,44 @@
# MetaPlugin · Full Hash — Rationale
## What deviation / exception is being requested
`metapluginfullhash-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`.
Its only bind is its own cache under `/DATA/AppData/metapluginfullhash/`, declared in
`x-compose-app.folders` and owned `$PUID:$PGID`. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. Its only state is a rebuildable cache under
`/DATA/AppData/metapluginfullhash/`, which is the unit Maison archives on uninstall.
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

@@ -0,0 +1,183 @@
name: metapluginjellyfinnfo
# =============================================================================
# MetaPlugin · Jellyfin NFO — the `jellyfin-nfo` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `jellyfin-nfo` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginjellyfinnfo-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginjellyfinnfo-*` prefix + the per-stack `metapluginjellyfinnfo`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginjellyfinnfo-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginjellyfinnfo-plugin:
image: ghcr.io/worph/metamesh-plugin-jellyfin-nfo:1.0.0
cpu_shares: 30
container_name: metapluginjellyfinnfo-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginjellyfinnfo-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
networks:
- metapluginjellyfinnfo
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginjellyfinnfo:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginjellyfinnfo
hostname: metapluginjellyfinnfo
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: "metapluginjellyfinnfo-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginjellyfinnfo-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginjellyfinnfo-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginjellyfinnfo-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginjellyfinnfo-plugin
networks:
- metapluginjellyfinnfo
- pcs
networks:
metapluginjellyfinnfo:
name: metapluginjellyfinnfo
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginjellyfinnfo
webui_port: 80
index: /manifest
description:
en_us: |
**The `jellyfin-nfo` enrichment plugin for MetaMesh, as its own app.**
Reads the `.nfo` files a Jellyfin or Kodi library already carries and lifts their ids — `imdbid`, `tmdbid`, `anidbid` — onto the MetaMesh record.
The cheapest identity source there is: the work was already done by
whatever wrote the NFO, so it costs no API call and no key.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginjellyfinnfo-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `jellyfin-nfo`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `jellyfin-nfo` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `jellyfin-nfo` de MetaMesh, en app autonome.**
Lit les fichiers `.nfo` d'une bibliothèque Jellyfin ou Kodi et reporte leurs identifiants — `imdbid`, `tmdbid`, `anidbid` — sur l'enregistrement MetaMesh.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginjellyfinnfo-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Reads Jellyfin/Kodi .nfo ids into MetaMesh records
fr_fr: Importe les identifiants .nfo Jellyfin/Kodi dans MetaMesh
title:
en_us: MetaPlugin · Jellyfin NFO
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginJellyfinNFO/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginJellyfinNFO/thumbnail.png
store_app_id: metapluginjellyfinnfo
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `jellyfin-nfo` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `jellyfin-nfo` running and
addressable at `http://metapluginjellyfinnfo-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginjellyfinnfo-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+42
View File
@@ -0,0 +1,42 @@
# MetaPlugin · Jellyfin NFO — Rationale
## What deviation / exception is being requested
`metapluginjellyfinnfo-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+176
View File
@@ -0,0 +1,176 @@
name: metapluginlanguage
# =============================================================================
# MetaPlugin · Language — the `language` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `language` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginlanguage-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginlanguage-*` prefix + the per-stack `metapluginlanguage`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginlanguage-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginlanguage-plugin:
image: ghcr.io/worph/metamesh-plugin-language:1.0.1
cpu_shares: 30
container_name: metapluginlanguage-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginlanguage-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Nothing to configure: this plugin takes its work from the /process
# payload and writes back through the caller. No key, no WebDAV.
TZ: "$TZ"
networks:
- metapluginlanguage
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginlanguage:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginlanguage
hostname: metapluginlanguage
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: "metapluginlanguage-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginlanguage-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginlanguage-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginlanguage-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginlanguage-plugin
networks:
- metapluginlanguage
- pcs
networks:
metapluginlanguage:
name: metapluginlanguage
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginlanguage
webui_port: 80
index: /manifest
description:
en_us: |
**The `language` enrichment plugin for MetaMesh, as its own app.**
Aggregates the audio-track languages a probe found into the record's `languages/<lang3>` key-set.
Runs **after** a probe: it aggregates what **MetaPlugin · FFmpeg** found,
so install that one too or there is nothing to aggregate.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginlanguage-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `language`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `language` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `language` de MetaMesh, en app autonome.**
Agrège les langues des pistes audio détectées dans l'ensemble de clés `languages/<lang3>` de l'enregistrement.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginlanguage-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Audio-track languages as a MetaMesh key-set
fr_fr: Langues des pistes audio en ensemble de clés MetaMesh
title:
en_us: MetaPlugin · Language
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginLanguage/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginLanguage/thumbnail.png
store_app_id: metapluginlanguage
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `language` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `language` running and
addressable at `http://metapluginlanguage-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginlanguage-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+42
View File
@@ -0,0 +1,42 @@
# MetaPlugin · Language — Rationale
## What deviation / exception is being requested
`metapluginlanguage-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

@@ -0,0 +1,187 @@
name: metapluginopensubtitles
# =============================================================================
# MetaPlugin · OpenSubtitles — the `opensubtitles` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ WHAT CONSUMES IT. A MetaFeeder's enrichment sidecar slot: set
# `OPENSUBTITLES_PLUGIN_URL` on the feeder to `http://metapluginopensubtitles-plugin:8080` and it uses this
# shared instance instead of its own bundled copy. (The feeder SDK otherwise
# DERIVES the sidecar URL from its own hostname, so the explicit variable is
# what makes sharing possible at all.)
# **NOT MetaSort**: MetaSort starts its own plugin containers from its
# `plugins.yml` (`image:`) and has no URL slot, so installing this app does
# not add a plugin to MetaSort.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginopensubtitles-*` prefix + the per-stack `metapluginopensubtitles`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginopensubtitles-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: background.
# ---------------------------------------------------------------------------
metapluginopensubtitles-plugin:
image: ghcr.io/worph/metamesh-plugin-opensubtitle:1.0.0
cpu_shares: 30
container_name: metapluginopensubtitles-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginopensubtitles-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
networks:
- metapluginopensubtitles
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginopensubtitles:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginopensubtitles
hostname: metapluginopensubtitles
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: "metapluginopensubtitles-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginopensubtitles-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginopensubtitles-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginopensubtitles-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginopensubtitles-plugin
networks:
- metapluginopensubtitles
- pcs
networks:
metapluginopensubtitles:
name: metapluginopensubtitles
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginopensubtitles
webui_port: 80
index: /manifest
description:
en_us: |
**The `opensubtitles` enrichment plugin for MetaMesh, as its own app.**
Fetches subtitle sidecars from **OpenSubtitles** for videos that have none, and links them to the video.
**Bring your own account.** The plugin stores no credentials: the caller
POSTs its own API key, username and password to `/configure` before each
use. Without them it soft-skips.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginopensubtitles-plugin:8080`.
**Who can use it today:** a **MetaFeeder** app, through its
`OPENSUBTITLES_PLUGIN_URL` setting — one shared copy instead of one bundled per feeder.
**Not a MetaSort plugin.** MetaSort starts its own plugin containers from
`plugins.yml` and has no slot for a plugin URL.
fr_fr: |
**Le plugin d'enrichissement `opensubtitles` de MetaMesh, en app autonome.**
Récupère des sous-titres depuis **OpenSubtitles** pour les vidéos qui n'en ont pas et les relie à la vidéo.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginopensubtitles-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Fetches missing subtitles from OpenSubtitles
fr_fr: Récupère les sous-titres manquants depuis OpenSubtitles
title:
en_us: MetaPlugin · OpenSubtitles
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginOpenSubtitles/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginOpenSubtitles/thumbnail.png
store_app_id: metapluginopensubtitles
is_uncontrolled: false
tips:
before_install:
en_us: |
**What this is for:** running the `opensubtitles` plugin **once, shared**, instead of
one bundled copy inside every app that needs it.
**After installing:** set `OPENSUBTITLES_PLUGIN_URL` to `http://metapluginopensubtitles-plugin:8080` in the
MetaFeeder app that should use it, then restart that feeder. Left unset,
the feeder keeps using its own bundled copy.
**It is not a MetaSort plugin.** MetaSort spawns its own plugin containers
from `plugins.yml` (`image:`), with no URL slot — installing this app does
not add `opensubtitles` to MetaSort.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginopensubtitles-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

+42
View File
@@ -0,0 +1,42 @@
# MetaPlugin · OpenSubtitles — Rationale
## What deviation / exception is being requested
`metapluginopensubtitles-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

@@ -0,0 +1,183 @@
name: metapluginstillextractor
# =============================================================================
# MetaPlugin · Still Extractor — the `still-extractor` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `still-extractor` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginstillextractor-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginstillextractor-*` prefix + the per-stack `metapluginstillextractor`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginstillextractor-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: background.
# ---------------------------------------------------------------------------
metapluginstillextractor-plugin:
image: ghcr.io/worph/metamesh-plugin-still-extractor:1.0.1
cpu_shares: 30
container_name: metapluginstillextractor-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 512m
memswap_limit: 512m
hostname: metapluginstillextractor-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
networks:
- metapluginstillextractor
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginstillextractor:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginstillextractor
hostname: metapluginstillextractor
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: "metapluginstillextractor-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginstillextractor-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginstillextractor-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginstillextractor-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginstillextractor-plugin
networks:
- metapluginstillextractor
- pcs
networks:
metapluginstillextractor:
name: metapluginstillextractor
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginstillextractor
webui_port: 80
index: /manifest
description:
en_us: |
**The `still-extractor` enrichment plugin for MetaMesh, as its own app.**
Grabs a representative frame out of a video and stores it as the record's `still` — a best-effort episode thumbnail.
It **never** writes `poster`: a still is a frame, not artwork, and the two
are different keys on purpose. One HTTP seek + decode per candidate offset.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginstillextractor-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `still-extractor`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `still-extractor` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `still-extractor` de MetaMesh, en app autonome.**
Extrait une image représentative d'une vidéo et l'enregistre comme `still` — une vignette d'épisode au mieux.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginstillextractor-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: A representative frame as each video's still thumbnail
fr_fr: Une image représentative comme vignette de chaque vidéo
title:
en_us: MetaPlugin · Still Extractor
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginStillExtractor/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginStillExtractor/thumbnail.png
store_app_id: metapluginstillextractor
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `still-extractor` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `still-extractor` running and
addressable at `http://metapluginstillextractor-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginstillextractor-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

@@ -0,0 +1,42 @@
# MetaPlugin · Still Extractor — Rationale
## What deviation / exception is being requested
`metapluginstillextractor-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+184
View File
@@ -0,0 +1,184 @@
name: metapluginsubtitle
# =============================================================================
# MetaPlugin · Subtitle — the `subtitle` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `subtitle` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginsubtitle-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginsubtitle-*` prefix + the per-stack `metapluginsubtitle`
# network, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginsubtitle-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: fast.
# ---------------------------------------------------------------------------
metapluginsubtitle-plugin:
image: ghcr.io/worph/metamesh-plugin-subtitle:1.0.0
cpu_shares: 30
container_name: metapluginsubtitle-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 256m
memswap_limit: 256m
hostname: metapluginsubtitle-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
networks:
- metapluginsubtitle
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginsubtitle:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginsubtitle
hostname: metapluginsubtitle
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: "metapluginsubtitle-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginsubtitle-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginsubtitle-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginsubtitle-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginsubtitle-plugin
networks:
- metapluginsubtitle
- pcs
networks:
metapluginsubtitle:
name: metapluginsubtitle
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginsubtitle
webui_port: 80
index: /manifest
description:
en_us: |
**The `subtitle` enrichment plugin for MetaMesh, as its own app.**
Links subtitle sidecar files sitting next to a video to that video, and detects each one's language.
It only *links* what is already on disk. For subtitles embedded in the
container see **MetaPlugin · Subtitle Extractor**; to fetch missing ones
see **MetaPlugin · OpenSubtitles**.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginsubtitle-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `subtitle`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `subtitle` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `subtitle` de MetaMesh, en app autonome.**
Associe les fichiers de sous-titres posés à côté d'une vidéo à cette vidéo et détecte la langue de chacun.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginsubtitle-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Links subtitle sidecars to their video and detects language
fr_fr: Associe les sous-titres à leur vidéo et détecte la langue
title:
en_us: MetaPlugin · Subtitle
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginSubtitle/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginSubtitle/thumbnail.png
store_app_id: metapluginsubtitle
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `subtitle` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `subtitle` running and
addressable at `http://metapluginsubtitle-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginsubtitle-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+42
View File
@@ -0,0 +1,42 @@
# MetaPlugin · Subtitle — Rationale
## What deviation / exception is being requested
`metapluginsubtitle-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`. It binds no host path at all. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. It is stateless: there is nothing to back up, and
a reinstall loses nothing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

@@ -0,0 +1,195 @@
name: metapluginsubtitleextractor
# =============================================================================
# MetaPlugin · Subtitle Extractor — the `subtitle-extractor` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ NOTHING IN THE FAMILY CONSUMES IT BY URL YET — install it knowing that.
# MetaSort starts its own plugin containers from `plugins.yml` (`image:`) and
# has no URL slot. The feeder SDK has exactly three sidecar slots
# (`FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL`, `OPENSUBTITLES_PLUGIN_URL`) and
# `subtitle-extractor` is not one of them. What this app gives you today is the plugin
# running and addressable at `http://metapluginsubtitleextractor-plugin:8080` for anything that
# speaks the four-verb plugin contract — your own caller, a probe, a future
# consumer. It is packaging ahead of the consumer, on purpose.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metapluginsubtitleextractor-*` prefix + the per-stack `metapluginsubtitleextractor`
# network + /DATA/AppData/metapluginsubtitleextractor/ data root, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metapluginsubtitleextractor-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: background.
# ---------------------------------------------------------------------------
metapluginsubtitleextractor-plugin:
image: ghcr.io/worph/metamesh-plugin-subtitle-extractor:1.0.0
cpu_shares: 30
container_name: metapluginsubtitleextractor-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 512m
memswap_limit: 512m
hostname: metapluginsubtitleextractor-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
volumes:
# Plugin cache. Persisted so a recreate does not redo work that is
# expensive to redo; safe to delete, it rebuilds.
- ${DATA_ROOT:-/DATA}/AppData/metapluginsubtitleextractor/cache:/cache:rw
networks:
- metapluginsubtitleextractor
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metapluginsubtitleextractor:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metapluginsubtitleextractor
hostname: metapluginsubtitleextractor
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: "metapluginsubtitleextractor-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metapluginsubtitleextractor-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metapluginsubtitleextractor-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metapluginsubtitleextractor-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metapluginsubtitleextractor-plugin
networks:
- metapluginsubtitleextractor
- pcs
networks:
metapluginsubtitleextractor:
name: metapluginsubtitleextractor
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metapluginsubtitleextractor
webui_port: 80
index: /manifest
description:
en_us: |
**The `subtitle-extractor` enrichment plugin for MetaMesh, as its own app.**
Extracts text subtitle tracks embedded inside a video container, writes them out as sidecars and links them back to the video.
⚠ **Known cost:** one full read of the video per run. It is on the
background queue for that reason, and on a peer that also serves playback
that read competes with streaming.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metapluginsubtitleextractor-plugin:8080`.
⚠ **No MetaMesh app calls it by URL yet.** MetaSort starts its own plugin
containers from `plugins.yml`, and the MetaFeeder SDK's sidecar slots cover
only filename-parser, TMDB and OpenSubtitles — not `subtitle-extractor`. Installing this
gives you the plugin running and addressable on the `pcs` network for
anything that speaks the plugin contract; it does **not** add `subtitle-extractor` to
MetaSort.
fr_fr: |
**Le plugin d'enrichissement `subtitle-extractor` de MetaMesh, en app autonome.**
Extrait les pistes de sous-titres texte intégrées à un conteneur vidéo, les écrit en fichiers séparés et les relie à la vidéo.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metapluginsubtitleextractor-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: Extracts embedded text subtitles out of video containers
fr_fr: Extrait les sous-titres texte intégrés aux conteneurs vidéo
title:
en_us: MetaPlugin · Subtitle Extractor
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginSubtitleExtractor/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginSubtitleExtractor/thumbnail.png
store_app_id: metapluginsubtitleextractor
is_uncontrolled: false
tips:
before_install:
en_us: |
⚠ **Read this first: nothing calls `subtitle-extractor` by URL yet.** MetaSort spawns
its own plugin containers from `plugins.yml` (`image:`) and has no URL
slot, and the MetaFeeder SDK's three sidecar slots are filename-parser,
TMDB and OpenSubtitles only. Install this if you want `subtitle-extractor` running and
addressable at `http://metapluginsubtitleextractor-plugin:8080` for your own caller — not
expecting it to light up MetaSort or a MetaFeeder on its own.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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/metapluginsubtitleextractor/cache
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metapluginsubtitleextractor-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

@@ -0,0 +1,44 @@
# MetaPlugin · Subtitle Extractor — Rationale
## What deviation / exception is being requested
`metapluginsubtitleextractor-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`.
Its only bind is its own cache under `/DATA/AppData/metapluginsubtitleextractor/`, declared in
`x-compose-app.folders` and owned `$PUID:$PGID`. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. Its only state is a rebuildable cache under
`/DATA/AppData/metapluginsubtitleextractor/`, which is the unit Maison archives on uninstall.
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+199
View File
@@ -0,0 +1,199 @@
name: metaplugintmdb
# =============================================================================
# MetaPlugin · TMDB — the `tmdb` enrichment plugin, as a standalone app.
#
# A MetaMesh enrichment plugin is a HEADLESS worker: it speaks the four-verb
# plugin HTTP contract (`/manifest`, `/health`, `/configure`, `/process`) and
# nothing else. There is no dashboard — the web UI on this tile is the plugin's
# own `/manifest`, which is how you confirm it is alive and what version it is.
#
# ⚠ WHAT CONSUMES IT. A MetaFeeder's enrichment sidecar slot: set
# `TMDB_PLUGIN_URL` on the feeder to `http://metaplugintmdb-plugin:8080` and it uses this
# shared instance instead of its own bundled copy. (The feeder SDK otherwise
# DERIVES the sidecar URL from its own hostname, so the explicit variable is
# what makes sharing possible at all.)
# **NOT MetaSort**: MetaSort starts its own plugin containers from its
# `plugins.yml` (`image:`) and has no URL slot, so installing this app does
# not add a plugin to MetaSort.
#
# ⚠ IT HOLDS NO CREDENTIALS. The caller POSTs `/configure` with its own key
# (TMDB token, OpenSubtitles account) before use. Two consumers sharing one
# instance are last-writer-wins on that config — same key, no problem; different
# keys, install a second copy.
#
# Everything is scoped under the `metaplugintmdb-*` prefix + the per-stack `metaplugintmdb`
# network + /DATA/AppData/metaplugintmdb/ data root, so it installs beside the other Meta* apps with zero
# collisions.
# =============================================================================
services:
# ---------------------------------------------------------------------------
# metaplugintmdb-plugin — the plugin. Listens on 8080; joined to `pcs` so any
# consumer on the box can reach it by name. Default queue: background.
# ---------------------------------------------------------------------------
metaplugintmdb-plugin:
image: ghcr.io/worph/metamesh-plugin-tmdb:1.0.3
cpu_shares: 30
container_name: metaplugintmdb-plugin
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the limits the bundled sidecars run with.
mem_limit: 384m
memswap_limit: 384m
hostname: metaplugintmdb-plugin
restart: unless-stopped
user: "0:0"
expose:
- 8080
environment:
# Where the plugin writes the artefacts it produces (posters, stills,
# extracted subtitles) — the shared MetaCore's WebDAV over `pcs`.
# LOAD-BEARING: the published images resolve WebDAV from this var, and
# without it they log "WebDAV client unavailable" and silently write
# nothing. Every record then fails MetaWatch's poster gate.
WEBDAV_URL: "http://metacore-app:9000/webdav"
TZ: "$TZ"
volumes:
# Plugin cache. Persisted so a recreate does not redo work that is
# expensive to redo; safe to delete, it rebuilds.
- ${DATA_ROOT:-/DATA}/AppData/metaplugintmdb/cache:/cache:rw
networks:
- metaplugintmdb
- pcs
# ---------------------------------------------------------------------------
# AppShield gate — the plugin's `/manifest` behind Yundera OIDC SSO.
# container_name == subdomain prefix (the auth-registrar derives the client_id
# from it). Consumers still reach the plugin directly on `pcs`; this gate
# exists so `/process` and `/configure` are never reachable from the internet
# without a session.
# ---------------------------------------------------------------------------
metaplugintmdb:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metaplugintmdb
hostname: metaplugintmdb
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: "metaplugintmdb-plugin"
BACKEND_PORT: "8080"
LISTEN_PORT: "80"
labels:
caddy_0: metaplugintmdb-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metaplugintmdb-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metaplugintmdb-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metaplugintmdb-plugin
networks:
- metaplugintmdb
- pcs
networks:
metaplugintmdb:
name: metaplugintmdb
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Developer
developer: Aptero Team
main: metaplugintmdb
webui_port: 80
index: /manifest
description:
en_us: |
**The `tmdb` enrichment plugin for MetaMesh, as its own app.**
Looks a title up on **The Movie Database** and writes the identity a record is anchored by — `tmdbid`, `imdbid`, localized titles — plus the poster and backdrop artwork.
**Bring your own key.** The plugin stores no credentials: the caller
POSTs its own TMDB token to `/configure` before each use.
**Requires MetaCore** — the artwork is written to its WebDAV, and a record
without a poster is hidden by MetaWatch's quality gate.
**Headless.** It answers the plugin HTTP contract — `/manifest`, `/health`,
`/configure`, `/process` — and has no dashboard; this tile opens its
`/manifest`. Point a consumer at `http://metaplugintmdb-plugin:8080`.
**Who can use it today:** a **MetaFeeder** app, through its
`TMDB_PLUGIN_URL` setting — one shared copy instead of one bundled per feeder.
**Not a MetaSort plugin.** MetaSort starts its own plugin containers from
`plugins.yml` and has no slot for a plugin URL.
fr_fr: |
**Le plugin d'enrichissement `tmdb` de MetaMesh, en app autonome.**
Recherche un titre sur **The Movie Database** et écrit l'identité d'un enregistrement — `tmdbid`, `imdbid`, titres localisés — ainsi que l'affiche et l'image d'arrière-plan.
**Sans interface.** Il répond au contrat HTTP des plugins — `/manifest`,
`/health`, `/configure`, `/process` — et n'a pas de tableau de bord ; cette
tuile ouvre son `/manifest`. Dirigez un consommateur vers
`http://metaplugintmdb-plugin:8080`.
**Ce n'est pas un plugin MetaSort** : MetaSort démarre ses propres
conteneurs de plugins depuis `plugins.yml` et n'accepte pas d'URL.
tagline:
en_us: TMDB identity and artwork for MetaMesh records
fr_fr: Identité et visuels TMDB pour les enregistrements MetaMesh
title:
en_us: MetaPlugin · TMDB
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginTMDB/icon.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaPluginTMDB/thumbnail.png
store_app_id: metaplugintmdb
is_uncontrolled: false
tips:
before_install:
en_us: |
**What this is for:** running the `tmdb` plugin **once, shared**, instead of
one bundled copy inside every app that needs it.
**After installing:** set `TMDB_PLUGIN_URL` to `http://metaplugintmdb-plugin:8080` in the
MetaFeeder app that should use it, then restart that feeder. Left unset,
the feeder keeps using its own bundled copy.
**It is not a MetaSort plugin.** MetaSort spawns its own plugin containers
from `plugins.yml` (`image:`), with no URL slot — installing this app does
not add `tmdb` to MetaSort.
**Requires MetaCore** on the same box: this plugin writes what it
produces to MetaCore's WebDAV (`http://metacore-app:9000/webdav`).
**Authentication:** the `/manifest` page is protected by Yundera's Authelia
(OIDC). Consumers on the `pcs` network bypass the gate by design.
# 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/metaplugintmdb/cache
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metaplugintmdb-${domain}
webui-path: "/manifest"
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+44
View File
@@ -0,0 +1,44 @@
# MetaPlugin · TMDB — Rationale
## What deviation / exception is being requested
`metaplugintmdb-plugin` runs `user: 0:0`.
## Why it is necessary
The `metamesh-plugin-*` images declare no `USER` and have always run as root, both
when MetaSort spawns them and when a MetaFeeder bundles them as an enrichment
sidecar. This listing is a packaging change — the same image, started by Maison
instead of by MetaSort — so it keeps the runtime user the image was built and
validated with.
## Security mitigations in place
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`),
which gates every path behind the PCS's Authelia SSO over OIDC. The plugin carries no
Caddy labels and publishes no host port, so `/process` and `/configure` are reachable
only from the `pcs` Docker network.
- The container mounts **no user directories** — not `/DATA/Documents`, `/DATA/Downloads`,
`/DATA/Media` or `/DATA/Gallery`.
Its only bind is its own cache under `/DATA/AppData/metaplugintmdb/`, declared in
`x-compose-app.folders` and owned `$PUID:$PGID`. It reads the files it works on over
MetaCore's WebDAV, never from a bind mount.
- It holds **no credentials of its own**: keys arrive per-caller over `/configure` and
are never written to this app's storage.
- `cpu_shares` and a hard `mem_limit` (swap disabled) set on every service; image tags
pinned; no `:latest`.
## Alternatives considered and rejected
- **Run as `$PUID:$PGID`.** Not validated for these images; a uid change belongs in the
plugin image, not in a store listing that is otherwise a pure repackaging.
- **No AppShield gate, no public route at all.** Rejected: the tile would have no way to
show whether the plugin is alive, and `/manifest` is the only status surface a headless
plugin has.
- **Expose it unauthenticated.** Rejected outright — `/process` takes URLs and does work
on request, and `/configure` accepts credentials.
## Data protection
The plugin holds no user media and no user account. Its only state is a rebuildable cache under
`/DATA/AppData/metaplugintmdb/`, which is the unit Maison archives on uninstall.
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB