Skip to content

Commit 59e14a1

Browse files
Merge pull request #48 from Juliusolsson05/fix/youtube-embed-parser
fix: use oEmbed to verify YouTube embeds
2 parents a67135b + ec8fdf3 commit 59e14a1

1 file changed

Lines changed: 20 additions & 27 deletions

File tree

  • src/app/api/v1/perspectives/live-status

src/app/api/v1/perspectives/live-status/route.ts

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { err, ok } from '@/server/lib/api-utils';
55
const CACHE_TTL = 600;
66
const CANONICAL_VIDEO_RE = /<link rel="canonical" href="https:\/\/www\.youtube\.com\/watch\?v=([^"&]+)"/;
77
const IS_LIVE_RE = /"isLive"\s*:\s*true/;
8-
const PLAYABLE_VIDEO_RE = /"playabilityStatus":\{"status":"OK","playableInEmbed":(true|false)[\s\S]{0,4000}?"videoId":"([^"]+)"/;
98

109
type CacheEntry = {
1110
isLive: boolean;
@@ -16,42 +15,36 @@ type CacheEntry = {
1615

1716
const cache = new Map<string, CacheEntry>();
1817

19-
function escapeRegExp(value: string) {
20-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21-
}
22-
23-
function parseLivePlayback(html: string) {
24-
const canonicalVideoId = html.match(CANONICAL_VIDEO_RE)?.[1] ?? null;
25-
26-
if (canonicalVideoId) {
27-
const canonicalPlayable = new RegExp(
28-
`"playabilityStatus":\\{"status":"OK","playableInEmbed":(true|false)[\\s\\S]{0,4000}?"videoId":"${escapeRegExp(canonicalVideoId)}"`,
29-
).exec(html);
18+
async function isEmbeddableVideo(videoId: string) {
19+
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
3020

31-
if (canonicalPlayable) {
32-
return {
33-
playableInEmbed: canonicalPlayable[1] === 'true',
34-
videoId: canonicalPlayable[1] === 'true' ? canonicalVideoId : null,
35-
};
36-
}
21+
try {
22+
const res = await fetch(oembedUrl, {
23+
headers: { 'User-Agent': 'Mozilla/5.0' },
24+
signal: AbortSignal.timeout(8000),
25+
});
3726

38-
return {
39-
playableInEmbed: true,
40-
videoId: canonicalVideoId,
41-
};
27+
return res.ok;
28+
} catch {
29+
return false;
4230
}
31+
}
4332

44-
const playableMatch = html.match(PLAYABLE_VIDEO_RE);
45-
if (!playableMatch) {
33+
async function parseLivePlayback(html: string) {
34+
const canonicalVideoId = html.match(CANONICAL_VIDEO_RE)?.[1] ?? null;
35+
36+
if (!canonicalVideoId) {
4637
return {
4738
playableInEmbed: false,
4839
videoId: null,
4940
};
5041
}
5142

43+
const playableInEmbed = await isEmbeddableVideo(canonicalVideoId);
44+
5245
return {
53-
playableInEmbed: playableMatch[1] === 'true',
54-
videoId: playableMatch[1] === 'true' ? playableMatch[2] : null,
46+
playableInEmbed,
47+
videoId: playableInEmbed ? canonicalVideoId : null,
5548
};
5649
}
5750

@@ -73,7 +66,7 @@ async function checkLiveStatus(handle: string): Promise<{ isLive: boolean; playa
7366
const html = await res.text();
7467
const isLive = IS_LIVE_RE.test(html);
7568
const playback = isLive
76-
? parseLivePlayback(html)
69+
? await parseLivePlayback(html)
7770
: { playableInEmbed: false, videoId: null };
7871
const playableInEmbed = playback.playableInEmbed;
7972
const videoId = playback.videoId;

0 commit comments

Comments
 (0)