feat: parse YouTube video links and page types

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
IgorVolochay
2026-10-25 10:48:59 +03:00
co-authored by Claude Sonnet 5.5
parent 05d8bff171
commit 8dbb0cf9ce
4 changed files with 118 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
.DS_Store
+9
View File
@@ -0,0 +1,9 @@
{
"name": "outlier-saver-extension",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"test": "node --test"
}
}
+34
View File
@@ -0,0 +1,34 @@
const ID_RE = /^[A-Za-z0-9_-]{11}$/;
const YOUTUBE_HOSTS = new Set(['www.youtube.com', 'youtube.com', 'm.youtube.com']);
export function isVideoId(value) {
return typeof value === 'string' && ID_RE.test(value);
}
// Returns { videoId, isShort } for a link to a single video, or null for anything else.
export function parseVideoLink(href, base = 'https://www.youtube.com') {
if (!href) return null;
let url;
try {
url = new URL(href, base);
} catch {
return null;
}
if (!YOUTUBE_HOSTS.has(url.hostname)) return null;
if (url.pathname === '/watch') {
const id = url.searchParams.get('v');
return isVideoId(id) ? { videoId: id, isShort: false } : null;
}
const shorts = url.pathname.match(/^\/shorts\/([^/]+)/);
if (shorts && isVideoId(shorts[1])) return { videoId: shorts[1], isShort: true };
return null;
}
export function pageFromPath(pathname) {
if (pathname === '/') return 'home';
if (pathname === '/watch') return 'watch';
if (pathname === '/results') return 'search';
if (/^\/(@[^/]+|channel\/[^/]+|c\/[^/]+|user\/[^/]+)/.test(pathname)) return 'channel';
return null;
}
+73
View File
@@ -0,0 +1,73 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { isVideoId, pageFromPath, parseVideoLink } from '../src/lib/video-id.js';
test('isVideoId accepts only 11-character IDs', () => {
assert.equal(isVideoId('dQw4w9WgXcQ'), true);
assert.equal(isVideoId('abc_DEF-123'), true);
for (const value of ['short', 'waytoolongvideoid', 'has space!!', '', null, undefined, 12345678901]) {
assert.equal(isVideoId(value), false, String(value));
}
});
test('parseVideoLink reads an absolute watch URL', () => {
assert.deepEqual(parseVideoLink('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), {
videoId: 'dQw4w9WgXcQ',
isShort: false,
});
});
test('parseVideoLink reads a relative watch href with extra params', () => {
assert.deepEqual(parseVideoLink('/watch?v=dQw4w9WgXcQ&t=42s&list=PL123'), {
videoId: 'dQw4w9WgXcQ',
isShort: false,
});
});
test('parseVideoLink marks /shorts/ links as shorts', () => {
assert.deepEqual(parseVideoLink('/shorts/abcdefghijk'), { videoId: 'abcdefghijk', isShort: true });
assert.deepEqual(parseVideoLink('/shorts/abcdefghijk?feature=share'), {
videoId: 'abcdefghijk',
isShort: true,
});
});
test('parseVideoLink ignores links that are not a single video', () => {
for (const href of ['/', '/@mrbeast', '/playlist?list=PL123', '/feed/subscriptions', '/results?search_query=x']) {
assert.equal(parseVideoLink(href), null, href);
}
});
test('parseVideoLink rejects malformed IDs', () => {
for (const href of [
'/watch',
'/watch?v=',
'/watch?v=short',
'/watch?v=waytoolongvideoid',
'/shorts/toolongidtoolong',
]) {
assert.equal(parseVideoLink(href), null, href);
}
});
test('parseVideoLink rejects other hosts and garbage', () => {
assert.equal(parseVideoLink('https://evil.example.com/watch?v=dQw4w9WgXcQ'), null);
for (const href of [null, undefined, '', 'http://[bad']) {
assert.equal(parseVideoLink(href), null, String(href));
}
});
test('pageFromPath maps the four supported page types', () => {
assert.equal(pageFromPath('/'), 'home');
assert.equal(pageFromPath('/watch'), 'watch');
assert.equal(pageFromPath('/results'), 'search');
for (const path of ['/@mrbeast', '/@mrbeast/videos', '/channel/UC123/videos', '/c/name', '/user/name']) {
assert.equal(pageFromPath(path), 'channel', path);
}
});
test('pageFromPath returns null for unsupported pages', () => {
for (const path of ['/feed/subscriptions', '/shorts/abcdefghijk', '/playlist', '/premium']) {
assert.equal(pageFromPath(path), null, path);
}
});