aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils/videos
diff options
context:
space:
mode:
Diffstat (limited to 'shared/extra-utils/videos')
-rw-r--r--shared/extra-utils/videos/blacklist-command.ts76
-rw-r--r--shared/extra-utils/videos/captions-command.ts65
-rw-r--r--shared/extra-utils/videos/captions.ts21
-rw-r--r--shared/extra-utils/videos/change-ownership-command.ts68
-rw-r--r--shared/extra-utils/videos/channels-command.ts178
-rw-r--r--shared/extra-utils/videos/channels.ts18
-rw-r--r--shared/extra-utils/videos/comments-command.ts152
-rw-r--r--shared/extra-utils/videos/history-command.ts58
-rw-r--r--shared/extra-utils/videos/imports-command.ts47
-rw-r--r--shared/extra-utils/videos/index.ts19
-rw-r--r--shared/extra-utils/videos/live-command.ts155
-rw-r--r--shared/extra-utils/videos/live.ts137
-rw-r--r--shared/extra-utils/videos/playlists-command.ts280
-rw-r--r--shared/extra-utils/videos/playlists.ts25
-rw-r--r--shared/extra-utils/videos/services-command.ts29
-rw-r--r--shared/extra-utils/videos/streaming-playlists-command.ts44
-rw-r--r--shared/extra-utils/videos/streaming-playlists.ts77
-rw-r--r--shared/extra-utils/videos/videos-command.ts687
-rw-r--r--shared/extra-utils/videos/videos.ts253
19 files changed, 0 insertions, 2389 deletions
diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts
deleted file mode 100644
index 3a2ef89ba..000000000
--- a/shared/extra-utils/videos/blacklist-command.ts
+++ /dev/null
@@ -1,76 +0,0 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoBlacklist, VideoBlacklistType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class BlacklistCommand extends AbstractCommand {
7
8 add (options: OverrideCommandOptions & {
9 videoId: number | string
10 reason?: string
11 unfederate?: boolean
12 }) {
13 const { videoId, reason, unfederate } = options
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return this.postBodyRequest({
17 ...options,
18
19 path,
20 fields: { reason, unfederate },
21 implicitToken: true,
22 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
23 })
24 }
25
26 update (options: OverrideCommandOptions & {
27 videoId: number | string
28 reason?: string
29 }) {
30 const { videoId, reason } = options
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return this.putBodyRequest({
34 ...options,
35
36 path,
37 fields: { reason },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 videoId: number | string
45 }) {
46 const { videoId } = options
47 const path = '/api/v1/videos/' + videoId + '/blacklist'
48
49 return this.deleteRequest({
50 ...options,
51
52 path,
53 implicitToken: true,
54 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
55 })
56 }
57
58 list (options: OverrideCommandOptions & {
59 sort?: string
60 type?: VideoBlacklistType
61 } = {}) {
62 const { sort, type } = options
63 const path = '/api/v1/videos/blacklist/'
64
65 const query = { sort, type }
66
67 return this.getRequestBody<ResultList<VideoBlacklist>>({
68 ...options,
69
70 path,
71 query,
72 implicitToken: true,
73 defaultExpectedStatus: HttpStatusCode.OK_200
74 })
75 }
76}
diff --git a/shared/extra-utils/videos/captions-command.ts b/shared/extra-utils/videos/captions-command.ts
deleted file mode 100644
index a65ea99e3..000000000
--- a/shared/extra-utils/videos/captions-command.ts
+++ /dev/null
@@ -1,65 +0,0 @@
1import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models'
2import { buildAbsoluteFixturePath } from '../miscs'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class CaptionsCommand extends AbstractCommand {
6
7 add (options: OverrideCommandOptions & {
8 videoId: string | number
9 language: string
10 fixture: string
11 mimeType?: string
12 }) {
13 const { videoId, language, fixture, mimeType } = options
14
15 const path = '/api/v1/videos/' + videoId + '/captions/' + language
16
17 const captionfile = buildAbsoluteFixturePath(fixture)
18 const captionfileAttach = mimeType
19 ? [ captionfile, { contentType: mimeType } ]
20 : captionfile
21
22 return this.putUploadRequest({
23 ...options,
24
25 path,
26 fields: {},
27 attaches: {
28 captionfile: captionfileAttach
29 },
30 implicitToken: true,
31 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
32 })
33 }
34
35 list (options: OverrideCommandOptions & {
36 videoId: string | number
37 }) {
38 const { videoId } = options
39 const path = '/api/v1/videos/' + videoId + '/captions'
40
41 return this.getRequestBody<ResultList<VideoCaption>>({
42 ...options,
43
44 path,
45 implicitToken: false,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 delete (options: OverrideCommandOptions & {
51 videoId: string | number
52 language: string
53 }) {
54 const { videoId, language } = options
55 const path = '/api/v1/videos/' + videoId + '/captions/' + language
56
57 return this.deleteRequest({
58 ...options,
59
60 path,
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65}
diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts
deleted file mode 100644
index 35e722408..000000000
--- a/shared/extra-utils/videos/captions.ts
+++ /dev/null
@@ -1,21 +0,0 @@
1import { expect } from 'chai'
2import request from 'supertest'
3import { HttpStatusCode } from '@shared/models'
4
5async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) {
6 const res = await request(url)
7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200)
9
10 if (toTest instanceof RegExp) {
11 expect(res.text).to.match(toTest)
12 } else {
13 expect(res.text).to.contain(toTest)
14 }
15}
16
17// ---------------------------------------------------------------------------
18
19export {
20 testCaptionFile
21}
diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts
deleted file mode 100644
index ad4c726ef..000000000
--- a/shared/extra-utils/videos/change-ownership-command.ts
+++ /dev/null
@@ -1,68 +0,0 @@
1
2import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ChangeOwnershipCommand extends AbstractCommand {
6
7 create (options: OverrideCommandOptions & {
8 videoId: number | string
9 username: string
10 }) {
11 const { videoId, username } = options
12 const path = '/api/v1/videos/' + videoId + '/give-ownership'
13
14 return this.postBodyRequest({
15 ...options,
16
17 path,
18 fields: { username },
19 implicitToken: true,
20 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
21 })
22 }
23
24 list (options: OverrideCommandOptions = {}) {
25 const path = '/api/v1/videos/ownership'
26
27 return this.getRequestBody<ResultList<VideoChangeOwnership>>({
28 ...options,
29
30 path,
31 query: { sort: '-createdAt' },
32 implicitToken: true,
33 defaultExpectedStatus: HttpStatusCode.OK_200
34 })
35 }
36
37 accept (options: OverrideCommandOptions & {
38 ownershipId: number
39 channelId: number
40 }) {
41 const { ownershipId, channelId } = options
42 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
43
44 return this.postBodyRequest({
45 ...options,
46
47 path,
48 fields: { channelId },
49 implicitToken: true,
50 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
51 })
52 }
53
54 refuse (options: OverrideCommandOptions & {
55 ownershipId: number
56 }) {
57 const { ownershipId } = options
58 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
59
60 return this.postBodyRequest({
61 ...options,
62
63 path,
64 implicitToken: true,
65 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
66 })
67 }
68}
diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts
deleted file mode 100644
index e406e570b..000000000
--- a/shared/extra-utils/videos/channels-command.ts
+++ /dev/null
@@ -1,178 +0,0 @@
1import { pick } from '@shared/core-utils'
2import { ActorFollow, HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
3import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { unwrapBody } from '../requests'
6import { AbstractCommand, OverrideCommandOptions } from '../shared'
7
8export class ChannelsCommand extends AbstractCommand {
9
10 list (options: OverrideCommandOptions & {
11 start?: number
12 count?: number
13 sort?: string
14 withStats?: boolean
15 } = {}) {
16 const path = '/api/v1/video-channels'
17
18 return this.getRequestBody<ResultList<VideoChannel>>({
19 ...options,
20
21 path,
22 query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
23 implicitToken: false,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 listByAccount (options: OverrideCommandOptions & {
29 accountName: string
30 start?: number
31 count?: number
32 sort?: string
33 withStats?: boolean
34 search?: string
35 }) {
36 const { accountName, sort = 'createdAt' } = options
37 const path = '/api/v1/accounts/' + accountName + '/video-channels'
38
39 return this.getRequestBody<ResultList<VideoChannel>>({
40 ...options,
41
42 path,
43 query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
44 implicitToken: false,
45 defaultExpectedStatus: HttpStatusCode.OK_200
46 })
47 }
48
49 async create (options: OverrideCommandOptions & {
50 attributes: Partial<VideoChannelCreate>
51 }) {
52 const path = '/api/v1/video-channels/'
53
54 // Default attributes
55 const defaultAttributes = {
56 displayName: 'my super video channel',
57 description: 'my super channel description',
58 support: 'my super channel support'
59 }
60 const attributes = { ...defaultAttributes, ...options.attributes }
61
62 const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
63 ...options,
64
65 path,
66 fields: attributes,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 }))
70
71 return body.videoChannel
72 }
73
74 update (options: OverrideCommandOptions & {
75 channelName: string
76 attributes: VideoChannelUpdate
77 }) {
78 const { channelName, attributes } = options
79 const path = '/api/v1/video-channels/' + channelName
80
81 return this.putBodyRequest({
82 ...options,
83
84 path,
85 fields: attributes,
86 implicitToken: true,
87 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
88 })
89 }
90
91 delete (options: OverrideCommandOptions & {
92 channelName: string
93 }) {
94 const path = '/api/v1/video-channels/' + options.channelName
95
96 return this.deleteRequest({
97 ...options,
98
99 path,
100 implicitToken: true,
101 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
102 })
103 }
104
105 get (options: OverrideCommandOptions & {
106 channelName: string
107 }) {
108 const path = '/api/v1/video-channels/' + options.channelName
109
110 return this.getRequestBody<VideoChannel>({
111 ...options,
112
113 path,
114 implicitToken: false,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 })
117 }
118
119 updateImage (options: OverrideCommandOptions & {
120 fixture: string
121 channelName: string | number
122 type: 'avatar' | 'banner'
123 }) {
124 const { channelName, fixture, type } = options
125
126 const path = `/api/v1/video-channels/${channelName}/${type}/pick`
127
128 return this.updateImageRequest({
129 ...options,
130
131 path,
132 fixture,
133 fieldname: type + 'file',
134
135 implicitToken: true,
136 defaultExpectedStatus: HttpStatusCode.OK_200
137 })
138 }
139
140 deleteImage (options: OverrideCommandOptions & {
141 channelName: string | number
142 type: 'avatar' | 'banner'
143 }) {
144 const { channelName, type } = options
145
146 const path = `/api/v1/video-channels/${channelName}/${type}`
147
148 return this.deleteRequest({
149 ...options,
150
151 path,
152 implicitToken: true,
153 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
154 })
155 }
156
157 listFollowers (options: OverrideCommandOptions & {
158 channelName: string
159 start?: number
160 count?: number
161 sort?: string
162 search?: string
163 }) {
164 const { channelName, start, count, sort, search } = options
165 const path = '/api/v1/video-channels/' + channelName + '/followers'
166
167 const query = { start, count, sort, search }
168
169 return this.getRequestBody<ResultList<ActorFollow>>({
170 ...options,
171
172 path,
173 query,
174 implicitToken: true,
175 defaultExpectedStatus: HttpStatusCode.OK_200
176 })
177 }
178}
diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts
deleted file mode 100644
index 756c47453..000000000
--- a/shared/extra-utils/videos/channels.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { PeerTubeServer } from '../server/server'
2
3function setDefaultVideoChannel (servers: PeerTubeServer[]) {
4 const tasks: Promise<any>[] = []
5
6 for (const server of servers) {
7 const p = server.users.getMyInfo()
8 .then(user => { server.store.channel = user.videoChannels[0] })
9
10 tasks.push(p)
11 }
12
13 return Promise.all(tasks)
14}
15
16export {
17 setDefaultVideoChannel
18}
diff --git a/shared/extra-utils/videos/comments-command.ts b/shared/extra-utils/videos/comments-command.ts
deleted file mode 100644
index f0d163a07..000000000
--- a/shared/extra-utils/videos/comments-command.ts
+++ /dev/null
@@ -1,152 +0,0 @@
1import { pick } from 'lodash'
2import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@shared/models'
3import { unwrapBody } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class CommentsCommand extends AbstractCommand {
7
8 private lastVideoId: number | string
9 private lastThreadId: number
10 private lastReplyId: number
11
12 listForAdmin (options: OverrideCommandOptions & {
13 start?: number
14 count?: number
15 sort?: string
16 isLocal?: boolean
17 search?: string
18 searchAccount?: string
19 searchVideo?: string
20 } = {}) {
21 const { sort = '-createdAt' } = options
22 const path = '/api/v1/videos/comments'
23
24 const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'search', 'searchAccount', 'searchVideo' ]) }
25
26 return this.getRequestBody<ResultList<VideoComment>>({
27 ...options,
28
29 path,
30 query,
31 implicitToken: true,
32 defaultExpectedStatus: HttpStatusCode.OK_200
33 })
34 }
35
36 listThreads (options: OverrideCommandOptions & {
37 videoId: number | string
38 start?: number
39 count?: number
40 sort?: string
41 }) {
42 const { start, count, sort, videoId } = options
43 const path = '/api/v1/videos/' + videoId + '/comment-threads'
44
45 return this.getRequestBody<VideoCommentThreads>({
46 ...options,
47
48 path,
49 query: { start, count, sort },
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 getThread (options: OverrideCommandOptions & {
56 videoId: number | string
57 threadId: number
58 }) {
59 const { videoId, threadId } = options
60 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
61
62 return this.getRequestBody<VideoCommentThreadTree>({
63 ...options,
64
65 path,
66 implicitToken: false,
67 defaultExpectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 async createThread (options: OverrideCommandOptions & {
72 videoId: number | string
73 text: string
74 }) {
75 const { videoId, text } = options
76 const path = '/api/v1/videos/' + videoId + '/comment-threads'
77
78 const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
79 ...options,
80
81 path,
82 fields: { text },
83 implicitToken: true,
84 defaultExpectedStatus: HttpStatusCode.OK_200
85 }))
86
87 this.lastThreadId = body.comment?.id
88 this.lastVideoId = videoId
89
90 return body.comment
91 }
92
93 async addReply (options: OverrideCommandOptions & {
94 videoId: number | string
95 toCommentId: number
96 text: string
97 }) {
98 const { videoId, toCommentId, text } = options
99 const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
100
101 const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
102 ...options,
103
104 path,
105 fields: { text },
106 implicitToken: true,
107 defaultExpectedStatus: HttpStatusCode.OK_200
108 }))
109
110 this.lastReplyId = body.comment?.id
111
112 return body.comment
113 }
114
115 async addReplyToLastReply (options: OverrideCommandOptions & {
116 text: string
117 }) {
118 return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
119 }
120
121 async addReplyToLastThread (options: OverrideCommandOptions & {
122 text: string
123 }) {
124 return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
125 }
126
127 async findCommentId (options: OverrideCommandOptions & {
128 videoId: number | string
129 text: string
130 }) {
131 const { videoId, text } = options
132 const { data } = await this.listThreads({ videoId, count: 25, sort: '-createdAt' })
133
134 return data.find(c => c.text === text).id
135 }
136
137 delete (options: OverrideCommandOptions & {
138 videoId: number | string
139 commentId: number
140 }) {
141 const { videoId, commentId } = options
142 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
143
144 return this.deleteRequest({
145 ...options,
146
147 path,
148 implicitToken: true,
149 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
150 })
151 }
152}
diff --git a/shared/extra-utils/videos/history-command.ts b/shared/extra-utils/videos/history-command.ts
deleted file mode 100644
index 13b7150c1..000000000
--- a/shared/extra-utils/videos/history-command.ts
+++ /dev/null
@@ -1,58 +0,0 @@
1import { HttpStatusCode, ResultList, Video } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class HistoryCommand extends AbstractCommand {
5
6 wathVideo (options: OverrideCommandOptions & {
7 videoId: number | string
8 currentTime: number
9 }) {
10 const { videoId, currentTime } = options
11
12 const path = '/api/v1/videos/' + videoId + '/watching'
13 const fields = { currentTime }
14
15 return this.putBodyRequest({
16 ...options,
17
18 path,
19 fields,
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
22 })
23 }
24
25 list (options: OverrideCommandOptions & {
26 search?: string
27 } = {}) {
28 const { search } = options
29 const path = '/api/v1/users/me/history/videos'
30
31 return this.getRequestBody<ResultList<Video>>({
32 ...options,
33
34 path,
35 query: {
36 search
37 },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.OK_200
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 beforeDate?: string
45 } = {}) {
46 const { beforeDate } = options
47 const path = '/api/v1/users/me/history/videos/remove'
48
49 return this.postBodyRequest({
50 ...options,
51
52 path,
53 fields: { beforeDate },
54 implicitToken: true,
55 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
56 })
57 }
58}
diff --git a/shared/extra-utils/videos/imports-command.ts b/shared/extra-utils/videos/imports-command.ts
deleted file mode 100644
index e4944694d..000000000
--- a/shared/extra-utils/videos/imports-command.ts
+++ /dev/null
@@ -1,47 +0,0 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoImport, VideoImportCreate } from '../../models/videos'
4import { unwrapBody } from '../requests'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class ImportsCommand extends AbstractCommand {
8
9 importVideo (options: OverrideCommandOptions & {
10 attributes: VideoImportCreate & { torrentfile?: string }
11 }) {
12 const { attributes } = options
13 const path = '/api/v1/videos/imports'
14
15 let attaches: any = {}
16 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
17
18 return unwrapBody<VideoImport>(this.postUploadRequest({
19 ...options,
20
21 path,
22 attaches,
23 fields: options.attributes,
24 implicitToken: true,
25 defaultExpectedStatus: HttpStatusCode.OK_200
26 }))
27 }
28
29 getMyVideoImports (options: OverrideCommandOptions & {
30 sort?: string
31 } = {}) {
32 const { sort } = options
33 const path = '/api/v1/users/me/videos/imports'
34
35 const query = {}
36 if (sort) query['sort'] = sort
37
38 return this.getRequestBody<ResultList<VideoImport>>({
39 ...options,
40
41 path,
42 query: { sort },
43 implicitToken: true,
44 defaultExpectedStatus: HttpStatusCode.OK_200
45 })
46 }
47}
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
deleted file mode 100644
index 26e663f46..000000000
--- a/shared/extra-utils/videos/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1export * from './blacklist-command'
2export * from './captions-command'
3export * from './captions'
4export * from './change-ownership-command'
5export * from './channels'
6export * from './channels-command'
7export * from './comments-command'
8export * from './history-command'
9export * from './imports-command'
10export * from './live-command'
11export * from './live'
12export * from './playlists-command'
13export * from './playlists'
14export * from './services-command'
15export * from './streaming-playlists-command'
16export * from './streaming-playlists'
17export * from './comments-command'
18export * from './videos-command'
19export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
deleted file mode 100644
index 74f5d3089..000000000
--- a/shared/extra-utils/videos/live-command.ts
+++ /dev/null
@@ -1,155 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readdir } from 'fs-extra'
4import { omit } from 'lodash'
5import { join } from 'path'
6import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
7import { wait } from '../miscs'
8import { unwrapBody } from '../requests'
9import { AbstractCommand, OverrideCommandOptions } from '../shared'
10import { sendRTMPStream, testFfmpegStreamError } from './live'
11
12export class LiveCommand extends AbstractCommand {
13
14 get (options: OverrideCommandOptions & {
15 videoId: number | string
16 }) {
17 const path = '/api/v1/videos/live'
18
19 return this.getRequestBody<LiveVideo>({
20 ...options,
21
22 path: path + '/' + options.videoId,
23 implicitToken: true,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 update (options: OverrideCommandOptions & {
29 videoId: number | string
30 fields: LiveVideoUpdate
31 }) {
32 const { videoId, fields } = options
33 const path = '/api/v1/videos/live'
34
35 return this.putBodyRequest({
36 ...options,
37
38 path: path + '/' + videoId,
39 fields,
40 implicitToken: true,
41 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
42 })
43 }
44
45 async create (options: OverrideCommandOptions & {
46 fields: LiveVideoCreate
47 }) {
48 const { fields } = options
49 const path = '/api/v1/videos/live'
50
51 const attaches: any = {}
52 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
53 if (fields.previewfile) attaches.previewfile = fields.previewfile
54
55 const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
56 ...options,
57
58 path,
59 attaches,
60 fields: omit(fields, 'thumbnailfile', 'previewfile'),
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 }))
64
65 return body.video
66 }
67
68 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
69 videoId: number | string
70 fixtureName?: string
71 copyCodecs?: boolean
72 }) {
73 const { videoId, fixtureName, copyCodecs } = options
74 const videoLive = await this.get({ videoId })
75
76 return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
77 }
78
79 async runAndTestStreamError (options: OverrideCommandOptions & {
80 videoId: number | string
81 shouldHaveError: boolean
82 }) {
83 const command = await this.sendRTMPStreamInVideo(options)
84
85 return testFfmpegStreamError(command, options.shouldHaveError)
86 }
87
88 waitUntilPublished (options: OverrideCommandOptions & {
89 videoId: number | string
90 }) {
91 const { videoId } = options
92 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
93 }
94
95 waitUntilWaiting (options: OverrideCommandOptions & {
96 videoId: number | string
97 }) {
98 const { videoId } = options
99 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
100 }
101
102 waitUntilEnded (options: OverrideCommandOptions & {
103 videoId: number | string
104 }) {
105 const { videoId } = options
106 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
107 }
108
109 waitUntilSegmentGeneration (options: OverrideCommandOptions & {
110 videoUUID: string
111 resolution: number
112 segment: number
113 }) {
114 const { resolution, segment, videoUUID } = options
115 const segmentName = `${resolution}-00000${segment}.ts`
116
117 return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
118 }
119
120 async waitUntilSaved (options: OverrideCommandOptions & {
121 videoId: number | string
122 }) {
123 let video: VideoDetails
124
125 do {
126 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
127
128 await wait(500)
129 } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
130 }
131
132 async countPlaylists (options: OverrideCommandOptions & {
133 videoUUID: string
134 }) {
135 const basePath = this.server.servers.buildDirectory('streaming-playlists')
136 const hlsPath = join(basePath, 'hls', options.videoUUID)
137
138 const files = await readdir(hlsPath)
139
140 return files.filter(f => f.endsWith('.m3u8')).length
141 }
142
143 private async waitUntilState (options: OverrideCommandOptions & {
144 videoId: number | string
145 state: VideoState
146 }) {
147 let video: VideoDetails
148
149 do {
150 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
151
152 await wait(500)
153 } while (video.state.id !== options.state)
154 }
155}
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
deleted file mode 100644
index d3665bc90..000000000
--- a/shared/extra-utils/videos/live.ts
+++ /dev/null
@@ -1,137 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
5import { pathExists, readdir } from 'fs-extra'
6import { join } from 'path'
7import { buildAbsoluteFixturePath, wait } from '../miscs'
8import { PeerTubeServer } from '../server/server'
9
10function sendRTMPStream (options: {
11 rtmpBaseUrl: string
12 streamKey: string
13 fixtureName?: string // default video_short.mp4
14 copyCodecs?: boolean // default false
15}) {
16 const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options
17
18 const fixture = buildAbsoluteFixturePath(fixtureName)
19
20 const command = ffmpeg(fixture)
21 command.inputOption('-stream_loop -1')
22 command.inputOption('-re')
23
24 if (copyCodecs) {
25 command.outputOption('-c copy')
26 } else {
27 command.outputOption('-c:v libx264')
28 command.outputOption('-g 50')
29 command.outputOption('-keyint_min 2')
30 command.outputOption('-r 60')
31 }
32
33 command.outputOption('-f flv')
34
35 const rtmpUrl = rtmpBaseUrl + '/' + streamKey
36 command.output(rtmpUrl)
37
38 command.on('error', err => {
39 if (err?.message?.includes('Exiting normally')) return
40
41 if (process.env.DEBUG) console.error(err)
42 })
43
44 if (process.env.DEBUG) {
45 command.on('stderr', data => console.log(data))
46 }
47
48 command.run()
49
50 return command
51}
52
53function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
54 return new Promise<void>((res, rej) => {
55 command.on('error', err => {
56 return rej(err)
57 })
58
59 setTimeout(() => {
60 res()
61 }, successAfterMS)
62 })
63}
64
65async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
66 let error: Error
67
68 try {
69 await waitFfmpegUntilError(command, 35000)
70 } catch (err) {
71 error = err
72 }
73
74 await stopFfmpeg(command)
75
76 if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error')
77 if (!shouldHaveError && error) throw error
78}
79
80async function stopFfmpeg (command: FfmpegCommand) {
81 command.kill('SIGINT')
82
83 await wait(500)
84}
85
86async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
87 for (const server of servers) {
88 await server.live.waitUntilPublished({ videoId })
89 }
90}
91
92async function waitUntilLiveSavedOnAllServers (servers: PeerTubeServer[], videoId: string) {
93 for (const server of servers) {
94 await server.live.waitUntilSaved({ videoId })
95 }
96}
97
98async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
99 const basePath = server.servers.buildDirectory('streaming-playlists')
100 const hlsPath = join(basePath, 'hls', videoUUID)
101
102 if (resolutions.length === 0) {
103 const result = await pathExists(hlsPath)
104 expect(result).to.be.false
105
106 return
107 }
108
109 const files = await readdir(hlsPath)
110
111 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
112 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
113
114 for (const resolution of resolutions) {
115 const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
116 expect(fragmentedFile).to.exist
117
118 const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
119 expect(playlistFile).to.exist
120 }
121
122 const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
123 expect(masterPlaylistFile).to.exist
124
125 const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
126 expect(shaFile).to.exist
127}
128
129export {
130 sendRTMPStream,
131 waitFfmpegUntilError,
132 testFfmpegStreamError,
133 stopFfmpeg,
134 waitUntilLivePublishedOnAllServers,
135 waitUntilLiveSavedOnAllServers,
136 checkLiveCleanupAfterSave
137}
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
deleted file mode 100644
index ce23900d3..000000000
--- a/shared/extra-utils/videos/playlists-command.ts
+++ /dev/null
@@ -1,280 +0,0 @@
1import { omit } from 'lodash'
2import { pick } from '@shared/core-utils'
3import {
4 BooleanBothQuery,
5 HttpStatusCode,
6 ResultList,
7 VideoExistInPlaylist,
8 VideoPlaylist,
9 VideoPlaylistCreate,
10 VideoPlaylistCreateResult,
11 VideoPlaylistElement,
12 VideoPlaylistElementCreate,
13 VideoPlaylistElementCreateResult,
14 VideoPlaylistElementUpdate,
15 VideoPlaylistReorder,
16 VideoPlaylistType,
17 VideoPlaylistUpdate
18} from '@shared/models'
19import { unwrapBody } from '../requests'
20import { AbstractCommand, OverrideCommandOptions } from '../shared'
21
22export class PlaylistsCommand extends AbstractCommand {
23
24 list (options: OverrideCommandOptions & {
25 start?: number
26 count?: number
27 sort?: string
28 }) {
29 const path = '/api/v1/video-playlists'
30 const query = pick(options, [ 'start', 'count', 'sort' ])
31
32 return this.getRequestBody<ResultList<VideoPlaylist>>({
33 ...options,
34
35 path,
36 query,
37 implicitToken: false,
38 defaultExpectedStatus: HttpStatusCode.OK_200
39 })
40 }
41
42 listByChannel (options: OverrideCommandOptions & {
43 handle: string
44 start?: number
45 count?: number
46 sort?: string
47 }) {
48 const path = '/api/v1/video-channels/' + options.handle + '/video-playlists'
49 const query = pick(options, [ 'start', 'count', 'sort' ])
50
51 return this.getRequestBody<ResultList<VideoPlaylist>>({
52 ...options,
53
54 path,
55 query,
56 implicitToken: false,
57 defaultExpectedStatus: HttpStatusCode.OK_200
58 })
59 }
60
61 listByAccount (options: OverrideCommandOptions & {
62 handle: string
63 start?: number
64 count?: number
65 sort?: string
66 search?: string
67 playlistType?: VideoPlaylistType
68 }) {
69 const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
70 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
71
72 return this.getRequestBody<ResultList<VideoPlaylist>>({
73 ...options,
74
75 path,
76 query,
77 implicitToken: false,
78 defaultExpectedStatus: HttpStatusCode.OK_200
79 })
80 }
81
82 get (options: OverrideCommandOptions & {
83 playlistId: number | string
84 }) {
85 const { playlistId } = options
86 const path = '/api/v1/video-playlists/' + playlistId
87
88 return this.getRequestBody<VideoPlaylist>({
89 ...options,
90
91 path,
92 implicitToken: false,
93 defaultExpectedStatus: HttpStatusCode.OK_200
94 })
95 }
96
97 listVideos (options: OverrideCommandOptions & {
98 playlistId: number | string
99 start?: number
100 count?: number
101 query?: { nsfw?: BooleanBothQuery }
102 }) {
103 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
104 const query = options.query ?? {}
105
106 return this.getRequestBody<ResultList<VideoPlaylistElement>>({
107 ...options,
108
109 path,
110 query: {
111 ...query,
112 start: options.start,
113 count: options.count
114 },
115 implicitToken: true,
116 defaultExpectedStatus: HttpStatusCode.OK_200
117 })
118 }
119
120 delete (options: OverrideCommandOptions & {
121 playlistId: number | string
122 }) {
123 const path = '/api/v1/video-playlists/' + options.playlistId
124
125 return this.deleteRequest({
126 ...options,
127
128 path,
129 implicitToken: true,
130 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
131 })
132 }
133
134 async create (options: OverrideCommandOptions & {
135 attributes: VideoPlaylistCreate
136 }) {
137 const path = '/api/v1/video-playlists'
138
139 const fields = omit(options.attributes, 'thumbnailfile')
140
141 const attaches = options.attributes.thumbnailfile
142 ? { thumbnailfile: options.attributes.thumbnailfile }
143 : {}
144
145 const body = await unwrapBody<{ videoPlaylist: VideoPlaylistCreateResult }>(this.postUploadRequest({
146 ...options,
147
148 path,
149 fields,
150 attaches,
151 implicitToken: true,
152 defaultExpectedStatus: HttpStatusCode.OK_200
153 }))
154
155 return body.videoPlaylist
156 }
157
158 update (options: OverrideCommandOptions & {
159 attributes: VideoPlaylistUpdate
160 playlistId: number | string
161 }) {
162 const path = '/api/v1/video-playlists/' + options.playlistId
163
164 const fields = omit(options.attributes, 'thumbnailfile')
165
166 const attaches = options.attributes.thumbnailfile
167 ? { thumbnailfile: options.attributes.thumbnailfile }
168 : {}
169
170 return this.putUploadRequest({
171 ...options,
172
173 path,
174 fields,
175 attaches,
176 implicitToken: true,
177 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
178 })
179 }
180
181 async addElement (options: OverrideCommandOptions & {
182 playlistId: number | string
183 attributes: VideoPlaylistElementCreate | { videoId: string }
184 }) {
185 const attributes = {
186 ...options.attributes,
187
188 videoId: await this.server.videos.getId({ ...options, uuid: options.attributes.videoId })
189 }
190
191 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
192
193 const body = await unwrapBody<{ videoPlaylistElement: VideoPlaylistElementCreateResult }>(this.postBodyRequest({
194 ...options,
195
196 path,
197 fields: attributes,
198 implicitToken: true,
199 defaultExpectedStatus: HttpStatusCode.OK_200
200 }))
201
202 return body.videoPlaylistElement
203 }
204
205 updateElement (options: OverrideCommandOptions & {
206 playlistId: number | string
207 elementId: number | string
208 attributes: VideoPlaylistElementUpdate
209 }) {
210 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
211
212 return this.putBodyRequest({
213 ...options,
214
215 path,
216 fields: options.attributes,
217 implicitToken: true,
218 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
219 })
220 }
221
222 removeElement (options: OverrideCommandOptions & {
223 playlistId: number | string
224 elementId: number
225 }) {
226 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
227
228 return this.deleteRequest({
229 ...options,
230
231 path,
232 implicitToken: true,
233 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
234 })
235 }
236
237 reorderElements (options: OverrideCommandOptions & {
238 playlistId: number | string
239 attributes: VideoPlaylistReorder
240 }) {
241 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
242
243 return this.postBodyRequest({
244 ...options,
245
246 path,
247 fields: options.attributes,
248 implicitToken: true,
249 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
250 })
251 }
252
253 getPrivacies (options: OverrideCommandOptions = {}) {
254 const path = '/api/v1/video-playlists/privacies'
255
256 return this.getRequestBody<{ [ id: number ]: string }>({
257 ...options,
258
259 path,
260 implicitToken: false,
261 defaultExpectedStatus: HttpStatusCode.OK_200
262 })
263 }
264
265 videosExist (options: OverrideCommandOptions & {
266 videoIds: number[]
267 }) {
268 const { videoIds } = options
269 const path = '/api/v1/users/me/video-playlists/videos-exist'
270
271 return this.getRequestBody<VideoExistInPlaylist>({
272 ...options,
273
274 path,
275 query: { videoIds },
276 implicitToken: true,
277 defaultExpectedStatus: HttpStatusCode.OK_200
278 })
279 }
280}
diff --git a/shared/extra-utils/videos/playlists.ts b/shared/extra-utils/videos/playlists.ts
deleted file mode 100644
index 3dde52bb9..000000000
--- a/shared/extra-utils/videos/playlists.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { expect } from 'chai'
2import { readdir } from 'fs-extra'
3import { join } from 'path'
4import { root } from '../miscs'
5
6async function checkPlaylistFilesWereRemoved (
7 playlistUUID: string,
8 internalServerNumber: number,
9 directories = [ 'thumbnails' ]
10) {
11 const testDirectory = 'test' + internalServerNumber
12
13 for (const directory of directories) {
14 const directoryPath = join(root(), testDirectory, directory)
15
16 const files = await readdir(directoryPath)
17 for (const file of files) {
18 expect(file).to.not.contain(playlistUUID)
19 }
20 }
21}
22
23export {
24 checkPlaylistFilesWereRemoved
25}
diff --git a/shared/extra-utils/videos/services-command.ts b/shared/extra-utils/videos/services-command.ts
deleted file mode 100644
index 06760df42..000000000
--- a/shared/extra-utils/videos/services-command.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import { HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class ServicesCommand extends AbstractCommand {
5
6 getOEmbed (options: OverrideCommandOptions & {
7 oembedUrl: string
8 format?: string
9 maxHeight?: number
10 maxWidth?: number
11 }) {
12 const path = '/services/oembed'
13 const query = {
14 url: options.oembedUrl,
15 format: options.format,
16 maxheight: options.maxHeight,
17 maxwidth: options.maxWidth
18 }
19
20 return this.getRequest({
21 ...options,
22
23 path,
24 query,
25 implicitToken: false,
26 defaultExpectedStatus: HttpStatusCode.OK_200
27 })
28 }
29}
diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
deleted file mode 100644
index 5d40d35cb..000000000
--- a/shared/extra-utils/videos/streaming-playlists-command.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import { HttpStatusCode } from '@shared/models'
2import { unwrapBody, unwrapTextOrDecode, unwrapBodyOrDecodeToJSON } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class StreamingPlaylistsCommand extends AbstractCommand {
6
7 get (options: OverrideCommandOptions & {
8 url: string
9 }) {
10 return unwrapTextOrDecode(this.getRawRequest({
11 ...options,
12
13 url: options.url,
14 implicitToken: false,
15 defaultExpectedStatus: HttpStatusCode.OK_200
16 }))
17 }
18
19 getSegment (options: OverrideCommandOptions & {
20 url: string
21 range?: string
22 }) {
23 return unwrapBody<Buffer>(this.getRawRequest({
24 ...options,
25
26 url: options.url,
27 range: options.range,
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 }))
31 }
32
33 getSegmentSha256 (options: OverrideCommandOptions & {
34 url: string
35 }) {
36 return unwrapBodyOrDecodeToJSON<{ [ id: string ]: string }>(this.getRawRequest({
37 ...options,
38
39 url: options.url,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 }))
43 }
44}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
deleted file mode 100644
index 6671e3fa6..000000000
--- a/shared/extra-utils/videos/streaming-playlists.ts
+++ /dev/null
@@ -1,77 +0,0 @@
1import { expect } from 'chai'
2import { basename } from 'path'
3import { sha256 } from '@server/helpers/core-utils'
4import { removeFragmentedMP4Ext } from '@shared/core-utils'
5import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
6import { PeerTubeServer } from '../server'
7
8async function checkSegmentHash (options: {
9 server: PeerTubeServer
10 baseUrlPlaylist: string
11 baseUrlSegment: string
12 resolution: number
13 hlsPlaylist: VideoStreamingPlaylist
14}) {
15 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options
16 const command = server.streamingPlaylists
17
18 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
19 const videoName = basename(file.fileUrl)
20
21 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
22
23 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
24
25 const length = parseInt(matches[1], 10)
26 const offset = parseInt(matches[2], 10)
27 const range = `${offset}-${offset + length - 1}`
28
29 const segmentBody = await command.getSegment({
30 url: `${baseUrlSegment}/${videoName}`,
31 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
32 range: `bytes=${range}`
33 })
34
35 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
36 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
37}
38
39async function checkLiveSegmentHash (options: {
40 server: PeerTubeServer
41 baseUrlSegment: string
42 videoUUID: string
43 segmentName: string
44 hlsPlaylist: VideoStreamingPlaylist
45}) {
46 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
47 const command = server.streamingPlaylists
48
49 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
50 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
51
52 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
53}
54
55async function checkResolutionsInMasterPlaylist (options: {
56 server: PeerTubeServer
57 playlistUrl: string
58 resolutions: number[]
59}) {
60 const { server, playlistUrl, resolutions } = options
61
62 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
63
64 for (const resolution of resolutions) {
65 const reg = new RegExp(
66 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
67 )
68
69 expect(masterPlaylist).to.match(reg)
70 }
71}
72
73export {
74 checkSegmentHash,
75 checkLiveSegmentHash,
76 checkResolutionsInMasterPlaylist
77}
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
deleted file mode 100644
index 7ec9c3647..000000000
--- a/shared/extra-utils/videos/videos-command.ts
+++ /dev/null
@@ -1,687 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { createReadStream, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got'
6import { omit } from 'lodash'
7import validator from 'validator'
8import { buildUUID } from '@server/helpers/uuid'
9import { loadLanguages } from '@server/initializers/constants'
10import { pick } from '@shared/core-utils'
11import {
12 HttpStatusCode,
13 ResultList,
14 UserVideoRateType,
15 Video,
16 VideoCreate,
17 VideoCreateResult,
18 VideoDetails,
19 VideoFileMetadata,
20 VideoPrivacy,
21 VideosCommonQuery,
22 VideoTranscodingCreate
23} from '@shared/models'
24import { buildAbsoluteFixturePath, wait } from '../miscs'
25import { unwrapBody } from '../requests'
26import { PeerTubeServer, waitJobs } from '../server'
27import { AbstractCommand, OverrideCommandOptions } from '../shared'
28
29export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
30 fixture?: string
31 thumbnailfile?: string
32 previewfile?: string
33}
34
35export class VideosCommand extends AbstractCommand {
36
37 constructor (server: PeerTubeServer) {
38 super(server)
39
40 loadLanguages()
41 }
42
43 getCategories (options: OverrideCommandOptions = {}) {
44 const path = '/api/v1/videos/categories'
45
46 return this.getRequestBody<{ [id: number]: string }>({
47 ...options,
48 path,
49
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 getLicences (options: OverrideCommandOptions = {}) {
56 const path = '/api/v1/videos/licences'
57
58 return this.getRequestBody<{ [id: number]: string }>({
59 ...options,
60 path,
61
62 implicitToken: false,
63 defaultExpectedStatus: HttpStatusCode.OK_200
64 })
65 }
66
67 getLanguages (options: OverrideCommandOptions = {}) {
68 const path = '/api/v1/videos/languages'
69
70 return this.getRequestBody<{ [id: string]: string }>({
71 ...options,
72 path,
73
74 implicitToken: false,
75 defaultExpectedStatus: HttpStatusCode.OK_200
76 })
77 }
78
79 getPrivacies (options: OverrideCommandOptions = {}) {
80 const path = '/api/v1/videos/privacies'
81
82 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
83 ...options,
84 path,
85
86 implicitToken: false,
87 defaultExpectedStatus: HttpStatusCode.OK_200
88 })
89 }
90
91 // ---------------------------------------------------------------------------
92
93 getDescription (options: OverrideCommandOptions & {
94 descriptionPath: string
95 }) {
96 return this.getRequestBody<{ description: string }>({
97 ...options,
98 path: options.descriptionPath,
99
100 implicitToken: false,
101 defaultExpectedStatus: HttpStatusCode.OK_200
102 })
103 }
104
105 getFileMetadata (options: OverrideCommandOptions & {
106 url: string
107 }) {
108 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
109 ...options,
110
111 url: options.url,
112 implicitToken: false,
113 defaultExpectedStatus: HttpStatusCode.OK_200
114 }))
115 }
116
117 // ---------------------------------------------------------------------------
118
119 view (options: OverrideCommandOptions & {
120 id: number | string
121 xForwardedFor?: string
122 }) {
123 const { id, xForwardedFor } = options
124 const path = '/api/v1/videos/' + id + '/views'
125
126 return this.postBodyRequest({
127 ...options,
128
129 path,
130 xForwardedFor,
131 implicitToken: false,
132 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
133 })
134 }
135
136 rate (options: OverrideCommandOptions & {
137 id: number | string
138 rating: UserVideoRateType
139 }) {
140 const { id, rating } = options
141 const path = '/api/v1/videos/' + id + '/rate'
142
143 return this.putBodyRequest({
144 ...options,
145
146 path,
147 fields: { rating },
148 implicitToken: true,
149 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
150 })
151 }
152
153 // ---------------------------------------------------------------------------
154
155 get (options: OverrideCommandOptions & {
156 id: number | string
157 }) {
158 const path = '/api/v1/videos/' + options.id
159
160 return this.getRequestBody<VideoDetails>({
161 ...options,
162
163 path,
164 implicitToken: false,
165 defaultExpectedStatus: HttpStatusCode.OK_200
166 })
167 }
168
169 getWithToken (options: OverrideCommandOptions & {
170 id: number | string
171 }) {
172 return this.get({
173 ...options,
174
175 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
176 })
177 }
178
179 async getId (options: OverrideCommandOptions & {
180 uuid: number | string
181 }) {
182 const { uuid } = options
183
184 if (validator.isUUID('' + uuid) === false) return uuid as number
185
186 const { id } = await this.get({ ...options, id: uuid })
187
188 return id
189 }
190
191 async listFiles (options: OverrideCommandOptions & {
192 id: number | string
193 }) {
194 const video = await this.get(options)
195
196 const files = video.files || []
197 const hlsFiles = video.streamingPlaylists[0]?.files || []
198
199 return files.concat(hlsFiles)
200 }
201
202 // ---------------------------------------------------------------------------
203
204 listMyVideos (options: OverrideCommandOptions & {
205 start?: number
206 count?: number
207 sort?: string
208 search?: string
209 isLive?: boolean
210 channelId?: number
211 } = {}) {
212 const path = '/api/v1/users/me/videos'
213
214 return this.getRequestBody<ResultList<Video>>({
215 ...options,
216
217 path,
218 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
219 implicitToken: true,
220 defaultExpectedStatus: HttpStatusCode.OK_200
221 })
222 }
223
224 // ---------------------------------------------------------------------------
225
226 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
227 const path = '/api/v1/videos'
228
229 const query = this.buildListQuery(options)
230
231 return this.getRequestBody<ResultList<Video>>({
232 ...options,
233
234 path,
235 query: { sort: 'name', ...query },
236 implicitToken: false,
237 defaultExpectedStatus: HttpStatusCode.OK_200
238 })
239 }
240
241 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
242 return this.list({
243 ...options,
244
245 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
246 })
247 }
248
249 listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
250 handle: string
251 }) {
252 const { handle, search } = options
253 const path = '/api/v1/accounts/' + handle + '/videos'
254
255 return this.getRequestBody<ResultList<Video>>({
256 ...options,
257
258 path,
259 query: { search, ...this.buildListQuery(options) },
260 implicitToken: true,
261 defaultExpectedStatus: HttpStatusCode.OK_200
262 })
263 }
264
265 listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
266 handle: string
267 }) {
268 const { handle } = options
269 const path = '/api/v1/video-channels/' + handle + '/videos'
270
271 return this.getRequestBody<ResultList<Video>>({
272 ...options,
273
274 path,
275 query: this.buildListQuery(options),
276 implicitToken: true,
277 defaultExpectedStatus: HttpStatusCode.OK_200
278 })
279 }
280
281 // ---------------------------------------------------------------------------
282
283 async find (options: OverrideCommandOptions & {
284 name: string
285 }) {
286 const { data } = await this.list(options)
287
288 return data.find(v => v.name === options.name)
289 }
290
291 // ---------------------------------------------------------------------------
292
293 update (options: OverrideCommandOptions & {
294 id: number | string
295 attributes?: VideoEdit
296 }) {
297 const { id, attributes = {} } = options
298 const path = '/api/v1/videos/' + id
299
300 // Upload request
301 if (attributes.thumbnailfile || attributes.previewfile) {
302 const attaches: any = {}
303 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
304 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
305
306 return this.putUploadRequest({
307 ...options,
308
309 path,
310 fields: options.attributes,
311 attaches: {
312 thumbnailfile: attributes.thumbnailfile,
313 previewfile: attributes.previewfile
314 },
315 implicitToken: true,
316 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
317 })
318 }
319
320 return this.putBodyRequest({
321 ...options,
322
323 path,
324 fields: options.attributes,
325 implicitToken: true,
326 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
327 })
328 }
329
330 remove (options: OverrideCommandOptions & {
331 id: number | string
332 }) {
333 const path = '/api/v1/videos/' + options.id
334
335 return unwrapBody(this.deleteRequest({
336 ...options,
337
338 path,
339 implicitToken: true,
340 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
341 }))
342 }
343
344 async removeAll () {
345 const { data } = await this.list()
346
347 for (const v of data) {
348 await this.remove({ id: v.id })
349 }
350 }
351
352 // ---------------------------------------------------------------------------
353
354 async upload (options: OverrideCommandOptions & {
355 attributes?: VideoEdit
356 mode?: 'legacy' | 'resumable' // default legacy
357 } = {}) {
358 const { mode = 'legacy' } = options
359 let defaultChannelId = 1
360
361 try {
362 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
363 defaultChannelId = videoChannels[0].id
364 } catch (e) { /* empty */ }
365
366 // Override default attributes
367 const attributes = {
368 name: 'my super video',
369 category: 5,
370 licence: 4,
371 language: 'zh',
372 channelId: defaultChannelId,
373 nsfw: true,
374 waitTranscoding: false,
375 description: 'my super description',
376 support: 'my super support text',
377 tags: [ 'tag' ],
378 privacy: VideoPrivacy.PUBLIC,
379 commentsEnabled: true,
380 downloadEnabled: true,
381 fixture: 'video_short.webm',
382
383 ...options.attributes
384 }
385
386 const created = mode === 'legacy'
387 ? await this.buildLegacyUpload({ ...options, attributes })
388 : await this.buildResumeUpload({ ...options, attributes })
389
390 // Wait torrent generation
391 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
392 if (expectedStatus === HttpStatusCode.OK_200) {
393 let video: VideoDetails
394
395 do {
396 video = await this.getWithToken({ ...options, id: created.uuid })
397
398 await wait(50)
399 } while (!video.files[0].torrentUrl)
400 }
401
402 return created
403 }
404
405 async buildLegacyUpload (options: OverrideCommandOptions & {
406 attributes: VideoEdit
407 }): Promise<VideoCreateResult> {
408 const path = '/api/v1/videos/upload'
409
410 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
411 ...options,
412
413 path,
414 fields: this.buildUploadFields(options.attributes),
415 attaches: this.buildUploadAttaches(options.attributes),
416 implicitToken: true,
417 defaultExpectedStatus: HttpStatusCode.OK_200
418 })).then(body => body.video || body as any)
419 }
420
421 async buildResumeUpload (options: OverrideCommandOptions & {
422 attributes: VideoEdit
423 }): Promise<VideoCreateResult> {
424 const { attributes, expectedStatus } = options
425
426 let size = 0
427 let videoFilePath: string
428 let mimetype = 'video/mp4'
429
430 if (attributes.fixture) {
431 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
432 size = (await stat(videoFilePath)).size
433
434 if (videoFilePath.endsWith('.mkv')) {
435 mimetype = 'video/x-matroska'
436 } else if (videoFilePath.endsWith('.webm')) {
437 mimetype = 'video/webm'
438 }
439 }
440
441 // Do not check status automatically, we'll check it manually
442 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
443 const initStatus = initializeSessionRes.status
444
445 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
446 const locationHeader = initializeSessionRes.header['location']
447 expect(locationHeader).to.not.be.undefined
448
449 const pathUploadId = locationHeader.split('?')[1]
450
451 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
452
453 if (result.statusCode === HttpStatusCode.OK_200) {
454 await this.endResumableUpload({ ...options, expectedStatus: HttpStatusCode.NO_CONTENT_204, pathUploadId })
455 }
456
457 return result.body?.video || result.body as any
458 }
459
460 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
461 ? HttpStatusCode.CREATED_201
462 : expectedStatus
463
464 expect(initStatus).to.equal(expectedInitStatus)
465
466 return initializeSessionRes.body.video || initializeSessionRes.body
467 }
468
469 async prepareResumableUpload (options: OverrideCommandOptions & {
470 attributes: VideoEdit
471 size: number
472 mimetype: string
473
474 originalName?: string
475 lastModified?: number
476 }) {
477 const { attributes, originalName, lastModified, size, mimetype } = options
478
479 const path = '/api/v1/videos/upload-resumable'
480
481 return this.postUploadRequest({
482 ...options,
483
484 path,
485 headers: {
486 'X-Upload-Content-Type': mimetype,
487 'X-Upload-Content-Length': size.toString()
488 },
489 fields: {
490 filename: attributes.fixture,
491 originalName,
492 lastModified,
493
494 ...this.buildUploadFields(options.attributes)
495 },
496
497 // Fixture will be sent later
498 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
499 implicitToken: true,
500
501 defaultExpectedStatus: null
502 })
503 }
504
505 sendResumableChunks (options: OverrideCommandOptions & {
506 pathUploadId: string
507 videoFilePath: string
508 size: number
509 contentLength?: number
510 contentRangeBuilder?: (start: number, chunk: any) => string
511 }) {
512 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
513
514 const path = '/api/v1/videos/upload-resumable'
515 let start = 0
516
517 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
518 const url = this.server.url
519
520 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
521 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
522 readable.on('data', async function onData (chunk) {
523 readable.pause()
524
525 const headers = {
526 'Authorization': 'Bearer ' + token,
527 'Content-Type': 'application/octet-stream',
528 'Content-Range': contentRangeBuilder
529 ? contentRangeBuilder(start, chunk)
530 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
531 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
532 }
533
534 const res = await got<{ video: VideoCreateResult }>({
535 url,
536 method: 'put',
537 headers,
538 path: path + '?' + pathUploadId,
539 body: chunk,
540 responseType: 'json',
541 throwHttpErrors: false
542 })
543
544 start += chunk.length
545
546 if (res.statusCode === expectedStatus) {
547 return resolve(res)
548 }
549
550 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
551 readable.off('data', onData)
552 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
553 }
554
555 readable.resume()
556 })
557 })
558 }
559
560 endResumableUpload (options: OverrideCommandOptions & {
561 pathUploadId: string
562 }) {
563 return this.deleteRequest({
564 ...options,
565
566 path: '/api/v1/videos/upload-resumable',
567 rawQuery: options.pathUploadId,
568 implicitToken: true,
569 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
570 })
571 }
572
573 quickUpload (options: OverrideCommandOptions & {
574 name: string
575 nsfw?: boolean
576 privacy?: VideoPrivacy
577 fixture?: string
578 }) {
579 const attributes: VideoEdit = { name: options.name }
580 if (options.nsfw) attributes.nsfw = options.nsfw
581 if (options.privacy) attributes.privacy = options.privacy
582 if (options.fixture) attributes.fixture = options.fixture
583
584 return this.upload({ ...options, attributes })
585 }
586
587 async randomUpload (options: OverrideCommandOptions & {
588 wait?: boolean // default true
589 additionalParams?: VideoEdit & { prefixName?: string }
590 } = {}) {
591 const { wait = true, additionalParams } = options
592 const prefixName = additionalParams?.prefixName || ''
593 const name = prefixName + buildUUID()
594
595 const attributes = { name, ...additionalParams }
596
597 const result = await this.upload({ ...options, attributes })
598
599 if (wait) await waitJobs([ this.server ])
600
601 return { ...result, name }
602 }
603
604 // ---------------------------------------------------------------------------
605
606 removeHLSFiles (options: OverrideCommandOptions & {
607 videoId: number | string
608 }) {
609 const path = '/api/v1/videos/' + options.videoId + '/hls'
610
611 return this.deleteRequest({
612 ...options,
613
614 path,
615 implicitToken: true,
616 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
617 })
618 }
619
620 removeWebTorrentFiles (options: OverrideCommandOptions & {
621 videoId: number | string
622 }) {
623 const path = '/api/v1/videos/' + options.videoId + '/webtorrent'
624
625 return this.deleteRequest({
626 ...options,
627
628 path,
629 implicitToken: true,
630 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
631 })
632 }
633
634 runTranscoding (options: OverrideCommandOptions & {
635 videoId: number | string
636 transcodingType: 'hls' | 'webtorrent'
637 }) {
638 const path = '/api/v1/videos/' + options.videoId + '/transcoding'
639
640 const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
641
642 return this.postBodyRequest({
643 ...options,
644
645 path,
646 fields,
647 implicitToken: true,
648 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
649 })
650 }
651
652 // ---------------------------------------------------------------------------
653
654 private buildListQuery (options: VideosCommonQuery) {
655 return pick(options, [
656 'start',
657 'count',
658 'sort',
659 'nsfw',
660 'isLive',
661 'categoryOneOf',
662 'licenceOneOf',
663 'languageOneOf',
664 'tagsOneOf',
665 'tagsAllOf',
666 'isLocal',
667 'include',
668 'skipCount'
669 ])
670 }
671
672 private buildUploadFields (attributes: VideoEdit) {
673 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
674 }
675
676 private buildUploadAttaches (attributes: VideoEdit) {
677 const attaches: { [ name: string ]: string } = {}
678
679 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
680 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
681 }
682
683 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
684
685 return attaches
686 }
687}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
deleted file mode 100644
index 4d2784dde..000000000
--- a/shared/extra-utils/videos/videos.ts
+++ /dev/null
@@ -1,253 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { basename, join } from 'path'
6import { getLowercaseExtension } from '@server/helpers/core-utils'
7import { uuidRegex } from '@shared/core-utils'
8import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
9import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
10import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
11import { makeRawRequest } from '../requests/requests'
12import { waitJobs } from '../server'
13import { PeerTubeServer } from '../server/server'
14import { VideoEdit } from './videos-command'
15
16async function checkVideoFilesWereRemoved (options: {
17 server: PeerTubeServer
18 video: VideoDetails
19 captions?: VideoCaption[]
20 onlyVideoFiles?: boolean // default false
21}) {
22 const { video, server, captions = [], onlyVideoFiles = false } = options
23
24 const webtorrentFiles = video.files || []
25 const hlsFiles = video.streamingPlaylists[0]?.files || []
26
27 const thumbnailName = basename(video.thumbnailPath)
28 const previewName = basename(video.previewPath)
29
30 const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
31
32 const captionNames = captions.map(c => basename(c.captionPath))
33
34 const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl))
35 const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
36
37 let directories: { [ directory: string ]: string[] } = {
38 videos: webtorrentFilenames,
39 redundancy: webtorrentFilenames,
40 [join('playlists', 'hls')]: hlsFilenames,
41 [join('redundancy', 'hls')]: hlsFilenames
42 }
43
44 if (onlyVideoFiles !== true) {
45 directories = {
46 ...directories,
47
48 thumbnails: [ thumbnailName ],
49 previews: [ previewName ],
50 torrents: torrentNames,
51 captions: captionNames
52 }
53 }
54
55 for (const directory of Object.keys(directories)) {
56 const directoryPath = server.servers.buildDirectory(directory)
57
58 const directoryExists = await pathExists(directoryPath)
59 if (directoryExists === false) continue
60
61 const existingFiles = await readdir(directoryPath)
62 for (const existingFile of existingFiles) {
63 for (const shouldNotExist of directories[directory]) {
64 expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
65 }
66 }
67 }
68}
69
70async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
71 for (const server of servers) {
72 server.store.videoDetails = await server.videos.get({ id: uuid })
73 }
74}
75
76function checkUploadVideoParam (
77 server: PeerTubeServer,
78 token: string,
79 attributes: Partial<VideoEdit>,
80 expectedStatus = HttpStatusCode.OK_200,
81 mode: 'legacy' | 'resumable' = 'legacy'
82) {
83 return mode === 'legacy'
84 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
85 : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
86}
87
88async function completeVideoCheck (
89 server: PeerTubeServer,
90 video: any,
91 attributes: {
92 name: string
93 category: number
94 licence: number
95 language: string
96 nsfw: boolean
97 commentsEnabled: boolean
98 downloadEnabled: boolean
99 description: string
100 publishedAt?: string
101 support: string
102 originallyPublishedAt?: string
103 account: {
104 name: string
105 host: string
106 }
107 isLocal: boolean
108 tags: string[]
109 privacy: number
110 likes?: number
111 dislikes?: number
112 duration: number
113 channel: {
114 displayName: string
115 name: string
116 description: string
117 isLocal: boolean
118 }
119 fixture: string
120 files: {
121 resolution: number
122 size: number
123 }[]
124 thumbnailfile?: string
125 previewfile?: string
126 }
127) {
128 if (!attributes.likes) attributes.likes = 0
129 if (!attributes.dislikes) attributes.dislikes = 0
130
131 const host = new URL(server.url).host
132 const originHost = attributes.account.host
133
134 expect(video.name).to.equal(attributes.name)
135 expect(video.category.id).to.equal(attributes.category)
136 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
137 expect(video.licence.id).to.equal(attributes.licence)
138 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
139 expect(video.language.id).to.equal(attributes.language)
140 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
141 expect(video.privacy.id).to.deep.equal(attributes.privacy)
142 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
143 expect(video.nsfw).to.equal(attributes.nsfw)
144 expect(video.description).to.equal(attributes.description)
145 expect(video.account.id).to.be.a('number')
146 expect(video.account.host).to.equal(attributes.account.host)
147 expect(video.account.name).to.equal(attributes.account.name)
148 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
149 expect(video.channel.name).to.equal(attributes.channel.name)
150 expect(video.likes).to.equal(attributes.likes)
151 expect(video.dislikes).to.equal(attributes.dislikes)
152 expect(video.isLocal).to.equal(attributes.isLocal)
153 expect(video.duration).to.equal(attributes.duration)
154 expect(video.url).to.contain(originHost)
155 expect(dateIsValid(video.createdAt)).to.be.true
156 expect(dateIsValid(video.publishedAt)).to.be.true
157 expect(dateIsValid(video.updatedAt)).to.be.true
158
159 if (attributes.publishedAt) {
160 expect(video.publishedAt).to.equal(attributes.publishedAt)
161 }
162
163 if (attributes.originallyPublishedAt) {
164 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
165 } else {
166 expect(video.originallyPublishedAt).to.be.null
167 }
168
169 const videoDetails = await server.videos.get({ id: video.uuid })
170
171 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
172 expect(videoDetails.tags).to.deep.equal(attributes.tags)
173 expect(videoDetails.account.name).to.equal(attributes.account.name)
174 expect(videoDetails.account.host).to.equal(attributes.account.host)
175 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
176 expect(video.channel.name).to.equal(attributes.channel.name)
177 expect(videoDetails.channel.host).to.equal(attributes.account.host)
178 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
179 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
180 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
181 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
182 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
183
184 for (const attributeFile of attributes.files) {
185 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
186 expect(file).not.to.be.undefined
187
188 let extension = getLowercaseExtension(attributes.fixture)
189 // Transcoding enabled: extension will always be .mp4
190 if (attributes.files.length > 1) extension = '.mp4'
191
192 expect(file.magnetUri).to.have.lengthOf.above(2)
193
194 expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
195 expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
196
197 expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
198 expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
199
200 await Promise.all([
201 makeRawRequest(file.torrentUrl, 200),
202 makeRawRequest(file.torrentDownloadUrl, 200),
203 makeRawRequest(file.metadataUrl, 200)
204 ])
205
206 expect(file.resolution.id).to.equal(attributeFile.resolution)
207 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
208
209 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
210 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
211 expect(
212 file.size,
213 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
214 ).to.be.above(minSize).and.below(maxSize)
215
216 const torrent = await webtorrentAdd(file.magnetUri, true)
217 expect(torrent.files).to.be.an('array')
218 expect(torrent.files.length).to.equal(1)
219 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
220 }
221
222 expect(videoDetails.thumbnailPath).to.exist
223 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
224
225 if (attributes.previewfile) {
226 expect(videoDetails.previewPath).to.exist
227 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
228 }
229}
230
231// serverNumber starts from 1
232async function uploadRandomVideoOnServers (
233 servers: PeerTubeServer[],
234 serverNumber: number,
235 additionalParams?: VideoEdit & { prefixName?: string }
236) {
237 const server = servers.find(s => s.serverNumber === serverNumber)
238 const res = await server.videos.randomUpload({ wait: false, additionalParams })
239
240 await waitJobs(servers)
241
242 return res
243}
244
245// ---------------------------------------------------------------------------
246
247export {
248 checkUploadVideoParam,
249 completeVideoCheck,
250 uploadRandomVideoOnServers,
251 checkVideoFilesWereRemoved,
252 saveVideoInServers
253}