Init commit

This commit is contained in:
ShreejitPanchal
2026-05-19 19:56:02 +08:00
commit 6601501eff
4047 changed files with 2185372 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
{
"name": "@peertube/peertube-core-utils",
"private": true,
"version": "0.0.0",
"main": "dist/index.js",
"files": [ "dist" ],
"exports": {
"types": "./dist/index.d.ts",
"peertube:tsx": "./src/index.ts",
"default": "./dist/index.js"
},
"type": "module",
"devDependencies": {},
"scripts": {
"build": "tsc",
"watch": "tsc -w"
},
"dependencies": {}
}

View File

@@ -0,0 +1,14 @@
import { AbusePredefinedReasons, AbusePredefinedReasonsString, AbusePredefinedReasonsType } from '@peertube/peertube-models'
export const abusePredefinedReasonsMap: {
[key in AbusePredefinedReasonsString]: AbusePredefinedReasonsType
} = {
violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE,
hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE,
spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING,
privacy: AbusePredefinedReasons.PRIVACY,
rights: AbusePredefinedReasons.RIGHTS,
serverRules: AbusePredefinedReasons.SERVER_RULES,
thumbnails: AbusePredefinedReasons.THUMBNAILS,
captions: AbusePredefinedReasons.CAPTIONS
} as const

View File

@@ -0,0 +1 @@
export * from './abuse-predefined-reasons.js'

View File

@@ -0,0 +1,76 @@
export function findCommonElement<T> (array1: T[], array2: T[]) {
for (const a of array1) {
for (const b of array2) {
if (a === b) return a
}
}
return null
}
// Avoid conflict with other toArray() functions
export function arrayify<T> (element: T | T[]) {
if (Array.isArray(element)) return element
if (element === undefined || element === null) return []
return [ element ]
}
export function unarray<T> (element: T | T[]) {
if (Array.isArray(element)) {
if (element.length === 0) return undefined
return element[0]
}
return element
}
// Avoid conflict with other uniq() functions
export function uniqify<T> (elements: T[]) {
return Array.from(new Set(elements))
}
// Thanks: https://stackoverflow.com/a/12646864
export function shuffle<T> (elements: T[]) {
const shuffled = [ ...elements ]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[ shuffled[i], shuffled[j] ] = [ shuffled[j], shuffled[i] ]
}
return shuffled
}
export function sortBy<T> (obj: T[], key1: string, key2?: string): T[] {
return obj.sort((a, b) => {
const elem1 = key2 ? a[key1][key2] : a[key1]
const elem2 = key2 ? b[key1][key2] : b[key1]
if (elem1 < elem2) return -1
if (elem1 === elem2) return 0
return 1
})
}
export function maxBy<T> (arr: T[], property: keyof T) {
let result: T
for (const obj of arr) {
if (!result || result[property] < obj[property]) result = obj
}
return result
}
export function minBy<T> (arr: T[], property: keyof T) {
let result: T
for (const obj of arr) {
if (!result || result[property] > obj[property]) result = obj
}
return result
}

View File

