Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ Add-on skills (activated when corresponding add-on is installed):
- `gotosocial.md` — GoToSocial ActivityPub microblog: post, follow, search, moderate (block_user/mute inline; defederate/block_domain/import_blocklist queued for operator confirmation), media prune, federation health
- `writefreely.md` — WriteFreely federated blog: create/update/publish/unpublish posts, list collections, fetch public posts, export; minimalist publisher (no comments, no moderation queue — WF is publish-oriented only)
- `matrix-dendrite.md` — Matrix homeserver on Dendrite: create/join/leave rooms, send messages, sync, invite users, federation health; appservice registration prep for F.12 bridges; :8448-vs-well-known either/or federation story
- `funkwhale.md` — Funkwhale federated music pod: library listing, search, upload, follow remote channels/libraries, playlists, listening history, moderation (block_user/mute inline; block_domain/defederate queued), media prune; on-disk or S3 audio storage via storage-translators.funkwhale()
- `calibre-server.md` — Calibre content server: search, browse, download ebooks via OPDS
- `calibre-web.md` — Calibre-Web reader: search, shelves, reading status, download
- `miniflux.md` — Miniflux RSS reader: subscribe feeds, read articles, star, mark read
Expand Down
197 changes: 197 additions & 0 deletions bundles/funkwhale/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Funkwhale — federated music server.
#
# Six-container bundle: api + celeryworker + celerybeat + nginx + postgres + redis.
# All on crow-federation + an internal default network. Caddy reverse-proxies
# :443 → funkwhale-nginx:80, which in turn serves static files and proxies
# /api + /federation + websockets to funkwhale-api:5000.
#
# Data:
# ~/.crow/funkwhale/postgres/ Postgres data dir
# ~/.crow/funkwhale/redis/ Redis persistence (optional AOF/RDB)
# ~/.crow/funkwhale/data/ Funkwhale /data (audio uploads, transcodes)
# ~/.crow/funkwhale/music/ Optional read-only in-place library (FUNKWHALE_MUSIC_DIR)
#
# Audio storage: on-disk by default. Set FUNKWHALE_S3_* env vars to route
# uploads to MinIO / external S3 — storage-translators.funkwhale() handles
# the env-var mapping to AWS_* names Funkwhale expects.
#
# Image: funkwhale/funkwhale:1.4 (pinned at impl time; verify upstream
# release notes + CVE feed before bumping).

networks:
crow-federation:
external: true
default:

services:
postgres:
image: postgres:15-alpine
container_name: crow-funkwhale-postgres
networks:
- default
environment:
POSTGRES_USER: funkwhale
POSTGRES_PASSWORD: ${FUNKWHALE_POSTGRES_PASSWORD}
POSTGRES_DB: funkwhale
volumes:
- ${FUNKWHALE_DATA_DIR:-~/.crow/funkwhale}/postgres:/var/lib/postgresql/data
init: true
mem_limit: 512m
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U funkwhale"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s

redis:
image: redis:7-alpine
container_name: crow-funkwhale-redis
networks:
- default
volumes:
- ${FUNKWHALE_DATA_DIR:-~/.crow/funkwhale}/redis:/data
init: true
mem_limit: 256m
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10

api:
image: funkwhale/funkwhale:1.4
container_name: crow-funkwhale-api
networks:
- default
- crow-federation
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
FUNKWHALE_HOSTNAME: ${FUNKWHALE_HOSTNAME}
FUNKWHALE_PROTOCOL: https
DJANGO_SECRET_KEY: ${FUNKWHALE_DJANGO_SECRET_KEY}
DATABASE_URL: postgresql://funkwhale:${FUNKWHALE_POSTGRES_PASSWORD}@postgres:5432/funkwhale
CACHE_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
DJANGO_ALLOWED_HOSTS: ${FUNKWHALE_HOSTNAME},funkwhale-api,api,localhost
FUNKWHALE_WEB_WORKERS: "2"
MEDIA_ROOT: /srv/funkwhale/data/media
STATIC_ROOT: /srv/funkwhale/data/static
MUSIC_DIRECTORY_PATH: /music
MUSIC_DIRECTORY_SERVE_PATH: /music
# S3 storage (activated when AWS_ACCESS_KEY_ID is non-empty — see
# scripts/configure-storage.mjs which writes the translated env vars
# into .env when FUNKWHALE_S3_* is configured at install time)
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
AWS_STORAGE_BUCKET_NAME: ${AWS_STORAGE_BUCKET_NAME:-}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-}
AWS_S3_REGION_NAME: ${AWS_S3_REGION_NAME:-us-east-1}
AWS_LOCATION: ${AWS_LOCATION:-}
AWS_QUERYSTRING_AUTH: ${AWS_QUERYSTRING_AUTH:-true}
AWS_QUERYSTRING_EXPIRE: ${AWS_QUERYSTRING_EXPIRE:-3600}
volumes:
- ${FUNKWHALE_DATA_DIR:-~/.crow/funkwhale}/data:/srv/funkwhale/data
- ${FUNKWHALE_MUSIC_DIR:-~/.crow/funkwhale/music}:/music:ro
command: >
sh -c "funkwhale-manage migrate --noinput &&
funkwhale-manage collectstatic --noinput &&
gunicorn config.asgi:application -w $${FUNKWHALE_WEB_WORKERS:-2}
-k uvicorn.workers.UvicornWorker -b 0.0.0.0:5000
--access-logfile - --error-logfile -"
init: true
mem_limit: 1500m
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5000/api/v1/instance/nodeinfo/2.0/ >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 120s

