Skip to content

Commit

Permalink
support YouTube clips via HTML og:video:url parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
KararTY committed Dec 18, 2024
1 parent f25e999 commit 7ce4201
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/usercontent
/src/lib/worker/initialUpdate/worker.js
/src/lib/worker/toHTML/worker.js
/src/lib/worker/toHTML/worker.js
/src/lib/worker/youtubeClipURL/worker.js
18 changes: 18 additions & 0 deletions buildWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,23 @@ async function buildToHTMLWorker() {
writeFileSync('src/lib/worker/toHTML/worker.js', result.outputFiles[0].contents);
}

async function buildYoutubeClipURL() {
const result = await esbuild.build({
entryPoints: ['worker/youtubeClipURL.js'],
bundle: true,
platform: 'node',
target: 'node22',
format: 'esm',
outfile: './src/lib/worker/youtubeClipURL.js',
write: false,
});

// Ensure dist directory exists
mkdirSync('src/lib/worker/youtubeClipURL', { recursive: true });
writeFileSync('src/lib/worker/youtubeClipURL/worker.js', result.outputFiles[0].contents);
}


buildInitialUpdateWorker().catch(() => process.exit(1));
buildToHTMLWorker().catch(() => process.exit(1));
buildYoutubeClipURL().catch(() => process.exit(1));
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"start": "node -r dotenv/config build",
"dev": "vite dev & npm run dev:usercontent",
"dev:usercontent": "npx sirv-cli usercontent -p=5175",
"build": "node buildWorker.js && vite build && cp -a ./.svelte-kit/output/server/chunks/worker.js ./build/server/chunks/ && cp -a ./.svelte-kit/output/server/chunks/worker2.js ./build/server/chunks/",
"build": "node buildWorker.js && vite build && cp -a ./.svelte-kit/output/server/chunks/worker.js ./build/server/chunks/ && cp -a ./.svelte-kit/output/server/chunks/worker2.js ./build/server/chunks/ && cp -a ./.svelte-kit/output/server/chunks/worker3.js ./build/server/chunks/",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
Expand Down
10 changes: 10 additions & 0 deletions src/lib/api/utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const headers = new Headers({ 'content-type': 'application/json' });

