Skip to content

Commit e72a40f

Browse files
add better parsing for youtube embed links
also introduces support for timestamps, playlists and youtube music links, tracking info is still stripped, embeds use the timestamp
1 parent 28d8e94 commit e72a40f

2 files changed

Lines changed: 59 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Add support for timestamps, playlists and youtube music links for the youtube embeds

src/app/components/url-preview/ClientPreview.tsx

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,20 @@ export function EmbedOpenButton({ url }: EmbedOpenButtonProps) {
7171
}
7272

7373
type YoutubeElementProps = {
74-
videoId: string;
74+
videoInfo: YoutubeLink;
7575
embedData: OEmbed;
7676
};
7777

78-
export const YoutubeElement = as<'div', YoutubeElementProps>(({ videoId, embedData }) => {
79-
const thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
80-
const iframeSrc = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}?autoplay=1`;
81-
const videoUrl = `https://youtube.com/watch?v=${videoId}`;
78+
export const YoutubeElement = as<'div', YoutubeElementProps>(({ videoInfo, embedData }) => {
79+
const thumbnailUrl = `https://i.ytimg.com/vi/${videoInfo.videoId}/hqdefault.jpg`;
80+
81+
const timestamp = videoInfo.timestamp ? `&start=${videoInfo.timestamp}` : '';
82+
const playlist = videoInfo.playlist ? `&${videoInfo.playlist}` : '';
83+
84+
const iframeSrc = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoInfo.videoId)}?autoplay=1${timestamp}`;
85+
const videoUrl = videoInfo.isMusic
86+
? `https://music.youtube.com/watch?v=${videoInfo.videoId}${timestamp}${playlist}`
87+
: `https://youtube.com/watch?v=${videoInfo.videoId}${timestamp}${playlist}`;
8288

8389
const [blurHash, setBlurHash] = useState<string | undefined>();
8490

@@ -141,19 +147,56 @@ export const YoutubeElement = as<'div', YoutubeElementProps>(({ videoId, embedDa
141147
});
142148

143149
export const youtubeUrl = (url: string) =>
144-
url.match(/(https:\/\/)(www\.|m\.|)(youtube\.com|youtu\.be)\//);
150+
url.match(/(https:\/\/)(www\.|music\.|m\.|)(youtube\.com|youtu\.be)\//);
151+
152+
type YoutubeLink = {
153+
videoId: string;
154+
timestamp?: string;
155+
playlist?: string;
156+
isMusic: boolean;
157+
};
158+
159+
function parseYoutubeLink(url: string): YoutubeLink | null {
160+
const urlsplit = url.split('/');
161+
const path = urlsplit[urlsplit.length - 1];
162+
163+
let videoId: string | undefined;
164+
let params: string[];
165+
166+
if (url.includes('youtu.be')) {
167+
const split = path.split('?');
168+
[videoId] = split;
169+
params = split[1].split('&');
170+
} else {
171+
params = path.split('?')[1].split('&');
172+
videoId = params.find((s) => s.startsWith('v='), params)?.split('v=')[1];
173+
}
174+
175+
if (!videoId) return null;
176+
177+
// playlist is not used for the embed, it can be appended as is
178+
const playlist = params.find((s) => s.startsWith('list='), params);
179+
const timestamp = params.find((s) => s.startsWith('t='), params)?.split('t=')[1];
180+
181+
return {
182+
videoId,
183+
timestamp,
184+
playlist,
185+
isMusic: url.includes('music.youtube.com'),
186+
};
187+
}
145188

146189
export const ClientPreview = as<'div', { url: string }>(({ url, ...props }, ref) => {
147190
const [showYoutube] = useSetting(settingsAtom, 'clientPreviewYoutube');
148191

149192
// this component is overly complicated, because it was designed to support more embed types than just youtube
150193
// i'm leaving this mess here to support later expansion
151194
const isYoutube = !!youtubeUrl(url);
152-
const videoId = isYoutube ? url.match(/(?:shorts\/|watch\?v=|youtu\.be\/)(.{11})/)?.[1] : null;
195+
const videoInfo = isYoutube ? parseYoutubeLink(url) : null;
153196

154197
const fetchUrl =
155-
isYoutube && videoId
156-
? `https://www.youtube.com/oembed?url=${encodeURIComponent(`https://youtube.com/watch?v=${videoId}`)}`
198+
isYoutube && videoInfo
199+
? `https://www.youtube.com/oembed?url=${encodeURIComponent(`https://youtube.com/watch?v=${videoInfo.videoId}`)}`
157200
: url;
158201

159202
const [embedStatus, loadEmbed] = useAsyncCallback(
@@ -168,12 +211,12 @@ export const ClientPreview = as<'div', { url: string }>(({ url, ...props }, ref)
168211

169212
let previewContent;
170213

171-
if (isYoutube && videoId) {
214+
if (isYoutube && videoInfo) {
172215
if (showYoutube) {
173216
if (embedStatus.status === AsyncStatus.Error) return null;
174217

175218
if (embedStatus.status === AsyncStatus.Success && embedStatus.data) {
176-
previewContent = <YoutubeElement videoId={videoId} embedData={embedStatus.data} />;
219+
previewContent = <YoutubeElement videoInfo={videoInfo} embedData={embedStatus.data} />;
177220
} else {
178221
previewContent = (
179222
<Box grow="Yes" alignItems="Center" justifyContent="Center">

0 commit comments

Comments
 (0)