mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-05 19:42:24 +02:00
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
This commit is contained in:
parent
04d1da5621
commit
3a4992633e
2196 changed files with 12690 additions and 11574 deletions
14
packages/core-utils/src/abuse/abuse-predefined-reasons.ts
Normal file
14
packages/core-utils/src/abuse/abuse-predefined-reasons.ts
Normal 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
|
1
packages/core-utils/src/abuse/index.ts
Normal file
1
packages/core-utils/src/abuse/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './abuse-predefined-reasons.js'
|
41
packages/core-utils/src/common/array.ts
Normal file
41
packages/core-utils/src/common/array.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
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
|
||||
function arrayify <T> (element: T | T[]) {
|
||||
if (Array.isArray(element)) return element
|
||||
|
||||
return [ element ]
|
||||
}
|
||||
|
||||
// Avoid conflict with other uniq() functions
|
||||
function uniqify <T> (elements: T[]) {
|
||||
return Array.from(new Set(elements))
|
||||
}
|
||||
|
||||
// Thanks: https://stackoverflow.com/a/12646864
|
||||
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 {
|
||||
uniqify,
|
||||
findCommonElement,
|
||||
shuffle,
|
||||
arrayify
|
||||
}
|
114
packages/core-utils/src/common/date.ts
Normal file
114
packages/core-utils/src/common/date.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
function isToday (d: Date) {
|
||||
const today = new Date()
|
||||
|
||||
return areDatesEqual(d, today)
|
||||
}
|
||||
|
||||
function isYesterday (d: Date) {
|
||||
const yesterday = new Date()
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
|
||||
return areDatesEqual(d, yesterday)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function isThisMonth (d: Date) {
|
||||
const thisMonth = new Date().getMonth()
|
||||
|
||||
return d.getMonth() === thisMonth
|
||||
}
|
||||
|
||||
function isLastMonth (d: Date) {
|
||||
const now = new Date()
|
||||
|
||||
return getDaysDifferences(now, d) <= 30
|
||||
}
|
||||
|
||||
function isLastWeek (d: Date) {
|
||||
const now = new Date()
|
||||
|
||||
return getDaysDifferences(now, d) <= 7
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function timeToInt (time: number | string) {
|
||||
if (!time) return 0
|
||||
if (typeof time === 'number') return time
|
||||
|
||||
const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
|
||||
const matches = time.match(reg)
|
||||
|
||||
if (!matches) return 0
|
||||
|
||||
const hours = parseInt(matches[2] || '0', 10)
|
||||
const minutes = parseInt(matches[4] || '0', 10)
|
||||
const seconds = parseInt(matches[6] || '0', 10)
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds
|
||||
}
|
||||
|
||||
function secondsToTime (seconds: number, full = false, symbol?: string) {
|
||||
let time = ''
|
||||
|
||||
if (seconds === 0 && !full) return '0s'
|
||||
|
||||
const hourSymbol = (symbol || 'h')
|
||||
const minuteSymbol = (symbol || 'm')
|
||||
const secondsSymbol = full ? '' : 's'
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
if (hours >= 1) time = hours + hourSymbol
|
||||
else if (full) time = '0' + hourSymbol
|
||||
|
||||
seconds %= 3600
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
|
||||
else if (minutes >= 1) time += minutes + minuteSymbol
|
||||
else if (full) time += '00' + minuteSymbol
|
||||
|
||||
seconds %= 60
|
||||
if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
|
||||
else if (seconds >= 1) time += seconds + secondsSymbol
|
||||
else if (full) time += '00'
|
||||
|
||||
return time
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isYesterday,
|
||||
isThisWeek,
|
||||
isThisMonth,
|
||||
isToday,
|
||||
isLastMonth,
|
||||
isLastWeek,
|
||||
timeToInt,
|
||||
secondsToTime
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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)
|
||||
}
|
10
packages/core-utils/src/common/index.ts
Normal file
10
packages/core-utils/src/common/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export * from './array.js'
|
||||
export * from './random.js'
|
||||
export * from './date.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'
|
13
packages/core-utils/src/common/number.ts
Normal file
13
packages/core-utils/src/common/number.ts
Normal 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
|
||||
}
|
86
packages/core-utils/src/common/object.ts
Normal file
86
packages/core-utils/src/common/object.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
function pick <O extends object, 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function objectKeysTyped <O extends object, K extends keyof O> (object: O): K[] {
|
||||
return (Object.keys(object) as K[])
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
function hasKey <T extends object> (obj: T, k: keyof any): k is keyof T {
|
||||
return k in obj
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function shallowCopy <T> (o: T): T {
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(o)), o)
|
||||
}
|
||||
|
||||
function simpleObjectsDeepEqual (a: any, b: any) {
|
||||
if (a === b) 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
|
||||
}
|
||||
|
||||
export {
|
||||
pick,
|
||||
omit,
|
||||
objectKeysTyped,
|
||||
getKeys,
|
||||
hasKey,
|
||||
shallowCopy,
|
||||
sortObjectComparator,
|
||||
simpleObjectsDeepEqual
|
||||
}
|
58
packages/core-utils/src/common/promises.ts
Normal file
58
packages/core-utils/src/common/promises.ts
Normal 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) ])
|
||||
})
|
||||
}
|
||||
}
|
8
packages/core-utils/src/common/random.ts
Normal file
8
packages/core-utils/src/common/random.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// high excluded
|
||||
function randomInt (low: number, high: number) {
|
||||
return Math.floor(Math.random() * (high - low) + low)
|
||||
}
|
||||
|
||||
export {
|
||||
randomInt
|
||||
}
|
5
packages/core-utils/src/common/regexp.ts
Normal file
5
packages/core-utils/src/common/regexp.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
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, '')
|
||||
}
|
7
packages/core-utils/src/common/time.ts
Normal file
7
packages/core-utils/src/common/time.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
function wait (milliseconds: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
}
|
||||
|
||||
export {
|
||||
wait
|
||||
}
|
150
packages/core-utils/src/common/url.ts
Normal file
150
packages/core-utils/src/common/url.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { Video, VideoPlaylist } from '@peertube/peertube-models'
|
||||
import { secondsToTime } from './date.js'
|
||||
|
||||
function addQueryParams (url: string, params: { [ id: string ]: string }) {
|
||||
const objUrl = new URL(url)
|
||||
|
||||
for (const key of Object.keys(params)) {
|
||||
objUrl.searchParams.append(key, params[key])
|
||||
}
|
||||
|
||||
return objUrl.toString()
|
||||
}
|
||||
|
||||
function removeQueryParams (url: string) {
|
||||
const objUrl = new URL(url)
|
||||
|
||||
objUrl.searchParams.forEach((_v, k) => objUrl.searchParams.delete(k))
|
||||
|
||||
return objUrl.toString()
|
||||
}
|
||||
|
||||
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: Pick<VideoPlaylist, 'uuid'>) {
|
||||
return '/video-playlists/embed/' + playlist.uuid
|
||||
}
|
||||
|
||||
function buildPlaylistEmbedLink (playlist: Pick<VideoPlaylist, 'uuid'>, base?: string) {
|
||||
return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist)
|
||||
}
|
||||
|
||||
function buildVideoEmbedPath (video: Pick<Video, 'uuid'>) {
|
||||
return '/videos/embed/' + video.uuid
|
||||
}
|
||||
|
||||
function buildVideoEmbedLink (video: Pick<Video, '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
|
||||
}) {
|
||||
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')
|
||||
|
||||
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,
|
||||
|
||||
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
|
||||
}
|
11
packages/core-utils/src/common/version.ts
Normal file
11
packages/core-utils/src/common/version.ts
Normal 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
|
||||
}
|
119
packages/core-utils/src/i18n/i18n.ts
Normal file
119
packages/core-utils/src/i18n/i18n.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
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усский',
|
||||
'sq': 'Shqip',
|
||||
'sv-SE': 'Svenska',
|
||||
'th-TH': 'ไทย',
|
||||
'tok': 'Toki Pona',
|
||||
'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',
|
||||
'sv': 'sv-SE',
|
||||
'th': 'th-TH',
|
||||
'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 POSSIBLE_LOCALES = (Object.keys(I18N_LOCALES) as string[]).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 = POSSIBLE_LOCALES.map(l => '/' + l)
|
||||
export function is18nPath (path: string) {
|
||||
return possiblePaths.includes(path)
|
||||
}
|
||||
|
||||
export function is18nLocale (locale: string) {
|
||||
return POSSIBLE_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)
|
||||
}
|
1
packages/core-utils/src/i18n/index.ts
Normal file
1
packages/core-utils/src/i18n/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './i18n.js'
|
7
packages/core-utils/src/index.ts
Normal file
7
packages/core-utils/src/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
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'
|
60
packages/core-utils/src/plugins/hooks.ts
Normal file
60
packages/core-utils/src/plugins/hooks.ts
Normal 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
|
||||
}
|
1
packages/core-utils/src/plugins/index.ts
Normal file
1
packages/core-utils/src/plugins/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './hooks.js'
|
71
packages/core-utils/src/renderer/html.ts
Normal file
71
packages/core-utils/src/renderer/html.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
export function getDefaultSanitizeOptions () {
|
||||
return {
|
||||
allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
|
||||
allowedSchemes: [ 'http', 'https' ],
|
||||
allowedAttributes: {
|
||||
'a': [ 'href', 'class', 'target', 'rel' ],
|
||||
'*': [ '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 getTextOnlySanitizeOptions () {
|
||||
return {
|
||||
allowedTags: [] as string[]
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) {
|
||||
const base = getDefaultSanitizeOptions()
|
||||
|
||||
return {
|
||||
allowedTags: [
|
||||
...base.allowedTags,
|
||||
...additionalAllowedTags,
|
||||
'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img'
|
||||
],
|
||||
allowedSchemes: [
|
||||
...base.allowedSchemes,
|
||||
|
||||
'mailto'
|
||||
],
|
||||
allowedAttributes: {
|
||||
...base.allowedAttributes,
|
||||
|
||||
'img': [ 'src', 'alt' ],
|
||||
'*': [ 'data-*', 'style' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thanks: https://stackoverflow.com/a/12034334
|
||||
export function escapeHTML (stringParam: string) {
|
||||
if (!stringParam) return ''
|
||||
|
||||
const entityMap: { [id: string ]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
'\'': ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
}
|
||||
|
||||
return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
|
||||
}
|
2
packages/core-utils/src/renderer/index.ts
Normal file
2
packages/core-utils/src/renderer/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './markdown.js'
|
||||
export * from './html.js'
|
24
packages/core-utils/src/renderer/markdown.ts
Normal file
24
packages/core-utils/src/renderer/markdown.ts
Normal 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'
|
||||
])
|
1
packages/core-utils/src/users/index.ts
Normal file
1
packages/core-utils/src/users/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './user-role.js'
|
37
packages/core-utils/src/users/user-role.ts
Normal file
37
packages/core-utils/src/users/user-role.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
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.REMOVE_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
|
||||
],
|
||||
|
||||
[UserRole.USER]: []
|
||||
}
|
||||
|
||||
export function hasUserRight (userRole: UserRoleType, userRight: UserRightType) {
|
||||
const userRights = userRoleRights[userRole]
|
||||
|
||||
return userRights.includes(UserRight.ALL) || userRights.includes(userRight)
|
||||
}
|
113
packages/core-utils/src/videos/bitrate.ts
Normal file
113
packages/core-utils/src/videos/bitrate.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
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
|
||||
]
|
||||
|
||||
for (const toTestResolution of resolutionsOrder) {
|
||||
if (toTestResolution <= resolution) {
|
||||
return Math.floor(resolution * resolution * ratio * fps * bitPerPixel[toTestResolution])
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unknown resolution ' + resolution)
|
||||
}
|
24
packages/core-utils/src/videos/common.ts
Normal file
24
packages/core-utils/src/videos/common.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
|
||||
function getAllPrivacies () {
|
||||
return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
|
||||
return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||
}
|
||||
|
||||
export {
|
||||
getAllPrivacies,
|
||||
getAllFiles,
|
||||
getHLS
|
||||
}
|
2
packages/core-utils/src/videos/index.ts
Normal file
2
packages/core-utils/src/videos/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './bitrate.js'
|
||||
export * from './common.js'
|
Loading…
Add table
Add a link
Reference in a new issue