@@ -0,0 +1,167 @@
export function isToday (d: Date) {
const today = new Date()
return areDatesEqual(d, today)
}
export function isYesterday (d: Date) {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
return areDatesEqual(d, yesterday)
}
export function isThisWeek (d: Date) {
const minDateOfThisWeek = new Date()
minDateOfThisWeek.setHours(0, 0, 0)
// getDay() -> Sunday - Saturday : 0 - 6
// We want to start our week on Monday
let dayOfWeek = minDateOfThisWeek.getDay() - 1
if (dayOfWeek < 0) dayOfWeek = 6 // Sunday
minDateOfThisWeek.setDate(minDateOfThisWeek.getDate() - dayOfWeek)
return d >= minDateOfThisWeek
}
export function isThisMonth (d: Date) {
const thisMonth = new Date().getMonth()
return d.getMonth() === thisMonth
}
export function isLastMonth (d: Date) {
const now = new Date()
return getDaysDifferences(now, d) <= 30
}
export function isLastWeek (d: Date) {
const now = new Date()
return getDaysDifferences(now, d) <= 7
}
// ---------------------------------------------------------------------------
export const timecodeRegexString = `(\\d+[h:])?(\\d+[m:])?\\d+s?`
export function timeToInt (time: number | string) {
if (!time) return 0
if (typeof time === 'number') return Math.floor(time)
// Try with 00h00m00s format first
const reg = new RegExp(`^((?<hours>\\d+)h)?((?<minutes>\\d+)m)?((?<seconds>\\d+)s?)?$`)
const matches = time.match(reg)
if (matches) {
const hours = parseInt(matches.groups['hours'] || '0', 10)
const minutes = parseInt(matches.groups['minutes'] || '0', 10)
const seconds = parseInt(matches.groups['seconds'] || '0', 10)
return hours * 3600 + minutes * 60 + seconds
}
// ':' format fallback
const parts = time.split(':').reverse()
const iMultiplier = {
0: 1,
1: 60,
2: 3600
}
let result = 0
for (let i = 0; i < parts.length; i++) {
const partInt = parseInt(parts[i], 10)
if (isNaN(partInt)) return 0
result += iMultiplier[i] * partInt
}
return result
}
export function secondsToTime (options: {
seconds: number
format: 'short' | 'full' | 'locale-string' // default 'short'
symbol?: string
} | number) {
let seconds: number
let format: 'short' | 'full' | 'locale-string' = 'short'
let symbol: string
if (typeof options === 'number') {
seconds = options
} else {
seconds = options.seconds
format = options.format ?? 'short'
symbol = options.symbol
}
let time = ''
if (seconds === 0 && format !== 'full') return '0s'
const formatNumber = (value: number) => {
if (format === 'locale-string') return value.toLocaleString()
return value
}
const hourSymbol = (symbol || 'h')
const minuteSymbol = (symbol || 'm')
const secondsSymbol = format === 'full' ? '' : 's'
const hours = Math.floor(seconds / 3600)
if (hours >= 1 && hours < 10 && format === 'full') time = '0' + hours + hourSymbol
else if (hours >= 1) time = formatNumber(hours) + hourSymbol
else if (format === 'full') time = '00' + hourSymbol
seconds %= 3600
const minutes = Math.floor(seconds / 60)
if (minutes >= 1 && minutes < 10 && format === 'full') time += '0' + minutes + minuteSymbol
else if (minutes >= 1) time += formatNumber(minutes) + minuteSymbol
else if (format === 'full') time += '00' + minuteSymbol
seconds = Math.round(seconds) % 60
if (seconds >= 1 && seconds < 10 && format === 'full') time += '0' + seconds + secondsSymbol
else if (seconds >= 1) time += formatNumber(seconds) + secondsSymbol
else if (format === 'full') time += '00'
return time
}
export function millisecondsToTime (options: {
seconds: number
format: 'short' | 'full' | 'locale-string' // default 'short'
symbol?: string
} | number) {
return secondsToTime(typeof options === 'number' ? options / 1000 : { ...options, seconds: options.seconds / 1000 })
}
export function millisecondsToVttTime (inputArg: number) {
const input = Math.round(inputArg || 0)
const hours = String(Math.floor(input / 3600_000)).padStart(2, '0')
const minutes = String(Math.floor((input % 3600_000) / 60_000)).padStart(2, '0')
const seconds = String(Math.floor(input % 60_000 / 1000)).padStart(2, '0')
const ms = String(input % 1000).padStart(3, '0')
return `${hours}:${minutes}:${seconds}.${ms}`
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
function areDatesEqual (d1: Date, d2: Date) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
}
function getDaysDifferences (d1: Date, d2: Date) {
return (d1.getTime() - d2.getTime()) / (86400000)
}

View File

@@ -0,0 +1,13 @@
export function findAppropriateImage<T extends { width: number, height: number }> (images: T[], wantedWidth: number) {
if (!images || images.length === 0) return undefined
const imagesSorted = images.sort((a, b) => a.width - b.width)
for (const image of imagesSorted) {
if (image.width >= wantedWidth) {
return image
}
}
return images[images.length - 1] // Biggest one
}

View File

@@ -0,0 +1,11 @@
export * from './array.js'
export * from './random.js'
export * from './date.js'
export * from './image.js'
export * from './number.js'
export * from './object.js'
export * from './regexp.js'
export * from './time.js'
export * from './promises.js'
export * from './url.js'
export * from './version.js'

View File

@@ -0,0 +1,13 @@
export function forceNumber (value: any) {
return parseInt(value + '')
}
export function isOdd (num: number) {
return (num % 2) !== 0
}
export function toEven (num: number) {
if (isOdd(num)) return num + 1
return num
}

View File

