feat(embeds): add TikTok, Twitch, Dailymotion, Streamable, Spotify, SoundCloud players
Generalize the inline-embed system: parseMediaEmbed() resolves any supported URL to an embed spec (provider + kind + embed URL), and a single MediaEmbedCard renders the media-forward click-to-play facade for all of them — landscape 16:9 (YouTube, Vimeo, Dailymotion, Streamable, Twitch), portrait 9:16 (Shorts, TikTok), and short fixed-height audio players (Spotify, SoundCloud). Same privacy facade as before: nothing loads from the third party until the user presses play, gated by the 'Inline Media Players' setting. Twitch embeds pass the current page hostname as the required parent param. Non-embeddable fallbacks (e.g. vm.tiktok.com short links, non-media tweets) keep their existing rich OG cards; X/Twitter intentionally keeps its rich tweet card. All parsers/builders are pure + unit-tested (videoEmbed.test.ts, 10 cases). Desktop Tauri CSP frame-src updated for the new hosts (separate commit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,65 +5,115 @@ import {
|
||||
getYoutubeShortsId,
|
||||
isYouTubeShorts,
|
||||
getVimeoVideoId,
|
||||
parseVideoEmbed,
|
||||
getTikTokVideoId,
|
||||
getDailymotionId,
|
||||
getStreamableId,
|
||||
getTwitchTarget,
|
||||
getSpotifyEmbedTarget,
|
||||
isSoundCloudTrack,
|
||||
buildVideoEmbedUrl,
|
||||
spotifyEmbedHeight,
|
||||
parseMediaEmbed,
|
||||
} from './videoEmbed';
|
||||
|
||||
test('getYouTubeVideoId: watch / youtu.be / embed / shorts', () => {
|
||||
const HOST = 'chat.lotusguild.org';
|
||||
|
||||
test('YouTube: watch / youtu.be / embed / shorts', () => {
|
||||
assert.equal(getYouTubeVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
|
||||
assert.equal(getYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
|
||||
assert.equal(getYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ?t=42'), 'dQw4w9WgXcQ');
|
||||
assert.equal(getYouTubeVideoId('https://www.youtube.com/embed/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
|
||||
assert.equal(getYouTubeVideoId('https://youtube.com/shorts/abc123DEF_-'), 'abc123DEF_-');
|
||||
});
|
||||
|
||||
test('getYouTubeVideoId: non-YouTube / malformed → null', () => {
|
||||
assert.equal(getYouTubeVideoId('https://vimeo.com/123'), null);
|
||||
assert.equal(getYouTubeVideoId('not a url'), null);
|
||||
assert.equal(getYouTubeVideoId('https://www.youtube.com/'), null);
|
||||
});
|
||||
|
||||
test('Shorts detection', () => {
|
||||
assert.equal(isYouTubeShorts('https://www.youtube.com/shorts/abc123'), true);
|
||||
assert.equal(isYouTubeShorts('https://www.youtube.com/watch?v=abc123'), false);
|
||||
assert.equal(getYoutubeShortsId('https://youtube.com/shorts/abc123'), 'abc123');
|
||||
});
|
||||
|
||||
test('getVimeoVideoId', () => {
|
||||
test('Vimeo', () => {
|
||||
assert.equal(getVimeoVideoId('https://vimeo.com/123456789'), '123456789');
|
||||
assert.equal(getVimeoVideoId('https://vimeo.com/123456789/abcdef'), '123456789');
|
||||
assert.equal(getVimeoVideoId('https://vimeo.com/channels/staffpicks'), null);
|
||||
assert.equal(getVimeoVideoId('https://youtube.com/watch?v=x'), null);
|
||||
});
|
||||
|
||||
test('parseVideoEmbed: routes provider + portrait for shorts', () => {
|
||||
assert.deepEqual(parseVideoEmbed('https://youtube.com/shorts/abc'), {
|
||||
provider: 'youtube',
|
||||
id: 'abc',
|
||||
portrait: true,
|
||||
test('TikTok: canonical /video/<id> only', () => {
|
||||
assert.equal(getTikTokVideoId('https://www.tiktok.com/@user/video/7234567890123456789'), '7234567890123456789');
|
||||
assert.equal(getTikTokVideoId('https://vm.tiktok.com/ZMabc/'), null); // short link redirects
|
||||
assert.equal(getTikTokVideoId('https://www.tiktok.com/@user'), null);
|
||||
});
|
||||
|
||||
test('Dailymotion + Streamable', () => {
|
||||
assert.equal(getDailymotionId('https://www.dailymotion.com/video/x8abcde'), 'x8abcde');
|
||||
assert.equal(getDailymotionId('https://dai.ly/x8abcde'), 'x8abcde');
|
||||
assert.equal(getStreamableId('https://streamable.com/abc12'), 'abc12');
|
||||
assert.equal(getStreamableId('https://streamable.com/e/abc12'), null); // already an embed path
|
||||
});
|
||||
|
||||
test('Twitch: channel / video / clip', () => {
|
||||
assert.deepEqual(getTwitchTarget('https://www.twitch.tv/somestreamer'), {
|
||||
type: 'channel',
|
||||
value: 'somestreamer',
|
||||
});
|
||||
assert.deepEqual(parseVideoEmbed('https://www.youtube.com/watch?v=xyz'), {
|
||||
provider: 'youtube',
|
||||
assert.deepEqual(getTwitchTarget('https://www.twitch.tv/videos/123456789'), {
|
||||
type: 'video',
|
||||
value: '123456789',
|
||||
});
|
||||
assert.deepEqual(getTwitchTarget('https://clips.twitch.tv/FunnyClipSlug'), {
|
||||
type: 'clip',
|
||||
value: 'FunnyClipSlug',
|
||||
});
|
||||
assert.deepEqual(getTwitchTarget('https://www.twitch.tv/streamer/clip/CoolSlug'), {
|
||||
type: 'clip',
|
||||
value: 'CoolSlug',
|
||||
});
|
||||
});
|
||||
|
||||
test('Spotify target + height', () => {
|
||||
assert.deepEqual(getSpotifyEmbedTarget('https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT'), {
|
||||
type: 'track',
|
||||
id: '4cOdK2wGLETKBW3PvgPWqT',
|
||||
});
|
||||
assert.deepEqual(getSpotifyEmbedTarget('https://open.spotify.com/intl-de/album/xyz'), {
|
||||
type: 'album',
|
||||
id: 'xyz',
|
||||
portrait: false,
|
||||
});
|
||||
assert.deepEqual(parseVideoEmbed('https://vimeo.com/42'), {
|
||||
provider: 'vimeo',
|
||||
id: '42',
|
||||
portrait: false,
|
||||
});
|
||||
assert.equal(parseVideoEmbed('https://example.com/video'), null);
|
||||
assert.equal(getSpotifyEmbedTarget('https://open.spotify.com/'), null);
|
||||
assert.equal(spotifyEmbedHeight('track'), 152);
|
||||
assert.equal(spotifyEmbedHeight('album'), 352);
|
||||
});
|
||||
|
||||
test('buildVideoEmbedUrl: cookie-less YouTube + Vimeo, id encoded', () => {
|
||||
test('SoundCloud track detection', () => {
|
||||
assert.equal(isSoundCloudTrack('https://soundcloud.com/artist/some-track'), true);
|
||||
assert.equal(isSoundCloudTrack('https://soundcloud.com/artist'), false); // bare profile
|
||||
});
|
||||
|
||||
test('buildVideoEmbedUrl: cookie-less YouTube + Vimeo', () => {
|
||||
assert.equal(
|
||||
buildVideoEmbedUrl('youtube', 'dQw4w9WgXcQ'),
|
||||
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&rel=0',
|
||||
);
|
||||
assert.equal(
|
||||
buildVideoEmbedUrl('vimeo', '123456789'),
|
||||
'https://player.vimeo.com/video/123456789?autoplay=1',
|
||||
);
|
||||
// id is URL-encoded (defense-in-depth; ids are normally already safe)
|
||||
assert.ok(buildVideoEmbedUrl('youtube', 'a/b').includes('a%2Fb'));
|
||||
assert.equal(buildVideoEmbedUrl('vimeo', '42'), 'https://player.vimeo.com/video/42?autoplay=1');
|
||||
});
|
||||
|
||||
test('parseMediaEmbed: routes provider + kind, builds embed URLs', () => {
|
||||
assert.deepEqual(parseMediaEmbed('https://youtube.com/shorts/abc', HOST), {
|
||||
provider: 'youtube',
|
||||
kind: 'portrait',
|
||||
embedUrl: 'https://www.youtube-nocookie.com/embed/abc?autoplay=1&rel=0',
|
||||
});
|
||||
assert.equal(parseMediaEmbed('https://www.youtube.com/watch?v=xyz', HOST)?.kind, 'landscape');
|
||||
assert.equal(parseMediaEmbed('https://www.tiktok.com/@u/video/123', HOST)?.provider, 'tiktok');
|
||||
assert.equal(parseMediaEmbed('https://www.tiktok.com/@u/video/123', HOST)?.kind, 'portrait');
|
||||
assert.equal(parseMediaEmbed('https://streamable.com/abc', HOST)?.provider, 'streamable');
|
||||
const spotify = parseMediaEmbed('https://open.spotify.com/track/abc', HOST);
|
||||
assert.equal(spotify?.kind, 'audio');
|
||||
assert.equal(spotify?.height, 152);
|
||||
assert.equal(parseMediaEmbed('https://soundcloud.com/a/b', HOST)?.provider, 'soundcloud');
|
||||
assert.equal(parseMediaEmbed('https://example.com/x', HOST), null);
|
||||
});
|
||||
|
||||
test('parseMediaEmbed: Twitch embed carries the parent host', () => {
|
||||
const chan = parseMediaEmbed('https://twitch.tv/streamer', HOST);
|
||||
assert.equal(chan?.provider, 'twitch');
|
||||
assert.ok(chan?.embedUrl.includes('channel=streamer'));
|
||||
assert.ok(chan?.embedUrl.includes(`parent=${HOST}`));
|
||||
const clip = parseMediaEmbed('https://clips.twitch.tv/Slug', HOST);
|
||||
assert.ok(clip?.embedUrl.startsWith('https://clips.twitch.tv/embed?clip=Slug'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user