All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m31s
- Sjablonen kunnen nu secret_format ("hex" of "laravel") en secret_bytes
opgeven. De generator maakte altijd url-veilige base64, en dat is precies wat
Laravel, Homarr en LibreChat weigeren
- Daarmee kwamen twee fouten uit v0.8.00 aan het licht: Homarr en LibreChat
kregen sleutels die hun containers niet accepteren. Rechtgezet
- Snipe-IT en Invoice Ninja alsnog toegevoegd; die vielen vorige ronde af omdat
hun APP_KEY niet te genereren was
- 19 vervangers voor big tech erbij, van 197 naar 218 apps:
Baikal en Radicale (Google Agenda/Contacten), LibreTranslate (Translate),
CryptPad, Collabora en ONLYOFFICE (Google Docs), Formbricks (Forms),
Jitsi Meet (Meet/Zoom), PeerTube, Invidious en Tube Archivist (YouTube),
Owncast (Twitch), Mastodon (X), Pixelfed (Instagram), Lemmy (Reddit),
Cal.com (Calendly), Rallly (Doodle), DocuSeal (DocuSign), Penpot (Figma)
- Communicatie van 12 naar 16 apps, productiviteit van 21 naar 31
- Nieuwe JS-test voor de sleutelformaten; alle 242 images in de catalogus
opnieuw tegen hun registry gecontroleerd
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
863 lines
48 KiB
Python
863 lines
48 KiB
Python
"""Bron van de *arr-sjablonen: de losse apps én de gecombineerde ARR-stack.
|
|
|
|
Elk onderdeel bestaat twee keer — als losse app in de store en als schakelaar in
|
|
`apps/arr-stack`. Dat zijn twee verpakkingen van dezelfde container, en ze horen
|
|
niet uiteen te lopen: hetzelfde image, dezelfde interne poort, dezelfde mounts.
|
|
Daarom komen ze allebei uit de tabel hieronder.
|
|
|
|
python3 tools/arr_sjablonen.py
|
|
|
|
`tests/test_arr_sjablonen.py` draait dit opnieuw en vergelijkt het resultaat met
|
|
wat er in `apps/` staat, zodat een handmatige wijziging aan één van de twee
|
|
opvalt in plaats van jarenlang mee te reizen.
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
|
|
APPS = Path(__file__).resolve().parents[1] / "apps"
|
|
|
|
HELP = ("Media en downloads horen onder één map te staan, die als /data in elke "
|
|
"container komt. Alleen dan kan een voltooide download met een hardlink "
|
|
"naar je bibliotheek: geen kopie, geen dubbele opslag, en je torrent "
|
|
"blijft seeden. Zet je ze op gescheiden paden, dan kopieert elke "
|
|
"download opnieuw. Eronder: media/films, media/series, torrents/ en usenet/.")
|
|
|
|
TZ = "Europe/Amsterdam"
|
|
APPDATA = "/opt/serverup/appdata"
|
|
DATAROOT = "/mnt/data"
|
|
|
|
# naam, image, poorten, vlaggen
|
|
# poorten: lijst van (veld, titel, hostdefault, containerpoort, proto, omschrijving)
|
|
SPEC = [
|
|
# ── De *arr-bibliotheken ────────────────────────────────────────────────
|
|
dict(dir="sonarr", naam="Sonarr", icon="sonarr",
|
|
image="lscr.io/linuxserver/sonarr:latest",
|
|
omschrijving="Zoekt, downloadt en hernoemt series. Houdt bij welke afleveringen je mist en pakt ze op zodra ze verschijnen.",
|
|
tags=["media", "series", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8989, 8989, "tcp", "")],
|
|
lsio=True, data=True, sectie="Bibliotheken"),
|
|
dict(dir="radarr", naam="Radarr", icon="radarr",
|
|
image="lscr.io/linuxserver/radarr:latest",
|
|
omschrijving="Zoekt, downloadt en hernoemt films. Dezelfde aanpak als Sonarr, maar dan voor je filmbibliotheek.",
|
|
tags=["media", "films", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 7878, 7878, "tcp", "")],
|
|
lsio=True, data=True, sectie="Bibliotheken"),
|
|
dict(dir="lidarr", naam="Lidarr", icon="lidarr",
|
|
image="lscr.io/linuxserver/lidarr:latest",
|
|
omschrijving="Beheert je muziekbibliotheek: volgt artiesten en haalt nieuwe albums binnen.",
|
|
tags=["media", "muziek", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8686, 8686, "tcp", "")],
|
|
lsio=True, data=True, sectie="Bibliotheken"),
|
|
dict(dir="readarr", naam="Readarr", icon="readarr",
|
|
image="lscr.io/linuxserver/readarr:develop",
|
|
omschrijving="Beheert e-boeken en luisterboeken. Let op: de ontwikkeling is stilgelegd; het draait nog wel, maar er komen geen nieuwe versies meer.",
|
|
tags=["media", "boeken", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8787, 8787, "tcp", "")],
|
|
lsio=True, data=True, sectie="Bibliotheken"),
|
|
dict(dir="whisparr", naam="Whisparr", icon="whisparr",
|
|
image="ghcr.io/hotio/whisparr:latest",
|
|
omschrijving="Dezelfde automatisering als Sonarr en Radarr, gericht op materiaal voor volwassenen.",
|
|
tags=["media", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 6969, 6969, "tcp", "")],
|
|
lsio=True, data=True, sectie="Bibliotheken"),
|
|
dict(dir="mylar3", naam="Mylar3", icon="mylar",
|
|
image="lscr.io/linuxserver/mylar3:latest",
|
|
omschrijving="Volgt stripseries en haalt nieuwe uitgaven binnen. De *arr-aanpak, maar voor comics.",
|
|
tags=["media", "strips", "boeken"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8115, 8090, "tcp", "Standaard 8090; hier verplaatst omdat die poort al bezet is")],
|
|
lsio=True, data=True, sectie="Bibliotheken"),
|
|
dict(dir="kapowarr", naam="Kapowarr", icon="kapowarr",
|
|
image="mrcas/kapowarr:latest",
|
|
omschrijving="Bouwt en beheert een stripbibliotheek, met eigen bestandsnaamgeving en kwaliteitsregels.",
|
|
tags=["media", "strips", "boeken"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 5656, 5656, "tcp", "")],
|
|
data=True, config_pad="/app/db", sectie="Bibliotheken"),
|
|
|
|
# ── Indexers ────────────────────────────────────────────────────────────
|
|
dict(dir="prowlarr", naam="Prowlarr", icon="prowlarr",
|
|
image="lscr.io/linuxserver/prowlarr:latest",
|
|
omschrijving="Beheert je indexers op één plek en duwt ze door naar Sonarr, Radarr en de rest. Scheelt overal apart instellen.",
|
|
tags=["media", "indexers", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 9696, 9696, "tcp", "")],
|
|
lsio=True, sectie="Indexers"),
|
|
dict(dir="jackett", naam="Jackett", icon="jackett",
|
|
image="lscr.io/linuxserver/jackett:latest",
|
|
omschrijving="Vertaalt trackers naar één API voor je *arr-apps. Ouder alternatief voor Prowlarr; meestal heb je er maar één nodig.",
|
|
tags=["media", "indexers", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 9117, 9117, "tcp", "")],
|
|
lsio=True, sectie="Indexers"),
|
|
dict(dir="nzbhydra2", naam="NZBHydra2", icon="nzbhydra2",
|
|
image="lscr.io/linuxserver/nzbhydra2:latest",
|
|
omschrijving="Bundelt meerdere usenet-indexers tot één zoekbron, met eigen statistieken per indexer.",
|
|
tags=["usenet", "indexers", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 5076, 5076, "tcp", "")],
|
|
lsio=True, sectie="Indexers"),
|
|
dict(dir="flaresolverr", naam="FlareSolverr", icon="flaresolverr",
|
|
image="ghcr.io/flaresolverr/flaresolverr:latest",
|
|
omschrijving="Haalt pagina's op die achter een Cloudflare-controle zitten, zodat je indexers er alsnog bij kunnen.",
|
|
tags=["indexers", "downloaden", "proxy"], cats=["downloaden"],
|
|
poorten=[("port", "Poort", 8191, 8191, "tcp", "")],
|
|
env_extra=["LOG_LEVEL=info"], sectie="Indexers"),
|
|
dict(dir="byparr", naam="Byparr", icon="byparr",
|
|
image="ghcr.io/thephaseless/byparr:latest",
|
|
omschrijving="Nieuwer alternatief voor FlareSolverr, gebouwd op SeleniumBase. Spreekt dezelfde API, dus je kunt het er zo voor in de plaats zetten.",
|
|
tags=["indexers", "downloaden", "proxy"], cats=["downloaden"],
|
|
poorten=[("port", "Poort", 8119, 8191, "tcp", "Byparr luistert intern op 8191, net als FlareSolverr")],
|
|
sectie="Indexers"),
|
|
|
|
# ── Downloadclients ─────────────────────────────────────────────────────
|
|
dict(dir="nzbget", naam="NZBGet", icon="nzbget",
|
|
image="lscr.io/linuxserver/nzbget:latest",
|
|
omschrijving="Zuinige usenet-downloader. Lichter dan SABnzbd en daarmee prettig op een kleine server.",
|
|
tags=["usenet", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 6789, 6789, "tcp", "")],
|
|
lsio=True, data=True, client=True, sectie="Downloadclients"),
|
|
dict(dir="deluge", naam="Deluge", icon="deluge",
|
|
image="lscr.io/linuxserver/deluge:latest",
|
|
omschrijving="Lichte torrentclient met webinterface en een uitgebreid plugin-systeem.",
|
|
tags=["torrents", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 8113, 8112, "tcp", "Standaard 8112; hier verplaatst omdat die poort al bezet is"),
|
|
("port_torrent", "Inkomende poort", 6882, 6881, "both", "Zet deze poort open in je router voor betere snelheden")],
|
|
lsio=True, data=True, client=True, sectie="Downloadclients"),
|
|
dict(dir="transmission", naam="Transmission", icon="transmission",
|
|
image="lscr.io/linuxserver/transmission:latest",
|
|
omschrijving="Sobere, stabiele torrentclient. Weinig knoppen, doet gewoon zijn werk.",
|
|
tags=["torrents", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 8114, 9091, "tcp", "Standaard 9091; hier verplaatst omdat die poort al bezet is"),
|
|
("port_torrent", "Inkomende poort", 6883, 51413, "both", "Zet deze poort open in je router voor betere snelheden")],
|
|
lsio=True, data=True, client=True, sectie="Downloadclients"),
|
|
dict(dir="slskd", naam="slskd", icon="slskd",
|
|
image="slskd/slskd:latest",
|
|
omschrijving="Soulseek-client met webinterface. In combinatie met Soularr vult het gaten in je muziekbibliotheek die de gewone indexers niet hebben.",
|
|
tags=["muziek", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Webinterface", 5030, 5030, "tcp", ""),
|
|
("port_listen", "Luisterpoort", 50300, 50300, "tcp", "Zet deze poort open in je router, anders kun je alleen downloaden van gebruikers die zelf bereikbaar zijn")],
|
|
data=True, client=True, config_pad="/app",
|
|
env_extra=["SLSKD_REMOTE_CONFIGURATION=true"], sectie="Downloadclients"),
|
|
dict(dir="gluetun", naam="Gluetun", icon="gluetun",
|
|
image="qmcgaw/gluetun:latest",
|
|
omschrijving="VPN-container waar je andere containers doorheen laat lopen. Zet je downloadclient erachter met `network_mode: service:gluetun`.",
|
|
tags=["vpn", "netwerk", "privacy"], cats=["netwerk", "beveiliging"],
|
|
poorten=[], speciaal="gluetun", sectie="Downloadclients"),
|
|
|
|
# ── Ondertitels en verwerking ───────────────────────────────────────────
|
|
dict(dir="bazarr", naam="Bazarr", icon="bazarr",
|
|
image="lscr.io/linuxserver/bazarr:latest",
|
|
omschrijving="Zoekt en downloadt ondertitels bij wat Sonarr en Radarr al hebben binnengehaald.",
|
|
tags=["media", "ondertitels"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 6767, 6767, "tcp", "")],
|
|
lsio=True, data=True, sectie="Ondertitels en verwerking"),
|
|
dict(dir="lingarr", naam="Lingarr", icon="lingarr",
|
|
image="lingarr/lingarr:latest",
|
|
omschrijving="Vertaalt ondertitels automatisch, met een lokaal model of een vertaaldienst naar keuze.",
|
|
tags=["media", "ondertitels", "ai"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 9876, 9876, "tcp", "")],
|
|
data=True, config_pad="/app/config", sectie="Ondertitels en verwerking"),
|
|
dict(dir="unpackerr", naam="Unpackerr", icon="unpackerr",
|
|
image="ghcr.io/unpackerr/unpackerr:latest",
|
|
omschrijving="Pakt gedownloade rar-archieven uit zodra ze klaar zijn, en ruimt de restanten op.",
|
|
tags=["downloaden", "automatisering"], cats=["downloaden"],
|
|
poorten=[], data=True, gebruiker=True, speciaal="unpackerr",
|
|
sectie="Ondertitels en verwerking"),
|
|
dict(dir="tdarr", naam="Tdarr", icon="tdarr",
|
|
image="ghcr.io/haveagitgat/tdarr:latest",
|
|
omschrijving="Zet je bibliotheek om naar een zuiniger formaat, verdeeld over meerdere machines als je wilt.",
|
|
tags=["media", "encoding"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8265, 8265, "tcp", ""),
|
|
("port_server", "Serverpoort", 8266, 8266, "tcp", "Waar de rekenknooppunten verbinding mee maken")],
|
|
lsio=True, data=True, speciaal="tdarr", sectie="Ondertitels en verwerking"),
|
|
|
|
# ── Verzoeken en gebruikers ─────────────────────────────────────────────
|
|
dict(dir="overseerr", naam="Overseerr", icon="overseerr",
|
|
image="sctx/overseerr:latest",
|
|
omschrijving="Laat huisgenoten films en series aanvragen, gekoppeld aan Plex. Voor Jellyfin en Emby gebruik je Jellyseerr.",
|
|
tags=["media", "verzoeken"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8120, 5055, "tcp", "Standaard 5055; hier verplaatst omdat Jellyseerr die al gebruikt")],
|
|
config_pad="/app/config", env_extra=["LOG_LEVEL=info"],
|
|
sectie="Verzoeken en gebruikers"),
|
|
dict(dir="wizarr", naam="Wizarr", icon="wizarr",
|
|
image="ghcr.io/wizarrrr/wizarr:latest",
|
|
omschrijving="Nodigt nieuwe gebruikers uit voor Plex, Jellyfin of Emby met een uitnodigingslink, inclusief uitleg voor de nieuwkomer.",
|
|
tags=["media", "gebruikers"], cats=["media", "beheer"],
|
|
poorten=[("port", "Webinterface", 5690, 5690, "tcp", "")],
|
|
config_pad="/data/database", sectie="Verzoeken en gebruikers"),
|
|
dict(dir="requestrr", naam="Requestrr", icon="requestrr",
|
|
image="thomst08/requestrr:latest",
|
|
omschrijving="Discord-bot waarmee je huisgenoten films en series kunnen aanvragen zonder een webinterface te openen.",
|
|
tags=["media", "verzoeken", "discord"], cats=["media", "communicatie"],
|
|
poorten=[("port", "Webinterface", 4545, 4545, "tcp", "")],
|
|
config_pad="/root/config", sectie="Verzoeken en gebruikers"),
|
|
dict(dir="suggestarr", naam="SuggestArr", icon="suggestarr",
|
|
image="ciuse99/suggestarr:latest",
|
|
omschrijving="Kijkt wat er gekeken is en zet automatisch aanbevelingen klaar in Sonarr en Radarr.",
|
|
tags=["media", "aanbevelingen"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8116, 5000, "tcp", "Standaard 5000; hier verplaatst omdat die poort al bezet is")],
|
|
config_pad="/app/config", sectie="Verzoeken en gebruikers"),
|
|
dict(dir="pulsarr", naam="Pulsarr", icon="pulsarr",
|
|
image="lakker/pulsarr:latest",
|
|
omschrijving="Koppelt Plex-watchlists aan Sonarr en Radarr: wat je op je lijst zet, wordt opgehaald.",
|
|
tags=["media", "watchlist"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 8117, 3003, "tcp", "Standaard 3003; hier verplaatst omdat die poort al bezet is")],
|
|
config_pad="/app/data", sectie="Verzoeken en gebruikers"),
|
|
dict(dir="prefetcharr", naam="Prefetcharr", icon="sonarr",
|
|
image="phueber/prefetcharr:latest",
|
|
omschrijving="Laat Sonarr het volgende seizoen ophalen zodra je aan de laatste afleveringen van het huidige begint.",
|
|
tags=["media", "series", "automatisering"], cats=["media"],
|
|
poorten=[], config_pad="/config", sectie="Verzoeken en gebruikers"),
|
|
|
|
# ── Opruimen en onderhoud ───────────────────────────────────────────────
|
|
dict(dir="cleanuparr", naam="Cleanuparr", icon="cleanuparr",
|
|
image="ghcr.io/cleanuparr/cleanuparr:latest",
|
|
omschrijving="Ruimt dode en kwaadaardige torrents op, en downloads waar niemand meer naar verwijst.",
|
|
tags=["downloaden", "onderhoud"], cats=["downloaden", "beheer"],
|
|
poorten=[("port", "Webinterface", 11011, 11011, "tcp", "")],
|
|
data=True, config_pad="/config", sectie="Opruimen en onderhoud"),
|
|
dict(dir="decluttarr", naam="Decluttarr", icon="decluttarr",
|
|
image="ghcr.io/manimatter/decluttarr:latest",
|
|
omschrijving="Houdt de downloadwachtrij schoon: gooit weg wat vastloopt of niet meer nodig is.",
|
|
tags=["downloaden", "onderhoud"], cats=["downloaden", "beheer"],
|
|
poorten=[], speciaal="decluttarr", sectie="Opruimen en onderhoud"),
|
|
dict(dir="janitorr", naam="Janitorr", icon="janitorr",
|
|
image="ghcr.io/schaka/janitorr:native-stable",
|
|
omschrijving="Ruimt oude media op voordat je schijf volloopt, met regels per bibliotheek.",
|
|
tags=["media", "opruimen", "opslag"], cats=["media", "opslag"],
|
|
poorten=[("port", "Webinterface", 8978, 8978, "tcp", "")],
|
|
data=True, config_pad="/config", sectie="Opruimen en onderhoud"),
|
|
dict(dir="maintainerr", naam="Maintainerr", icon="maintainerr",
|
|
image="ghcr.io/maintainerr/maintainerr:latest",
|
|
omschrijving="Ruimt media op volgens regels die je zelf samenstelt, met een voorvertoning van wat er weg zou gaan.",
|
|
tags=["media", "opruimen"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 6246, 6246, "tcp", "")],
|
|
gebruiker=True, config_pad="/opt/data", sectie="Opruimen en onderhoud"),
|
|
dict(dir="cross-seed", naam="cross-seed", icon="cross-seed",
|
|
image="ghcr.io/cross-seed/cross-seed:latest",
|
|
omschrijving="Zoekt dezelfde bestanden bij andere trackers, zodat je één download op meerdere plekken kunt seeden.",
|
|
tags=["torrents", "downloaden"], cats=["downloaden"],
|
|
poorten=[("port", "Poort", 2468, 2468, "tcp", "")],
|
|
data=True, config_pad="/config", commando="daemon",
|
|
sectie="Opruimen en onderhoud"),
|
|
|
|
# ── Instellingen synchroniseren ─────────────────────────────────────────
|
|
dict(dir="recyclarr", naam="Recyclarr", icon="recyclarr",
|
|
image="ghcr.io/recyclarr/recyclarr:latest",
|
|
omschrijving="Zet de kwaliteitsprofielen uit de TRaSH-gidsen in Sonarr en Radarr, en houdt ze bij.",
|
|
tags=["media", "configuratie"], cats=["media", "beheer"],
|
|
poorten=[], gebruiker=True, config_pad="/config",
|
|
sectie="Instellingen synchroniseren"),
|
|
dict(dir="configarr", naam="Configarr", icon="configarr",
|
|
image="ghcr.io/raydak-labs/configarr:latest",
|
|
omschrijving="Zelfde idee als Recyclarr, maar met eigen aanpassingen bovenop de TRaSH-gidsen.",
|
|
tags=["media", "configuratie"], cats=["media", "beheer"],
|
|
poorten=[], config_pad="/app/config", sectie="Instellingen synchroniseren"),
|
|
dict(dir="profilarr", naam="Profilarr", icon="profilarr",
|
|
image="santiagosayshey/profilarr:latest",
|
|
omschrijving="Beheert kwaliteitsprofielen en custom formats met versiebeheer, en deelt ze tussen instanties.",
|
|
tags=["media", "configuratie"], cats=["media", "beheer"],
|
|
poorten=[("port", "Webinterface", 6868, 6868, "tcp", "")],
|
|
config_pad="/config", sectie="Instellingen synchroniseren"),
|
|
|
|
# ── Metadata en artwork ─────────────────────────────────────────────────
|
|
dict(dir="kometa", naam="Kometa", icon="kometa",
|
|
image="kometateam/kometa:latest",
|
|
omschrijving="Bouwt collecties, overlays en metadata op in Plex, Jellyfin of Emby. Voorheen Plex Meta Manager.",
|
|
tags=["media", "metadata"], cats=["media"],
|
|
poorten=[], data=True, config_pad="/config",
|
|
env_extra=["KOMETA_RUN=false"], sectie="Metadata en artwork"),
|
|
dict(dir="posterizarr", naam="Posterizarr", icon="posterizarr",
|
|
image="ghcr.io/fscorrupt/posterizarr:latest",
|
|
omschrijving="Maakt automatisch posters en titelkaarten voor je bibliotheek, in een stijl die je zelf instelt.",
|
|
tags=["media", "artwork"], cats=["media"],
|
|
poorten=[], data=True, config_pad="/config",
|
|
env_extra=["RUN_TIME=disabled"], sectie="Metadata en artwork"),
|
|
dict(dir="trailarr", naam="Trailarr", icon="trailarr",
|
|
image="nandyalu/trailarr:latest",
|
|
omschrijving="Haalt trailers op bij de films en series in Radarr en Sonarr, en zet ze naast je media.",
|
|
tags=["media", "trailers"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 7889, 7889, "tcp", "")],
|
|
lsio=True, data=True, config_pad="/config", sectie="Metadata en artwork"),
|
|
|
|
# ── Statistiek en meldingen ─────────────────────────────────────────────
|
|
dict(dir="tautulli", naam="Tautulli", icon="tautulli",
|
|
image="lscr.io/linuxserver/tautulli:latest",
|
|
omschrijving="Laat zien wie wat kijkt op je Plex-server, met grafieken en meldingen.",
|
|
tags=["media", "statistiek", "monitoring"], cats=["media", "monitoring"],
|
|
poorten=[("port", "Webinterface", 8181, 8181, "tcp", "")],
|
|
lsio=True, sectie="Statistiek en meldingen"),
|
|
dict(dir="notifiarr", naam="Notifiarr", icon="notifiarr",
|
|
image="golift/notifiarr:latest",
|
|
omschrijving="Stuurt meldingen van je *arr-apps naar Discord, en houdt tegelijk je server in de gaten.",
|
|
tags=["meldingen", "monitoring", "discord"], cats=["communicatie", "monitoring"],
|
|
poorten=[("port", "Webinterface", 5454, 5454, "tcp", "")],
|
|
speciaal="notifiarr", config_pad="/config", sectie="Statistiek en meldingen"),
|
|
dict(dir="exportarr", naam="Exportarr", icon="prometheus",
|
|
image="ghcr.io/onedr0p/exportarr:latest",
|
|
omschrijving="Zet de cijfers van één *arr-app om in Prometheus-metrics, zodat je er in Grafana naar kunt kijken.",
|
|
tags=["monitoring", "metrics"], cats=["monitoring"],
|
|
poorten=[("port", "Metrics-poort", 9707, 9707, "tcp", "")],
|
|
speciaal="exportarr", sectie="Statistiek en meldingen"),
|
|
dict(dir="autopulse", naam="Autopulse", icon="autopulse",
|
|
image="ghcr.io/dan-online/autopulse:latest",
|
|
omschrijving="Laat Plex of Jellyfin meteen een nieuwe aflevering zien, in plaats van te wachten op de volgende scan.",
|
|
tags=["media", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Poort", 2875, 2875, "tcp", "")],
|
|
config_pad="/app/config", sectie="Statistiek en meldingen"),
|
|
|
|
# ── Muziek ──────────────────────────────────────────────────────────────
|
|
dict(dir="soularr", naam="Soularr", icon="soulseek",
|
|
image="mrusse08/soularr:latest",
|
|
omschrijving="Laat Lidarr zoeken via Soulseek (slskd) voor albums die op de gewone indexers niet te vinden zijn.",
|
|
tags=["muziek", "downloaden"], cats=["downloaden", "media"],
|
|
poorten=[], data=True, config_pad="/data", sectie="Muziek"),
|
|
dict(dir="neutarr", naam="NeutArr", icon="huntarr",
|
|
image="iampuid0/neutarr:latest",
|
|
omschrijving="Zoekt gericht naar ontbrekende afleveringen en naar wat beter kan. Opvolger van Huntarr, dat in februari 2026 offline ging na een reeks lekken; dit is de fork van de laatste schone versie.",
|
|
tags=["media", "automatisering"], cats=["media"],
|
|
poorten=[("port", "Webinterface", 9705, 9705, "tcp", "")],
|
|
sectie="Opruimen en onderhoud"),
|
|
dict(dir="homarr", naam="Homarr", icon="homarr",
|
|
image="ghcr.io/homarr-labs/homarr:latest",
|
|
omschrijving="Startpagina die je *arr-apps, downloadclients en mediaserver op één scherm laat zien, met live cijfers per app.",
|
|
tags=["dashboard", "overzicht"], cats=["beheer"],
|
|
poorten=[("port", "Webinterface", 7575, 7575, "tcp", "")],
|
|
config_pad="/appdata", speciaal="homarr",
|
|
sectie="Statistiek en meldingen"),
|
|
dict(dir="jellystat", naam="Jellystat", icon="jellystat",
|
|
image="cyfershepard/jellystat:latest",
|
|
omschrijving="Kijkstatistieken voor Jellyfin: wie keek wat, hoe lang en hoe vaak. Brengt zijn eigen database mee.",
|
|
tags=["media", "statistiek", "monitoring"], cats=["media", "monitoring"],
|
|
poorten=[("port", "Webinterface", 8118, 3000, "tcp", "Standaard 3000; hier verplaatst omdat die poort al bezet is")],
|
|
config_pad="/app/backend/backup-data", speciaal="jellystat",
|
|
sectie="Statistiek en meldingen"),
|
|
]
|
|
|
|
|
|
HELP_DATA = HELP
|
|
|
|
def _velden(app):
|
|
"""De variabelengroepen van één sjabloon."""
|
|
items = [{"name": "service_name", "type": "str", "title": "Servicenaam",
|
|
"default": app["dir"].replace("-", ""), "required": True}]
|
|
for veld, titel, host, _c, _p, oms in app["poorten"]:
|
|
it = {"name": veld, "type": "int", "title": titel,
|
|
"default": host, "required": True}
|
|
if oms:
|
|
it["description"] = oms
|
|
items.append(it)
|
|
items.append({"name": "appdata_dir", "type": "str", "title": "Appdata-map",
|
|
"default": APPDATA, "required": True,
|
|
"description": "Basismap voor de instellingen van deze app"})
|
|
if app.get("data"):
|
|
items.append({
|
|
"name": "data_root", "type": "str", "title": "Gedeelde datamap",
|
|
"default": DATAROOT, "required": True,
|
|
"description": "Eén map met je media én je downloads, gekoppeld als /data",
|
|
"help": HELP,
|
|
})
|
|
items.append({"name": "timezone", "type": "str", "title": "Tijdzone",
|
|
"default": TZ, "required": True})
|
|
groepen = [{"title": "Algemeen", "items": items}]
|
|
if app.get("lsio") or app.get("gebruiker"):
|
|
groepen.append({"title": "Gebruiker", "items": [
|
|
{"name": "puid", "type": "int", "title": "Gebruikers-ID (PUID)",
|
|
"default": 1000, "required": True,
|
|
"description": "`id -u` op je server; bepaalt de eigenaar van de bestanden"},
|
|
{"name": "pgid", "type": "int", "title": "Groeps-ID (PGID)",
|
|
"default": 1000, "required": True},
|
|
]})
|
|
groepen.extend(app.get("extra_groepen", []))
|
|
return groepen
|
|
|
|
|
|
def _compose(app):
|
|
"""Het compose-bestand van één sjabloon."""
|
|
r = ["services:", " << service_name >>:",
|
|
f" image: {app['image']}",
|
|
" container_name: << service_name >>"]
|
|
env = []
|
|
if app.get("lsio") or app.get("gebruiker"):
|
|
env += ["PUID=<< puid >>", "PGID=<< pgid >>"]
|
|
env.append("TZ=<< timezone >>")
|
|
if app.get("lsio"):
|
|
# Zonder group-write kunnen de andere *arr-apps niet bij de bestanden
|
|
# die deze aanmaakt.
|
|
env.append("UMASK=002")
|
|
env += app.get("env_extra", [])
|
|
env += app.get("env_speciaal", [])
|
|
if env:
|
|
r.append(" environment:")
|
|
r += [f" - {e}" for e in env]
|
|
vols = [f" - << appdata_dir >>/<< service_name >>:{app.get('config_pad', '/config')}"]
|
|
if app.get("data"):
|
|
vols.append(" - << data_root >>:/data")
|
|
vols += app.get("vol_extra", [])
|
|
r.append(" volumes:")
|
|
r += vols
|
|
if app.get("commando"):
|
|
r.append(f" command: {app['commando']}")
|
|
if app["poorten"]:
|
|
r.append(" ports:")
|
|
for veld, _t, _h, container, proto, _o in app["poorten"]:
|
|
if proto in ("tcp", "both"):
|
|
r.append(f' - "<< {veld} >>:{container}"')
|
|
if proto in ("udp", "both"):
|
|
r.append(f' - "<< {veld} >>:{container}/udp"')
|
|
r += app.get("compose_extra", [])
|
|
r.append(" restart: unless-stopped")
|
|
r += app.get("extra_diensten", [])
|
|
return "\n".join(r) + "\n"
|
|
|
|
|
|
# ── Sjablonen met een eigen vorm ─────────────────────────────────────────────
|
|
|
|
def _speciaal(app):
|
|
"""Aanvullingen voor apps die niet in het standaardpatroon passen."""
|
|
s = app.get("speciaal")
|
|
if s == "unpackerr":
|
|
app["extra_groepen"] = [{"title": "Koppelingen", "items": [
|
|
{"name": "sonarr_url", "type": "str", "title": "Sonarr",
|
|
"default": "http://sonarr:8989", "required": False,
|
|
"connect": {"app": "sonarr", "scheme": "http", "port": 8989},
|
|
"help": "Het adres waarop Unpackerr Sonarr kan bereiken. Draaien "
|
|
"ze allebei op hetzelfde gedeelde netwerk, dan is de "
|
|
"containernaam genoeg."},
|
|
{"name": "sonarr_apikey", "type": "str", "title": "Sonarr API-sleutel",
|
|
"default": "", "required": False, "secret": True,
|
|
"description": "Te vinden in Sonarr onder Settings → General"},
|
|
{"name": "radarr_url", "type": "str", "title": "Radarr",
|
|
"default": "http://radarr:7878", "required": False,
|
|
"connect": {"app": "radarr", "scheme": "http", "port": 7878},
|
|
"help": "Het adres waarop Unpackerr Radarr kan bereiken."},
|
|
{"name": "radarr_apikey", "type": "str", "title": "Radarr API-sleutel",
|
|
"default": "", "required": False, "secret": True,
|
|
"description": "Te vinden in Radarr onder Settings → General"},
|
|
]}]
|
|
app["env_speciaal"] = [
|
|
"UN_SONARR_0_URL=<< sonarr_url >>",
|
|
"UN_SONARR_0_API_KEY=<< sonarr_apikey >>",
|
|
"UN_RADARR_0_URL=<< radarr_url >>",
|
|
"UN_RADARR_0_API_KEY=<< radarr_apikey >>",
|
|
]
|
|
elif s == "decluttarr":
|
|
app["extra_groepen"] = [{"title": "Koppelingen", "items": [
|
|
{"name": "sonarr_url", "type": "str", "title": "Sonarr",
|
|
"default": "http://sonarr:8989", "required": False,
|
|
"connect": {"app": "sonarr", "scheme": "http", "port": 8989},
|
|
"help": "Adres van Sonarr. Laat leeg als je Sonarr niet gebruikt."},
|
|
{"name": "sonarr_apikey", "type": "str", "title": "Sonarr API-sleutel",
|
|
"default": "", "required": False, "secret": True,
|
|
"description": "Te vinden in Sonarr onder Settings → General"},
|
|
{"name": "radarr_url", "type": "str", "title": "Radarr",
|
|
"default": "http://radarr:7878", "required": False,
|
|
"connect": {"app": "radarr", "scheme": "http", "port": 7878},
|
|
"help": "Adres van Radarr. Laat leeg als je Radarr niet gebruikt."},
|
|
{"name": "radarr_apikey", "type": "str", "title": "Radarr API-sleutel",
|
|
"default": "", "required": False, "secret": True,
|
|
"description": "Te vinden in Radarr onder Settings → General"},
|
|
]}]
|
|
app["env_speciaal"] = [
|
|
"SONARR_URL=<< sonarr_url >>", "SONARR_KEY=<< sonarr_apikey >>",
|
|
"RADARR_URL=<< radarr_url >>", "RADARR_KEY=<< radarr_apikey >>",
|
|
"LOG_LEVEL=INFO",
|
|
]
|
|
app["config_pad"] = "/config"
|
|
elif s == "exportarr":
|
|
app["extra_groepen"] = [{"title": "Welke app meten we?", "items": [
|
|
{"name": "arr_soort", "type": "enum", "title": "Soort",
|
|
"default": "sonarr", "required": True,
|
|
"config": {"options": ["sonarr", "radarr", "lidarr", "readarr", "prowlarr"]},
|
|
"description": "Eén Exportarr meet één app; draai er meerdere voor meerdere apps"},
|
|
{"name": "arr_url", "type": "str", "title": "Adres van die app",
|
|
"default": "http://sonarr:8989", "required": True,
|
|
"connect": {"app": "sonarr", "scheme": "http", "port": 8989},
|
|
"help": "Het adres waarop Exportarr de *arr-app kan bereiken."},
|
|
{"name": "arr_apikey", "type": "str", "title": "API-sleutel",
|
|
"default": "", "required": True, "secret": True,
|
|
"description": "Te vinden in de app onder Settings → General"},
|
|
]}]
|
|
app["env_speciaal"] = ["URL=<< arr_url >>", "APIKEY=<< arr_apikey >>",
|
|
"PORT=<< port >>"]
|
|
app["commando"] = "<< arr_soort >>"
|
|
app["config_pad"] = "/config"
|
|
elif s == "notifiarr":
|
|
app["env_speciaal"] = ["DN_API_KEY=<< notifiarr_apikey >>"]
|
|
app["extra_groepen"] = [{"title": "Toegang", "items": [
|
|
{"name": "notifiarr_apikey", "type": "str", "title": "Notifiarr API-sleutel",
|
|
"default": "", "required": True, "secret": True,
|
|
"description": "Haal je op in je profiel op notifiarr.com"},
|
|
]}]
|
|
elif s == "tdarr":
|
|
app["env_speciaal"] = ["serverIP=0.0.0.0", "serverPort=8266",
|
|
"webUIPort=8265", "internalNode=true"]
|
|
app["config_pad"] = "/app/server"
|
|
app["vol_extra"] = [" - << appdata_dir >>/<< service_name >>-configs:/app/configs"]
|
|
elif s == "gluetun":
|
|
app["extra_groepen"] = [{"title": "VPN", "items": [
|
|
{"name": "vpn_provider", "type": "str", "title": "VPN-aanbieder",
|
|
"default": "mullvad", "required": True,
|
|
"description": "Bijvoorbeeld mullvad, nordvpn, protonvpn, airvpn of surfshark"},
|
|
{"name": "vpn_type", "type": "enum", "title": "Protocol",
|
|
"default": "wireguard", "required": True,
|
|
"config": {"options": ["wireguard", "openvpn"]}},
|
|
{"name": "vpn_landen", "type": "str", "title": "Landen",
|
|
"default": "Netherlands", "required": False,
|
|
"description": "Komma-gescheiden; leeg laten kiest willekeurig"},
|
|
{"name": "wireguard_private_key", "type": "str", "title": "WireGuard privésleutel",
|
|
"default": "", "required": False, "secret": True,
|
|
"needs": [{"name": "vpn_type", "value": "wireguard"}],
|
|
"config": {"placeholder": "alleen bij wireguard"}},
|
|
{"name": "wireguard_addresses", "type": "str", "title": "WireGuard adressen",
|
|
"default": "", "required": False,
|
|
"needs": [{"name": "vpn_type", "value": "wireguard"}],
|
|
"config": {"placeholder": "10.64.0.2/32"}},
|
|
{"name": "openvpn_user", "type": "str", "title": "OpenVPN gebruiker",
|
|
"default": "", "required": False,
|
|
"needs": [{"name": "vpn_type", "value": "openvpn"}]},
|
|
{"name": "openvpn_password", "type": "str", "title": "OpenVPN wachtwoord",
|
|
"default": "", "required": False, "secret": True,
|
|
"needs": [{"name": "vpn_type", "value": "openvpn"}]},
|
|
]}]
|
|
app["env_speciaal"] = [
|
|
"VPN_SERVICE_PROVIDER=<< vpn_provider >>",
|
|
"VPN_TYPE=<< vpn_type >>",
|
|
"SERVER_COUNTRIES=<< vpn_landen >>",
|
|
"WIREGUARD_PRIVATE_KEY=<< wireguard_private_key >>",
|
|
"WIREGUARD_ADDRESSES=<< wireguard_addresses >>",
|
|
"OPENVPN_USER=<< openvpn_user >>",
|
|
"OPENVPN_PASSWORD=<< openvpn_password >>",
|
|
]
|
|
app["config_pad"] = "/gluetun"
|
|
app["compose_extra"] = [
|
|
" cap_add:", " - NET_ADMIN",
|
|
" devices:", " - /dev/net/tun:/dev/net/tun",
|
|
]
|
|
elif s == "homarr":
|
|
app["extra_groepen"] = [{"title": "Toegang", "items": [
|
|
{"name": "homarr_encryption_key", "type": "str",
|
|
"title": "Versleutelingssleutel", "default": "", "required": True,
|
|
"secret": True, "generate": "auto",
|
|
# Homarr accepteert alleen 64 hexadecimale tekens; een gewone
|
|
# willekeurige string weigert hij bij het opstarten.
|
|
"secret_format": "hex", "secret_bytes": 32,
|
|
"description": "64 tekens; versleutelt de opgeslagen API-sleutels van je apps"},
|
|
]}]
|
|
app["env_speciaal"] = ["SECRET_ENCRYPTION_KEY=<< homarr_encryption_key >>"]
|
|
elif s == "jellystat":
|
|
app["extra_groepen"] = [{"title": "Database", "items": [
|
|
{"name": "db_password", "type": "str", "title": "Databasewachtwoord",
|
|
"default": "", "required": True, "secret": True, "generate": "auto"},
|
|
{"name": "jwt_secret", "type": "str", "title": "JWT-sleutel",
|
|
"default": "", "required": True, "secret": True, "generate": "auto",
|
|
"description": "Ondertekent de inlogsessies"},
|
|
]}]
|
|
app["env_speciaal"] = [
|
|
"POSTGRES_USER=jellystat",
|
|
"POSTGRES_PASSWORD=<< db_password >>",
|
|
"POSTGRES_IP=<< service_name >>-db",
|
|
"POSTGRES_PORT=5432",
|
|
"JWT_SECRET=<< jwt_secret >>",
|
|
]
|
|
app["extra_diensten"] = [
|
|
" << service_name >>-db:",
|
|
" image: postgres:16-alpine",
|
|
" container_name: << service_name >>-db",
|
|
" environment:",
|
|
" - POSTGRES_USER=jellystat",
|
|
" - POSTGRES_PASSWORD=<< db_password >>",
|
|
" - POSTGRES_DB=jfstat",
|
|
" volumes:",
|
|
" - << appdata_dir >>/<< service_name >>-db:/var/lib/postgresql/data",
|
|
" restart: unless-stopped",
|
|
]
|
|
app["compose_extra"] = [" depends_on:", " - << service_name >>-db"]
|
|
return app
|
|
|
|
|
|
def schrijf_los(app):
|
|
app = _speciaal(dict(app))
|
|
if True:
|
|
d = APPS / app["dir"]
|
|
(d / "files").mkdir(parents=True, exist_ok=True)
|
|
tmpl = {
|
|
"kind": "compose",
|
|
"metadata": {
|
|
"name": app["naam"],
|
|
"description": app["omschrijving"],
|
|
"tags": app["tags"],
|
|
"icon": {"provider": "selfhst", "id": app["icon"]},
|
|
"version": {"name": "latest"},
|
|
"categories": app["cats"],
|
|
},
|
|
"variables": _velden(app),
|
|
}
|
|
(d / "template.json").write_text(
|
|
json.dumps(tmpl, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
(d / "files" / "compose.yaml").write_text(_compose(app), encoding="utf-8")
|
|
|
|
|
|
# ── De gecombineerde ARR-stack ───────────────────────────────────────────────
|
|
# Onderdelen die al als los sjabloon bestonden en dus niet in SPEC staan.
|
|
BESTAAND = [
|
|
dict(dir="qbittorrent", naam="qBittorrent",
|
|
image="lscr.io/linuxserver/qbittorrent:latest",
|
|
omschrijving="Torrentclient met webinterface. De meest gebruikte keuze naast de *arr-apps.",
|
|
poorten=[("port", "Webinterface", 8097, 8090, "tcp", ""),
|
|
("port_torrent", "Inkomende poort", 6881, 6881, "both", "")],
|
|
lsio=True, data=True, client=True, sectie="Downloadclients",
|
|
env_extra=["WEBUI_PORT=8090"]),
|
|
dict(dir="sabnzbd", naam="SABnzbd",
|
|
image="lscr.io/linuxserver/sabnzbd:latest",
|
|
omschrijving="Usenet-downloader met webinterface.",
|
|
poorten=[("port", "Webinterface", 8098, 8080, "tcp", "")],
|
|
lsio=True, data=True, client=True, sectie="Downloadclients"),
|
|
dict(dir="jellyseerr", naam="Jellyseerr",
|
|
image="fallenbagel/jellyseerr:latest",
|
|
omschrijving="Laat huisgenoten films en series aanvragen, gekoppeld aan Jellyfin, Emby of Plex.",
|
|
poorten=[("port", "Webinterface", 5055, 5055, "tcp", "")],
|
|
config_pad="/app/config", env_extra=["LOG_LEVEL=info"],
|
|
sectie="Verzoeken en gebruikers"),
|
|
]
|
|
|
|
# Containers die achter de VPN gaan zodra Gluetun aan staat.
|
|
CLIENTS = ("qbittorrent", "sabnzbd", "nzbget", "deluge", "transmission", "slskd")
|
|
|
|
VOLGORDE = ["Bibliotheken", "Indexers", "Downloadclients",
|
|
"Ondertitels en verwerking", "Verzoeken en gebruikers",
|
|
"Opruimen en onderhoud", "Instellingen synchroniseren",
|
|
"Metadata en artwork", "Statistiek en meldingen", "Muziek"]
|
|
|
|
# Standaard aan: genoeg om meteen series en films binnen te halen.
|
|
STANDAARD_AAN = {"sonarr", "radarr", "prowlarr", "bazarr", "qbittorrent",
|
|
"jellyseerr"}
|
|
|
|
|
|
def onderdelen():
|
|
uit = [_speciaal(dict(a)) for a in SPEC] + [_speciaal(dict(a)) for a in BESTAAND]
|
|
uit.sort(key=lambda a: (VOLGORDE.index(a["sectie"]), a["dir"]))
|
|
return uit
|
|
|
|
|
|
def _naam(app):
|
|
return app["dir"].replace("-", "")
|
|
|
|
|
|
def _poortveld(app, veld):
|
|
"""Veldnaam in de stack voor een poortveld uit het losse sjabloon."""
|
|
achter = "" if veld == "port" else "_" + veld[len("port_"):]
|
|
return f"port_{_naam(app)}{achter}"
|
|
|
|
|
|
def _hernoem(regels, app):
|
|
"""Verwijzingen uit het losse sjabloon omzetten naar de stack.
|
|
|
|
Twee dingen verschuiven. Poortvelden: los heet Exportarrs poort `port`, in de
|
|
stack `port_exportarr` — zonder vertaling rendert dat leeg. En verwijzingen
|
|
naar een buurcontainer: los heet de database van Jellystat
|
|
`<service_name>-db`, in de stack `<service_name>-jellystat-db`.
|
|
"""
|
|
naam = _naam(app)
|
|
uit = []
|
|
for r in regels:
|
|
for veld, *_ in app["poorten"]:
|
|
r = r.replace(f"<< {veld} >>", f"<< {_poortveld(app, veld)} >>")
|
|
r = r.replace("<< service_name >>-", f"<< service_name >>-{naam}-")
|
|
uit.append(r)
|
|
return uit
|
|
|
|
|
|
def stack_template():
|
|
algemeen = [
|
|
{"name": "service_name", "type": "str", "title": "Naam van de stack",
|
|
"default": "arr", "required": True,
|
|
"description": "Wordt het voorvoegsel van elke container, bv. arr-sonarr"},
|
|
{"name": "appdata_dir", "type": "str", "title": "Appdata-map",
|
|
"default": APPDATA, "required": True,
|
|
"description": "Basismap voor de instellingen van elk onderdeel"},
|
|
{"name": "data_root", "type": "str", "title": "Gedeelde datamap",
|
|
"default": DATAROOT, "required": True,
|
|
"description": "Eén map met je media én je downloads, gekoppeld als /data",
|
|
"help": HELP_DATA},
|
|
{"name": "timezone", "type": "str", "title": "Tijdzone",
|
|
"default": TZ, "required": True},
|
|
{"name": "puid", "type": "int", "title": "Gebruikers-ID (PUID)",
|
|
"default": 1000, "required": True,
|
|
"description": "`id -u` op je server; bepaalt de eigenaar van de bestanden"},
|
|
{"name": "pgid", "type": "int", "title": "Groeps-ID (PGID)",
|
|
"default": 1000, "required": True},
|
|
]
|
|
groepen = [{"title": "Algemeen", "items": algemeen}]
|
|
for app in onderdelen():
|
|
items = []
|
|
for veld, titel, host, _c, _p, oms in app["poorten"]:
|
|
it = {"name": _poortveld(app, veld), "type": "int", "title": titel,
|
|
"default": host, "required": True,
|
|
# De poorten staan al goed; ze horen niet in de weg te zitten
|
|
# als je vijftig onderdelen doorloopt.
|
|
"advanced": True, "step": "instellingen"}
|
|
if oms:
|
|
it["description"] = oms
|
|
items.append(it)
|
|
# Eigen velden van een onderdeel (Gluetun, Unpackerr, …) komen mee uit
|
|
# het losse sjabloon, met dezelfde namen.
|
|
for grp in app.get("extra_groepen", []):
|
|
for it in grp.get("items", []):
|
|
items.append(dict(it))
|
|
groepen.append({
|
|
"title": app["naam"],
|
|
"section": app["sectie"],
|
|
"toggle": f"enable_{_naam(app)}",
|
|
"toggle_default": app["dir"] in STANDAARD_AAN,
|
|
"description": app["omschrijving"],
|
|
"items": items,
|
|
})
|
|
return {
|
|
"kind": "compose",
|
|
"metadata": {
|
|
"name": "ARR-stack",
|
|
"description": (
|
|
"Alles voor media-automatisering in één keer, met een schakelaar "
|
|
"per onderdeel: de *arr-apps, indexers, downloadclients, "
|
|
"ondertitels, verzoeken, opruimen en statistiek. Elk onderdeel "
|
|
"bestaat ook als losse app in de store; hier zet je in één keer "
|
|
"neer wat je nodig hebt, op één gedeelde /data-map zodat "
|
|
"hardlinks werken."),
|
|
"tags": ["media", "automatisering", "films", "series", "downloaden"],
|
|
"icon": {"provider": "selfhst", "id": "sonarr"},
|
|
"version": {"name": "latest"},
|
|
"categories": ["media", "downloaden"],
|
|
},
|
|
"variables": groepen,
|
|
}
|
|
|
|
|
|
def _stack_dienst(app):
|
|
"""Het compose-blok van één onderdeel binnen de stack."""
|
|
naam = _naam(app)
|
|
achter_vpn = app["dir"] in CLIENTS
|
|
r = [f" << service_name >>-{naam}:",
|
|
f" image: {app['image']}",
|
|
f" container_name: << service_name >>-{naam}"]
|
|
env = []
|
|
if app.get("lsio") or app.get("gebruiker"):
|
|
env += ["PUID=<< puid >>", "PGID=<< pgid >>"]
|
|
env.append("TZ=<< timezone >>")
|
|
if app.get("lsio"):
|
|
env.append("UMASK=002")
|
|
env += _hernoem(app.get("env_extra", []) + app.get("env_speciaal", []), app)
|
|
if env:
|
|
r.append(" environment:")
|
|
r += [f" - {e}" for e in env]
|
|
r.append(" volumes:")
|
|
r.append(f" - << appdata_dir >>/<< service_name >>/{naam}:"
|
|
f"{app.get('config_pad', '/config')}")
|
|
if app.get("data"):
|
|
r.append(" - << data_root >>:/data")
|
|
for extra in app.get("vol_extra", []):
|
|
r.append(extra.replace("<< service_name >>", f"<< service_name >>/{naam}"))
|
|
if app.get("commando"):
|
|
r += _hernoem([f" command: {app['commando']}"], app)
|
|
|
|
|
|
poorten = _hernoem([f' - "<< {veld} >>:{container}"'
|
|
+ ("" if proto != "udp" else "")
|
|
for veld, _t, _h, container, proto, _o in app["poorten"]
|
|
for _ in [0]], app)
|
|
poorten = []
|
|
for veld, _t, _h, container, proto, _o in app["poorten"]:
|
|
v = _poortveld(app, veld)
|
|
if proto in ("tcp", "both"):
|
|
poorten.append(f' - "<< {v} >>:{container}"')
|
|
if proto in ("udp", "both"):
|
|
poorten.append(f' - "<< {v} >>:{container}/udp"')
|
|
|
|
if achter_vpn:
|
|
# Achter de VPN deelt de client de netwerknamespace van Gluetun; zijn
|
|
# poorten staan daar, en `ports` naast `network_mode` weigert compose.
|
|
r.append("<%- if enable_gluetun %>")
|
|
r.append(' network_mode: "service:<< service_name >>-gluetun"')
|
|
r.append(" depends_on:")
|
|
r.append(" - << service_name >>-gluetun")
|
|
r.append("<%- endif %>")
|
|
if poorten:
|
|
r.append("<%- if not enable_gluetun %>")
|
|
r.append(" ports:")
|
|
r += poorten
|
|
r.append("<%- endif %>")
|
|
elif poorten:
|
|
r.append(" ports:")
|
|
r += poorten
|
|
r += _hernoem(app.get("compose_extra", []), app)
|
|
r.append(" restart: unless-stopped")
|
|
# Een onderdeel mag meer dan één container zijn; Jellystat brengt zijn eigen
|
|
# database mee. Die krijgt hetzelfde voorvoegsel en dezelfde schakelaar.
|
|
r += _hernoem(app.get("extra_diensten", []), app)
|
|
return r
|
|
|
|
|
|
def stack_compose():
|
|
r = ["# ARR-stack — kies per onderdeel of je het wilt.",
|
|
"#",
|
|
"# Alles deelt één map, gekoppeld als /data. Dat is geen smaakkwestie:",
|
|
"# staan je downloads en je bibliotheek op verschillende mounts, dan kan",
|
|
"# Sonarr geen hardlink maken en kopieert elke voltooide download naar je",
|
|
"# bibliotheek. Traag, en je hebt alles tijdelijk dubbel op de schijf.",
|
|
"#",
|
|
"# /data/media/{films,series,muziek,boeken} je bibliotheek",
|
|
"# /data/torrents/ torrents (blijven seeden)",
|
|
"# /data/usenet/ usenet-downloads",
|
|
"#",
|
|
"# Draai je met Gluetun, dan zitten de downloadclients in het netwerk van",
|
|
"# die container en bereik je hun webinterface via de Gluetun-poorten.",
|
|
"services:", ""]
|
|
for app in onderdelen():
|
|
if app["dir"] == "gluetun":
|
|
continue # die krijgt onderaan een eigen blok
|
|
r.append(f"<%- if enable_{_naam(app)} %>")
|
|
r += _stack_dienst(app)
|
|
r.append("<%- endif %>")
|
|
r.append("")
|
|
r += _gluetun_blok()
|
|
return "\n".join(r) + "\n"
|
|
|
|
|
|
def _gluetun_blok():
|
|
"""Gluetun publiceert de webinterfaces van elke client die erachter zit."""
|
|
g = next(a for a in onderdelen() if a["dir"] == "gluetun")
|
|
r = ["<%- if enable_gluetun %>"] + _stack_dienst(g)
|
|
r.remove(" restart: unless-stopped")
|
|
r.append(" ports:")
|
|
r.append(" # De downloadclients delen dit netwerk, dus hun webinterfaces")
|
|
r.append(" # staan hier. Inkomende torrentpoorten niet: die regelt je")
|
|
r.append(" # VPN-aanbieder met port forwarding.")
|
|
for app in onderdelen():
|
|
if app["dir"] not in CLIENTS:
|
|
continue
|
|
r.append(f"<%- if enable_{_naam(app)} %>")
|
|
for veld, _t, _h, container, proto, _o in app["poorten"]:
|
|
if veld != "port":
|
|
continue # inkomende poorten gaan niet via de host
|
|
r.append(f' - "<< {_poortveld(app, veld)} >>:{container}"')
|
|
r.append("<%- endif %>")
|
|
r.append(" restart: unless-stopped")
|
|
r.append("<%- endif %>")
|
|
return r
|
|
|
|
|
|
def schrijf_stack():
|
|
d = APPS / "arr-stack"
|
|
(d / "files").mkdir(parents=True, exist_ok=True)
|
|
(d / "template.json").write_text(
|
|
json.dumps(stack_template(), indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8")
|
|
(d / "files" / "compose.yaml").write_text(stack_compose(), encoding="utf-8")
|
|
return len(onderdelen())
|
|
|
|
|
|
def main():
|
|
for app in SPEC:
|
|
schrijf_los(app)
|
|
n = schrijf_stack()
|
|
print(f"{len(SPEC)} losse sjablonen en arr-stack ({n} onderdelen) geschreven")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|