Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
296 lines
8.1 KiB
TypeScript
296 lines
8.1 KiB
TypeScript
/**
|
|
* Regex patterns for matching Twitch URLs
|
|
* Matches:
|
|
* - https://twitch.tv/videos/1234567890
|
|
* - https://www.twitch.tv/videos/1234567890
|
|
* - https://twitch.tv/examplechannel/clip/ClipName-123
|
|
* - https://www.twitch.tv/examplechannel/clip/ClipName-123
|
|
* - https://clips.twitch.tv/ClipName
|
|
* - https://www.clips.twitch.tv/ClipName
|
|
* - https://twitch.tv/examplechannel (channel)
|
|
* - https://www.twitch.tv/examplechannel
|
|
*/
|
|
export const TWITCH_REGEX =
|
|
/^(https?:\/\/)?(www\.)?(twitch\.tv|clips\.twitch\.tv)\/(?:videos\/(\d+)|(\w+)\/clip\/([\w-]+)|([\w-]+)(?:\/)?)?(\?.*)?$/
|
|
|
|
export const TWITCH_REGEX_GLOBAL =
|
|
/^(https?:\/\/)?(www\.)?(twitch\.tv|clips\.twitch\.tv)\/(?:videos\/(\d+)|(\w+)\/clip\/([\w-]+)|([\w-]+)(?:\/)?)?(\?.*)?$/g
|
|
|
|
/**
|
|
* Validates if a URL is a valid Twitch video, clip or channel URL
|
|
*
|
|
* @param url - The URL to validate
|
|
* @returns true if the URL is a valid Twitch URL
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* isValidTwitchUrl('https://www.twitch.tv/videos/1234567890') // true
|
|
* isValidTwitchUrl('https://www.twitch.tv/examplechannel/clip/ExampleClipName') // true
|
|
* isValidTwitchUrl('https://www.twitch.tv/examplechannel') // true
|
|
* isValidTwitchUrl('https://clips.twitch.tv/ExampleClipName') // true
|
|
* isValidTwitchUrl('invalid') // false
|
|
* ```
|
|
*/
|
|
export const isValidTwitchUrl = (url: string) => {
|
|
return url.match(TWITCH_REGEX)
|
|
}
|
|
|
|
export interface GetEmbedUrlOptions {
|
|
url: string
|
|
allowFullscreen?: boolean
|
|
autoplay?: boolean
|
|
muted?: boolean
|
|
time?: string
|
|
parent?: string
|
|
}
|
|
|
|
export interface TwitchEmbedAttributes {
|
|
src: string
|
|
autoplay?: boolean
|
|
muted?: boolean
|
|
time?: string
|
|
}
|
|
|
|
/**
|
|
* Extracts the video, clip or channel identifier from a Twitch URL
|
|
*
|
|
* @param url - The Twitch URL
|
|
* @returns Object containing type ('video', 'clip' or 'channel') and ID, or null if invalid
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* getTwitchIdentifier('https://www.twitch.tv/videos/1234567890')
|
|
* // { type: 'video', id: '1234567890' }
|
|
*
|
|
* getTwitchIdentifier('https://www.twitch.tv/examplechannel/clip/ExampleClipName-ABC123')
|
|
* // { type: 'clip', id: 'ExampleClipName-ABC123' }
|
|
*
|
|
* getTwitchIdentifier('https://www.twitch.tv/examplechannel')
|
|
* // { type: 'channel', id: 'examplechannel' }
|
|
* ```
|
|
*/
|
|
export const getTwitchIdentifier = (
|
|
url: string,
|
|
): { type: 'video' | 'clip' | 'channel'; id: string } | null => {
|
|
if (!isValidTwitchUrl(url)) {
|
|
return null
|
|
}
|
|
|
|
// Remove query parameters
|
|
const cleanUrl = url.split('?')[0]
|
|
|
|
// Handle clip URLs from clips.twitch.tv
|
|
if (cleanUrl.includes('clips.twitch.tv/')) {
|
|
const clipRegex = /clips\.twitch\.tv\/([\w-]+)/
|
|
const match = cleanUrl.match(clipRegex)
|
|
return match ? { type: 'clip', id: match[1] } : null
|
|
}
|
|
|
|
// Handle twitch.tv URLs
|
|
if (cleanUrl.includes('twitch.tv/')) {
|
|
// Check if it's a video URL (videos/ID)
|
|
const videoRegex = /twitch\.tv\/videos\/(\d+)/
|
|
const videoMatch = cleanUrl.match(videoRegex)
|
|
if (videoMatch) {
|
|
return { type: 'video', id: videoMatch[1] }
|
|
}
|
|
|
|
// Check if it's a clip URL (channel/clip/clipname)
|
|
const channelClipRegex = /twitch\.tv\/([\w-]+)\/clip\/([\w-]+)/
|
|
const clipMatch = cleanUrl.match(channelClipRegex)
|
|
if (clipMatch) {
|
|
return { type: 'clip', id: clipMatch[2] }
|
|
}
|
|
|
|
// Otherwise it's a channel URL
|
|
const channelRegex = /twitch\.tv\/([\w-]+)(?:\/)?$/
|
|
const channelMatch = cleanUrl.match(channelRegex)
|
|
if (channelMatch) {
|
|
return { type: 'channel', id: channelMatch[1] }
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Generates an embed URL from a Twitch video, clip or channel URL with optional parameters
|
|
*
|
|
* @param options - The embed URL options
|
|
* @returns The embed URL or null if invalid
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* getEmbedUrlFromTwitchUrl({
|
|
* url: 'https://www.twitch.tv/videos/1234567890',
|
|
* parent: 'example.com',
|
|
* muted: true,
|
|
* time: '1h2m3s'
|
|
* })
|
|
* // Returns: https://player.twitch.tv/?video=1234567890&parent=example.com&muted=true&time=1h2m3s
|
|
*
|
|
* getEmbedUrlFromTwitchUrl({
|
|
* url: 'https://www.twitch.tv/examplechannel/clip/ExampleClipName-ABC123',
|
|
* parent: 'example.com',
|
|
* autoplay: true,
|
|
* muted: true
|
|
* })
|
|
* // Returns: https://clips.twitch.tv/embed?clip=ExampleClipName-ABC123&parent=example.com&autoplay=true&muted=true
|
|
*
|
|
* getEmbedUrlFromTwitchUrl({
|
|
* url: 'https://www.twitch.tv/examplechannel',
|
|
* parent: 'example.com'
|
|
* })
|
|
* // Returns: https://player.twitch.tv/?channel=examplechannel&parent=example.com
|
|
* ```
|
|
*/
|
|
export const getEmbedUrlFromTwitchUrl = (options: GetEmbedUrlOptions): string | null => {
|
|
const { url, allowFullscreen = true, autoplay = false, muted = false, time, parent } = options
|
|
|
|
const identifier = getTwitchIdentifier(url)
|
|
|
|
if (!identifier) {
|
|
return null
|
|
}
|
|
|
|
const parentDomain = parent || 'localhost'
|
|
|
|
// Handle clip URLs
|
|
if (identifier.type === 'clip') {
|
|
const clipUrl = new URL('https://clips.twitch.tv/embed')
|
|
clipUrl.searchParams.set('clip', identifier.id)
|
|
clipUrl.searchParams.set('parent', parentDomain)
|
|
|
|
if (autoplay) {
|
|
clipUrl.searchParams.set('autoplay', 'true')
|
|
}
|
|
|
|
if (muted) {
|
|
clipUrl.searchParams.set('muted', 'true')
|
|
}
|
|
|
|
return clipUrl.toString()
|
|
}
|
|
|
|
// Handle video URLs
|
|
if (identifier.type === 'video') {
|
|
const videoUrl = new URL('https://player.twitch.tv/')
|
|
videoUrl.searchParams.set('video', identifier.id)
|
|
videoUrl.searchParams.set('parent', parentDomain)
|
|
|
|
if (allowFullscreen) {
|
|
videoUrl.searchParams.set('allowfullscreen', 'true')
|
|
}
|
|
|
|
if (autoplay) {
|
|
videoUrl.searchParams.set('autoplay', 'true')
|
|
}
|
|
|
|
if (muted) {
|
|
videoUrl.searchParams.set('muted', 'true')
|
|
}
|
|
|
|
if (time) {
|
|
videoUrl.searchParams.set('time', time)
|
|
}
|
|
|
|
return videoUrl.toString()
|
|
}
|
|
|
|
// Handle channel URLs
|
|
if (identifier.type === 'channel') {
|
|
const channelUrl = new URL('https://player.twitch.tv/')
|
|
channelUrl.searchParams.set('channel', identifier.id)
|
|
channelUrl.searchParams.set('parent', parentDomain)
|
|
|
|
if (allowFullscreen) {
|
|
channelUrl.searchParams.set('allowfullscreen', 'true')
|
|
}
|
|
|
|
if (autoplay) {
|
|
channelUrl.searchParams.set('autoplay', 'true')
|
|
}
|
|
|
|
if (muted) {
|
|
channelUrl.searchParams.set('muted', 'true')
|
|
}
|
|
|
|
return channelUrl.toString()
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Parses a Twitch embed URL into canonical Twitch node attributes.
|
|
*
|
|
* @param url - The Twitch embed URL
|
|
* @returns Canonical node attributes or null if the URL is not a supported Twitch embed URL
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* getAttributesFromTwitchEmbedUrl('https://player.twitch.tv/?video=1234567890&parent=example.com&muted=true')
|
|
* // Returns: { src: 'https://www.twitch.tv/videos/1234567890', muted: true }
|
|
*
|
|
* getAttributesFromTwitchEmbedUrl('https://clips.twitch.tv/embed?clip=ExampleClipName-ABC123&parent=example.com')
|
|
* // Returns: { src: 'https://clips.twitch.tv/ExampleClipName-ABC123' }
|
|
* ```
|
|
*/
|
|
export const getAttributesFromTwitchEmbedUrl = (url: string): TwitchEmbedAttributes | null => {
|
|
let parsedUrl: URL
|
|
|
|
try {
|
|
parsedUrl = new URL(url)
|
|
} catch {
|
|
return null
|
|
}
|
|
|
|
const hostname = parsedUrl.hostname.replace(/^www\./, '')
|
|
const result: TwitchEmbedAttributes = {
|
|
src: '',
|
|
}
|
|
|
|
if (hostname === 'player.twitch.tv') {
|
|
const videoId = parsedUrl.searchParams.get('video')
|
|
const channelName = parsedUrl.searchParams.get('channel')
|
|
|
|
if (videoId) {
|
|
result.src = `https://www.twitch.tv/videos/${videoId}`
|
|
} else if (channelName) {
|
|
result.src = `https://www.twitch.tv/${channelName}`
|
|
} else {
|
|
return null
|
|
}
|
|
} else if (hostname === 'clips.twitch.tv' && parsedUrl.pathname === '/embed') {
|
|
const clipId = parsedUrl.searchParams.get('clip')
|
|
|
|
if (!clipId) {
|
|
return null
|
|
}
|
|
|
|
result.src = `https://clips.twitch.tv/${clipId}`
|
|
} else {
|
|
return null
|
|
}
|
|
|
|
if (parsedUrl.searchParams.get('autoplay') === 'true') {
|
|
result.autoplay = true
|
|
}
|
|
|
|
if (parsedUrl.searchParams.get('muted') === 'true') {
|
|
result.muted = true
|
|
}
|
|
|
|
const time = parsedUrl.searchParams.get('time')
|
|
|
|
if (time) {
|
|
result.time = time
|
|
}
|
|
|
|
if (!isValidTwitchUrl(result.src)) {
|
|
return null
|
|
}
|
|
|
|
return result
|
|
}
|