@@ -0,0 +1,85 @@
import { Jsonify } from 'type-fest'
// Forbid _attributes key to prevent pick on a sequelize model, that doesn't work for attributes
export function pick<O extends (object & { _attributes?: never }), K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
const result: any = {}
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
result[key] = object[key]
}
}
return result
}
export function omit<O extends object, K extends keyof O> (object: O, keys: K[]): Exclude<O, K> {
const result: any = {}
const keysSet = new Set(keys) as Set<string>
for (const [ key, value ] of Object.entries(object)) {
if (keysSet.has(key)) continue
result[key] = value
}
return result
}
export function objectKeysTyped<O extends object, K extends keyof O> (object: O): K[] {
return (Object.keys(object) as K[])
}
export function getKeys<O extends object, K extends keyof O> (object: O, keys: K[]): K[] {
return (Object.keys(object) as K[]).filter(k => keys.includes(k))
}
export function hasKey<T extends object> (obj: T, k: keyof any): k is keyof T {
return k in obj
}
export function sortObjectComparator (key: string, order: 'asc' | 'desc') {
return (a: any, b: any) => {
if (a[key] < b[key]) {
return order === 'asc' ? -1 : 1
}
if (a[key] > b[key]) {
return order === 'asc' ? 1 : -1
}
return 0
}
}
export function shallowCopy<T> (o: T): T {
return Object.assign(Object.create(Object.getPrototypeOf(o)), o)
}
export function exists (value: any) {
return value !== undefined && value !== null
}
// ---------------------------------------------------------------------------
export function simpleObjectsDeepEqual<T, U> (a: Jsonify<T>, b: Jsonify<U>) {
if (a as any === b as any) return true
if (a === undefined && b === undefined) return true
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
return false
}
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!keysB.includes(key)) return false
if (!simpleObjectsDeepEqual(a[key], b[key])) return false
}
return true
}

View File

@@ -0,0 +1,58 @@
export function isPromise <T = unknown> (value: T | Promise<T>): value is Promise<T> {
return value && typeof (value as Promise<T>).then === 'function'
}
export function isCatchable (value: any) {
return value && typeof value.catch === 'function'
}
export function timeoutPromise <T> (promise: Promise<T>, timeoutMs: number) {
let timer: ReturnType<typeof setTimeout>
return Promise.race([
promise,
new Promise((_res, rej) => {
timer = setTimeout(() => rej(new Error('Timeout')), timeoutMs)
})
]).finally(() => clearTimeout(timer))
}
export function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
return function promisified (): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
// eslint-disable-next-line no-useless-call
func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
})
}
}
// Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
export function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
return function promisified (arg: T): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
// eslint-disable-next-line no-useless-call
func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
})
}
}
// eslint-disable-next-line max-len
export function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
return function promisified (arg1: T, arg2: U): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
// eslint-disable-next-line no-useless-call
func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
})
}
}
// eslint-disable-next-line max-len
export function promisify3<T, U, V, A> (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise<A> {
return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
// eslint-disable-next-line no-useless-call
func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ])
})
}
}

View File

@@ -0,0 +1,4 @@
// high excluded
export function randomInt (low: number, high: number) {
return Math.floor(Math.random() * (high - low) + low)
}

View File

@@ -0,0 +1,9 @@
export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
export function removeFragmentedMP4Ext (path: string) {
return path.replace(/-fragmented.mp4$/i, '')
}
export function removeVTTExt (path: string) {
return path.replace(/\.vtt$/i, '')
}

View File

@@ -0,0 +1,3 @@
export function wait (milliseconds: number) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}

View File

