feat(embeds): fullscreen, wider cards, Instagram/Tidal/Reddit; fix TikTok/X/Twitch
Feedback fixes + new platforms: - Fullscreen: add a universal 'Fullscreen' button on video embeds (requests fullscreen on the media container) so Shorts/TikTok/etc. can go fullscreen regardless of each player's own controls. Portrait media enlarged (220->300). - Drop the iframe sandbox (CSP-allowlisted trusted hosts; sandbox was breaking player features and likely TikTok). - Wider, responsive embed/tweet cards (min(34rem,92vw)) so Twitch player chrome isn't cramped and X posts stop getting clipped. - Instagram (p/reel/tv), Tidal (track/album/playlist/video), and Reddit posts now embed. Reddit uses redditmedia.com to bypass the homeserver's blocked preview (Reddit serves it a bot-check 'please wait for verification' page); a bot-wall title filter keeps that garbage out of any caption. New pure parsers unit-tested (videoEmbed.test.ts, 17 cases). Desktop + live web CSP frame-src updated for instagram.com, embed.tidal.com, redditmedia.com. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ import {
|
||||
isSoundCloudTrack,
|
||||
getAppleMusicEmbed,
|
||||
getTweetId,
|
||||
getTidalEmbed,
|
||||
getInstagramEmbed,
|
||||
getRedditPostEmbed,
|
||||
buildVideoEmbedUrl,
|
||||
spotifyEmbedHeight,
|
||||
parseMediaEmbed,
|
||||
@@ -136,6 +139,39 @@ test('parseMediaEmbed: Apple Music → audio', () => {
|
||||
assert.equal(m?.height, 175);
|
||||
});
|
||||
|
||||
test('Tidal: track (audio) vs video (landscape)', () => {
|
||||
assert.deepEqual(getTidalEmbed('https://tidal.com/browse/track/12345'), {
|
||||
kind: 'audio',
|
||||
embedUrl: 'https://embed.tidal.com/tracks/12345',
|
||||
height: 120,
|
||||
});
|
||||
assert.equal(getTidalEmbed('https://listen.tidal.com/album/999')?.embedUrl, 'https://embed.tidal.com/albums/999');
|
||||
assert.equal(getTidalEmbed('https://tidal.com/video/555')?.kind, 'landscape');
|
||||
assert.equal(getTidalEmbed('https://tidal.com/browse'), null);
|
||||
});
|
||||
|
||||
test('Instagram: p / reel / tv → embed path', () => {
|
||||
assert.equal(getInstagramEmbed('https://www.instagram.com/p/AbC123_-/'), 'https://www.instagram.com/p/AbC123_-/embed/');
|
||||
assert.equal(getInstagramEmbed('https://instagram.com/reel/XyZ/'), 'https://www.instagram.com/reel/XyZ/embed/');
|
||||
assert.equal(getInstagramEmbed('https://www.instagram.com/reels/XyZ/'), 'https://www.instagram.com/reel/XyZ/embed/');
|
||||
assert.equal(getInstagramEmbed('https://www.instagram.com/someuser/'), null);
|
||||
});
|
||||
|
||||
test('Reddit post embed → redditmedia', () => {
|
||||
assert.equal(
|
||||
getRedditPostEmbed('https://www.reddit.com/r/aww/comments/abc123/cute_cat/'),
|
||||
'https://www.redditmedia.com/r/aww/comments/abc123/?ref_source=embed&ref=share&embed=true&theme=dark',
|
||||
);
|
||||
assert.equal(getRedditPostEmbed('https://old.reddit.com/r/aww/comments/xyz/'), 'https://www.redditmedia.com/r/aww/comments/xyz/?ref_source=embed&ref=share&embed=true&theme=dark');
|
||||
assert.equal(getRedditPostEmbed('https://www.reddit.com/r/aww/'), null); // subreddit, not a post
|
||||
});
|
||||
|
||||
test('parseMediaEmbed: Instagram/Reddit → rich, Tidal → audio', () => {
|
||||
assert.equal(parseMediaEmbed('https://www.instagram.com/p/abc/', 'h')?.kind, 'rich');
|
||||
assert.equal(parseMediaEmbed('https://www.reddit.com/r/x/comments/y/z/', 'h')?.provider, 'reddit');
|
||||
assert.equal(parseMediaEmbed('https://tidal.com/browse/track/1', 'h')?.provider, 'tidal');
|
||||
});
|
||||
|
||||
test('parseMediaEmbed: Twitch embed carries the parent host', () => {
|
||||
const chan = parseMediaEmbed('https://twitch.tv/streamer', HOST);
|
||||
assert.equal(chan?.provider, 'twitch');
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
//
|
||||
// "kind" drives how the card lays the embed out:
|
||||
// landscape → 16:9 video portrait → 9:16 video
|
||||
// audio → short fixed-height player (Spotify/SoundCloud)
|
||||
// audio → short fixed-height player (Spotify/SoundCloud/Tidal/Apple Music)
|
||||
// rich → tall fixed-height post embed (Instagram / Reddit)
|
||||
|
||||
export type EmbedKind = 'landscape' | 'portrait' | 'audio';
|
||||
export type EmbedKind = 'landscape' | 'portrait' | 'audio' | 'rich';
|
||||
|
||||
export type MediaEmbed = {
|
||||
provider: string;
|
||||
@@ -201,6 +202,62 @@ export function getTweetId(url: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tidal ----------------------------------------------------------------
|
||||
|
||||
export function getTidalEmbed(
|
||||
url: string,
|
||||
): { kind: 'audio' | 'landscape'; embedUrl: string; height?: number } | null {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const h = u.hostname.replace(/^(www|listen|desktop)\./, '');
|
||||
if (h !== 'tidal.com') return null;
|
||||
const p = u.pathname.replace(/^\/browse/, '');
|
||||
let m = p.match(/^\/track\/(\d+)/);
|
||||
if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/tracks/${m[1]}`, height: 120 };
|
||||
m = p.match(/^\/album\/(\d+)/);
|
||||
if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/albums/${m[1]}`, height: 400 };
|
||||
m = p.match(/^\/playlist\/([0-9a-fA-F-]+)/);
|
||||
if (m)
|
||||
return { kind: 'audio', embedUrl: `https://embed.tidal.com/playlists/${m[1]}`, height: 400 };
|
||||
m = p.match(/^\/video\/(\d+)/);
|
||||
if (m) return { kind: 'landscape', embedUrl: `https://embed.tidal.com/videos/${m[1]}` };
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Instagram ------------------------------------------------------------
|
||||
|
||||
export function getInstagramEmbed(url: string): string | null {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
if (u.hostname !== 'instagram.com' && u.hostname !== 'www.instagram.com') return null;
|
||||
const m = u.pathname.match(/^\/(p|reel|reels|tv)\/([A-Za-z0-9_-]+)/);
|
||||
if (!m) return null;
|
||||
const type = m[1] === 'reels' ? 'reel' : m[1];
|
||||
return `https://www.instagram.com/${type}/${m[2]}/embed/`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Reddit (post embed via redditmedia — bypasses the homeserver's blocked
|
||||
// preview fetch, which Reddit serves a bot-check "please wait" page to) ---
|
||||
|
||||
export function getRedditPostEmbed(url: string): string | null {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const h = u.hostname.replace(/^(www|old|new|np|i)\./, '');
|
||||
if (h !== 'reddit.com') return null;
|
||||
const m = u.pathname.match(/^\/r\/([A-Za-z0-9_]+)\/comments\/([A-Za-z0-9]+)/);
|
||||
if (!m) return null;
|
||||
return `https://www.redditmedia.com/r/${m[1]}/comments/${m[2]}/?ref_source=embed&ref=share&embed=true&theme=dark`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Embed-URL builders ---------------------------------------------------
|
||||
|
||||
const enc = encodeURIComponent;
|
||||
@@ -294,5 +351,15 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
|
||||
height: apple.height,
|
||||
};
|
||||
|
||||
const tidal = getTidalEmbed(url);
|
||||
if (tidal)
|
||||
return { provider: 'tidal', kind: tidal.kind, embedUrl: tidal.embedUrl, height: tidal.height };
|
||||
|
||||
const insta = getInstagramEmbed(url);
|
||||
if (insta) return { provider: 'instagram', kind: 'rich', embedUrl: insta, height: 720 };
|
||||
|
||||
const reddit = getRedditPostEmbed(url);
|
||||
if (reddit) return { provider: 'reddit', kind: 'rich', embedUrl: reddit, height: 480 };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user