Skip to content

Commit 64082c4

Browse files
authored
Add better parsing for youtube embed links (#534)
### Description Replace the previous hacky regex method with a slightly better link parser. Add support for timestamps, playlists and youtube music links, tracking info is still stripped and embeds use the timestamp. Previously none of these features were supported, this might also be slightly more robust. #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [ ] My code follows the style guidelines of this project - [X] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [X] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail).
2 parents 746f777 + a7a3498 commit 64082c4

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 (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)