From 8dbb0cf9cebff89fd323eb9149cb2b7d5f7002aa Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 25 Oct 2026 10:48:59 +0300 Subject: [PATCH] feat: parse YouTube video links and page types Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 2 ++ package.json | 9 ++++++ src/lib/video-id.js | 34 ++++++++++++++++++++ test/video-id.test.js | 73 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/lib/video-id.js create mode 100644 test/video-id.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/package.json b/package.json new file mode 100644 index 0000000..62b9e25 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "outlier-saver-extension", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "test": "node --test" + } +} diff --git a/src/lib/video-id.js b/src/lib/video-id.js new file mode 100644 index 0000000..40af4f0 --- /dev/null +++ b/src/lib/video-id.js @@ -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; +} diff --git a/test/video-id.test.js b/test/video-id.test.js new file mode 100644 index 0000000..9649719 --- /dev/null +++ b/test/video-id.test.js @@ -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); + } +});