Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 3ca7bdf670788b368fa71765ae6b93f739f56b8f)
+++ src/routes/posts.js	(revision 4f97b7090dcb51d0cbd1921a0dbaf3e6f051408b)
@@ -284,5 +284,7 @@
     for (const m of c.matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) db.prepare('UPDATE audio_tracks SET fedi_open = 1 WHERE id = ? AND site_id = ?').run(m[1], siteId);
     for (const m of c.matchAll(/\[\[album:([^\]]+)\]\]/g)) db.prepare('UPDATE audio_tracks SET fedi_open = 1 WHERE site_id = ? AND album = ?').run(siteId, m[1].trim());
-    for (const m of c.matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) db.prepare('UPDATE audio_tracks SET fedi_open = 1 WHERE id IN (SELECT track_id FROM playlist_tracks WHERE playlist_id = ?)').run(m[1]);
+    // playlists.id is a GLOBAL key, so the site filter has to sit on the tracks: without it a
+    // post on site A embedding site B's playlist would open B's files — permanently.
+    for (const m of c.matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) db.prepare('UPDATE audio_tracks SET fedi_open = 1 WHERE site_id = ? AND id IN (SELECT track_id FROM playlist_tracks WHERE playlist_id = ?)').run(siteId, m[1]);
   } catch { /* non-fatal */ }
 }