@@ -0,0 +1,190 @@
import { Video, VideoPlaylist } from '@peertube/peertube-models'
import { secondsToTime } from './date.js'
function addQueryParams (url: string, params: { [ id: string ]: string }, baseUrl?: string) {
const objUrl = new URL(url, baseUrl)
for (const key of Object.keys(params)) {
objUrl.searchParams.append(key, params[key])
}
return objUrl.toString()
}
function removeQueryParams (url: string, baseUrl?: string) {
const objUrl = new URL(url, baseUrl)
objUrl.searchParams.forEach((_v, k) => objUrl.searchParams.delete(k))
return objUrl.toString()
}
function queryParamsToObject (entries: URLSearchParams) {
const result: { [ id: string ]: string | number | boolean } = {}
for (const [ key, value ] of entries) {
result[key] = value
}
return result
}
// ---------------------------------------------------------------------------
function buildDownloadFilesUrl (options: {
baseUrl: string
videoUUID: string
videoFiles: number[]
videoFileToken?: string
extension?: string
}) {
const { baseUrl, videoFiles, videoUUID, videoFileToken, extension = '' } = options
let url = `${baseUrl}/download/videos/generate/${videoUUID}${extension}?`
url += videoFiles.map(f => 'videoFileIds=' + f).join('&')
if (videoFileToken) url += `&videoFileToken=${videoFileToken}`
return url
}
function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) {
return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist)
}
function buildPlaylistWatchPath (playlist: Pick<VideoPlaylist, 'shortUUID'>) {
return '/w/p/' + playlist.shortUUID
}
function buildVideoWatchPath (video: Pick<Video, 'shortUUID'>) {
return '/w/' + video.shortUUID
}
function buildVideoLink (video: Pick<Video, 'shortUUID'>, base?: string) {
return (base ?? window.location.origin) + buildVideoWatchPath(video)
}
function buildPlaylistEmbedPath (playlist: Partial<Pick<VideoPlaylist, 'shortUUID' | 'uuid'>>) {
return '/video-playlists/embed/' + (playlist.shortUUID || playlist.uuid)
}
function buildPlaylistEmbedLink (playlist: Partial<Pick<VideoPlaylist, 'shortUUID' | 'uuid'>>, base?: string) {
return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist)
}
function buildVideoEmbedPath (video: Partial<Pick<Video, 'shortUUID' | 'uuid'>>) {
return '/videos/embed/' + (video.shortUUID || video.uuid)
}
function buildVideoEmbedLink (video: Partial<Pick<Video, 'shortUUID' | 'uuid'>>, base?: string) {
return (base ?? window.location.origin) + buildVideoEmbedPath(video)
}
function decorateVideoLink (options: {
url: string
startTime?: number
stopTime?: number
subtitle?: string
loop?: boolean
autoplay?: boolean
muted?: boolean
// Embed options
title?: boolean
warningTitle?: boolean
controls?: boolean
controlBar?: boolean
peertubeLink?: boolean
p2p?: boolean
api?: boolean
version?: number
}) {
const { url } = options
const params = new URLSearchParams()
if (options.startTime !== undefined && options.startTime !== null) {
const startTimeInt = Math.floor(options.startTime)
params.set('start', secondsToTime(startTimeInt))
}
if (options.stopTime) {
const stopTimeInt = Math.floor(options.stopTime)
params.set('stop', secondsToTime(stopTimeInt))
}
if (options.subtitle) params.set('subtitle', options.subtitle)
if (options.loop === true) params.set('loop', '1')
if (options.autoplay === true) params.set('autoplay', '1')
if (options.muted === true) params.set('muted', '1')
if (options.title === false) params.set('title', '0')
if (options.warningTitle === false) params.set('warningTitle', '0')
if (options.controls === false) params.set('controls', '0')
if (options.controlBar === false) params.set('controlBar', '0')
if (options.peertubeLink === false) params.set('peertubeLink', '0')
if (options.p2p !== undefined) params.set('p2p', options.p2p ? '1' : '0')
if (options.api !== undefined) params.set('api', options.api ? '1' : '0')
if (options.version !== undefined) params.set('v', options.version + '')
return buildUrl(url, params)
}
function decoratePlaylistLink (options: {
url: string
playlistPosition?: number
}) {
const { url } = options
const params = new URLSearchParams()
if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition)
return buildUrl(url, params)
}
// ---------------------------------------------------------------------------
export {
addQueryParams,
removeQueryParams,
queryParamsToObject,
buildDownloadFilesUrl,
buildPlaylistLink,
buildVideoLink,
buildVideoWatchPath,
buildPlaylistWatchPath,
buildPlaylistEmbedPath,
buildVideoEmbedPath,
buildPlaylistEmbedLink,
buildVideoEmbedLink,
decorateVideoLink,
decoratePlaylistLink
}
function buildUrl (url: string, params: URLSearchParams) {
let hasParams = false
params.forEach(() => { hasParams = true })
if (hasParams) return url + '?' + params.toString()
return url
}

View File

@@ -0,0 +1,11 @@
// Thanks https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
function compareSemVer (a: string, b: string) {
if (a.startsWith(b + '-')) return -1
if (b.startsWith(a + '-')) return 1
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' })
}
export {
compareSemVer
}

View File

