diff options
Diffstat (limited to 'packages/core-utils')
28 files changed, 1010 insertions, 0 deletions
diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json new file mode 100644 index 000000000..d3bf18335 --- /dev/null +++ b/packages/core-utils/package.json | |||
@@ -0,0 +1,19 @@ | |||
1 | { | ||
2 | "name": "@peertube/peertube-core-utils", | ||
3 | "private": true, | ||
4 | "version": "0.0.0", | ||
5 | "main": "dist/index.js", | ||
6 | "files": [ "dist" ], | ||
7 | "exports": { | ||
8 | "types": "./dist/index.d.ts", | ||
9 | "peertube:tsx": "./src/index.ts", | ||
10 | "default": "./dist/index.js" | ||
11 | }, | ||
12 | "type": "module", | ||
13 | "devDependencies": {}, | ||
14 | "scripts": { | ||
15 | "build": "tsc", | ||
16 | "watch": "tsc -w" | ||
17 | }, | ||
18 | "dependencies": {} | ||
19 | } | ||
diff --git a/packages/core-utils/src/abuse/abuse-predefined-reasons.ts b/packages/core-utils/src/abuse/abuse-predefined-reasons.ts new file mode 100644 index 000000000..68534a1e0 --- /dev/null +++ b/packages/core-utils/src/abuse/abuse-predefined-reasons.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { AbusePredefinedReasons, AbusePredefinedReasonsString, AbusePredefinedReasonsType } from '@peertube/peertube-models' | ||
2 | |||
3 | export const abusePredefinedReasonsMap: { | ||
4 | [key in AbusePredefinedReasonsString]: AbusePredefinedReasonsType | ||
5 | } = { | ||
6 | violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE, | ||
7 | hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE, | ||
8 | spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING, | ||
9 | privacy: AbusePredefinedReasons.PRIVACY, | ||
10 | rights: AbusePredefinedReasons.RIGHTS, | ||
11 | serverRules: AbusePredefinedReasons.SERVER_RULES, | ||
12 | thumbnails: AbusePredefinedReasons.THUMBNAILS, | ||
13 | captions: AbusePredefinedReasons.CAPTIONS | ||
14 | } as const | ||
diff --git a/packages/core-utils/src/abuse/index.ts b/packages/core-utils/src/abuse/index.ts new file mode 100644 index 000000000..b79b86155 --- /dev/null +++ b/packages/core-utils/src/abuse/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './abuse-predefined-reasons.js' | |||
diff --git a/packages/core-utils/src/common/array.ts b/packages/core-utils/src/common/array.ts new file mode 100644 index 000000000..878ed1ffe --- /dev/null +++ b/packages/core-utils/src/common/array.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | function findCommonElement <T> (array1: T[], array2: T[]) { | ||
2 | for (const a of array1) { | ||
3 | for (const b of array2) { | ||
4 | if (a === b) return a | ||
5 | } | ||
6 | } | ||
7 | |||
8 | return null | ||
9 | } | ||
10 | |||
11 | // Avoid conflict with other toArray() functions | ||
12 | function arrayify <T> (element: T | T[]) { | ||
13 | if (Array.isArray(element)) return element | ||
14 | |||
15 | return [ element ] | ||
16 | } | ||
17 | |||
18 | // Avoid conflict with other uniq() functions | ||
19 | function uniqify <T> (elements: T[]) { | ||
20 | return Array.from(new Set(elements)) | ||
21 | } | ||
22 | |||
23 | // Thanks: https://stackoverflow.com/a/12646864 | ||
24 | function shuffle <T> (elements: T[]) { | ||
25 | const shuffled = [ ...elements ] | ||
26 | |||
27 | for (let i = shuffled.length - 1; i > 0; i--) { | ||
28 | const j = Math.floor(Math.random() * (i + 1)); | ||
29 | |||
30 | [ shuffled[i], shuffled[j] ] = [ shuffled[j], shuffled[i] ] | ||
31 | } | ||
32 | |||
33 | return shuffled | ||
34 | } | ||
35 | |||
36 | export { | ||
37 | uniqify, | ||
38 | findCommonElement, | ||
39 | shuffle, | ||
40 | arrayify | ||
41 | } | ||
diff --git a/packages/core-utils/src/common/date.ts b/packages/core-utils/src/common/date.ts new file mode 100644 index 000000000..f0684ff86 --- /dev/null +++ b/packages/core-utils/src/common/date.ts | |||
@@ -0,0 +1,114 @@ | |||
1 | function isToday (d: Date) { | ||
2 | const today = new Date() | ||
3 | |||
4 | return areDatesEqual(d, today) | ||
5 | } | ||
6 | |||
7 | function isYesterday (d: Date) { | ||
8 | const yesterday = new Date() | ||
9 | yesterday.setDate(yesterday.getDate() - 1) | ||
10 | |||
11 | return areDatesEqual(d, yesterday) | ||
12 | } | ||
13 | |||
14 | function isThisWeek (d: Date) { | ||
15 | const minDateOfThisWeek = new Date() | ||
16 | minDateOfThisWeek.setHours(0, 0, 0) | ||
17 | |||
18 | // getDay() -> Sunday - Saturday : 0 - 6 | ||
19 | // We want to start our week on Monday | ||
20 | let dayOfWeek = minDateOfThisWeek.getDay() - 1 | ||
21 | if (dayOfWeek < 0) dayOfWeek = 6 // Sunday | ||
22 | |||
23 | minDateOfThisWeek.setDate(minDateOfThisWeek.getDate() - dayOfWeek) | ||
24 | |||
25 | return d >= minDateOfThisWeek | ||
26 | } | ||
27 | |||
28 | function isThisMonth (d: Date) { | ||
29 | const thisMonth = new Date().getMonth() | ||
30 | |||
31 | return d.getMonth() === thisMonth | ||
32 | } | ||
33 | |||
34 | function isLastMonth (d: Date) { | ||
35 | const now = new Date() | ||
36 | |||
37 | return getDaysDifferences(now, d) <= 30 | ||
38 | } | ||
39 | |||
40 | function isLastWeek (d: Date) { | ||
41 | const now = new Date() | ||
42 | |||
43 | return getDaysDifferences(now, d) <= 7 | ||
44 | } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | function timeToInt (time: number | string) { | ||
49 | if (!time) return 0 | ||
50 | if (typeof time === 'number') return time | ||
51 | |||
52 | const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/ | ||
53 | const matches = time.match(reg) | ||
54 | |||
55 | if (!matches) return 0 | ||
56 | |||
57 | const hours = parseInt(matches[2] || '0', 10) | ||
58 | const minutes = parseInt(matches[4] || '0', 10) | ||
59 | const seconds = parseInt(matches[6] || '0', 10) | ||
60 | |||
61 | return hours * 3600 + minutes * 60 + seconds | ||
62 | } | ||
63 | |||
64 | function secondsToTime (seconds: number, full = false, symbol?: string) { | ||
65 | let time = '' | ||
66 | |||
67 | if (seconds === 0 && !full) return '0s' | ||
68 | |||
69 | const hourSymbol = (symbol || 'h') | ||
70 | const minuteSymbol = (symbol || 'm') | ||
71 | const secondsSymbol = full ? '' : 's' | ||
72 | |||
73 | const hours = Math.floor(seconds / 3600) | ||
74 | if (hours >= 1) time = hours + hourSymbol | ||
75 | else if (full) time = '0' + hourSymbol | ||
76 | |||
77 | seconds %= 3600 | ||
78 | const minutes = Math.floor(seconds / 60) | ||
79 | if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol | ||
80 | else if (minutes >= 1) time += minutes + minuteSymbol | ||
81 | else if (full) time += '00' + minuteSymbol | ||
82 | |||
83 | seconds %= 60 | ||
84 | if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol | ||
85 | else if (seconds >= 1) time += seconds + secondsSymbol | ||
86 | else if (full) time += '00' | ||
87 | |||
88 | return time | ||
89 | } | ||
90 | |||
91 | // --------------------------------------------------------------------------- | ||
92 | |||
93 | export { | ||
94 | isYesterday, | ||
95 | isThisWeek, | ||
96 | isThisMonth, | ||
97 | isToday, | ||
98 | isLastMonth, | ||
99 | isLastWeek, | ||
100 | timeToInt, | ||
101 | secondsToTime | ||
102 | } | ||
103 | |||
104 | // --------------------------------------------------------------------------- | ||
105 | |||
106 | function areDatesEqual (d1: Date, d2: Date) { | ||
107 | return d1.getFullYear() === d2.getFullYear() && | ||
108 | d1.getMonth() === d2.getMonth() && | ||
109 | d1.getDate() === d2.getDate() | ||
110 | } | ||
111 | |||
112 | function getDaysDifferences (d1: Date, d2: Date) { | ||
113 | return (d1.getTime() - d2.getTime()) / (86400000) | ||
114 | } | ||
diff --git a/packages/core-utils/src/common/index.ts b/packages/core-utils/src/common/index.ts new file mode 100644 index 000000000..d7d8599aa --- /dev/null +++ b/packages/core-utils/src/common/index.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | export * from './array.js' | ||
2 | export * from './random.js' | ||
3 | export * from './date.js' | ||
4 | export * from './number.js' | ||
5 | export * from './object.js' | ||
6 | export * from './regexp.js' | ||
7 | export * from './time.js' | ||
8 | export * from './promises.js' | ||
9 | export * from './url.js' | ||
10 | export * from './version.js' | ||
diff --git a/packages/core-utils/src/common/number.ts b/packages/core-utils/src/common/number.ts new file mode 100644 index 000000000..ce5a6041a --- /dev/null +++ b/packages/core-utils/src/common/number.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | export function forceNumber (value: any) { | ||
2 | return parseInt(value + '') | ||
3 | } | ||
4 | |||
5 | export function isOdd (num: number) { | ||
6 | return (num % 2) !== 0 | ||
7 | } | ||
8 | |||
9 | export function toEven (num: number) { | ||
10 | if (isOdd(num)) return num + 1 | ||
11 | |||
12 | return num | ||
13 | } | ||
diff --git a/packages/core-utils/src/common/object.ts b/packages/core-utils/src/common/object.ts new file mode 100644 index 000000000..1276bfcc7 --- /dev/null +++ b/packages/core-utils/src/common/object.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | function pick <O extends object, K extends keyof O> (object: O, keys: K[]): Pick<O, K> { | ||
2 | const result: any = {} | ||
3 | |||
4 | for (const key of keys) { | ||
5 | if (Object.prototype.hasOwnProperty.call(object, key)) { | ||
6 | result[key] = object[key] | ||
7 | } | ||
8 | } | ||
9 | |||
10 | return result | ||
11 | } | ||
12 | |||
13 | function omit <O extends object, K extends keyof O> (object: O, keys: K[]): Exclude<O, K> { | ||
14 | const result: any = {} | ||
15 | const keysSet = new Set(keys) as Set<string> | ||
16 | |||
17 | for (const [ key, value ] of Object.entries(object)) { | ||
18 | if (keysSet.has(key)) continue | ||
19 | |||
20 | result[key] = value | ||
21 | } | ||
22 | |||
23 | return result | ||
24 | } | ||
25 | |||
26 | function objectKeysTyped <O extends object, K extends keyof O> (object: O): K[] { | ||
27 | return (Object.keys(object) as K[]) | ||
28 | } | ||
29 | |||
30 | function getKeys <O extends object, K extends keyof O> (object: O, keys: K[]): K[] { | ||
31 | return (Object.keys(object) as K[]).filter(k => keys.includes(k)) | ||
32 | } | ||
33 | |||
34 | function hasKey <T extends object> (obj: T, k: keyof any): k is keyof T { | ||
35 | return k in obj | ||
36 | } | ||
37 | |||
38 | function sortObjectComparator (key: string, order: 'asc' | 'desc') { | ||
39 | return (a: any, b: any) => { | ||
40 | if (a[key] < b[key]) { | ||
41 | return order === 'asc' ? -1 : 1 | ||
42 | } | ||
43 | |||
44 | if (a[key] > b[key]) { | ||
45 | return order === 'asc' ? 1 : -1 | ||
46 | } | ||
47 | |||
48 | return 0 | ||
49 | } | ||
50 | } | ||
51 | |||
52 | function shallowCopy <T> (o: T): T { | ||
53 | return Object.assign(Object.create(Object.getPrototypeOf(o)), o) | ||
54 | } | ||
55 | |||
56 | function simpleObjectsDeepEqual (a: any, b: any) { | ||
57 | if (a === b) return true | ||
58 | |||
59 | if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { | ||
60 | return false | ||
61 | } | ||
62 | |||
63 | const keysA = Object.keys(a) | ||
64 | const keysB = Object.keys(b) | ||
65 | |||
66 | if (keysA.length !== keysB.length) return false | ||
67 | |||
68 | for (const key of keysA) { | ||
69 | if (!keysB.includes(key)) return false | ||
70 | |||
71 | if (!simpleObjectsDeepEqual(a[key], b[key])) return false | ||
72 | } | ||
73 | |||
74 | return true | ||
75 | } | ||
76 | |||
77 | export { | ||
78 | pick, | ||
79 | omit, | ||
80 | objectKeysTyped, | ||
81 | getKeys, | ||
82 | hasKey, | ||
83 | shallowCopy, | ||
84 | sortObjectComparator, | ||
85 | simpleObjectsDeepEqual | ||
86 | } | ||
diff --git a/packages/core-utils/src/common/promises.ts b/packages/core-utils/src/common/promises.ts new file mode 100644 index 000000000..e3792d12e --- /dev/null +++ b/packages/core-utils/src/common/promises.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | export function isPromise <T = unknown> (value: T | Promise<T>): value is Promise<T> { | ||
2 | return value && typeof (value as Promise<T>).then === 'function' | ||
3 | } | ||
4 | |||
5 | export function isCatchable (value: any) { | ||
6 | return value && typeof value.catch === 'function' | ||
7 | } | ||
8 | |||
9 | export function timeoutPromise <T> (promise: Promise<T>, timeoutMs: number) { | ||
10 | let timer: ReturnType<typeof setTimeout> | ||
11 | |||
12 | return Promise.race([ | ||
13 | promise, | ||
14 | |||
15 | new Promise((_res, rej) => { | ||
16 | timer = setTimeout(() => rej(new Error('Timeout')), timeoutMs) | ||
17 | }) | ||
18 | ]).finally(() => clearTimeout(timer)) | ||
19 | } | ||
20 | |||
21 | export function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | ||
22 | return function promisified (): Promise<A> { | ||
23 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | ||
24 | // eslint-disable-next-line no-useless-call | ||
25 | func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ]) | ||
26 | }) | ||
27 | } | ||
28 | } | ||
29 | |||
30 | // Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2 | ||
31 | export function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> { | ||
32 | return function promisified (arg: T): Promise<A> { | ||
33 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | ||
34 | // eslint-disable-next-line no-useless-call | ||
35 | func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ]) | ||
36 | }) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | // eslint-disable-next-line max-len | ||
41 | export function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> { | ||
42 | return function promisified (arg1: T, arg2: U): Promise<A> { | ||
43 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | ||
44 | // eslint-disable-next-line no-useless-call | ||
45 | func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ]) | ||
46 | }) | ||
47 | } | ||
48 | } | ||
49 | |||
50 | // eslint-disable-next-line max-len | ||
51 | 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> { | ||
52 | return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> { | ||
53 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | ||
54 | // eslint-disable-next-line no-useless-call | ||
55 | func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ]) | ||
56 | }) | ||
57 | } | ||
58 | } | ||
diff --git a/packages/core-utils/src/common/random.ts b/packages/core-utils/src/common/random.ts new file mode 100644 index 000000000..705735d09 --- /dev/null +++ b/packages/core-utils/src/common/random.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | // high excluded | ||
2 | function randomInt (low: number, high: number) { | ||
3 | return Math.floor(Math.random() * (high - low) + low) | ||
4 | } | ||
5 | |||
6 | export { | ||
7 | randomInt | ||
8 | } | ||
diff --git a/packages/core-utils/src/common/regexp.ts b/packages/core-utils/src/common/regexp.ts new file mode 100644 index 000000000..59eb87eb6 --- /dev/null +++ b/packages/core-utils/src/common/regexp.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | ||
2 | |||
3 | export function removeFragmentedMP4Ext (path: string) { | ||
4 | return path.replace(/-fragmented.mp4$/i, '') | ||
5 | } | ||
diff --git a/packages/core-utils/src/common/time.ts b/packages/core-utils/src/common/time.ts new file mode 100644 index 000000000..2992609ca --- /dev/null +++ b/packages/core-utils/src/common/time.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | function wait (milliseconds: number) { | ||
2 | return new Promise(resolve => setTimeout(resolve, milliseconds)) | ||
3 | } | ||
4 | |||
5 | export { | ||
6 | wait | ||
7 | } | ||
diff --git a/packages/core-utils/src/common/url.ts b/packages/core-utils/src/common/url.ts new file mode 100644 index 000000000..449b6c9dc --- /dev/null +++ b/packages/core-utils/src/common/url.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | import { Video, VideoPlaylist } from '@peertube/peertube-models' | ||
2 | import { secondsToTime } from './date.js' | ||
3 | |||
4 | function addQueryParams (url: string, params: { [ id: string ]: string }) { | ||
5 | const objUrl = new URL(url) | ||
6 | |||
7 | for (const key of Object.keys(params)) { | ||
8 | objUrl.searchParams.append(key, params[key]) | ||
9 | } | ||
10 | |||
11 | return objUrl.toString() | ||
12 | } | ||
13 | |||
14 | function removeQueryParams (url: string) { | ||
15 | const objUrl = new URL(url) | ||
16 | |||
17 | objUrl.searchParams.forEach((_v, k) => objUrl.searchParams.delete(k)) | ||
18 | |||
19 | return objUrl.toString() | ||
20 | } | ||
21 | |||
22 | function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) { | ||
23 | return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist) | ||
24 | } | ||
25 | |||
26 | function buildPlaylistWatchPath (playlist: Pick<VideoPlaylist, 'shortUUID'>) { | ||
27 | return '/w/p/' + playlist.shortUUID | ||
28 | } | ||
29 | |||
30 | function buildVideoWatchPath (video: Pick<Video, 'shortUUID'>) { | ||
31 | return '/w/' + video.shortUUID | ||
32 | } | ||
33 | |||
34 | function buildVideoLink (video: Pick<Video, 'shortUUID'>, base?: string) { | ||
35 | return (base ?? window.location.origin) + buildVideoWatchPath(video) | ||
36 | } | ||
37 | |||
38 | function buildPlaylistEmbedPath (playlist: Pick<VideoPlaylist, 'uuid'>) { | ||
39 | return '/video-playlists/embed/' + playlist.uuid | ||
40 | } | ||
41 | |||
42 | function buildPlaylistEmbedLink (playlist: Pick<VideoPlaylist, 'uuid'>, base?: string) { | ||
43 | return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist) | ||
44 | } | ||
45 | |||
46 | function buildVideoEmbedPath (video: Pick<Video, 'uuid'>) { | ||
47 | return '/videos/embed/' + video.uuid | ||
48 | } | ||
49 | |||
50 | function buildVideoEmbedLink (video: Pick<Video, 'uuid'>, base?: string) { | ||
51 | return (base ?? window.location.origin) + buildVideoEmbedPath(video) | ||
52 | } | ||
53 | |||
54 | function decorateVideoLink (options: { | ||
55 | url: string | ||
56 | |||
57 | startTime?: number | ||
58 | stopTime?: number | ||
59 | |||
60 | subtitle?: string | ||
61 | |||
62 | loop?: boolean | ||
63 | autoplay?: boolean | ||
64 | muted?: boolean | ||
65 | |||
66 | // Embed options | ||
67 | title?: boolean | ||
68 | warningTitle?: boolean | ||
69 | |||
70 | controls?: boolean | ||
71 | controlBar?: boolean | ||
72 | |||
73 | peertubeLink?: boolean | ||
74 | p2p?: boolean | ||
75 | }) { | ||
76 | const { url } = options | ||
77 | |||
78 | const params = new URLSearchParams() | ||
79 | |||
80 | if (options.startTime !== undefined && options.startTime !== null) { | ||
81 | const startTimeInt = Math.floor(options.startTime) | ||
82 | params.set('start', secondsToTime(startTimeInt)) | ||
83 | } | ||
84 | |||
85 | if (options.stopTime) { | ||
86 | const stopTimeInt = Math.floor(options.stopTime) | ||
87 | params.set('stop', secondsToTime(stopTimeInt)) | ||
88 | } | ||
89 | |||
90 | if (options.subtitle) params.set('subtitle', options.subtitle) | ||
91 | |||
92 | if (options.loop === true) params.set('loop', '1') | ||
93 | if (options.autoplay === true) params.set('autoplay', '1') | ||
94 | if (options.muted === true) params.set('muted', '1') | ||
95 | if (options.title === false) params.set('title', '0') | ||
96 | if (options.warningTitle === false) params.set('warningTitle', '0') | ||
97 | |||
98 | if (options.controls === false) params.set('controls', '0') | ||
99 | if (options.controlBar === false) params.set('controlBar', '0') | ||
100 | |||
101 | if (options.peertubeLink === false) params.set('peertubeLink', '0') | ||
102 | if (options.p2p !== undefined) params.set('p2p', options.p2p ? '1' : '0') | ||
103 | |||
104 | return buildUrl(url, params) | ||
105 | } | ||
106 | |||
107 | function decoratePlaylistLink (options: { | ||
108 | url: string | ||
109 | |||
110 | playlistPosition?: number | ||
111 | }) { | ||
112 | const { url } = options | ||
113 | |||
114 | const params = new URLSearchParams() | ||
115 | |||
116 | if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition) | ||
117 | |||
118 | return buildUrl(url, params) | ||
119 | } | ||
120 | |||
121 | // --------------------------------------------------------------------------- | ||
122 | |||
123 | export { | ||
124 | addQueryParams, | ||
125 | removeQueryParams, | ||
126 | |||
127 | buildPlaylistLink, | ||
128 | buildVideoLink, | ||
129 | |||
130 | buildVideoWatchPath, | ||
131 | buildPlaylistWatchPath, | ||
132 | |||
133 | buildPlaylistEmbedPath, | ||
134 | buildVideoEmbedPath, | ||
135 | |||
136 | buildPlaylistEmbedLink, | ||
137 | buildVideoEmbedLink, | ||
138 | |||
139 | decorateVideoLink, | ||
140 | decoratePlaylistLink | ||
141 | } | ||
142 | |||
143 | function buildUrl (url: string, params: URLSearchParams) { | ||
144 | let hasParams = false | ||
145 | params.forEach(() => { hasParams = true }) | ||
146 | |||
147 | if (hasParams) return url + '?' + params.toString() | ||
148 | |||
149 | return url | ||
150 | } | ||
diff --git a/packages/core-utils/src/common/version.ts b/packages/core-utils/src/common/version.ts new file mode 100644 index 000000000..305287233 --- /dev/null +++ b/packages/core-utils/src/common/version.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | // Thanks https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb | ||
2 | function compareSemVer (a: string, b: string) { | ||
3 | if (a.startsWith(b + '-')) return -1 | ||
4 | if (b.startsWith(a + '-')) return 1 | ||
5 | |||
6 | return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' }) | ||
7 | } | ||
8 | |||
9 | export { | ||
10 | compareSemVer | ||
11 | } | ||
diff --git a/packages/core-utils/src/i18n/i18n.ts b/packages/core-utils/src/i18n/i18n.ts new file mode 100644 index 000000000..54b54077a --- /dev/null +++ b/packages/core-utils/src/i18n/i18n.ts | |||
@@ -0,0 +1,119 @@ | |||
1 | export const LOCALE_FILES = [ 'player', 'server' ] | ||
2 | |||
3 | export const I18N_LOCALES = { | ||
4 | // Always first to avoid issues when using express acceptLanguages function when no accept language header is set | ||
5 | 'en-US': 'English', | ||
6 | |||
7 | // Keep it alphabetically sorted | ||
8 | 'ar': 'العربية', | ||
9 | 'ca-ES': 'Català', | ||
10 | 'cs-CZ': 'Čeština', | ||
11 | 'de-DE': 'Deutsch', | ||
12 | 'el-GR': 'ελληνικά', | ||
13 | 'eo': 'Esperanto', | ||
14 | 'es-ES': 'Español', | ||
15 | 'eu-ES': 'Euskara', | ||
16 | 'fa-IR': 'فارسی', | ||
17 | 'fi-FI': 'Suomi', | ||
18 | 'fr-FR': 'Français', | ||
19 | 'gd': 'Gàidhlig', | ||
20 | 'gl-ES': 'Galego', | ||
21 | 'hr': 'Hrvatski', | ||
22 | 'hu-HU': 'Magyar', | ||
23 | 'is': 'Íslenska', | ||
24 | 'it-IT': 'Italiano', | ||
25 | 'ja-JP': '日本語', | ||
26 | 'kab': 'Taqbaylit', | ||
27 | 'nb-NO': 'Norsk bokmål', | ||
28 | 'nl-NL': 'Nederlands', | ||
29 | 'nn': 'Norsk nynorsk', | ||
30 | 'oc': 'Occitan', | ||
31 | 'pl-PL': 'Polski', | ||
32 | 'pt-BR': 'Português (Brasil)', | ||
33 | 'pt-PT': 'Português (Portugal)', | ||
34 | 'ru-RU': 'Pусский', | ||
35 | 'sq': 'Shqip', | ||
36 | 'sv-SE': 'Svenska', | ||
37 | 'th-TH': 'ไทย', | ||
38 | 'tok': 'Toki Pona', | ||
39 | 'uk-UA': 'украї́нська мо́ва', | ||
40 | 'vi-VN': 'Tiếng Việt', | ||
41 | 'zh-Hans-CN': '简体中文(中国)', | ||
42 | 'zh-Hant-TW': '繁體中文(台灣)' | ||
43 | } | ||
44 | |||
45 | // Keep it alphabetically sorted | ||
46 | const I18N_LOCALE_ALIAS = { | ||
47 | 'ar-001': 'ar', | ||
48 | 'ca': 'ca-ES', | ||
49 | 'cs': 'cs-CZ', | ||
50 | 'de': 'de-DE', | ||
51 | 'el': 'el-GR', | ||
52 | 'en': 'en-US', | ||
53 | 'es': 'es-ES', | ||
54 | 'eu': 'eu-ES', | ||
55 | 'fa': 'fa-IR', | ||
56 | 'fi': 'fi-FI', | ||
57 | 'fr': 'fr-FR', | ||
58 | 'gl': 'gl-ES', | ||
59 | 'hu': 'hu-HU', | ||
60 | 'it': 'it-IT', | ||
61 | 'ja': 'ja-JP', | ||
62 | 'nb': 'nb-NO', | ||
63 | 'nl': 'nl-NL', | ||
64 | 'pl': 'pl-PL', | ||
65 | 'pt': 'pt-BR', | ||
66 | 'ru': 'ru-RU', | ||
67 | 'sv': 'sv-SE', | ||
68 | 'th': 'th-TH', | ||
69 | 'uk': 'uk-UA', | ||
70 | 'vi': 'vi-VN', | ||
71 | 'zh-CN': 'zh-Hans-CN', | ||
72 | 'zh-Hans': 'zh-Hans-CN', | ||
73 | 'zh-Hant': 'zh-Hant-TW', | ||
74 | 'zh-TW': 'zh-Hant-TW', | ||
75 | 'zh': 'zh-Hans-CN' | ||
76 | } | ||
77 | |||
78 | export const POSSIBLE_LOCALES = (Object.keys(I18N_LOCALES) as string[]).concat(Object.keys(I18N_LOCALE_ALIAS)) | ||
79 | |||
80 | export function getDefaultLocale () { | ||
81 | return 'en-US' | ||
82 | } | ||
83 | |||
84 | export function isDefaultLocale (locale: string) { | ||
85 | return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale()) | ||
86 | } | ||
87 | |||
88 | export function peertubeTranslate (str: string, translations?: { [ id: string ]: string }) { | ||
89 | if (!translations?.[str]) return str | ||
90 | |||
91 | return translations[str] | ||
92 | } | ||
93 | |||
94 | const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l) | ||
95 | export function is18nPath (path: string) { | ||
96 | return possiblePaths.includes(path) | ||
97 | } | ||
98 | |||
99 | export function is18nLocale (locale: string) { | ||
100 | return POSSIBLE_LOCALES.includes(locale) | ||
101 | } | ||
102 | |||
103 | export function getCompleteLocale (locale: string) { | ||
104 | if (!locale) return locale | ||
105 | |||
106 | const found = (I18N_LOCALE_ALIAS as any)[locale] as string | ||
107 | |||
108 | return found || locale | ||
109 | } | ||
110 | |||
111 | export function getShortLocale (locale: string) { | ||
112 | if (locale.includes('-') === false) return locale | ||
113 | |||
114 | return locale.split('-')[0] | ||
115 | } | ||
116 | |||
117 | export function buildFileLocale (locale: string) { | ||
118 | return getCompleteLocale(locale) | ||
119 | } | ||
diff --git a/packages/core-utils/src/i18n/index.ts b/packages/core-utils/src/i18n/index.ts new file mode 100644 index 000000000..758e54b73 --- /dev/null +++ b/packages/core-utils/src/i18n/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './i18n.js' | |||
diff --git a/packages/core-utils/src/index.ts b/packages/core-utils/src/index.ts new file mode 100644 index 000000000..3ca5d9d47 --- /dev/null +++ b/packages/core-utils/src/index.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | export * from './abuse/index.js' | ||
2 | export * from './common/index.js' | ||
3 | export * from './i18n/index.js' | ||
4 | export * from './plugins/index.js' | ||
5 | export * from './renderer/index.js' | ||
6 | export * from './users/index.js' | ||
7 | export * from './videos/index.js' | ||
diff --git a/packages/core-utils/src/plugins/hooks.ts b/packages/core-utils/src/plugins/hooks.ts new file mode 100644 index 000000000..fe7c4a74f --- /dev/null +++ b/packages/core-utils/src/plugins/hooks.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import { HookType, HookType_Type, RegisteredExternalAuthConfig } from '@peertube/peertube-models' | ||
2 | import { isCatchable, isPromise } from '../common/promises.js' | ||
3 | |||
4 | function getHookType (hookName: string) { | ||
5 | if (hookName.startsWith('filter:')) return HookType.FILTER | ||
6 | if (hookName.startsWith('action:')) return HookType.ACTION | ||
7 | |||
8 | return HookType.STATIC | ||
9 | } | ||
10 | |||
11 | async function internalRunHook <T> (options: { | ||
12 | handler: Function | ||
13 | hookType: HookType_Type | ||
14 | result: T | ||
15 | params: any | ||
16 | onError: (err: Error) => void | ||
17 | }) { | ||
18 | const { handler, hookType, result, params, onError } = options | ||
19 | |||
20 | try { | ||
21 | if (hookType === HookType.FILTER) { | ||
22 | const p = handler(result, params) | ||
23 | |||
24 | const newResult = isPromise(p) | ||
25 | ? await p | ||
26 | : p | ||
27 | |||
28 | return newResult | ||
29 | } | ||
30 | |||
31 | // Action/static hooks do not have result value | ||
32 | const p = handler(params) | ||
33 | |||
34 | if (hookType === HookType.STATIC) { | ||
35 | if (isPromise(p)) await p | ||
36 | |||
37 | return undefined | ||
38 | } | ||
39 | |||
40 | if (hookType === HookType.ACTION) { | ||
41 | if (isCatchable(p)) p.catch((err: any) => onError(err)) | ||
42 | |||
43 | return undefined | ||
44 | } | ||
45 | } catch (err) { | ||
46 | onError(err) | ||
47 | } | ||
48 | |||
49 | return result | ||
50 | } | ||
51 | |||
52 | function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) { | ||
53 | return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | ||
54 | } | ||
55 | |||
56 | export { | ||
57 | getHookType, | ||
58 | internalRunHook, | ||
59 | getExternalAuthHref | ||
60 | } | ||
diff --git a/packages/core-utils/src/plugins/index.ts b/packages/core-utils/src/plugins/index.ts new file mode 100644 index 000000000..3462bf41e --- /dev/null +++ b/packages/core-utils/src/plugins/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './hooks.js' | |||
diff --git a/packages/core-utils/src/renderer/html.ts b/packages/core-utils/src/renderer/html.ts new file mode 100644 index 000000000..365bf7612 --- /dev/null +++ b/packages/core-utils/src/renderer/html.ts | |||
@@ -0,0 +1,71 @@ | |||
1 | export function getDefaultSanitizeOptions () { | ||
2 | return { | ||
3 | allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], | ||
4 | allowedSchemes: [ 'http', 'https' ], | ||
5 | allowedAttributes: { | ||
6 | 'a': [ 'href', 'class', 'target', 'rel' ], | ||
7 | '*': [ 'data-*' ] | ||
8 | }, | ||
9 | transformTags: { | ||
10 | a: (tagName: string, attribs: any) => { | ||
11 | let rel = 'noopener noreferrer' | ||
12 | if (attribs.rel === 'me') rel += ' me' | ||
13 | |||
14 | return { | ||
15 | tagName, | ||
16 | attribs: Object.assign(attribs, { | ||
17 | target: '_blank', | ||
18 | rel | ||
19 | }) | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | } | ||
24 | } | ||
25 | |||
26 | export function getTextOnlySanitizeOptions () { | ||
27 | return { | ||
28 | allowedTags: [] as string[] | ||
29 | } | ||
30 | } | ||
31 | |||
32 | export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) { | ||
33 | const base = getDefaultSanitizeOptions() | ||
34 | |||
35 | return { | ||
36 | allowedTags: [ | ||
37 | ...base.allowedTags, | ||
38 | ...additionalAllowedTags, | ||
39 | 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' | ||
40 | ], | ||
41 | allowedSchemes: [ | ||
42 | ...base.allowedSchemes, | ||
43 | |||
44 | 'mailto' | ||
45 | ], | ||
46 | allowedAttributes: { | ||
47 | ...base.allowedAttributes, | ||
48 | |||
49 | 'img': [ 'src', 'alt' ], | ||
50 | '*': [ 'data-*', 'style' ] | ||
51 | } | ||
52 | } | ||
53 | } | ||
54 | |||
55 | // Thanks: https://stackoverflow.com/a/12034334 | ||
56 | export function escapeHTML (stringParam: string) { | ||
57 | if (!stringParam) return '' | ||
58 | |||
59 | const entityMap: { [id: string ]: string } = { | ||
60 | '&': '&', | ||
61 | '<': '<', | ||
62 | '>': '>', | ||
63 | '"': '"', | ||
64 | '\'': ''', | ||
65 | '/': '/', | ||
66 | '`': '`', | ||
67 | '=': '=' | ||
68 | } | ||
69 | |||
70 | return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s]) | ||
71 | } | ||
diff --git a/packages/core-utils/src/renderer/index.ts b/packages/core-utils/src/renderer/index.ts new file mode 100644 index 000000000..0dd0a8808 --- /dev/null +++ b/packages/core-utils/src/renderer/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './markdown.js' | ||
2 | export * from './html.js' | ||
diff --git a/packages/core-utils/src/renderer/markdown.ts b/packages/core-utils/src/renderer/markdown.ts new file mode 100644 index 000000000..ddf608d7b --- /dev/null +++ b/packages/core-utils/src/renderer/markdown.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | export const TEXT_RULES = [ | ||
2 | 'linkify', | ||
3 | 'autolink', | ||
4 | 'emphasis', | ||
5 | 'link', | ||
6 | 'newline', | ||
7 | 'entity', | ||
8 | 'list' | ||
9 | ] | ||
10 | |||
11 | export const TEXT_WITH_HTML_RULES = TEXT_RULES.concat([ | ||
12 | 'html_inline', | ||
13 | 'html_block' | ||
14 | ]) | ||
15 | |||
16 | export const ENHANCED_RULES = TEXT_RULES.concat([ 'image' ]) | ||
17 | export const ENHANCED_WITH_HTML_RULES = TEXT_WITH_HTML_RULES.concat([ 'image' ]) | ||
18 | |||
19 | export const COMPLETE_RULES = ENHANCED_WITH_HTML_RULES.concat([ | ||
20 | 'block', | ||
21 | 'inline', | ||
22 | 'heading', | ||
23 | 'paragraph' | ||
24 | ]) | ||
diff --git a/packages/core-utils/src/users/index.ts b/packages/core-utils/src/users/index.ts new file mode 100644 index 000000000..3fd9dc448 --- /dev/null +++ b/packages/core-utils/src/users/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './user-role.js' | |||
diff --git a/packages/core-utils/src/users/user-role.ts b/packages/core-utils/src/users/user-role.ts new file mode 100644 index 000000000..0add3a0a8 --- /dev/null +++ b/packages/core-utils/src/users/user-role.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import { UserRight, UserRightType, UserRole, UserRoleType } from '@peertube/peertube-models' | ||
2 | |||
3 | export const USER_ROLE_LABELS: { [ id in UserRoleType ]: string } = { | ||
4 | [UserRole.USER]: 'User', | ||
5 | [UserRole.MODERATOR]: 'Moderator', | ||
6 | [UserRole.ADMINISTRATOR]: 'Administrator' | ||
7 | } | ||
8 | |||
9 | const userRoleRights: { [ id in UserRoleType ]: UserRightType[] } = { | ||
10 | [UserRole.ADMINISTRATOR]: [ | ||
11 | UserRight.ALL | ||
12 | ], | ||
13 | |||
14 | [UserRole.MODERATOR]: [ | ||
15 | UserRight.MANAGE_VIDEO_BLACKLIST, | ||
16 | UserRight.MANAGE_ABUSES, | ||
17 | UserRight.MANAGE_ANY_VIDEO_CHANNEL, | ||
18 | UserRight.REMOVE_ANY_VIDEO, | ||
19 | UserRight.REMOVE_ANY_VIDEO_PLAYLIST, | ||
20 | UserRight.REMOVE_ANY_VIDEO_COMMENT, | ||
21 | UserRight.UPDATE_ANY_VIDEO, | ||
22 | UserRight.SEE_ALL_VIDEOS, | ||
23 | UserRight.MANAGE_ACCOUNTS_BLOCKLIST, | ||
24 | UserRight.MANAGE_SERVERS_BLOCKLIST, | ||
25 | UserRight.MANAGE_USERS, | ||
26 | UserRight.SEE_ALL_COMMENTS, | ||
27 | UserRight.MANAGE_REGISTRATIONS | ||
28 | ], | ||
29 | |||
30 | [UserRole.USER]: [] | ||
31 | } | ||
32 | |||
33 | export function hasUserRight (userRole: UserRoleType, userRight: UserRightType) { | ||
34 | const userRights = userRoleRights[userRole] | ||
35 | |||
36 | return userRights.includes(UserRight.ALL) || userRights.includes(userRight) | ||
37 | } | ||
diff --git a/packages/core-utils/src/videos/bitrate.ts b/packages/core-utils/src/videos/bitrate.ts new file mode 100644 index 000000000..b28eaf460 --- /dev/null +++ b/packages/core-utils/src/videos/bitrate.ts | |||
@@ -0,0 +1,113 @@ | |||
1 | import { VideoResolution, VideoResolutionType } from '@peertube/peertube-models' | ||
2 | |||
3 | type BitPerPixel = { [ id in VideoResolutionType ]: number } | ||
4 | |||
5 | // https://bitmovin.com/video-bitrate-streaming-hls-dash/ | ||
6 | |||
7 | const minLimitBitPerPixel: BitPerPixel = { | ||
8 | [VideoResolution.H_NOVIDEO]: 0, | ||
9 | [VideoResolution.H_144P]: 0.02, | ||
10 | [VideoResolution.H_240P]: 0.02, | ||
11 | [VideoResolution.H_360P]: 0.02, | ||
12 | [VideoResolution.H_480P]: 0.02, | ||
13 | [VideoResolution.H_720P]: 0.02, | ||
14 | [VideoResolution.H_1080P]: 0.02, | ||
15 | [VideoResolution.H_1440P]: 0.02, | ||
16 | [VideoResolution.H_4K]: 0.02 | ||
17 | } | ||
18 | |||
19 | const averageBitPerPixel: BitPerPixel = { | ||
20 | [VideoResolution.H_NOVIDEO]: 0, | ||
21 | [VideoResolution.H_144P]: 0.19, | ||
22 | [VideoResolution.H_240P]: 0.17, | ||
23 | [VideoResolution.H_360P]: 0.15, | ||
24 | [VideoResolution.H_480P]: 0.12, | ||
25 | [VideoResolution.H_720P]: 0.11, | ||
26 | [VideoResolution.H_1080P]: 0.10, | ||
27 | [VideoResolution.H_1440P]: 0.09, | ||
28 | [VideoResolution.H_4K]: 0.08 | ||
29 | } | ||
30 | |||
31 | const maxBitPerPixel: BitPerPixel = { | ||
32 | [VideoResolution.H_NOVIDEO]: 0, | ||
33 | [VideoResolution.H_144P]: 0.32, | ||
34 | [VideoResolution.H_240P]: 0.29, | ||
35 | [VideoResolution.H_360P]: 0.26, | ||
36 | [VideoResolution.H_480P]: 0.22, | ||
37 | [VideoResolution.H_720P]: 0.19, | ||
38 | [VideoResolution.H_1080P]: 0.17, | ||
39 | [VideoResolution.H_1440P]: 0.16, | ||
40 | [VideoResolution.H_4K]: 0.14 | ||
41 | } | ||
42 | |||
43 | function getAverageTheoreticalBitrate (options: { | ||
44 | resolution: number | ||
45 | ratio: number | ||
46 | fps: number | ||
47 | }) { | ||
48 | const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel }) | ||
49 | if (!targetBitrate) return 192 * 1000 | ||
50 | |||
51 | return targetBitrate | ||
52 | } | ||
53 | |||
54 | function getMaxTheoreticalBitrate (options: { | ||
55 | resolution: number | ||
56 | ratio: number | ||
57 | fps: number | ||
58 | }) { | ||
59 | const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel }) | ||
60 | if (!targetBitrate) return 256 * 1000 | ||
61 | |||
62 | return targetBitrate | ||
63 | } | ||
64 | |||
65 | function getMinTheoreticalBitrate (options: { | ||
66 | resolution: number | ||
67 | ratio: number | ||
68 | fps: number | ||
69 | }) { | ||
70 | const minLimitBitrate = calculateBitrate({ ...options, bitPerPixel: minLimitBitPerPixel }) | ||
71 | if (!minLimitBitrate) return 10 * 1000 | ||
72 | |||
73 | return minLimitBitrate | ||
74 | } | ||
75 | |||
76 | // --------------------------------------------------------------------------- | ||
77 | |||
78 | export { | ||
79 | getAverageTheoreticalBitrate, | ||
80 | getMaxTheoreticalBitrate, | ||
81 | getMinTheoreticalBitrate | ||
82 | } | ||
83 | |||
84 | // --------------------------------------------------------------------------- | ||
85 | |||
86 | function calculateBitrate (options: { | ||
87 | bitPerPixel: BitPerPixel | ||
88 | resolution: number | ||
89 | ratio: number | ||
90 | fps: number | ||
91 | }) { | ||
92 | const { bitPerPixel, resolution, ratio, fps } = options | ||
93 | |||
94 | const resolutionsOrder = [ | ||
95 | VideoResolution.H_4K, | ||
96 | VideoResolution.H_1440P, | ||
97 | VideoResolution.H_1080P, | ||
98 | VideoResolution.H_720P, | ||
99 | VideoResolution.H_480P, | ||
100 | VideoResolution.H_360P, | ||
101 | VideoResolution.H_240P, | ||
102 | VideoResolution.H_144P, | ||
103 | VideoResolution.H_NOVIDEO | ||
104 | ] | ||
105 | |||
106 | for (const toTestResolution of resolutionsOrder) { | ||
107 | if (toTestResolution <= resolution) { | ||
108 | return Math.floor(resolution * resolution * ratio * fps * bitPerPixel[toTestResolution]) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | throw new Error('Unknown resolution ' + resolution) | ||
113 | } | ||
diff --git a/packages/core-utils/src/videos/common.ts b/packages/core-utils/src/videos/common.ts new file mode 100644 index 000000000..47564fb2a --- /dev/null +++ b/packages/core-utils/src/videos/common.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models' | ||
2 | |||
3 | function getAllPrivacies () { | ||
4 | return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ] | ||
5 | } | ||
6 | |||
7 | function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) { | ||
8 | const files = video.files | ||
9 | |||
10 | const hls = getHLS(video) | ||
11 | if (hls) return files.concat(hls.files) | ||
12 | |||
13 | return files | ||
14 | } | ||
15 | |||
16 | function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) { | ||
17 | return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | getAllPrivacies, | ||
22 | getAllFiles, | ||
23 | getHLS | ||
24 | } | ||
diff --git a/packages/core-utils/src/videos/index.ts b/packages/core-utils/src/videos/index.ts new file mode 100644 index 000000000..7d3dacdd4 --- /dev/null +++ b/packages/core-utils/src/videos/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './bitrate.js' | ||
2 | export * from './common.js' | ||
diff --git a/packages/core-utils/tsconfig.json b/packages/core-utils/tsconfig.json new file mode 100644 index 000000000..56ebffbb3 --- /dev/null +++ b/packages/core-utils/tsconfig.json | |||
@@ -0,0 +1,11 @@ | |||
1 | { | ||
2 | "extends": "../../tsconfig.base.json", | ||
3 | "compilerOptions": { | ||
4 | "outDir": "./dist", | ||
5 | "rootDir": "src", | ||
6 | "tsBuildInfoFile": "./dist/.tsbuildinfo" | ||
7 | }, | ||
8 | "references": [ | ||
9 | { "path": "../models" } | ||
10 | ] | ||
11 | } | ||