Index: src/services/PlaylistService.js
===================================================================
--- src/services/PlaylistService.js	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/services/PlaylistService.js	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -14,57 +14,4 @@
 import db from '../config/database.js';
 import { v4 as uuid } from 'uuid';
-import { SOORTEN } from '../assets/js/shared/post-music-type.js';
-
-/**
- * Een volledige datum of niets (shaer-756s).
- *
- * STRIKT, en dat is de hele functie. `year` bestaat al en blijft; dit veld
- * bestaat juist omdat een jaartal geen uitgavedatum is. Zou hij "2024"
- * doorlaten en er 2024-01-01 van maken, dan stond er straks een dag op de
- * federatie die niemand ooit heeft ingevoerd -- en dan hadden we het veld net
- * zo goed niet kunnen toevoegen.
- *
- * Ook 2024-02-31 valt af: dat is geen strengheid om de strengheid, Date rolt
- * hem stilletjes door naar 2 maart en dan slaan we iets anders op dan er
- * ingetypt is.
- */
-function normDatum(v) {
-  const s = String(v == null ? '' : v).trim();
-  if (!s) return null;
-  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
-  const d = new Date(`${s}T00:00:00Z`);
-  return Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== s ? null : s;
-}
-
-/** Een MusicBrainz-id of niets. Zelfde vorm als sites.mb_artist_id. */
-function normMbid(v) {
-  const s = String(v == null ? '' : v).trim().toLowerCase();
-  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(s) ? s : null;
-}
-
-/**
- * De drie soorten die een playlist kan zijn, op EEN plek.
- *
- * Stond eerder vijf keer als `data.kind === 'playlist' ? 'playlist' : 'album'`
- * verspreid over dit bestand. Met twee soorten kon dat nog; bij een derde is
- * het een fout die staat te wachten, want een van de vijf vergeten betekent dat
- * een mixtape stilletjes als album wordt opgeslagen en als Album de deur uit
- * gaat.
- *
- * Album blijft de terugval: een onbekende waarde hoort niet stilzwijgend iets
- * nieuws te worden. De lijst zelf staat in de gedeelde pure module, want de
- * editor in de browser moet dezelfde drie kennen.
- */
-export function normKind(v) {
-  const s = String(v == null ? '' : v).trim().toLowerCase();
-  return SOORTEN.includes(s) ? s : 'album';
-}
-
-/**
- * Draagt deze soort uitgavegegevens? Alleen een album. Een afspeellijst en een
- * mixtape hebben geen uitgavedatum en geen release-id: het zijn samenstellingen
- * van andermans of eigen werk, geen uitgave op zichzelf.
- */
-export const isUitgave = (kind) => normKind(kind) === 'album';
 
 class PlaylistService {
@@ -105,5 +52,4 @@
     const rows = db.prepare(`
       SELECT p.id, p.title, p.artist, p.year, p.cover_url, p.kind,
-             p.release_date, p.mb_release_id,
              p.created_at, p.updated_at,
              (SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id = p.id) AS track_count
@@ -118,7 +64,5 @@
       year: r.year || 0,
       cover: r.cover_url || '',
-      kind: normKind(r.kind),
-      release_date: r.release_date || '',
-      mb_release_id: r.mb_release_id || '',
+      kind: r.kind || 'album',
       track_count: r.track_count,
       created_at: r.created_at,
@@ -133,14 +77,14 @@
    * Returns null if the playlist doesn't exist.
    *
-   * `urlFor` is an optional callback that takes a media filename and returns
-   * its stream URL. If not provided, tracks come back with no `url` and the
-   * caller has to resolve them. The render pipeline in posts.js always passes
-   * urlFor.
-   */
-  static get(siteId, id, urlFor) {
+   * `signUrl` is an optional callback that takes a media filename and returns
+   * a (possibly signed) URL. If not provided, tracks come back with no `url`
+   * and the caller has to resolve them. The render pipeline in posts.js
+   * always passes signUrl.
+   */
+  static get(siteId, id, signUrl) {
     id = this.normalizeId(id);
     if (!id) return null;
     const p = db.prepare(`
-      SELECT id, title, artist, year, cover_url, kind, release_date, mb_release_id, created_at, updated_at
+      SELECT id, title, artist, year, cover_url, kind, created_at, updated_at
       FROM playlists WHERE site_id = ? AND id = ?
     `).get(siteId, id);
@@ -150,6 +94,5 @@
     // filenames (only tracks with a media file are playable).
     const tracks = db.prepare(`
-      SELECT t.id, t.title, t.artist, t.duration, t.cover_url,
-             t.link_spotify, t.link_youtube, t.link_soundcloud, m.filename
+      SELECT t.id, t.title, t.artist, t.duration, t.cover_url, m.filename
       FROM playlist_tracks pt
       JOIN audio_tracks t   ON t.id = pt.track_id
@@ -159,19 +102,4 @@
     `).all(id);
 
-    const mappedTracks = tracks
-      // Link-only tracks (no media file) remain in the list with url ''.
-      .map(t => ({
-        id: t.id,
-        title: t.title || 'Untitled',
-        artist: t.artist || p.artist || '',
-        cover: t.cover_url || p.cover_url || '',
-        duration: t.duration || 0,
-        link_spotify: t.link_spotify || '',
-        link_youtube: t.link_youtube || '',
-        link_soundcloud: t.link_soundcloud || '',
-        url: (t.filename && urlFor) ? urlFor(t.filename) : '',
-      }));
-    // No playlist cover? Fall back to the first track cover so the card isn't empty.
-    const fallbackCover = (mappedTracks.find(t => t.cover) || {}).cover || '';
     return {
       id: p.id,
@@ -179,16 +107,16 @@
       artist: p.artist || '',
       year: p.year || 0,
-      cover: p.cover_url || fallbackCover,
-      kind: normKind(p.kind),
-      // created_at hoort erbij omdat de AP-kant er `published` van maakt. Zonder
-      // dit veld viel buildAlbumObject terug op 1970, en dat stond op 16-8
-      // gewoon op de federatie.
-      created_at: p.created_at,
-      // Leeg als het een afspeellijst is -- de opslag houdt ze daar al leeg,
-      // maar dit is de plek waar de editor leest en die mag niet afhangen van
-      // wat er toevallig in de kolom stond.
-      release_date: isUitgave(p.kind) ? (p.release_date || '') : '',
-      mb_release_id: isUitgave(p.kind) ? (p.mb_release_id || '') : '',
-      tracks: mappedTracks,
+      cover: p.cover_url || '',
+      kind: (p.kind === 'playlist') ? 'playlist' : 'album',
+      tracks: tracks
+        .filter(t => t.filename)  // skip orphaned references
+        .map(t => ({
+          id: t.id,
+          title: t.title || 'Untitled',
+          artist: t.artist || p.artist || '',
+          cover: t.cover_url || p.cover_url || '',
+          duration: t.duration || 0,
+          url: signUrl ? signUrl(t.filename).url : null,
+        })),
     };
   }
@@ -204,20 +132,10 @@
     const id = this.generateId(siteId, title);
     const now = new Date().toISOString();
-    const kind = normKind(data.kind);
-
-    // Alleen een UITGAVE draagt deze twee. Een afspeellijst heeft geen
-    // uitgavedatum en geen release-id, en dat onderscheid is precies wat de
-    // keuze album/playlist betekent (shaer-cyg). Het afdwingen gebeurt HIER en
-    // niet alleen in het scherm: een scherm kun je omzeilen -- de API ligt open
-    // voor de post-editor -- en dan staat er stille rommel op een mixtape die
-    // later als Album de deur uit gaat.
-    const uitgave = isUitgave(kind);
-    const releaseDate = uitgave ? normDatum(data.release_date) : null;
-    const mbRelease = uitgave ? normMbid(data.mb_release_id) : null;
+    const kind = data.kind === 'playlist' ? 'playlist' : 'album';
 
     const tx = db.transaction(() => {
       db.prepare(`
-        INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind, release_date, mb_release_id, created_at, updated_at)
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+        INSERT INTO playlists (id, site_id, title, artist, year, cover_url, kind, created_at, updated_at)
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
       `).run(
         id, siteId, title,
@@ -225,5 +143,5 @@
         Number.isFinite(+data.year) && +data.year > 0 ? +data.year : null,
         String(data.cover || '').trim() || null,
-        kind, releaseDate, mbRelease, now, now,
+        kind, now, now,
       );
       this._writeTracks(id, siteId, data.tracks);
@@ -246,5 +164,5 @@
     if (!id) return false;
     const existing = db.prepare(
-      'SELECT id, kind FROM playlists WHERE site_id = ? AND id = ?'
+      'SELECT id FROM playlists WHERE site_id = ? AND id = ?'
     ).get(siteId, id);
     if (!existing) return false;
@@ -252,9 +170,4 @@
     const fields = [];
     const values = [];
-    // Wat wordt het NA deze wijziging? `kind` hoeft niet in data te staan, dus
-    // val terug op wat er ligt.
-    const nieuwKind = Object.prototype.hasOwnProperty.call(data, 'kind')
-      ? normKind(data.kind)
-      : normKind(existing.kind);
     if (Object.prototype.hasOwnProperty.call(data, 'title')) {
       const v = String(data.title || '').trim();
@@ -273,21 +186,5 @@
     }
     if (Object.prototype.hasOwnProperty.call(data, 'kind')) {
-      fields.push('kind = ?'); values.push(nieuwKind);
-    }
-    // De uitgavevelden. Wordt dit een afspeellijst, dan gaan ze ALTIJD leeg --
-    // ook als de aanroeper er niets over zei. Anders houdt een album dat je tot
-    // mixtape ombouwt zijn uitgavedatum en zijn release-id, en die duiken dan
-    // weer op zodra iemand hem terugzet. Een veld dat niet meer mag bestaan
-    // hoort weg te zijn, niet te wachten.
-    if (nieuwKind !== 'album') {
-      fields.push('release_date = ?'); values.push(null);
-      fields.push('mb_release_id = ?'); values.push(null);
-    } else {
-      if (Object.prototype.hasOwnProperty.call(data, 'release_date')) {
-        fields.push('release_date = ?'); values.push(normDatum(data.release_date));
-      }
-      if (Object.prototype.hasOwnProperty.call(data, 'mb_release_id')) {
-        fields.push('mb_release_id = ?'); values.push(normMbid(data.mb_release_id));
-      }
+      fields.push('kind = ?'); values.push(data.kind === 'playlist' ? 'playlist' : 'album');
     }
     fields.push('updated_at = ?'); values.push(new Date().toISOString());
@@ -316,5 +213,5 @@
    * Delete a playlist. Track references in playlist_tracks are removed
    * automatically via ON DELETE CASCADE. Posts that embed this playlist
-   * will render a "playlist not found" placeholder.
+   * will render a "playlist niet gevonden" placeholder.
    */
   static delete(siteId, id) {