@@ -0,0 +1,123 @@
export const LOCALE_FILES = [ 'player', 'server' ]
export const I18N_LOCALES = {
// Always first to avoid issues when using express acceptLanguages function when no accept language header is set
'en-US': 'English',
// Keep it alphabetically sorted
'ar': 'العربية',
'ca-ES': 'Català',
'cs-CZ': 'Čeština',
'de-DE': 'Deutsch',
'el-GR': 'ελληνικά',
'eo': 'Esperanto',
'es-ES': 'Español',
'eu-ES': 'Euskara',
'fa-IR': 'فارسی',
'fi-FI': 'Suomi',
'fr-FR': 'Français',
'gd': 'Gàidhlig',
'gl-ES': 'Galego',
'hr': 'Hrvatski',
'hu-HU': 'Magyar',
'is': 'Íslenska',
'it-IT': 'Italiano',
'ja-JP': '日本語',
'kab': 'Taqbaylit',
'nb-NO': 'Norsk bokmål',
'nl-NL': 'Nederlands',
'nn': 'Norsk nynorsk',
'oc': 'Occitan',
'pl-PL': 'Polski',
'pt-BR': 'Português (Brasil)',
'pt-PT': 'Português (Portugal)',
'ru-RU': 'Pусский',
'sk-SK': 'Slovenčina',
'sq': 'Shqip',
'sv-SE': 'Svenska',
'th-TH': 'ไทย',
'tok': 'Toki Pona',
'tr-TR': 'Türkçe',
'uk-UA': 'украї́нська мо́ва',
'vi-VN': 'Tiếng Việt',
'zh-Hans-CN': '简体中文(中国)',
'zh-Hant-TW': '繁體中文(台灣)'
}
// Keep it alphabetically sorted
const I18N_LOCALE_ALIAS = {
'ar-001': 'ar',
'ca': 'ca-ES',
'cs': 'cs-CZ',
'de': 'de-DE',
'el': 'el-GR',
'en': 'en-US',
'es': 'es-ES',
'eu': 'eu-ES',
'fa': 'fa-IR',
'fi': 'fi-FI',
'fr': 'fr-FR',
'gl': 'gl-ES',
'hu': 'hu-HU',
'it': 'it-IT',
'ja': 'ja-JP',
'nb': 'nb-NO',
'nl': 'nl-NL',
'pl': 'pl-PL',
'pt': 'pt-BR',
'ru': 'ru-RU',
'sk': 'sk-SK',
'sv': 'sv-SE',
'th': 'th-TH',
'tr': 'tr-TR',
'uk': 'uk-UA',
'vi': 'vi-VN',
'zh-CN': 'zh-Hans-CN',
'zh-Hans': 'zh-Hans-CN',
'zh-Hant': 'zh-Hant-TW',
'zh-TW': 'zh-Hant-TW',
'zh': 'zh-Hans-CN'
}
export const AVAILABLE_LOCALES = Object.keys(I18N_LOCALES).concat(Object.keys(I18N_LOCALE_ALIAS))
export function getDefaultLocale () {
return 'en-US'
}
export function isDefaultLocale (locale: string) {
return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale())
}
export function peertubeTranslate (str: string, translations?: { [id: string]: string }) {
if (!translations?.[str]) return str
return translations[str]
}
const possiblePaths = AVAILABLE_LOCALES.map(l => '/' + l)
export function is18nPath (path: string) {
return possiblePaths.includes(path)
}
export function is18nLocale (locale: string) {
return AVAILABLE_LOCALES.includes(locale)
}
export function getCompleteLocale (locale: string) {
if (!locale) return locale
const found = (I18N_LOCALE_ALIAS as any)[locale] as string
return found || locale
}
export function getShortLocale (locale: string) {
if (locale.includes('-') === false) return locale
return locale.split('-')[0]
}
export function buildFileLocale (locale: string) {
return getCompleteLocale(locale)
}

View File

@@ -0,0 +1 @@
export * from './i18n.js'

View File

@@ -0,0 +1,8 @@
export * from './abuse/index.js'
export * from './common/index.js'
export * from './i18n/index.js'
export * from './plugins/index.js'
export * from './renderer/index.js'
export * from './users/index.js'
export * from './videos/index.js'
export * from './string/index.js'

View File

