Index: src/config/paths.js
===================================================================
--- src/config/paths.js	(revision e2c3d09c54bd06c4a04e4c72e4ff59aa9374b705)
+++ src/config/paths.js	(revision 6c4ff7e385f99367e75efde44e5b671992c43e88)
@@ -34,2 +34,39 @@
   return path.resolve(process.env[envVar] || path.join(MEDIA_ROOT, sub));
 }
+
+/**
+ * Waar de gehoste audio staat.
+ *
+ * BEWUST BUITEN MEDIA_ROOT: de publieke /media-handler mag er niet bij, elke
+ * fetch loopt via de gated route in routes/audio.js. Diezelfde route resolvet
+ * met AUDIO_DIR + bestandsnaam, en negeert media.storage_path volledig.
+ */
+export const AUDIO_ROOT = path.resolve(
+  process.env.AUDIO_PATH || path.join(__dirname, '..', '..', 'storage', 'audio'),
+);
+
+/**
+ * Het echte pad van een audiobestand, op DEZELFDE manier als de speler het zoekt.
+ *
+ * Dit bestaat omdat die twee uit elkaar liepen en dat een verhuizing sloopte.
+ * Op sound-fabrics.com wees media.storage_path voor 124 van de 139 tracks nog
+ * naar /srv/prutfolio/storage/audio (van voor de dataverhuizing), terwijl de
+ * bestanden allang op ~/data/prutfolio/audio stonden. De site merkte er niets
+ * van, want de speler kijkt alleen naar de bestandsnaam. De exporter las wel
+ * storage_path, vond niets, en liet 124 nummers stil achter.
+ *
+ * Volgorde: eerst zoals de speler kijkt (bestandsnaam in AUDIO_ROOT), dan pas
+ * het opgeslagen pad. Zo klopt de export met wat de gebruiker hoort, en niet
+ * met wat de database ooit dacht.
+ *
+ * @returns {string|null} een bestaand pad, of null
+ */
+export function resolveAudioPath(storagePath, fs) {
+  const s = String(storagePath || '');
+  if (!s) return null;
+  const kandidaten = [path.join(AUDIO_ROOT, path.basename(s)), path.resolve(s)];
+  for (const p of kandidaten) {
+    try { if (fs.statSync(p).isFile()) return p; } catch { /* volgende kandidaat */ }
+  }
+  return null;
+}
