diff --git a/Apps/MetaFeederInternetArchive/docker-compose.yml b/Apps/MetaFeederInternetArchive/docker-compose.yml new file mode 100644 index 0000000..da7232b --- /dev/null +++ b/Apps/MetaFeederInternetArchive/docker-compose.yml @@ -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" diff --git a/Apps/MetaFeederInternetArchive/icon.png b/Apps/MetaFeederInternetArchive/icon.png new file mode 100644 index 0000000..7800e58 Binary files /dev/null and b/Apps/MetaFeederInternetArchive/icon.png differ diff --git a/Apps/MetaFeederInternetArchive/rationale.md b/Apps/MetaFeederInternetArchive/rationale.md new file mode 100644 index 0000000..8cf9614 --- /dev/null +++ b/Apps/MetaFeederInternetArchive/rationale.md @@ -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. diff --git a/Apps/MetaFeederInternetArchive/thumbnail.png b/Apps/MetaFeederInternetArchive/thumbnail.png new file mode 100644 index 0000000..7800e58 Binary files /dev/null and b/Apps/MetaFeederInternetArchive/thumbnail.png differ diff --git a/Apps/MetaFeederJamendo/docker-compose.yml b/Apps/MetaFeederJamendo/docker-compose.yml new file mode 100644 index 0000000..bf1a67c --- /dev/null +++ b/Apps/MetaFeederJamendo/docker-compose.yml @@ -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" diff --git a/Apps/MetaFeederJamendo/icon.png b/Apps/MetaFeederJamendo/icon.png new file mode 100644 index 0000000..e493a32 Binary files /dev/null and b/Apps/MetaFeederJamendo/icon.png differ diff --git a/Apps/MetaFeederJamendo/rationale.md b/Apps/MetaFeederJamendo/rationale.md new file mode 100644 index 0000000..14554f5 --- /dev/null +++ b/Apps/MetaFeederJamendo/rationale.md @@ -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. diff --git a/Apps/MetaFeederJamendo/thumbnail.png b/Apps/MetaFeederJamendo/thumbnail.png new file mode 100644 index 0000000..e493a32 Binary files /dev/null and b/Apps/MetaFeederJamendo/thumbnail.png differ diff --git a/Apps/MetaFeederMusicBrainz/docker-compose.yml b/Apps/MetaFeederMusicBrainz/docker-compose.yml new file mode 100644 index 0000000..2d91df3 --- /dev/null +++ b/Apps/MetaFeederMusicBrainz/docker-compose.yml @@ -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" diff --git a/Apps/MetaFeederMusicBrainz/icon.png b/Apps/MetaFeederMusicBrainz/icon.png new file mode 100644 index 0000000..90e7fc6 Binary files /dev/null and b/Apps/MetaFeederMusicBrainz/icon.png differ diff --git a/Apps/MetaFeederMusicBrainz/rationale.md b/Apps/MetaFeederMusicBrainz/rationale.md new file mode 100644 index 0000000..948c877 --- /dev/null +++ b/Apps/MetaFeederMusicBrainz/rationale.md @@ -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. diff --git a/Apps/MetaFeederMusicBrainz/thumbnail.png b/Apps/MetaFeederMusicBrainz/thumbnail.png new file mode 100644 index 0000000..90e7fc6 Binary files /dev/null and b/Apps/MetaFeederMusicBrainz/thumbnail.png differ diff --git a/Apps/MetaFeederYouTube/docker-compose.yml b/Apps/MetaFeederYouTube/docker-compose.yml new file mode 100644 index 0000000..01cd9e5 --- /dev/null +++ b/Apps/MetaFeederYouTube/docker-compose.yml @@ -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" diff --git a/Apps/MetaFeederYouTube/icon.png b/Apps/MetaFeederYouTube/icon.png new file mode 100644 index 0000000..07b02f0 Binary files /dev/null and b/Apps/MetaFeederYouTube/icon.png differ diff --git a/Apps/MetaFeederYouTube/rationale.md b/Apps/MetaFeederYouTube/rationale.md new file mode 100644 index 0000000..85ceca9 --- /dev/null +++ b/Apps/MetaFeederYouTube/rationale.md @@ -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. diff --git a/Apps/MetaFeederYouTube/thumbnail.png b/Apps/MetaFeederYouTube/thumbnail.png new file mode 100644 index 0000000..07b02f0 Binary files /dev/null and b/Apps/MetaFeederYouTube/thumbnail.png differ diff --git a/Apps/MetaPluginAnimeDetector/docker-compose.yml b/Apps/MetaPluginAnimeDetector/docker-compose.yml new file mode 100644 index 0000000..9955c9b --- /dev/null +++ b/Apps/MetaPluginAnimeDetector/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginAnimeDetector/icon.png b/Apps/MetaPluginAnimeDetector/icon.png new file mode 100644 index 0000000..365b94a Binary files /dev/null and b/Apps/MetaPluginAnimeDetector/icon.png differ diff --git a/Apps/MetaPluginAnimeDetector/rationale.md b/Apps/MetaPluginAnimeDetector/rationale.md new file mode 100644 index 0000000..5f3524f --- /dev/null +++ b/Apps/MetaPluginAnimeDetector/rationale.md @@ -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. diff --git a/Apps/MetaPluginAnimeDetector/thumbnail.png b/Apps/MetaPluginAnimeDetector/thumbnail.png new file mode 100644 index 0000000..365b94a Binary files /dev/null and b/Apps/MetaPluginAnimeDetector/thumbnail.png differ diff --git a/Apps/MetaPluginFFmpeg/docker-compose.yml b/Apps/MetaPluginFFmpeg/docker-compose.yml new file mode 100644 index 0000000..99fd0d6 --- /dev/null +++ b/Apps/MetaPluginFFmpeg/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginFFmpeg/icon.png b/Apps/MetaPluginFFmpeg/icon.png new file mode 100644 index 0000000..83ed0c1 Binary files /dev/null and b/Apps/MetaPluginFFmpeg/icon.png differ diff --git a/Apps/MetaPluginFFmpeg/rationale.md b/Apps/MetaPluginFFmpeg/rationale.md new file mode 100644 index 0000000..a2795e9 --- /dev/null +++ b/Apps/MetaPluginFFmpeg/rationale.md @@ -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. diff --git a/Apps/MetaPluginFFmpeg/thumbnail.png b/Apps/MetaPluginFFmpeg/thumbnail.png new file mode 100644 index 0000000..83ed0c1 Binary files /dev/null and b/Apps/MetaPluginFFmpeg/thumbnail.png differ diff --git a/Apps/MetaPluginFileInfo/docker-compose.yml b/Apps/MetaPluginFileInfo/docker-compose.yml new file mode 100644 index 0000000..e8c7849 --- /dev/null +++ b/Apps/MetaPluginFileInfo/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginFileInfo/icon.png b/Apps/MetaPluginFileInfo/icon.png new file mode 100644 index 0000000..02935e5 Binary files /dev/null and b/Apps/MetaPluginFileInfo/icon.png differ diff --git a/Apps/MetaPluginFileInfo/rationale.md b/Apps/MetaPluginFileInfo/rationale.md new file mode 100644 index 0000000..b817f6b --- /dev/null +++ b/Apps/MetaPluginFileInfo/rationale.md @@ -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. diff --git a/Apps/MetaPluginFileInfo/thumbnail.png b/Apps/MetaPluginFileInfo/thumbnail.png new file mode 100644 index 0000000..02935e5 Binary files /dev/null and b/Apps/MetaPluginFileInfo/thumbnail.png differ diff --git a/Apps/MetaPluginFilenameParser/docker-compose.yml b/Apps/MetaPluginFilenameParser/docker-compose.yml new file mode 100644 index 0000000..1894f46 --- /dev/null +++ b/Apps/MetaPluginFilenameParser/docker-compose.yml @@ -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/` 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" diff --git a/Apps/MetaPluginFilenameParser/icon.png b/Apps/MetaPluginFilenameParser/icon.png new file mode 100644 index 0000000..d818af4 Binary files /dev/null and b/Apps/MetaPluginFilenameParser/icon.png differ diff --git a/Apps/MetaPluginFilenameParser/rationale.md b/Apps/MetaPluginFilenameParser/rationale.md new file mode 100644 index 0000000..41e171f --- /dev/null +++ b/Apps/MetaPluginFilenameParser/rationale.md @@ -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. diff --git a/Apps/MetaPluginFilenameParser/thumbnail.png b/Apps/MetaPluginFilenameParser/thumbnail.png new file mode 100644 index 0000000..d818af4 Binary files /dev/null and b/Apps/MetaPluginFilenameParser/thumbnail.png differ diff --git a/Apps/MetaPluginFullHash/docker-compose.yml b/Apps/MetaPluginFullHash/docker-compose.yml new file mode 100644 index 0000000..7d83d19 --- /dev/null +++ b/Apps/MetaPluginFullHash/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginFullHash/icon.png b/Apps/MetaPluginFullHash/icon.png new file mode 100644 index 0000000..e901af7 Binary files /dev/null and b/Apps/MetaPluginFullHash/icon.png differ diff --git a/Apps/MetaPluginFullHash/rationale.md b/Apps/MetaPluginFullHash/rationale.md new file mode 100644 index 0000000..57072a5 --- /dev/null +++ b/Apps/MetaPluginFullHash/rationale.md @@ -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. diff --git a/Apps/MetaPluginFullHash/thumbnail.png b/Apps/MetaPluginFullHash/thumbnail.png new file mode 100644 index 0000000..e901af7 Binary files /dev/null and b/Apps/MetaPluginFullHash/thumbnail.png differ diff --git a/Apps/MetaPluginJellyfinNFO/docker-compose.yml b/Apps/MetaPluginJellyfinNFO/docker-compose.yml new file mode 100644 index 0000000..9c8bd6a --- /dev/null +++ b/Apps/MetaPluginJellyfinNFO/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginJellyfinNFO/icon.png b/Apps/MetaPluginJellyfinNFO/icon.png new file mode 100644 index 0000000..9ce0a90 Binary files /dev/null and b/Apps/MetaPluginJellyfinNFO/icon.png differ diff --git a/Apps/MetaPluginJellyfinNFO/rationale.md b/Apps/MetaPluginJellyfinNFO/rationale.md new file mode 100644 index 0000000..33a04dd --- /dev/null +++ b/Apps/MetaPluginJellyfinNFO/rationale.md @@ -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. diff --git a/Apps/MetaPluginJellyfinNFO/thumbnail.png b/Apps/MetaPluginJellyfinNFO/thumbnail.png new file mode 100644 index 0000000..9ce0a90 Binary files /dev/null and b/Apps/MetaPluginJellyfinNFO/thumbnail.png differ diff --git a/Apps/MetaPluginLanguage/docker-compose.yml b/Apps/MetaPluginLanguage/docker-compose.yml new file mode 100644 index 0000000..bd303f5 --- /dev/null +++ b/Apps/MetaPluginLanguage/docker-compose.yml @@ -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/` 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/` 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" diff --git a/Apps/MetaPluginLanguage/icon.png b/Apps/MetaPluginLanguage/icon.png new file mode 100644 index 0000000..fc2b7b4 Binary files /dev/null and b/Apps/MetaPluginLanguage/icon.png differ diff --git a/Apps/MetaPluginLanguage/rationale.md b/Apps/MetaPluginLanguage/rationale.md new file mode 100644 index 0000000..7b5d1a5 --- /dev/null +++ b/Apps/MetaPluginLanguage/rationale.md @@ -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. diff --git a/Apps/MetaPluginLanguage/thumbnail.png b/Apps/MetaPluginLanguage/thumbnail.png new file mode 100644 index 0000000..fc2b7b4 Binary files /dev/null and b/Apps/MetaPluginLanguage/thumbnail.png differ diff --git a/Apps/MetaPluginOpenSubtitles/docker-compose.yml b/Apps/MetaPluginOpenSubtitles/docker-compose.yml new file mode 100644 index 0000000..757ee9d --- /dev/null +++ b/Apps/MetaPluginOpenSubtitles/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginOpenSubtitles/icon.png b/Apps/MetaPluginOpenSubtitles/icon.png new file mode 100644 index 0000000..3193cfd Binary files /dev/null and b/Apps/MetaPluginOpenSubtitles/icon.png differ diff --git a/Apps/MetaPluginOpenSubtitles/rationale.md b/Apps/MetaPluginOpenSubtitles/rationale.md new file mode 100644 index 0000000..c9f3412 --- /dev/null +++ b/Apps/MetaPluginOpenSubtitles/rationale.md @@ -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. diff --git a/Apps/MetaPluginOpenSubtitles/thumbnail.png b/Apps/MetaPluginOpenSubtitles/thumbnail.png new file mode 100644 index 0000000..3193cfd Binary files /dev/null and b/Apps/MetaPluginOpenSubtitles/thumbnail.png differ diff --git a/Apps/MetaPluginStillExtractor/docker-compose.yml b/Apps/MetaPluginStillExtractor/docker-compose.yml new file mode 100644 index 0000000..99504ab --- /dev/null +++ b/Apps/MetaPluginStillExtractor/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginStillExtractor/icon.png b/Apps/MetaPluginStillExtractor/icon.png new file mode 100644 index 0000000..bd623f2 Binary files /dev/null and b/Apps/MetaPluginStillExtractor/icon.png differ diff --git a/Apps/MetaPluginStillExtractor/rationale.md b/Apps/MetaPluginStillExtractor/rationale.md new file mode 100644 index 0000000..498fd1e --- /dev/null +++ b/Apps/MetaPluginStillExtractor/rationale.md @@ -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. diff --git a/Apps/MetaPluginStillExtractor/thumbnail.png b/Apps/MetaPluginStillExtractor/thumbnail.png new file mode 100644 index 0000000..bd623f2 Binary files /dev/null and b/Apps/MetaPluginStillExtractor/thumbnail.png differ diff --git a/Apps/MetaPluginSubtitle/docker-compose.yml b/Apps/MetaPluginSubtitle/docker-compose.yml new file mode 100644 index 0000000..86a778d --- /dev/null +++ b/Apps/MetaPluginSubtitle/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginSubtitle/icon.png b/Apps/MetaPluginSubtitle/icon.png new file mode 100644 index 0000000..d93f231 Binary files /dev/null and b/Apps/MetaPluginSubtitle/icon.png differ diff --git a/Apps/MetaPluginSubtitle/rationale.md b/Apps/MetaPluginSubtitle/rationale.md new file mode 100644 index 0000000..cbe16b7 --- /dev/null +++ b/Apps/MetaPluginSubtitle/rationale.md @@ -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. diff --git a/Apps/MetaPluginSubtitle/thumbnail.png b/Apps/MetaPluginSubtitle/thumbnail.png new file mode 100644 index 0000000..d93f231 Binary files /dev/null and b/Apps/MetaPluginSubtitle/thumbnail.png differ diff --git a/Apps/MetaPluginSubtitleExtractor/docker-compose.yml b/Apps/MetaPluginSubtitleExtractor/docker-compose.yml new file mode 100644 index 0000000..9d64639 --- /dev/null +++ b/Apps/MetaPluginSubtitleExtractor/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginSubtitleExtractor/icon.png b/Apps/MetaPluginSubtitleExtractor/icon.png new file mode 100644 index 0000000..636d596 Binary files /dev/null and b/Apps/MetaPluginSubtitleExtractor/icon.png differ diff --git a/Apps/MetaPluginSubtitleExtractor/rationale.md b/Apps/MetaPluginSubtitleExtractor/rationale.md new file mode 100644 index 0000000..55a8ba4 --- /dev/null +++ b/Apps/MetaPluginSubtitleExtractor/rationale.md @@ -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. diff --git a/Apps/MetaPluginSubtitleExtractor/thumbnail.png b/Apps/MetaPluginSubtitleExtractor/thumbnail.png new file mode 100644 index 0000000..636d596 Binary files /dev/null and b/Apps/MetaPluginSubtitleExtractor/thumbnail.png differ diff --git a/Apps/MetaPluginTMDB/docker-compose.yml b/Apps/MetaPluginTMDB/docker-compose.yml new file mode 100644 index 0000000..773f186 --- /dev/null +++ b/Apps/MetaPluginTMDB/docker-compose.yml @@ -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" diff --git a/Apps/MetaPluginTMDB/icon.png b/Apps/MetaPluginTMDB/icon.png new file mode 100644 index 0000000..c97b2d8 Binary files /dev/null and b/Apps/MetaPluginTMDB/icon.png differ diff --git a/Apps/MetaPluginTMDB/rationale.md b/Apps/MetaPluginTMDB/rationale.md new file mode 100644 index 0000000..1884ec0 --- /dev/null +++ b/Apps/MetaPluginTMDB/rationale.md @@ -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. diff --git a/Apps/MetaPluginTMDB/thumbnail.png b/Apps/MetaPluginTMDB/thumbnail.png new file mode 100644 index 0000000..c97b2d8 Binary files /dev/null and b/Apps/MetaPluginTMDB/thumbnail.png differ diff --git a/CLAUDE.md b/CLAUDE.md index 2fae4a5..68739a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,8 @@ grep -rn 'gh/Worph/AppStore@main' Apps/ # must be empty - `docker-compose.yml` — compose + `x-casaos` metadata (the store entry) - `icon.png`, `thumbnail.png`, `screenshot-{1,2,3}.png` - optional `rationale.md`, `README.md`, `pre-install/` -- `category-list.json` — only `Media` and `Developer` are used here +- `category-list.json` — `Media` (every MetaFeeder and client app) and + `Developer` (the MetaPlugin family) - `recommend-list.json`, `featured-apps.json` — store front placement - `psd-source/meta-icons-v2/` — procedural icon generator for the family (`gen_mesh.py` mesh sphere + `meta_icons.py` per-app badge); see its README @@ -45,6 +46,48 @@ grep -rn 'gh/Worph/AppStore@main' Apps/ # must be empty - **Install order**: `MetaCore` first — every other app depends on it - **Multi-language**: descriptions/taglines carry `en_us`, `fr_fr`, `es_es`, `zh_cn`, `ko_kr`, `de_de` +## The three app families + +| Family | What it is | Shape | Category | +|---|---|---|---| +| `Meta*` | The services — Core, Sort, Fuse, Share, Gateway, Watch, Read, Stremio | backend + AppShield gate, real dashboard | Media | +| `MetaFeeder` | One upstream, one app (see the provider-naming rule below) | `-feeder` + AppShield gate, `index: /config` | Media | +| `MetaPlugin` | One `metamesh-plugin-*` enrichment worker, standalone | `-plugin` + AppShield gate, `index: /manifest` | Developer | + +**MetaFeeder apps are named by PROVIDER**, one provider per app — role/bundle +names (Card, Indexer, Comic, Book, Common, Paper, Subtitle) are retired. Gateway +feeder keys follow `-feeder`; the feeder's `upstream_id()` in its crate +is the authority for the provider spelling. + +### MetaPlugin: what these apps can and cannot do + +A `metamesh-plugin-*` image is a **headless** worker speaking four verbs — +`/manifest`, `/health`, `/configure`, `/process`. It has no UI, so the tile opens +`/manifest`, and it holds **no credentials**: the *caller* POSTs `/configure` with +its own TMDB token / OpenSubtitles account before use. Two consumers sharing one +instance are last-writer-wins on that config. + +⚠ **MetaSort cannot consume these apps.** `ContainerPluginConfig` +(`packages/meta-sort/.../container-plugins/types.ts`) takes an `image:` and +MetaSort spawns the container itself; there is no `url:` field. Until one exists, +installing a MetaPlugin app does **not** add that plugin to MetaSort. + +⚠ **Only three plugins have a consumer that takes a URL today** — the feeder +SDK's sidecar slots, `FILENAME_PARSER_URL`, `TMDB_PLUGIN_URL` and +`OPENSUBTITLES_PLUGIN_URL` (`meta-feeder-sdk/src/enrich.rs`). The other nine +MetaPlugin listings are packaging ahead of the consumer, and each one says so in +its own description and `tips` — keep that wording honest if the situation +changes. + +⚠ **`MetaPluginFilenameParser` cannot be shared between a feeder and MetaSort.** +`EMIT_TITLE_TAGS` is process-level and the two want opposite values (a feeder has +no real file and wants title guesses; MetaSort probes the real stream and must +not have them). Install a second copy rather than flipping it. + +Note the SDK **derives** a sidecar URL from the feeder's own hostname when the +variable is unset (`derive_sidecar_url`), which is why pointing a feeder at a +shared MetaPlugin app requires setting the variable explicitly. + ## Cross-repo notes - Apps here are the deployable face of the services in `MetaMesh/meta-root-v2`; diff --git a/README.md b/README.md index cf93603..f823944 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,18 @@ https://github.com/Worph/MetaAppStore/archive/refs/heads/main.zip | Library | `MetaSort`, `MetaFuse`, `MetaStremio` | | Network | `MetaShare` (content transport), `MetaGateway`, `MetaGatewayShared` | | Clients | `MetaWatch`, `MetaWatchShared`, `MetaRead` | -| Feeders | `MetaFeederAniList`, `MetaFeederArxiv`, `MetaFeederEuropePMC`, `MetaFeederGiphy`, `MetaFeederGutenberg`, `MetaFeederOpenSubtitles`, `MetaFeederProwlarr`, `MetaFeederSciHub`, `MetaFeederSuwayomi`, `MetaFeederTMDB`, `MetaFeederTribler`, `MetaFeederUsenet`, `MetaFeederWikiCommons` | +| Feeders | `MetaFeederAniList`, `MetaFeederArxiv`, `MetaFeederEuropePMC`, `MetaFeederGiphy`, `MetaFeederGutenberg`, `MetaFeederInternetArchive`, `MetaFeederJamendo`, `MetaFeederMusicBrainz`, `MetaFeederOpenSubtitles`, `MetaFeederProwlarr`, `MetaFeederSciHub`, `MetaFeederSuwayomi`, `MetaFeederTMDB`, `MetaFeederTribler`, `MetaFeederUsenet`, `MetaFeederWikiCommons`, `MetaFeederYouTube` | +| Plugins | `MetaPluginAnimeDetector`, `MetaPluginFFmpeg`, `MetaPluginFileInfo`, `MetaPluginFilenameParser`, `MetaPluginFullHash`, `MetaPluginJellyfinNFO`, `MetaPluginLanguage`, `MetaPluginOpenSubtitles`, `MetaPluginStillExtractor`, `MetaPluginSubtitle`, `MetaPluginSubtitleExtractor`, `MetaPluginTMDB` — headless enrichment workers, one per `metamesh-plugin-*` image | `MetaCore` is the dependency of every other app — install it first. +A **MetaPlugin** app is headless: no dashboard, the tile opens the plugin's +`/manifest`, and it holds no credentials of its own (the caller configures it). +MetaSort does **not** consume these — it starts its own plugin containers from +`plugins.yml` and has no slot for a plugin URL. Today the consumers that do take +a URL are the MetaFeeder sidecar slots for `filename-parser`, `tmdb` and +`opensubtitles`; each listing says where it stands. + Some apps pair with non-Meta apps that remain in [Worph/AppStore](https://github.com/Worph/AppStore): **Suwayomi** (the chapter tier `MetaFeederSuwayomi` reads) and **SegmentPlayer**. diff --git a/psd-source/meta-icons-v2/README.md b/psd-source/meta-icons-v2/README.md index e2f4590..daed7f3 100644 --- a/psd-source/meta-icons-v2/README.md +++ b/psd-source/meta-icons-v2/README.md @@ -7,7 +7,11 @@ family-coloured ring, white glyph, auto-fitted label. - **Neutral** (no badge): MetaCore. - **Ring colours:** teal = MetaShare, orange = MetaGateway and every MetaFeeder, - gold = MetaSort, green = MetaFuse, purple = MetaStremio. + gold = MetaSort, green = MetaFuse, purple = MetaStremio, **blue = every + MetaPlugin** (the headless enrichment workers). +- A glyph may be reused across two families — the ring colour and the label + separate them (`photo` is WikiCommons in orange and Still Extractor in blue). + Within one colour, never reuse a glyph. - MetaWatch / MetaRead (client apps) keep their half-moon icons; MetaMCP keeps its own. Regenerate (needs `python3`, `rsvg-convert`, ImageMagick `convert` + DejaVu Sans Bold): diff --git a/psd-source/meta-icons-v2/meta_icons.py b/psd-source/meta-icons-v2/meta_icons.py index d1b011c..54c6b03 100644 --- a/psd-source/meta-icons-v2/meta_icons.py +++ b/psd-source/meta-icons-v2/meta_icons.py @@ -14,6 +14,9 @@ FONT_IM = "DejaVu-Sans-Bold" # ImageMagick name, for measu FONT_SVG = "DejaVu Sans, Helvetica, Arial, sans-serif" DARK = "#2c3038" ORANGE, TEAL, GOLD, GREEN, PURPLE = "#e8821e", "#00b4b4", "#dca000", "#3ca050", "#8c50c8" +# Blue = the MetaPlugin family (headless enrichment workers). Distinct from the +# mesh gradient's blues, which never appear inside a badge. +BLUE = "#3a7bd5" W = "#ffffff" # Glyphs live in a 160x128 box, drawn white on the dark badge. @@ -53,8 +56,52 @@ G = { f'', "magnet": f'' f'', + # --- MetaFeeder additions ------------------------------------------------- + "bank": f'' + f'' + f'' + f'', + "note": f'' + f'' + f'' + f'' + f'', + "disc": f'' + f'' + f'', + "video": f'' + f'', + # --- MetaPlugin family ---------------------------------------------------- + "fileinfo": f'' + f'', + "film": f'' + f'' + f'' + f'' + f'', + "tag": f'' + f'', + "hash": f'' + f'' + f'', + "capout": f'' + f'' + f'' + f'' + f'', + "globe": f'' + f'' + f'', + "speech": f'' + f'A', + "star": f'', } + # app dir → (label, glyph, ring colour); None = neutral (no badge) APPS = { "MetaCore": None, @@ -76,6 +123,23 @@ APPS = { "MetaFeederSuwayomi": ("SUWAYOMI", "pages", ORANGE), "MetaFeederUsenet": ("USENET", "news", ORANGE), "MetaFeederTribler": ("TRIBLER", "magnet", ORANGE), + "MetaFeederInternetArchive": ("ARCHIVE", "bank", ORANGE), + "MetaFeederJamendo": ("JAMENDO", "note", ORANGE), + "MetaFeederMusicBrainz": ("MUSICBRAINZ", "disc", ORANGE), + "MetaFeederYouTube": ("YOUTUBE", "video", ORANGE), + # MetaPlugin family — headless enrichment workers, blue ring. + "MetaPluginFileInfo": ("FILE INFO", "fileinfo", BLUE), + "MetaPluginFFmpeg": ("FFMPEG", "film", BLUE), + "MetaPluginFilenameParser": ("FILENAME", "tag", BLUE), + "MetaPluginFullHash": ("FULL HASH", "hash", BLUE), + "MetaPluginTMDB": ("TMDB", "idcard", BLUE), + "MetaPluginSubtitle": ("SUBTITLE", "caption", BLUE), + "MetaPluginSubtitleExtractor": ("SUB EXTRACT", "capout", BLUE), + "MetaPluginStillExtractor": ("STILL", "photo", BLUE), + "MetaPluginOpenSubtitles": ("OPENSUBS", "globe", BLUE), + "MetaPluginLanguage": ("LANGUAGE", "speech", BLUE), + "MetaPluginAnimeDetector": ("ANIME", "star", BLUE), + "MetaPluginJellyfinNFO": ("NFO", "paper", BLUE), } BADGE_C, BADGE_R, RING = (770, 770), 226, 18 diff --git a/psd-source/meta-icons-v2/svg/MetaFeederInternetArchive.svg b/psd-source/meta-icons-v2/svg/MetaFeederInternetArchive.svg new file mode 100644 index 0000000..196bccb --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaFeederInternetArchive.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +ARCHIVE \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaFeederJamendo.svg b/psd-source/meta-icons-v2/svg/MetaFeederJamendo.svg new file mode 100644 index 0000000..770d04b --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaFeederJamendo.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +JAMENDO \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaFeederMusicBrainz.svg b/psd-source/meta-icons-v2/svg/MetaFeederMusicBrainz.svg new file mode 100644 index 0000000..c86e816 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaFeederMusicBrainz.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +MUSICBRAINZ \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaFeederYouTube.svg b/psd-source/meta-icons-v2/svg/MetaFeederYouTube.svg new file mode 100644 index 0000000..5209447 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaFeederYouTube.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +YOUTUBE \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginAnimeDetector.svg b/psd-source/meta-icons-v2/svg/MetaPluginAnimeDetector.svg new file mode 100644 index 0000000..2ee9bba --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginAnimeDetector.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +ANIME \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginFFmpeg.svg b/psd-source/meta-icons-v2/svg/MetaPluginFFmpeg.svg new file mode 100644 index 0000000..ba86322 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginFFmpeg.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +FFMPEG \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginFileInfo.svg b/psd-source/meta-icons-v2/svg/MetaPluginFileInfo.svg new file mode 100644 index 0000000..99d7c58 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginFileInfo.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +FILE INFO \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginFilenameParser.svg b/psd-source/meta-icons-v2/svg/MetaPluginFilenameParser.svg new file mode 100644 index 0000000..01f2676 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginFilenameParser.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +FILENAME \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginFullHash.svg b/psd-source/meta-icons-v2/svg/MetaPluginFullHash.svg new file mode 100644 index 0000000..d8b7e1b --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginFullHash.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +FULL HASH \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginJellyfinNFO.svg b/psd-source/meta-icons-v2/svg/MetaPluginJellyfinNFO.svg new file mode 100644 index 0000000..610418c --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginJellyfinNFO.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +NFO \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginLanguage.svg b/psd-source/meta-icons-v2/svg/MetaPluginLanguage.svg new file mode 100644 index 0000000..6ddc4b1 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginLanguage.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +ALANGUAGE \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginOpenSubtitles.svg b/psd-source/meta-icons-v2/svg/MetaPluginOpenSubtitles.svg new file mode 100644 index 0000000..d71ff6f --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginOpenSubtitles.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +OPENSUBS \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginStillExtractor.svg b/psd-source/meta-icons-v2/svg/MetaPluginStillExtractor.svg new file mode 100644 index 0000000..0a3b990 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginStillExtractor.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +STILL \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginSubtitle.svg b/psd-source/meta-icons-v2/svg/MetaPluginSubtitle.svg new file mode 100644 index 0000000..6e57bdd --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginSubtitle.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +SUBTITLE \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginSubtitleExtractor.svg b/psd-source/meta-icons-v2/svg/MetaPluginSubtitleExtractor.svg new file mode 100644 index 0000000..37984d4 --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginSubtitleExtractor.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +SUB EXTRACT \ No newline at end of file diff --git a/psd-source/meta-icons-v2/svg/MetaPluginTMDB.svg b/psd-source/meta-icons-v2/svg/MetaPluginTMDB.svg new file mode 100644 index 0000000..55ba25b --- /dev/null +++ b/psd-source/meta-icons-v2/svg/MetaPluginTMDB.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +TMDB \ No newline at end of file