Index: src/routes/embed.js
===================================================================
--- src/routes/embed.js	(revision 2923a951b1ae82d7e0ece377c760c02bf001fc91)
+++ src/routes/embed.js	(revision 30271e652b4f48014f6853585c1628a8ee02f3c8)
@@ -45,9 +45,8 @@
         for (const m of post.content.matchAll(/\[\[album:([^\]]+)\]\]/g)) for (const r of db.prepare('SELECT id FROM audio_tracks WHERE site_id = ? AND album = ? ORDER BY position').all(site.id, m[1].trim())) add(r.id);
         for (const m of post.content.matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) for (const r of db.prepare('SELECT track_id FROM playlist_tracks WHERE playlist_id = ? ORDER BY position').all(m[1])) add(r.track_id);
-        if (ids.length) {
-          const byId = new Map(tracks.map((t) => [t.id, t]));
-          const scoped = ids.map((id) => byId.get(id)).filter(Boolean);
-          if (scoped.length) tracks = scoped;
-        }
+        // Strictly scope to this post's tracks — do NOT fall back to all-site
+        // tracks (that showed unrelated songs for a link-only-track post).
+        const byId = new Map(tracks.map((t) => [t.id, t]));
+        tracks = ids.map((id) => byId.get(id)).filter(Boolean);
       }
     } catch { /* fall back to the full site player */ }
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 2923a951b1ae82d7e0ece377c760c02bf001fc91)
+++ src/routes/posts.js	(revision 30271e652b4f48014f6853585c1628a8ee02f3c8)
@@ -848,4 +848,5 @@
     canManageSite,
     siteAvatar,
+    postHasPlayableAudio: ActivityPubService.hasPlayableAudio(post.content || '', site.id),
     pageTitle: post.title + ' - ' + site.title,
     socialDescr: post.excerpt || '',
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 2923a951b1ae82d7e0ece377c760c02bf001fc91)
+++ src/services/ActivityPubService.js	(revision 30271e652b4f48014f6853585c1628a8ee02f3c8)
@@ -97,4 +97,17 @@
 }
 
+// Does a post's audio shortcodes reference at least one PLAYABLE (file-backed)
+// track? Link-only tracks (external Spotify/YouTube, media_id NULL) don't count —
+// they have no Klonkt-hosted audio to embed, so no player card / cover-suppression.
+export function hasPlayableAudio(content, siteId) {
+  if (!content || !/\[\[(track|album|playlist):/i.test(content)) return false;
+  try {
+    for (const m of content.matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) { const r = db.prepare('SELECT media_id FROM audio_tracks WHERE id = ?').get(m[1]); if (r && r.media_id) return true; }
+    for (const m of content.matchAll(/\[\[album:([^\]]+)\]\]/g)) { if (db.prepare('SELECT 1 FROM audio_tracks WHERE site_id = ? AND album = ? AND media_id IS NOT NULL LIMIT 1').get(siteId, m[1].trim())) return true; }
+    for (const m of content.matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) { if (db.prepare('SELECT 1 FROM playlist_tracks pt JOIN audio_tracks t ON t.id = pt.track_id WHERE pt.playlist_id = ? AND t.media_id IS NOT NULL LIMIT 1').get(m[1])) return true; }
+  } catch { /* non-fatal */ }
+  return false;
+}
+
 // A single post as an AS2 Note (the object), and as a Create activity (for outbox/delivery).
 export function buildNote(base, site, post) {
@@ -117,11 +130,13 @@
   };
   const hadAudio = /\[\[(track|album|playlist):/i.test(post.content || '');
+  const playable = hasPlayableAudio(post.content || '', site && site.id);
   const urls = [];
-  // Audio posts suppress image attachments so Mastodon renders the player CARD
-  // (twitter:player) instead of the cover — on Mastodon a media attachment and a
-  // link/player card are mutually exclusive, and the inline player is the point.
-  if (post.cover_image_url && !hadAudio) urls.push(abs(post.cover_image_url));
+  // Posts with PLAYABLE hosted audio suppress image attachments so Mastodon renders
+  // the player CARD (twitter:player) instead of the cover — media attachment and
+  // link/player card are mutually exclusive on Mastodon. Link-only audio (external)
+  // keeps its cover (no player card to show).
+  if (post.cover_image_url && !playable) urls.push(abs(post.cover_image_url));
   let body = post.content || '';
-  if (!hadAudio) for (const m of body.matchAll(/<img\b[^>]*\bsrc="([^"]+)"[^>]*>/gi)) urls.push(abs(m[1]));
+  if (!playable) for (const m of body.matchAll(/<img\b[^>]*\bsrc="([^"]+)"[^>]*>/gi)) urls.push(abs(m[1]));
   body = body.replace(/<img\b[^>]*>/gi, '');
   // Audio shortcodes: do NOT federate the raw audio file — Klonkt deliberately
@@ -960,4 +975,4 @@
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
-  getReplyUris, markNotificationsSeen, countUnseenNotifications,
+  getReplyUris, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
 };
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 2923a951b1ae82d7e0ece377c760c02bf001fc91)
+++ src/views/shell.ejs	(revision 30271e652b4f48014f6853585c1628a8ee02f3c8)
@@ -158,5 +158,5 @@
 // inline player that streams via the gated /audio/stream (no downloadable file).
 const _postAudio = !!(typeof post !== 'undefined' && post
-  && /\[\[(track|album|playlist):/i.test(post.content || post.content_html || '')
+  && typeof postHasPlayableAudio !== 'undefined' && postHasPlayableAudio
   && typeof premiumUnlocked !== 'undefined' && premiumUnlocked);
 const _embedUrl = _postAudio
