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
235 lines
4.9 KiB
TypeScript
235 lines
4.9 KiB
TypeScript
export const YOUTUBE_REGEX =
|
|
/^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu\.be|youtube-nocookie\.com))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/
|
|
export const YOUTUBE_REGEX_GLOBAL =
|
|
/^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu\.be|youtube-nocookie\.com))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/g
|
|
|
|
/**
|
|
* Checks whether a url is a youtube url that the extension can embed.
|
|
*
|
|
* The `src` attribute of a youtube node is declared with `default: null`, so a node
|
|
* parsed from an iframe without a `src` attribute legitimately carries no url. Guard
|
|
* against that here instead of letting the caller dereference it.
|
|
*/
|
|
export const isValidYoutubeUrl = (url: string | null | undefined) => {
|
|
if (!url) {
|
|
return null
|
|
}
|
|
|
|
return url.match(YOUTUBE_REGEX)
|
|
}
|
|
|
|
export interface GetEmbedUrlOptions {
|
|
url: string
|
|
allowFullscreen?: boolean
|
|
autoplay?: boolean
|
|
ccLanguage?: string
|
|
ccLoadPolicy?: boolean
|
|
controls?: boolean
|
|
disableKBcontrols?: boolean
|
|
enableIFrameApi?: boolean
|
|
endTime?: number
|
|
interfaceLanguage?: string
|
|
ivLoadPolicy?: number
|
|
loop?: boolean
|
|
modestBranding?: boolean
|
|
nocookie?: boolean
|
|
origin?: string
|
|
playlist?: string
|
|
progressBarColor?: string
|
|
startAt?: number
|
|
rel?: number
|
|
}
|
|
|
|
export interface YoutubeEmbedAttributes {
|
|
src: string
|
|
start?: number
|
|
}
|
|
|
|
export const getYoutubeEmbedUrl = (nocookie?: boolean, isPlaylist?: boolean) => {
|
|
if (isPlaylist) {
|
|
return 'https://www.youtube-nocookie.com/embed/videoseries?list='
|
|
}
|
|
return nocookie ? 'https://www.youtube-nocookie.com/embed/' : 'https://www.youtube.com/embed/'
|
|
}
|
|
|
|
export const getEmbedUrlFromYoutubeUrl = (options: GetEmbedUrlOptions) => {
|
|
const {
|
|
url,
|
|
allowFullscreen,
|
|
autoplay,
|
|
ccLanguage,
|
|
ccLoadPolicy,
|
|
controls,
|
|
disableKBcontrols,
|
|
enableIFrameApi,
|
|
endTime,
|
|
interfaceLanguage,
|
|
ivLoadPolicy,
|
|
loop,
|
|
modestBranding,
|
|
nocookie,
|
|
origin,
|
|
playlist,
|
|
progressBarColor,
|
|
startAt,
|
|
rel,
|
|
} = options
|
|
|
|
if (!isValidYoutubeUrl(url)) {
|
|
return null
|
|
}
|
|
|
|
// if is already an embed url, return it
|
|
if (url.includes('/embed/')) {
|
|
return url
|
|
}
|
|
|
|
// if is a youtu.be url, get the id after the /
|
|
if (url.includes('youtu.be')) {
|
|
const id = url.split('/').pop()
|
|
|
|
if (!id) {
|
|
return null
|
|
}
|
|
return `${getYoutubeEmbedUrl(nocookie)}${id}`
|
|
}
|
|
|
|
const videoIdRegex = /(?:(v|list)=|\/(?:shorts|live)\/)([-\w]+)/gm
|
|
const matches = videoIdRegex.exec(url)
|
|
|
|
if (!matches || !matches[2]) {
|
|
return null
|
|
}
|
|
|
|
let outputUrl = `${getYoutubeEmbedUrl(nocookie, matches[1] === 'list')}${matches[2]}`
|
|
|
|
const params = []
|
|
|
|
if (allowFullscreen === false) {
|
|
params.push('fs=0')
|
|
}
|
|
|
|
if (autoplay) {
|
|
params.push('autoplay=1')
|
|
}
|
|
|
|
if (ccLanguage) {
|
|
params.push(`cc_lang_pref=${ccLanguage}`)
|
|
}
|
|
|
|
if (ccLoadPolicy) {
|
|
params.push('cc_load_policy=1')
|
|
}
|
|
|
|
if (!controls) {
|
|
params.push('controls=0')
|
|
}
|
|
|
|
if (disableKBcontrols) {
|
|
params.push('disablekb=1')
|
|
}
|
|
|
|
if (enableIFrameApi) {
|
|
params.push('enablejsapi=1')
|
|
}
|
|
|
|
if (endTime) {
|
|
params.push(`end=${endTime}`)
|
|
}
|
|
|
|
if (interfaceLanguage) {
|
|
params.push(`hl=${interfaceLanguage}`)
|
|
}
|
|
|
|
if (ivLoadPolicy) {
|
|
params.push(`iv_load_policy=${ivLoadPolicy}`)
|
|
}
|
|
|
|
if (loop) {
|
|
params.push('loop=1')
|
|
}
|
|
|
|
if (modestBranding) {
|
|
params.push('modestbranding=1')
|
|
}
|
|
|
|
if (origin) {
|
|
params.push(`origin=${origin}`)
|
|
}
|
|
|
|
if (playlist) {
|
|
params.push(`playlist=${playlist}`)
|
|
}
|
|
|
|
if (startAt) {
|
|
params.push(`start=${startAt}`)
|
|
}
|
|
|
|
if (progressBarColor) {
|
|
params.push(`color=${progressBarColor}`)
|
|
}
|
|
|
|
if (rel !== undefined) {
|
|
params.push(`rel=${rel}`)
|
|
}
|
|
|
|
if (params.length) {
|
|
outputUrl += `${matches[1] === 'list' ? '&' : '?'}${params.join('&')}`
|
|
}
|
|
|
|
return outputUrl
|
|
}
|
|
|
|
export const getAttributesFromYoutubeEmbedUrl = (url: string): YoutubeEmbedAttributes | null => {
|
|
let parsedUrl: URL
|
|
|
|
try {
|
|
parsedUrl = new URL(url)
|
|
} catch {
|
|
return null
|
|
}
|
|
|
|
const hostname = parsedUrl.hostname.replace(/^www\./, '')
|
|
|
|
if (hostname !== 'youtube.com' && hostname !== 'youtube-nocookie.com') {
|
|
return null
|
|
}
|
|
|
|
let src: string | null = null
|
|
|
|
if (parsedUrl.pathname === '/embed/videoseries') {
|
|
const list = parsedUrl.searchParams.get('list')
|
|
|
|
if (!list) {
|
|
return null
|
|
}
|
|
|
|
src = `https://www.youtube.com/playlist?list=${list}`
|
|
} else {
|
|
const matches = parsedUrl.pathname.match(/^\/embed\/([\w-]+)$/)
|
|
|
|
if (!matches?.[1]) {
|
|
return null
|
|
}
|
|
|
|
src = `https://www.youtube.com/watch?v=${matches[1]}`
|
|
}
|
|
|
|
if (!isValidYoutubeUrl(src)) {
|
|
return null
|
|
}
|
|
|
|
const attributes: YoutubeEmbedAttributes = { src }
|
|
const start = parsedUrl.searchParams.get('start')
|
|
|
|
if (start) {
|
|
const parsedStart = Number.parseInt(start, 10)
|
|
|
|
if (!Number.isNaN(parsedStart)) {
|
|
attributes.start = parsedStart
|
|
}
|
|
}
|
|
|
|
return attributes
|
|
}
|