celeryworker:
image: funkwhale/funkwhale:1.4
container_name: crow-funkwhale-celeryworker
networks:
- default
depends_on:
api:
condition: service_healthy
environment:
FUNKWHALE_HOSTNAME: ${FUNKWHALE_HOSTNAME}
DJANGO_SECRET_KEY: ${FUNKWHALE_DJANGO_SECRET_KEY}
DATABASE_URL: postgresql://funkwhale:${FUNKWHALE_POSTGRES_PASSWORD}@postgres:5432/funkwhale
CACHE_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
C_FORCE_ROOT: "true"
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
AWS_STORAGE_BUCKET_NAME: ${AWS_STORAGE_BUCKET_NAME:-}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-}
AWS_S3_REGION_NAME: ${AWS_S3_REGION_NAME:-us-east-1}
volumes:
- ${FUNKWHALE_DATA_DIR:-~/.crow/funkwhale}/data:/srv/funkwhale/data
- ${FUNKWHALE_MUSIC_DIR:-~/.crow/funkwhale/music}:/music:ro
command: >
celery -A funkwhale_api.taskapp worker -l INFO --concurrency=${FUNKWHALE_CELERYD_CONCURRENCY:-2}
init: true
mem_limit: 768m
restart: unless-stopped

celerybeat:
image: funkwhale/funkwhale:1.4
container_name: crow-funkwhale-celerybeat
networks:
- default
depends_on:
api:
condition: service_healthy
environment:
FUNKWHALE_HOSTNAME: ${FUNKWHALE_HOSTNAME}
DJANGO_SECRET_KEY: ${FUNKWHALE_DJANGO_SECRET_KEY}
DATABASE_URL: postgresql://funkwhale:${FUNKWHALE_POSTGRES_PASSWORD}@postgres:5432/funkwhale
CACHE_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
C_FORCE_ROOT: "true"
volumes:
- ${FUNKWHALE_DATA_DIR:-~/.crow/funkwhale}/data:/srv/funkwhale/data
command: >
celery -A funkwhale_api.taskapp beat -l INFO
--schedule=/srv/funkwhale/data/celerybeat-schedule
init: true
mem_limit: 256m
restart: unless-stopped

