# Funkwhale 2.0.11: a channel (an actor of its own) and its tracks. alice_funkwhale follows the channel; a track uploaded # to it arrives as Audio with its file and duration; its deletion; the unfollow. Funkwhale's API takes fwuser's OAuth # token; a track is a fresh tone made by ffmpeg in its own container (Funkwhale skips a file it already has); what it # holds is read from the shared Postgres (database funkwhale). FWB=https://funkwhale.test:6443 fw() { curl -sk --resolve funkwhale.test:6443:127.0.0.1 -H "Authorization: Bearer $(cat "$here/.state/funkwhale.token")" "$@"; } fw_sql() { podman exec pasture-postgres psql -U pasture -d funkwhale -tAc "$1" 2>/dev/null; } p_home_has() { curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next((o['id'] for o in ((s.get('reblog') or s) for s in d) if '$1' in (o['content'] or '') or '$1' in ((o.get('privapub') or {}).get('title') or '')), ''))"; } echo "funkwhale" [ -s "$here/.state/funkwhale.token" ] && ok "Funkwhale token for fwuser" || { ko "Funkwhale token"; return 1; } [ "$(fw -o /dev/null -w '%{http_code}' "$FWB/api/v2/users/me")" = "200" ] && ok "fwuser's token works" || { ko "fwuser's token is refused"; return 1; } PT=$(privapub_token alice_funkwhale) PH="Authorization: Bearer $PT" [ -n "$PT" ] && ok "PrivaPub token for alice_funkwhale" || { ko "PrivaPub token for alice_funkwhale"; return 1; } echo " the channel and its follower" fw -o /dev/null -X POST "$FWB/api/v1/channels/" -H 'Content-Type: application/json' -d '{"name":"Pasture sounds","username":"pasturesounds", "description":{"text":"sounds of the pasture","content_type":"text/markdown"},"tags":["pasture"],"content_category":"music"}' channel=$(fw "$FWB/api/v1/channels/?scope=me" | j "print(next(c['uuid'] for c in d['results'] if c['actor']['preferred_username'] == 'pasturesounds'))") [ -n "$channel" ] && ok "fwuser runs the channel pasturesounds" || ko "the channel could not be made" channel_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=pasturesounds@funkwhale.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") [ -n "$channel_on_p" ] && ok "PrivaPub resolves @pasturesounds@funkwhale.test" || ko "PrivaPub cannot resolve the channel" # a run before this one left the follow: unfollow first, so the follow below is a new request curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$channel_on_p/unfollow"; sleep 3 curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$channel_on_p/follow" # (Funkwhale names its Accept after our Follow, `/accept`, which PrivaPub takes since 2026-10-05) until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$channel_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \ && ok "alice_funkwhale follows the channel (Accept arrived)" || ko "the channel's Accept never arrived" echo " tracks" run=$(date +%s); title="A pasture tone $run" podman exec pasture-funkwhale-api ffmpeg -loglevel error -y -f lavfi -i "sine=frequency=$((300 + run % 500)):duration=4" -c:a libvorbis \ -metadata title="$title" -metadata artist="Pasture sounds" "/tmp/tone-$run.ogg" podman cp "pasture-funkwhale-api:/tmp/tone-$run.ogg" "$here/.state/funkwhale-tone.ogg" upload=$(fw -X POST "$FWB/api/v2/uploads/" -F "audio_file=@$here/.state/funkwhale-tone.ogg" -F "channel=$channel" -F import_status=pending \ -F "import_metadata={\"title\":\"$title\"}" | j "print(d['uuid'])") [ -n "$upload" ] && ok "fwuser uploads a track to the channel" || ko "the upload was refused" until_true 60 '[ -n "$(p_home_has "$title")" ]' && ok "the track reaches alice_funkwhale's home" || ko "the track never arrived" track_on_p=$(p_home_has "$title") track=$(curl -s -H "$PH" "$P/api/v1/statuses/$track_on_p") [ "$(echo "$track" | j "p = d.get('privapub') or {}; print(p.get('object_type'))")" = "Audio" ] && ok "it arrives as Audio" || ko "the track is not Audio" echo "$track" | j "a = (d.get('privapub') or {}).get('audio') or {}; print(bool(a.get('url') or a.get('variants')) and (a.get('duration') or 0) > 3)" | grep -q True \ && ok "with its file and its duration" || ko "the track's file or duration is missing ($(echo "$track" | j "print((d.get('privapub') or {}).get('audio'))"))" echo " deletes" fw -o /dev/null -X DELETE "$FWB/api/v2/uploads/$upload/" until_true 60 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$track_on_p")" = "404" ]' \ && ok "the deleted track leaves PrivaPub" || ko "the deleted track stays on PrivaPub" echo " unfollow" curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$channel_on_p/unfollow" until_true 45 '[ "$(fw_sql "select count(*) from federation_follow f join federation_actor a on a.id = f.actor_id where a.fid like '"'%alice_funkwhale%'"'")" = "0" ]' \ && ok "alice_funkwhale's unfollow reaches the channel" || ko "the channel still counts alice_funkwhale" echo " statistics" stats_check funkwhale.test funkwhale