@@ -0,0 +1,60 @@
import { HookType, HookType_Type, RegisteredExternalAuthConfig } from '@peertube/peertube-models'
import { isCatchable, isPromise } from '../common/promises.js'
function getHookType (hookName: string) {
if (hookName.startsWith('filter:')) return HookType.FILTER
if (hookName.startsWith('action:')) return HookType.ACTION
return HookType.STATIC
}
async function internalRunHook <T> (options: {
handler: Function
hookType: HookType_Type
result: T
params: any
onError: (err: Error) => void
}) {
const { handler, hookType, result, params, onError } = options
try {
if (hookType === HookType.FILTER) {
const p = handler(result, params)
const newResult = isPromise(p)
? await p
: p
return newResult
}
// Action/static hooks do not have result value
const p = handler(params)
if (hookType === HookType.STATIC) {
if (isPromise(p)) await p
return undefined
}
if (hookType === HookType.ACTION) {
if (isCatchable(p)) p.catch((err: any) => onError(err))
return undefined
}
} catch (err) {
onError(err)
}
return result
}
function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) {
return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
}
export {
getHookType,
internalRunHook,
getExternalAuthHref
}

View File

@@ -0,0 +1 @@
export * from './hooks.js'

View File

@@ -0,0 +1,87 @@
export function getDefaultSanitizedTags () {
return [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ]
}
export function getDefaultSanitizedSchemes () {
return [ 'http', 'https' ]
}
export function getDefaultSanitizedHrefAttributes () {
return [ 'href', 'class', 'target', 'rel' ]
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// sanitize-html
// ---------------------------------------------------------------------------
export function getDefaultSanitizeOptions () {
return {
allowedTags: getDefaultSanitizedTags(),
allowedSchemes: getDefaultSanitizedSchemes(),
allowedAttributes: {
'a': getDefaultSanitizedHrefAttributes(),
'*': [ 'data-*' ]
},
transformTags: {
a: (tagName: string, attribs: any) => {
let rel = 'noopener noreferrer'
if (attribs.rel === 'me') rel += ' me'
return {
tagName,
attribs: Object.assign(attribs, {
target: '_blank',
rel
})
}
}
}
}
}
export function getMailHtmlSanitizeOptions () {
return {
allowedTags: [ 'a', 'strong' ],
allowedSchemes: getDefaultSanitizedSchemes(),
allowedAttributes: {
a: [ 'href', 'title' ]
}
}
}
export function getTextOnlySanitizeOptions () {
return {
allowedTags: [] as string[]
}
}
// ---------------------------------------------------------------------------
// Manual escapes
// ---------------------------------------------------------------------------
// Thanks: https://stackoverflow.com/a/12034334
export function escapeHTML (stringParam: string) {
if (!stringParam) return ''
const entityMap: { [id: string]: string } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
}
return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
}
export function escapeAttribute (value: string) {
if (!value) return ''
return String(value).replace(/"/g, '&quot;')
}

View File

@@ -0,0 +1,3 @@
export * from './html.js'
export * from './markdown.js'
export * from './rss.js'

View File

@@ -0,0 +1,24 @@
export const TEXT_RULES = [
'linkify',
'autolink',
'emphasis',
'link',
'newline',
'entity',
'list'
]
export const TEXT_WITH_HTML_RULES = TEXT_RULES.concat([
'html_inline',
'html_block'
])
export const ENHANCED_RULES = TEXT_RULES.concat([ 'image' ])
export const ENHANCED_WITH_HTML_RULES = TEXT_WITH_HTML_RULES.concat([ 'image' ])
export const COMPLETE_RULES = ENHANCED_WITH_HTML_RULES.concat([
'block',
'inline',
'heading',
'paragraph'
])

View File

@@ -0,0 +1,64 @@
import { VideoPrivacy, VideoPrivacyType } from '@peertube/peertube-models'
export function getInstanceRSSFeed (options: {
url: string
title: string
}) {
const { url, title } = options
return { url: `${url}/feeds/videos.xml`, title }
}
export function getChannelRSSFeeds (options: {
url: string
channel: { name: string, id: number }
titles: {
instanceVideosFeed: string
channelVideosFeed: string
channelPodcastFeed: string
}
}) {
const { url, titles, channel } = options
return [
{
url: getChannelPodcastFeed(url, channel),
title: titles.channelPodcastFeed
},
{
url: `${url}/feeds/videos.xml?videoChannelId=${channel.id}`,
title: titles.channelVideosFeed
},
getInstanceRSSFeed({ url, title: titles.instanceVideosFeed })
]
}
export function getVideoRSSFeeds (options: {
url: string
video: { name: string, uuid: string, privacy: VideoPrivacyType }
titles: {
instanceVideosFeed: string
videoCommentsFeed: string
}
}) {
const { url, titles, video } = options
if (video.privacy !== VideoPrivacy.PUBLIC) {
return [ getInstanceRSSFeed({ url, title: titles.instanceVideosFeed }) ]
}
return [
{
url: `${url}/feeds/video-comments.xml?videoId=${video.uuid}`,
title: titles.videoCommentsFeed
},
getInstanceRSSFeed({ url, title: titles.instanceVideosFeed })
]
}
export function getChannelPodcastFeed (url: string, channel: { id: number }) {
return `${url}/feeds/podcast/videos.xml?videoChannelId=${channel.id}`
}

View File

@@ -0,0 +1,33 @@
import { timeToInt, timecodeRegexString } from '../common/date.js'
const timecodeRegex = new RegExp(`^(${timecodeRegexString})\\s`)
export function parseChapters (text: string, maxTitleLength: number) {
if (!text) return []
const lines = text.split(/\r?\n|\r|\n/g)
const chapters: { timecode: number, title: string }[] = []
let lastTimecode: number
for (const line of lines) {
const matched = line.match(timecodeRegex)
if (!matched) continue
const timecodeText = matched[1]
const timecode = timeToInt(timecodeText)
if (lastTimecode !== undefined && timecode <= lastTimecode) continue
lastTimecode = timecode
const title = line.replace(matched[0], '')
chapters.push({ timecode, title: title.slice(0, maxTitleLength) })
}
// Only consider chapters if there are more than one
if (chapters.length > 1) return chapters
return []
}

View File

@@ -0,0 +1 @@
export * from './chapters.js'

View File

@@ -0,0 +1 @@
export * from './user-role.js'

View File

@@ -0,0 +1,39 @@
import { UserRight, UserRightType, UserRole, UserRoleType } from '@peertube/peertube-models'
export const USER_ROLE_LABELS: { [ id in UserRoleType ]: string } = {
[UserRole.USER]: 'User',
[UserRole.MODERATOR]: 'Moderator',
[UserRole.ADMINISTRATOR]: 'Administrator'
}
const userRoleRights: { [ id in UserRoleType ]: UserRightType[] } = {
[UserRole.ADMINISTRATOR]: [
UserRight.ALL
],
[UserRole.MODERATOR]: [
UserRight.MANAGE_VIDEO_BLACKLIST,
UserRight.MANAGE_ABUSES,
UserRight.MANAGE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO,
UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
UserRight.MANAGE_ANY_VIDEO_COMMENT,
UserRight.UPDATE_ANY_VIDEO,
UserRight.SEE_ALL_VIDEOS,
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
UserRight.MANAGE_SERVERS_BLOCKLIST,
UserRight.MANAGE_USERS,
UserRight.SEE_ALL_COMMENTS,
UserRight.MANAGE_REGISTRATIONS,
UserRight.MANAGE_INSTANCE_WATCHED_WORDS,
UserRight.MANAGE_INSTANCE_AUTO_TAGS
],
[UserRole.USER]: []
}
export function hasUserRight (userRole: UserRoleType, userRight: UserRightType) {
const userRights = userRoleRights[userRole]
return userRights.includes(UserRight.ALL) || userRights.includes(userRight)
}

View File

@@ -0,0 +1,118 @@
import { VideoResolution, VideoResolutionType } from '@peertube/peertube-models'
type BitPerPixel = { [ id in VideoResolutionType ]: number }
// https://bitmovin.com/video-bitrate-streaming-hls-dash/
const minLimitBitPerPixel: BitPerPixel = {
[VideoResolution.H_NOVIDEO]: 0,
[VideoResolution.H_144P]: 0.02,
[VideoResolution.H_240P]: 0.02,
[VideoResolution.H_360P]: 0.02,
[VideoResolution.H_480P]: 0.02,
[VideoResolution.H_720P]: 0.02,
[VideoResolution.H_1080P]: 0.02,
[VideoResolution.H_1440P]: 0.02,
[VideoResolution.H_4K]: 0.02
}
const averageBitPerPixel: BitPerPixel = {
[VideoResolution.H_NOVIDEO]: 0,
[VideoResolution.H_144P]: 0.19,
[VideoResolution.H_240P]: 0.17,
[VideoResolution.H_360P]: 0.15,
[VideoResolution.H_480P]: 0.12,
[VideoResolution.H_720P]: 0.11,
[VideoResolution.H_1080P]: 0.10,
[VideoResolution.H_1440P]: 0.09,
[VideoResolution.H_4K]: 0.08
}
const maxBitPerPixel: BitPerPixel = {
[VideoResolution.H_NOVIDEO]: 0,
[VideoResolution.H_144P]: 0.32,
[VideoResolution.H_240P]: 0.29,
[VideoResolution.H_360P]: 0.26,
[VideoResolution.H_480P]: 0.22,
[VideoResolution.H_720P]: 0.19,
[VideoResolution.H_1080P]: 0.17,
[VideoResolution.H_1440P]: 0.16,
[VideoResolution.H_4K]: 0.14
}
function getAverageTheoreticalBitrate (options: {
resolution: number
ratio: number
fps: number
}) {
const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel })
if (!targetBitrate) return 192 * 1000
return targetBitrate
}
function getMaxTheoreticalBitrate (options: {
resolution: number
ratio: number
fps: number
}) {
const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel })
if (!targetBitrate) return 256 * 1000
return targetBitrate
}
function getMinTheoreticalBitrate (options: {
resolution: number
ratio: number
fps: number
}) {
const minLimitBitrate = calculateBitrate({ ...options, bitPerPixel: minLimitBitPerPixel })
if (!minLimitBitrate) return 10 * 1000
return minLimitBitrate
}
// ---------------------------------------------------------------------------
export {
getAverageTheoreticalBitrate,
getMaxTheoreticalBitrate,
getMinTheoreticalBitrate
}
// ---------------------------------------------------------------------------
function calculateBitrate (options: {
bitPerPixel: BitPerPixel
resolution: number
ratio: number
fps: number
}) {
const { bitPerPixel, resolution, ratio, fps } = options
const resolutionsOrder = [
VideoResolution.H_4K,
VideoResolution.H_1440P,
VideoResolution.H_1080P,
VideoResolution.H_720P,
VideoResolution.H_480P,
VideoResolution.H_360P,
VideoResolution.H_240P,
VideoResolution.H_144P,
VideoResolution.H_NOVIDEO
]
const size1 = resolution
const size2 = ratio < 1 && ratio > 0
? resolution / ratio // Portrait mode
: resolution * ratio
for (const toTestResolution of resolutionsOrder) {
if (toTestResolution <= resolution) {
return Math.floor(size1 * size2 * fps * bitPerPixel[toTestResolution])
}
}
throw new Error('Unknown resolution ' + resolution)
}

