Index: src/services/ap-core.js
===================================================================
--- src/services/ap-core.js	(revision e1da6a16a466f007f29eb214c6484d2737adf481)
+++ src/services/ap-core.js	(revision 19a9a1e2a0cace2354e39699c98f3da1012e60bb)
@@ -265,6 +265,18 @@
  * en zonder `next`. Hem naar de laatste pagina terugbuigen zou opnieuw een
  * antwoord zijn dat over zichzelf liegt.
- */
-export function pagedCollection(id, items, { totalItems, page = false, perPage = PAGINA_GROOTTE, alGesneden = false, extra = {} } = {}) {
+ *
+ * `ongeordend` maakt er de NIET-geordende vorm van: `Collection` met
+ * `CollectionPage` en `items`, in plaats van `OrderedCollection` met
+ * `OrderedCollectionPage` en `orderedItems`. Dat is geen dialect maar de andere
+ * helft van AS2 -- en de bibliotheek hoort daar: een platenkast heeft geen
+ * volgorde die iets betekent, en `Library` is bij Funkwhale expliciet een
+ * `Collection`. Onze outbox is wél geordend (chronologie is daar de inhoud) en
+ * blijft dus zoals hij was.
+ *
+ * De pagina draagt in die vorm ook `first` en `last`. AS2 staat dat toe --
+ * CollectionPage erft van Collection -- en een lezer die halverwege binnenkomt
+ * kan zo terug naar het begin zonder eerst de wortel op te halen.
+ */
+export function pagedCollection(id, items, { totalItems, page = false, perPage = PAGINA_GROOTTE, alGesneden = false, ongeordend = false, extra = {} } = {}) {
   const lijst = items || [];
   const telling = totalItems === undefined ? lijst.length : totalItems;
@@ -283,10 +295,12 @@
       '@context': AP_CONTEXT,
       id: url(n),
-      type: 'OrderedCollectionPage',
+      type: ongeordend ? 'CollectionPage' : 'OrderedCollectionPage',
       partOf: id,
       totalItems: telling,
+      ...(ongeordend ? { first: url(1), last: url(paginas) } : {}),
+      ...(extra.attributedTo ? { attributedTo: extra.attributedTo } : {}),
       ...(n > 1 ? { prev: url(n - 1) } : {}),
       ...(n < paginas ? { next: url(n + 1) } : {}),
-      orderedItems: deel,
+      ...(ongeordend ? { items: deel } : { orderedItems: deel }),
     };
   }
@@ -294,10 +308,10 @@
     '@context': AP_CONTEXT,
     id,
-    type: 'OrderedCollection',
+    type: ongeordend ? 'Collection' : 'OrderedCollection',
     ...extra,
     totalItems: telling,
     first: url(1),
     last: url(paginas),
-    orderedItems: lijst,
+    ...(ongeordend ? { items: lijst } : { orderedItems: lijst }),
   };
 }
Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision e1da6a16a466f007f29eb214c6484d2737adf481)
+++ src/services/music/index.js	(revision 19a9a1e2a0cace2354e39699c98f3da1012e60bb)
@@ -253,8 +253,23 @@
   return pagedCollection(id, items, {
     page,
+    // Een platenkast is geen tijdlijn: `Collection`, niet `OrderedCollection`.
+    // Funkwhale's LibrarySerializer accepteert ook alleen die twee typen
+    // (as:Collection of fw:Library) en zijn CollectionPageSerializer alleen
+    // `CollectionPage` met `items`.
+    ongeordend: true,
     extra: {
       type: 'Library',
       name: site.title || site.slug,
       attributedTo: actorId(base, site.slug),
+      // WAAROM DIT VELD ER MOET STAAN. Funkwhale's LibrarySerializer noemt
+      // `audience` optioneel, maar zijn create() doet er meteen
+      // `privacy[validated_data["audience"]]` mee -- zonder de sleutel is dat
+      // een KeyError en geeft hun server een 500. Dat is wat open.audio op 15-8
+      // teruggaf toen Robin onze library-URI daar opzocht.
+      //
+      // Het is bovendien gewoon waar: alles hierin is fedi_open, dus openbaar.
+      // Bij hen is dit precies het verschil tussen privacy_level 'everyone' en
+      // 'me' -- oftewel of onze nummers daar afspeelbaar zijn.
+      audience: 'https://www.w3.org/ns/activitystreams#Public',
       // Vereist volgens hun docs. Openbaar, dus de telling is eerlijk en de
       // lijst blijft leeg -- wie ons volgt volgt de ACTOR, niet de bak.
Index: test/ap-collection-paging.test.js
===================================================================
--- test/ap-collection-paging.test.js	(revision e1da6a16a466f007f29eb214c6484d2737adf481)
+++ test/ap-collection-paging.test.js	(revision 19a9a1e2a0cace2354e39699c98f3da1012e60bb)
@@ -84,7 +84,14 @@
     assert.notEqual(pagina.body.id, wortel.body.id, `${pad}: pagina deelt zijn id met de collectie`);
 
-    // En hij mag niet zelf weer een `first` dragen -- dat is de lus waar
-    // open.audio in liep.
-    assert.equal(pagina.body.first, undefined, `${pad}: de pagina draagt zelf een first`);
+    // Een pagina MAG `first` dragen (AS2: CollectionPage erft van Collection,
+    // en Funkwhale eist het zelfs op library-pagina's). Wat niet mag is een
+    // `next` naar zichzelf: dat is de lus, en die zit in de verwijzing die een
+    // lezer volgt, niet in de aanwezigheid van het veld.
+    assert.notEqual(pagina.body.next, eerste, `${pad}: next wijst naar de pagina zelf`);
+
+    // De items horen bij het type: geordend -> orderedItems, ongeordend ->
+    // items. Een `Collection` met `orderedItems` is voor een strikte lezer leeg.
+    const sleutel = pagina.body.type === 'CollectionPage' ? 'items' : 'orderedItems';
+    assert.ok(Array.isArray(pagina.body[sleutel]), `${pad}: ${pagina.body.type} zonder ${sleutel}`);
   });
 }
