Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision fbfd7a199340cd773cd153df2388d88f2084eea2)
+++ src/routes/activitypub.js	(revision 6c4ff7e385f99367e75efde44e5b671992c43e88)
@@ -1043,4 +1043,19 @@
 });
 
+/**
+ * Mag deze aanvrager alles van `slug` zien? Waar bij de eigenaar zelf, en waar
+ * voor de actor waar `slug` naartoe verhuisd is (FEP-1580, Source Instance).
+ *
+ * Eén plek voor die vraag, want hij komt op meerdere collecties terug en twee
+ * antwoorden op dezelfde vraag lopen vroeg of laat uiteen.
+ */
+async function magAlles(req, slug) {
+  const auth = OAuth.verifyBearer(req.headers.authorization);
+  if (auth && auth.site.slug === slug) return true;
+  if (!req.headers['signature']) return false;
+  const v = await AP.verifyRequest(req).catch(() => null);
+  return !!(v && v.id && AP.isMoveTarget(slug, v.id));
+}
+
 // ── FEP-1580: de vertaaltabel van een verhuizing ──────────────────
 //
@@ -1115,8 +1130,8 @@
 // stap 3). Een playlist is een keuze hieruit; deze collectie is alles wat de
 // artiest heeft opengezet, ook wat in geen enkele playlist staat.
-router.get('/ap/users/:slug/tracks', (req, res) => {
+router.get('/ap/users/:slug/tracks', async (req, res) => {
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
-  AP.sendAP(res, AP.buildTrackCollection(baseUrl(req), site, AP.siteOpenTracks(site.id)));
+  AP.sendAP(res, AP.buildTrackCollection(baseUrl(req), site, AP.siteOpenTracks(site.id, { alles: await magAlles(req, site.slug) })));
 });
 
Index: src/routes/admin-migrate.js
===================================================================
--- src/routes/admin-migrate.js	(revision fbfd7a199340cd773cd153df2388d88f2084eea2)
+++ src/routes/admin-migrate.js	(revision 6c4ff7e385f99367e75efde44e5b671992c43e88)
@@ -29,7 +29,7 @@
 import { requireGod } from '../middleware/auth.js';
 import ActivityPubService from '../services/ActivityPubService.js';
-import { safeFetch, signedGetJson, noteId, noteVisibility } from '../services/ActivityPubService.js';
+import { safeFetch, signedGetJson, signedGetHeaders, noteId, noteVisibility } from '../services/ActivityPubService.js';
 import HtmlSanitizerService from '../services/HtmlSanitizerService.js';
-import { MEDIA_ROOT } from '../config/paths.js';
+import { MEDIA_ROOT, AUDIO_ROOT } from '../config/paths.js';
 import * as Migration from '../services/MigrationService.js';
 import { buildArchive, zipArchive } from '../services/ArchiveExportService.js';
@@ -111,5 +111,5 @@
       sourceUri: opgegeven || null,
       deps: {
-        getJson: signedGetJson, safeFetch, mediaRoot: MEDIA_ROOT, fs, path, noteId, noteVisibility,
+        getJson: signedGetJson, safeFetch, mediaRoot: MEDIA_ROOT, audioRoot: AUDIO_ROOT, fs, path, noteId, noteVisibility, signHeaders: signedGetHeaders,
         sanitize: (h) => HtmlSanitizerService.sanitize(h || ''),
       },
Index: src/routes/audio.js
===================================================================
--- src/routes/audio.js	(revision fbfd7a199340cd773cd153df2388d88f2084eea2)
+++ src/routes/audio.js	(revision 6c4ff7e385f99367e75efde44e5b671992c43e88)
@@ -30,4 +30,5 @@
 import db from '../config/database.js';
 import { recordPlay } from '../services/StatsService.js';
+import AP from '../services/ActivityPubService.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -71,8 +72,36 @@
 }
 
-router.get('/stream/:filename', (req, res) => {
+/**
+ * FEP-1580: de instantie waar dit account naartoe verhuisd is mag ALLE audio
+ * ophalen, ook wat niet fedi_open is.
+ *
+ * Zonder deze tak ziet de nieuwe Klonkt de tracklijst wel en krijgt hij de
+ * bestanden niet, en dan verhuis je een bibliotheek met alleen titels. Dat is
+ * precies de halve waarheid die deze hele ronde moest opruimen.
+ *
+ * Smal gehouden: een geldige handtekening, van precies de actor in moved_to, en
+ * alleen voor een bestand dat van DIE site is. moved_to komt er alleen te staan
+ * als de doel-actor ons in alsoKnownAs had, dus er heeft iemand met beheer aan
+ * beide kanten ja gezegd.
+ */
+async function isMoveTargetAudio(req, filename) {
+  if (!req.headers['signature'] || !filename) return false;
+  let rij;
+  try {
+    rij = db.prepare(`SELECT s.slug FROM audio_tracks t
+                        JOIN media m ON t.media_id = m.id
+                        JOIN sites s ON s.id = t.site_id
+                       WHERE m.storage_path = ? OR m.storage_path LIKE ? LIMIT 1`)
+      .get(filename, `%${filename}`);
+  } catch { return false; }
+  if (!rij || !rij.slug) return false;
+  const v = await AP.verifyRequest(req).catch(() => null);
+  return !!(v && v.id && AP.isMoveTarget(rij.slug, v.id));
+}
+
+router.get('/stream/:filename', async (req, res) => {
   const { filename } = req.params;
 
-  if (!isAllowedAudioRequest(req, filename)) {
+  if (!isAllowedAudioRequest(req, filename) && !(await isMoveTargetAudio(req, filename))) {
     return res.status(403).send('Direct access not allowed');
   }