View File

@@ -0,0 +1,76 @@
import { VideoDetails, VideoPrivacy, VideoResolution, VideoStreamingPlaylistType } from '@peertube/peertube-models'
export function getAllPrivacies () {
return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ]
}
export function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) {
const files = video.files
const hls = getHLS(video)
if (hls) return files.concat(hls.files)
return files
}
export function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
}
export function buildAspectRatio (options: { width: number, height: number }) {
const { width, height } = options
if (!width || !height) return null
return Math.round((width / height) * 10000) / 10000 // 4 decimals precision
}
const classicResolutions = new Set<number>([
VideoResolution.H_NOVIDEO,
VideoResolution.H_144P,
VideoResolution.H_240P,
VideoResolution.H_360P,
VideoResolution.H_480P,
VideoResolution.H_720P,
VideoResolution.H_1080P,
VideoResolution.H_1440P,
VideoResolution.H_4K
])
const resolutionConverter = {
3840: VideoResolution.H_4K,
1920: VideoResolution.H_1080P,
1280: VideoResolution.H_720P,
854: VideoResolution.H_480P,
640: VideoResolution.H_360P,
426: VideoResolution.H_240P,
256: VideoResolution.H_144P
}
export function getResolutionLabel (options: {
resolution: number
height: number
width: number
}) {
const { height, width } = options
if (options.resolution === 0) return 'Audio only'
let resolution = options.resolution
// Try to find a better resolution label
// For example with a video 1920x816 we prefer to display "1080p"
if (!classicResolutions.has(resolution) && typeof height === 'number' && typeof width === 'number') {
const max = Math.max(height, width)
const alternativeLabel = resolutionConverter[max]
if (alternativeLabel) resolution = resolutionConverter[max]
}
return `${resolution}p`
}
export function getResolutionAndFPSLabel (resolutionLabel: string, fps: number) {
if (fps && fps >= 50) return resolutionLabel + fps
return resolutionLabel
}

View File

@@ -0,0 +1,3 @@
export * from './bitrate.js'
export * from './common.js'
export * from './state.js'

View File

@@ -0,0 +1,12 @@
import { VideoState, VideoStateType } from '@peertube/peertube-models'
export function canVideoFileBeEdited (state: VideoStateType) {
const validStates = new Set<VideoStateType>([
VideoState.PUBLISHED,
VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED,
VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED,
VideoState.TRANSCODING_FAILED
])
return validStates.has(state)
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "src",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"references": [
{ "path": "../models" }
]
}