/**
* @param {string} clip
*/
export const getYouTubeClipURL = async (clip) => {
const body = JSON.stringify({ clip });

return fetch(`/api/utils/youtube`, { method: 'POST', body, headers });
};
19 changes: 19 additions & 0 deletions src/lib/components/editor/plugins/VideoEmbed/VideoEmbed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ export const getURLAndTitle = (

if (platform === 'youtube') {
const url = new URL('', src);

if (url.pathname.startsWith('/embed/')) {
const fullVideoSlug = url.pathname.split('/').pop();
const clipSlug = url.searchParams.get('clip');
const clipTId = url.searchParams.get('clipt');

const youtubeEmbedURL = new URL(`embed/${fullVideoSlug}`, 'https://www.youtube.com/');

if (clipSlug && clipTId) {
youtubeEmbedURL.searchParams.set('clip', clipSlug);
youtubeEmbedURL.searchParams.set('clipt', clipTId);
}

return {
url: youtubeEmbedURL.toString(),
title: 'YouTube clip',
};
}

const v = url.searchParams.get('v');
const youtuBE = url.hostname === 'youtu.be' ? url.pathname : null;
const vPathname = url.pathname.startsWith('/v/')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,42 @@
import { getEditor } from 'svelte-lexical';
import { mergeRegister } from '@lexical/utils';
import {
$createVideoEmbedNode as createVideoEmbedNode,
VideoEmbedNode,
} from './VideoEmbed';
import { getYouTubeClipURL } from '$lib/api/utils';
import { $createVideoEmbedNode as createVideoEmbedNode, VideoEmbedNode } from './VideoEmbed';
/** @type {import('lexical').LexicalEditor} */
const editor = getEditor();
/**
* @param {LexicalEditor} editor
* @param {VideoEmbedNode} node
*/
async function fixYouTubeClipURL(editor, node) {
const url = node.getSrc();
let res;
try {
const req = await getYouTubeClipURL(url);
if (req.status === 200) {
res = await req.json();
}
} catch {
// noop
}
if (res === url) {
return;
}
if (typeof res === 'string') {
editor.update(() => {
node.setSrc(res);
});
}
}
/** @param {import('./VideoEmbed').VideoEmbedPayload} payload */
function wrapperInsertVideoEmbed(payload) {
editor.update(() => {
Expand Down Expand Up @@ -59,7 +87,9 @@
editor.registerMutationListener(VideoEmbedNode, (mutatedNodes) => {
editor.update(() => {
for (const [key, mutation] of mutatedNodes) {
if (mutation === 'destroyed') continue;
if (mutation === 'destroyed') {
continue;
}
/** @type {VideoEmbedNode | null} */
const node = getNodeByKey(key);
Expand All @@ -84,6 +114,10 @@
const p = createParagraphNode();
node.insertAfter(p, false);
}
if (node.getPlatform() === 'youtube' && node.getSrc().includes('youtube.com/clip/')) {
fixYouTubeClipURL(editor, node);
}
}
});
}),
Expand Down
21 changes: 21 additions & 0 deletions src/lib/worker/youtubeClipURL/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @ts-ignore
import worker from './worker?nodeWorker';

/**
* @param {{ url: string }} workerData
* @returns {Promise<string>}
*/
export default function youtubeClipURL(workerData) {
return new Promise((resolve, reject) => {
const w = worker({ workerData });

w.on('message', resolve);
w.on('error', reject);

w.on('exit', (/** @type {number} */ code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
15 changes: 15 additions & 0 deletions src/routes/api/utils/youtube/+server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { error, json } from '@sveltejs/kit';

import youtubeClipURL from '$lib/worker/youtubeClipURL';

export async function POST({ request }) {
const { clip } = await request.json();

if (clip) {
const res = await youtubeClipURL({ url: clip });

return json(res);
}

return error(400);
}
10 changes: 6 additions & 4 deletions worker/toHTML.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import 'linkedom-global';
import { base64ToUint8Array } from 'uint8array-extras';

import { workerData, parentPort } from 'node:worker_threads';

import { getYjsAndEditor } from '$lib/yjs/getYjsAndEditor';
import { articleConfig } from '$lib/components/editor/config/article';
import { diffConfig } from '$lib/components/editor/config/diff';
import { $getRoot } from 'lexical';
import { $generateHtmlFromNodes } from '@lexical/html';
import { createHeadlessEditor } from '@lexical/headless';
import { base64ToUint8Array } from 'uint8array-extras';

import { getYjsAndEditor } from '$lib/yjs/getYjsAndEditor';
import { articleConfig } from '$lib/components/editor/config/article';
import { diffConfig } from '$lib/components/editor/config/diff';

const toHTMLWorker = async () => {
/**
Expand Down
38 changes: 38 additions & 0 deletions worker/youtubeClipURL.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'linkedom-global';

import { workerData, parentPort } from 'node:worker_threads';

const youtubeClipURLWorker = async () => {
/**
* @type {{ url: string }}
*/
const { url } = workerData;

const parsedURL = new URL('', url);

if (parsedURL.hostname !== 'www.youtube.com' && parsedURL.hostname !== 'youtube.com') {
parentPort?.postMessage(url);

return;
}

const text = await (await fetch(url)).text();

const html = new DOMParser().parseFromString(text, 'text/html');

/** @type {HTMLMetaElement?} */
let metaVideoURLTag;
try {
metaVideoURLTag = html.querySelector('meta[property="og:video:url"]');
} catch {
// noop
}

parentPort?.postMessage(metaVideoURLTag?.content || url);
};

try {
youtubeClipURLWorker();
} catch (error) {
console.error('toHTMLWorker error', error);
}

0 comments on commit 7ce4201

Please sign in to comment.