nginx:
image: funkwhale/nginx:1.4
container_name: crow-funkwhale-nginx
networks:
- default
- crow-federation
depends_on:
api:
condition: service_healthy
environment:
FUNKWHALE_API_IP: api
FUNKWHALE_API_PORT: "5000"
FUNKWHALE_HOSTNAME: ${FUNKWHALE_HOSTNAME}
FUNKWHALE_PROTOCOL: https
NGINX_MAX_BODY_SIZE: ${FUNKWHALE_NGINX_MAX_BODY_SIZE:-100M}
volumes:
- ${FUNKWHALE_DATA_DIR:-~/.crow/funkwhale}/data:/srv/funkwhale/data:ro
- ${FUNKWHALE_MUSIC_DIR:-~/.crow/funkwhale/music}:/music:ro
init: true
mem_limit: 128m
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/api/v1/instance/nodeinfo/2.0/ >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
48 changes: 48 additions & 0 deletions bundles/funkwhale/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"id": "funkwhale",
"name": "Funkwhale",
"version": "1.0.0",
"description": "Federated music server — self-hosted audio library + podcast streaming + fediverse-federated listening over ActivityPub. Upload your own library; follow remote channels and artists across the fediverse.",
"type": "bundle",
"author": "Crow",
"category": "federated-media",
"tags": ["music", "funkwhale", "activitypub", "fediverse", "federated", "audio", "podcasts"],
"icon": "music",
"docker": { "composefile": "docker-compose.yml" },
"server": {
"command": "node",
"args": ["server/index.js"],
"envKeys": ["FUNKWHALE_URL", "FUNKWHALE_ACCESS_TOKEN", "FUNKWHALE_HOSTNAME"]
},
"panel": "panel/funkwhale.js",
"panelRoutes": "panel/routes.js",
"skills": ["skills/funkwhale.md"],
"consent_required": true,
"install_consent_messages": {
"en": "Funkwhale joins the public fediverse over ActivityPub — your Funkwhale instance becomes addressable at the domain you configure, any library/channel you make public is discoverable and followable by remote Mastodon/GoToSocial/Pixelfed users, and published audio can be replicated to federated servers and cannot be fully recalled. Funkwhale stores your music library in Postgres metadata + on-disk (or S3) audio files; a modest library (1000 tracks) consumes 5-20 GB depending on format and bitrate. If you enable federation with remote pods, their library metadata is cached locally — this can grow to hundreds of MB. Funkwhale is hardware-gated: refused on hosts with <1.5 GB effective RAM after committed bundles; warns below 8 GB total host RAM. Uploading copyrighted material you don't have rights to is your legal responsibility — major fediverse hubs may defederate servers that become known for piracy.",
"es": "Funkwhale se une al fediverso público vía ActivityPub — tu instancia de Funkwhale será direccionable en el dominio que configures, cualquier biblioteca/canal que hagas público será descubrible y seguible por usuarios remotos de Mastodon/GoToSocial/Pixelfed, y el audio publicado puede replicarse a servidores federados y no puede recuperarse completamente. Funkwhale almacena tu biblioteca musical como metadatos en Postgres + archivos de audio en disco (o S3); una biblioteca modesta (1000 pistas) consume 5-20 GB según formato y bitrate. Si habilitas la federación con pods remotos, sus metadatos se cachean localmente — esto puede crecer a cientos de MB. Funkwhale está limitado por hardware: rechazado en hosts con <1.5 GB de RAM efectiva tras los paquetes comprometidos; advierte por debajo de 8 GB de RAM total. Subir material con copyright del que no tienes derechos es tu responsabilidad legal — los principales hubs del fediverso pueden dejar de federarse con servidores conocidos por piratería."
},
"requires": {
"env": ["FUNKWHALE_HOSTNAME", "FUNKWHALE_POSTGRES_PASSWORD", "FUNKWHALE_DJANGO_SECRET_KEY"],
"bundles": ["caddy"],
"min_ram_mb": 1500,
"recommended_ram_mb": 3000,
"min_disk_mb": 10000,
"recommended_disk_mb": 100000
},
"env_vars": [
{ "name": "FUNKWHALE_HOSTNAME", "description": "Public domain (e.g. music.example.com). Must be a subdomain; path-mounts break ActivityPub actor URLs.", "required": true },
{ "name": "FUNKWHALE_POSTGRES_PASSWORD", "description": "Password for the bundled Postgres role.", "required": true, "secret": true },
{ "name": "FUNKWHALE_DJANGO_SECRET_KEY", "description": "Django secret key (use 64+ random chars). Changing this invalidates existing sessions.", "required": true, "secret": true },
{ "name": "FUNKWHALE_URL", "description": "Internal URL the Crow MCP server uses to reach Funkwhale's API (over the crow-federation docker network).", "default": "http://funkwhale-api:5000", "required": false },
{ "name": "FUNKWHALE_ACCESS_TOKEN", "description": "OAuth2 / PAT access token for the admin account (create via Settings → Applications in the web UI, or POST /api/v1/oauth/apps + token exchange).", "required": false, "secret": true },
{ "name": "FUNKWHALE_S3_ENDPOINT", "description": "Optional S3-compatible endpoint for audio storage (defaults to on-disk). If set with FUNKWHALE_S3_BUCKET/ACCESS/SECRET, audio files go to S3 via the storage-translators mapping.", "required": false },
{ "name": "FUNKWHALE_S3_BUCKET", "description": "S3 bucket name for audio storage.", "required": false },
{ "name": "FUNKWHALE_S3_ACCESS_KEY", "description": "S3 access key.", "required": false, "secret": true },
{ "name": "FUNKWHALE_S3_SECRET_KEY", "description": "S3 secret key.", "required": false, "secret": true },
{ "name": "FUNKWHALE_S3_REGION", "description": "S3 region (default us-east-1).", "default": "us-east-1", "required": false }
],
"ports": [],
"webUI": null,
"notes": "Five containers (api + celeryworker + celerybeat + nginx + postgres + redis). No host port publish — expose via caddy_add_federation_site { domain: FUNKWHALE_HOSTNAME, upstream: 'funkwhale-nginx:80', profile: 'activitypub' }. Initial superuser created via `docker exec crow-funkwhale-api funkwhale-manage createsuperuser`. Audio storage defaults to on-disk; set FUNKWHALE_S3_* to wire MinIO/external S3 via the storage-translators funkwhale() mapping."
}
11 changes: 11 additions & 0 deletions bundles/funkwhale/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "crow-funkwhale",
"version": "1.0.0",
"description": "Funkwhale (federated music server) MCP server — library, upload, search, follow, playlists, moderation",
"type": "module",
"main": "server/index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.24.0"
}
}
Loading
Loading