Index: test/ap-library.test.js
===================================================================
--- test/ap-library.test.js	(revision e1da6a16a466f007f29eb214c6484d2737adf481)
+++ test/ap-library.test.js	(revision 19a9a1e2a0cace2354e39699c98f3da1012e60bb)
@@ -53,5 +53,5 @@
   // van de bak zijn poort overrulen.
   const lib = AP.buildLibrary(BASE, site, AP.siteOpenTracks('s1'));
-  assert.deepEqual(lib.orderedItems.map((a) => a.name), ['Het open nummer']);
+  assert.deepEqual(lib.items.map((a) => a.name), ['Het open nummer']);
 });
 
@@ -59,9 +59,49 @@
   // Dit is het veld waar het om begonnen was: het haakje waar een upload aan komt.
   const lib = AP.buildLibrary(BASE, site, AP.siteOpenTracks('s1'));
-  for (const a of lib.orderedItems) assert.equal(a.library, LIB);
+  for (const a of lib.items) assert.equal(a.library, LIB);
   // Ook als de track ergens anders vandaan komt -- de losse ophaal, de
   // trackcollectie, de playlist. Anders hangt het er maar op een plek.
   const los = AP.buildTrackAudio(BASE, site, AP.openTrack('s1', 't1'), { standalone: true });
   assert.equal(los.library, LIB);
+});
+
+// Wat hun serializers WERKELIJK eisen, uit de bron en niet uit de docs.
+// api/funkwhale_api/federation/serializers.py op develop, gelezen 15-8:
+//
+//   LibrarySerializer(PaginatedCollectionSerializer)   regel 1071
+//     type in [as:Collection, fw:Library] · name · id · first · last · totalItems
+//     audience: ChoiceField(required=False) MAAR create() doet
+//       privacy[validated_data["audience"]]  -- ontbreekt de sleutel, dan een
+//       KeyError en dus een 500. Zo gaf open.audio op 15-8 een 500 op onze URL.
+//     en het is de schakel naar privacy_level 'everyone' == afspeelbaar.
+//
+//   CollectionPageSerializer                            regel 1157
+//     type == as:CollectionPage (NIET OrderedCollectionPage) · items (NIET
+//     orderedItems) · id · first · last · partOf · totalItems
+//
+// Deze test staat er omdat die eisen nergens anders bij ons zijn opgeschreven.
+// Zonder hem is het volgende dat iemand de bibliotheek 'opruimt' naar de
+// gewone OrderedCollection-vorm, en dan is hij daar stil weer leeg.
+test('de bibliotheek voldoet aan Funkwhale LibrarySerializer', () => {
+  const lib = AP.buildLibrary(BASE, site, AP.siteOpenTracks('s1'));
+  assert.equal(lib.audience, 'https://www.w3.org/ns/activitystreams#Public',
+    'zonder audience een KeyError in hun create() -- 500');
+  assert.ok(Array.isArray(lib.items), 'Collection draagt items, geen orderedItems');
+  assert.equal(lib.orderedItems, undefined);
+  for (const veld of ['id', 'name', 'first', 'last', 'totalItems', 'attributedTo']) {
+    assert.ok(lib[veld] !== undefined, `${veld} ontbreekt`);
+  }
+});
+
+test('een bibliotheekpagina voldoet aan Funkwhale CollectionPageSerializer', () => {
+  const p = AP.buildLibrary(BASE, site, AP.siteOpenTracks('s1'), { page: 1 });
+  assert.equal(p.type, 'CollectionPage', 'OrderedCollectionPage wijzen ze af');
+  assert.ok(Array.isArray(p.items), 'items, niet orderedItems');
+  assert.equal(p.orderedItems, undefined);
+  assert.equal(p.partOf, LIB);
+  assert.equal(p.id, `${LIB}?page=1`);
+  for (const veld of ['first', 'last', 'totalItems']) {
+    assert.ok(p[veld] !== undefined, `${veld} ontbreekt op de pagina`);
+  }
 });
 
