aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/server-commands/videos
diff options
context:
space:
mode:
Diffstat (limited to 'shared/server-commands/videos')
-rw-r--r--shared/server-commands/videos/blacklist-command.ts75
-rw-r--r--shared/server-commands/videos/captions-command.ts65
-rw-r--r--shared/server-commands/videos/change-ownership-command.ts68
-rw-r--r--shared/server-commands/videos/channels-command.ts184
-rw-r--r--shared/server-commands/videos/channels.ts18
-rw-r--r--shared/server-commands/videos/comments-command.ts152
-rw-r--r--shared/server-commands/videos/history-command.ts58
-rw-r--r--shared/server-commands/videos/imports-command.ts47
-rw-r--r--shared/server-commands/videos/index.ts15
-rw-r--r--shared/server-commands/videos/live-command.ts155
-rw-r--r--shared/server-commands/videos/live.ts100
-rw-r--r--shared/server-commands/videos/playlists-command.ts280
-rw-r--r--shared/server-commands/videos/services-command.ts29
-rw-r--r--shared/server-commands/videos/streaming-playlists-command.ts44
-rw-r--r--shared/server-commands/videos/videos-command.ts678
15 files changed, 1968 insertions, 0 deletions
diff --git a/shared/server-commands/videos/blacklist-command.ts b/shared/server-commands/videos/blacklist-command.ts
new file mode 100644
index 000000000..47e23ebc8
--- /dev/null
+++ b/shared/server-commands/videos/blacklist-command.ts
@@ -0,0 +1,75 @@
1
2import { HttpStatusCode, ResultList, VideoBlacklist, VideoBlacklistType } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class BlacklistCommand extends AbstractCommand {
6
7 add (options: OverrideCommandOptions & {
8 videoId: number | string
9 reason?: string
10 unfederate?: boolean
11 }) {
12 const { videoId, reason, unfederate } = options
13 const path = '/api/v1/videos/' + videoId + '/blacklist'
14
15 return this.postBodyRequest({
16 ...options,
17
18 path,
19 fields: { reason, unfederate },
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
22 })
23 }
24
25 update (options: OverrideCommandOptions & {
26 videoId: number | string
27 reason?: string
28 }) {
29 const { videoId, reason } = options
30 const path = '/api/v1/videos/' + videoId + '/blacklist'
31
32 return this.putBodyRequest({
33 ...options,
34
35 path,
36 fields: { reason },
37 implicitToken: true,
38 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
39 })
40 }
41
42 remove (options: OverrideCommandOptions & {
43 videoId: number | string
44 }) {
45 const { videoId } = options
46 const path = '/api/v1/videos/' + videoId + '/blacklist'
47
48 return this.deleteRequest({
49 ...options,
50
51 path,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
54 })
55 }
56
57 list (options: OverrideCommandOptions & {
58 sort?: string
59 type?: VideoBlacklistType
60 } = {}) {
61 const { sort, type } = options
62 const path = '/api/v1/videos/blacklist/'
63
64 const query = { sort, type }
65
66 return this.getRequestBody<ResultList<VideoBlacklist>>({
67 ...options,
68
69 path,
70 query,
71 implicitToken: true,
72 defaultExpectedStatus: HttpStatusCode.OK_200
73 })
74 }
75}
diff --git a/shared/server-commands/videos/captions-command.ts b/shared/server-commands/videos/captions-command.ts
new file mode 100644
index 000000000..62bf9c5e6
--- /dev/null
+++ b/shared/server-commands/videos/captions-command.ts
@@ -0,0 +1,65 @@
1import { buildAbsoluteFixturePath } from '@shared/core-utils'
2import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models'
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/server-commands/videos/change-ownership-command.ts b/shared/server-commands/videos/change-ownership-command.ts
new file mode 100644
index 000000000..ad4c726ef
--- /dev/null
+++ b/shared/server-commands/videos/change-ownership-command.ts
@@ -0,0 +1,68 @@
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/server-commands/videos/channels-command.ts b/shared/server-commands/videos/channels-command.ts
new file mode 100644
index 000000000..8ab124658
--- /dev/null
+++ b/shared/server-commands/videos/channels-command.ts
@@ -0,0 +1,184 @@
1import { pick } from '@shared/core-utils'
2import {
3 ActorFollow,
4 HttpStatusCode,
5 ResultList,
6 VideoChannel,
7 VideoChannelCreate,
8 VideoChannelCreateResult,
9 VideoChannelUpdate
10} from '@shared/models'
11import { unwrapBody } from '../requests'
12import { AbstractCommand, OverrideCommandOptions } from '../shared'
13
14export class ChannelsCommand extends AbstractCommand {
15
16 list (options: OverrideCommandOptions & {
17 start?: number
18 count?: number
19 sort?: string
20 withStats?: boolean
21 } = {}) {
22 const path = '/api/v1/video-channels'
23
24 return this.getRequestBody<ResultList<VideoChannel>>({
25 ...options,
26
27 path,
28 query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
29 implicitToken: false,
30 defaultExpectedStatus: HttpStatusCode.OK_200
31 })
32 }
33
34 listByAccount (options: OverrideCommandOptions & {
35 accountName: string
36 start?: number
37 count?: number
38 sort?: string
39 withStats?: boolean
40 search?: string
41 }) {
42 const { accountName, sort = 'createdAt' } = options
43 const path = '/api/v1/accounts/' + accountName + '/video-channels'
44
45 return this.getRequestBody<ResultList<VideoChannel>>({
46 ...options,
47
48 path,
49 query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 async create (options: OverrideCommandOptions & {
56 attributes: Partial<VideoChannelCreate>
57 }) {
58 const path = '/api/v1/video-channels/'
59
60 // Default attributes
61 const defaultAttributes = {
62 displayName: 'my super video channel',
63 description: 'my super channel description',
64 support: 'my super channel support'
65 }
66 const attributes = { ...defaultAttributes, ...options.attributes }
67
68 const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
69 ...options,
70
71 path,
72 fields: attributes,
73 implicitToken: true,
74 defaultExpectedStatus: HttpStatusCode.OK_200
75 }))
76
77 return body.videoChannel
78 }
79
80 update (options: OverrideCommandOptions & {
81 channelName: string
82 attributes: VideoChannelUpdate
83 }) {
84 const { channelName, attributes } = options
85 const path = '/api/v1/video-channels/' + channelName
86
87 return this.putBodyRequest({
88 ...options,
89
90 path,
91 fields: attributes,
92 implicitToken: true,
93 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
94 })
95 }
96
97 delete (options: OverrideCommandOptions & {
98 channelName: string
99 }) {
100 const path = '/api/v1/video-channels/' + options.channelName
101
102 return this.deleteRequest({
103 ...options,
104
105 path,
106 implicitToken: true,
107 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
108 })
109 }
110
111 get (options: OverrideCommandOptions & {
112 channelName: string
113 }) {
114 const path = '/api/v1/video-channels/' + options.channelName
115
116 return this.getRequestBody<VideoChannel>({
117 ...options,
118
119 path,
120 implicitToken: false,
121 defaultExpectedStatus: HttpStatusCode.OK_200
122 })
123 }
124
125 updateImage (options: OverrideCommandOptions & {
126 fixture: string
127 channelName: string | number
128 type: 'avatar' | 'banner'
129 }) {
130 const { channelName, fixture, type } = options
131
132 const path = `/api/v1/video-channels/${channelName}/${type}/pick`
133
134 return this.updateImageRequest({
135 ...options,
136
137 path,
138 fixture,
139 fieldname: type + 'file',
140
141 implicitToken: true,
142 defaultExpectedStatus: HttpStatusCode.OK_200
143 })
144 }
145
146 deleteImage (options: OverrideCommandOptions & {
147 channelName: string | number
148 type: 'avatar' | 'banner'
149 }) {
150 const { channelName, type } = options
151
152 const path = `/api/v1/video-channels/${channelName}/${type}`
153
154 return this.deleteRequest({
155 ...options,
156
157 path,
158 implicitToken: true,
159 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
160 })
161 }
162
163 listFollowers (options: OverrideCommandOptions & {
164 channelName: string
165 start?: number
166 count?: number
167 sort?: string
168 search?: string
169 }) {
170 const { channelName, start, count, sort, search } = options
171 const path = '/api/v1/video-channels/' + channelName + '/followers'
172
173 const query = { start, count, sort, search }
174
175 return this.getRequestBody<ResultList<ActorFollow>>({
176 ...options,
177
178 path,
179 query,
180 implicitToken: true,
181 defaultExpectedStatus: HttpStatusCode.OK_200
182 })
183 }
184}
diff --git a/shared/server-commands/videos/channels.ts b/shared/server-commands/videos/channels.ts
new file mode 100644
index 000000000..756c47453
--- /dev/null
+++ b/shared/server-commands/videos/channels.ts
@@ -0,0 +1,18 @@
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/server-commands/videos/comments-command.ts b/shared/server-commands/videos/comments-command.ts
new file mode 100644
index 000000000..f0d163a07
--- /dev/null
+++ b/shared/server-commands/videos/comments-command.ts
@@ -0,0 +1,152 @@
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/server-commands/videos/history-command.ts b/shared/server-commands/videos/history-command.ts
new file mode 100644
index 000000000..13b7150c1
--- /dev/null
+++ b/shared/server-commands/videos/history-command.ts
@@ -0,0 +1,58 @@
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/server-commands/videos/imports-command.ts b/shared/server-commands/videos/imports-command.ts
new file mode 100644
index 000000000..e4944694d
--- /dev/null
+++ b/shared/server-commands/videos/imports-command.ts
@@ -0,0 +1,47 @@
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/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts
new file mode 100644
index 000000000..68a188b21
--- /dev/null
+++ b/shared/server-commands/videos/index.ts
@@ -0,0 +1,15 @@
1export * from './blacklist-command'
2export * from './captions-command'
3export * from './change-ownership-command'
4export * from './channels'
5export * from './channels-command'
6export * from './comments-command'
7export * from './history-command'
8export * from './imports-command'
9export * from './live-command'
10export * from './live'
11export * from './playlists-command'
12export * from './services-command'
13export * from './streaming-playlists-command'
14export * from './comments-command'
15export * from './videos-command'
diff --git a/shared/server-commands/videos/live-command.ts b/shared/server-commands/videos/live-command.ts
new file mode 100644
index 000000000..f7816eca0
--- /dev/null
+++ b/shared/server-commands/videos/live-command.ts
@@ -0,0 +1,155 @@
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 { wait } from '@shared/core-utils'
7import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
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/server-commands/videos/live.ts b/shared/server-commands/videos/live.ts
new file mode 100644
index 000000000..7a7faa911
--- /dev/null
+++ b/shared/server-commands/videos/live.ts
@@ -0,0 +1,100 @@
1import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
2import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
3import { PeerTubeServer } from '../server/server'
4
5function sendRTMPStream (options: {
6 rtmpBaseUrl: string
7 streamKey: string
8 fixtureName?: string // default video_short.mp4
9 copyCodecs?: boolean // default false
10}) {
11 const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options
12
13 const fixture = buildAbsoluteFixturePath(fixtureName)
14
15 const command = ffmpeg(fixture)
16 command.inputOption('-stream_loop -1')
17 command.inputOption('-re')
18
19 if (copyCodecs) {
20 command.outputOption('-c copy')
21 } else {
22 command.outputOption('-c:v libx264')
23 command.outputOption('-g 50')
24 command.outputOption('-keyint_min 2')
25 command.outputOption('-r 60')
26 }
27
28 command.outputOption('-f flv')
29
30 const rtmpUrl = rtmpBaseUrl + '/' + streamKey
31 command.output(rtmpUrl)
32
33 command.on('error', err => {
34 if (err?.message?.includes('Exiting normally')) return
35
36 if (process.env.DEBUG) console.error(err)
37 })
38
39 if (process.env.DEBUG) {
40 command.on('stderr', data => console.log(data))
41 }
42
43 command.run()
44
45 return command
46}
47
48function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
49 return new Promise<void>((res, rej) => {
50 command.on('error', err => {
51 return rej(err)
52 })
53
54 setTimeout(() => {
55 res()
56 }, successAfterMS)
57 })
58}
59
60async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
61 let error: Error
62
63 try {
64 await waitFfmpegUntilError(command, 35000)
65 } catch (err) {
66 error = err
67 }
68
69 await stopFfmpeg(command)
70
71 if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error')
72 if (!shouldHaveError && error) throw error
73}
74
75async function stopFfmpeg (command: FfmpegCommand) {
76 command.kill('SIGINT')
77
78 await wait(500)
79}
80
81async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
82 for (const server of servers) {
83 await server.live.waitUntilPublished({ videoId })
84 }
85}
86
87async function waitUntilLiveSavedOnAllServers (servers: PeerTubeServer[], videoId: string) {
88 for (const server of servers) {
89 await server.live.waitUntilSaved({ videoId })
90 }
91}
92
93export {
94 sendRTMPStream,
95 waitFfmpegUntilError,
96 testFfmpegStreamError,
97 stopFfmpeg,
98 waitUntilLivePublishedOnAllServers,
99 waitUntilLiveSavedOnAllServers
100}
diff --git a/shared/server-commands/videos/playlists-command.ts b/shared/server-commands/videos/playlists-command.ts
new file mode 100644
index 000000000..ce23900d3
--- /dev/null
+++ b/shared/server-commands/videos/playlists-command.ts
@@ -0,0 +1,280 @@
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/server-commands/videos/services-command.ts b/shared/server-commands/videos/services-command.ts
new file mode 100644
index 000000000..06760df42
--- /dev/null
+++ b/shared/server-commands/videos/services-command.ts
@@ -0,0 +1,29 @@
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/server-commands/videos/streaming-playlists-command.ts b/shared/server-commands/videos/streaming-playlists-command.ts
new file mode 100644
index 000000000..5d40d35cb
--- /dev/null
+++ b/shared/server-commands/videos/streaming-playlists-command.ts
@@ -0,0 +1,44 @@
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/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts
new file mode 100644
index 000000000..21753ddc4
--- /dev/null
+++ b/shared/server-commands/videos/videos-command.ts
@@ -0,0 +1,678 @@
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 { buildAbsoluteFixturePath, pick, wait } from '@shared/core-utils'
9import { buildUUID } from '@shared/extra-utils'
10import {
11 HttpStatusCode,
12 ResultList,
13 UserVideoRateType,
14 Video,
15 VideoCreate,
16 VideoCreateResult,
17 VideoDetails,
18 VideoFileMetadata,
19 VideoPrivacy,
20 VideosCommonQuery,
21 VideoTranscodingCreate
22} from '@shared/models'
23import { unwrapBody } from '../requests'
24import { waitJobs } from '../server'
25import { AbstractCommand, OverrideCommandOptions } from '../shared'
26
27export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
28 fixture?: string
29 thumbnailfile?: string
30 previewfile?: string
31}
32
33export class VideosCommand extends AbstractCommand {
34 getCategories (options: OverrideCommandOptions = {}) {
35 const path = '/api/v1/videos/categories'
36
37 return this.getRequestBody<{ [id: number]: string }>({
38 ...options,
39 path,
40
41 implicitToken: false,
42 defaultExpectedStatus: HttpStatusCode.OK_200
43 })
44 }
45
46 getLicences (options: OverrideCommandOptions = {}) {
47 const path = '/api/v1/videos/licences'
48
49 return this.getRequestBody<{ [id: number]: string }>({
50 ...options,
51 path,
52
53 implicitToken: false,
54 defaultExpectedStatus: HttpStatusCode.OK_200
55 })
56 }
57
58 getLanguages (options: OverrideCommandOptions = {}) {
59 const path = '/api/v1/videos/languages'
60
61 return this.getRequestBody<{ [id: string]: string }>({
62 ...options,
63 path,
64
65 implicitToken: false,
66 defaultExpectedStatus: HttpStatusCode.OK_200
67 })
68 }
69
70 getPrivacies (options: OverrideCommandOptions = {}) {
71 const path = '/api/v1/videos/privacies'
72
73 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
74 ...options,
75 path,
76
77 implicitToken: false,
78 defaultExpectedStatus: HttpStatusCode.OK_200
79 })
80 }
81
82 // ---------------------------------------------------------------------------
83
84 getDescription (options: OverrideCommandOptions & {
85 descriptionPath: string
86 }) {
87 return this.getRequestBody<{ description: string }>({
88 ...options,
89 path: options.descriptionPath,
90
91 implicitToken: false,
92 defaultExpectedStatus: HttpStatusCode.OK_200
93 })
94 }
95
96 getFileMetadata (options: OverrideCommandOptions & {
97 url: string
98 }) {
99 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
100 ...options,
101
102 url: options.url,
103 implicitToken: false,
104 defaultExpectedStatus: HttpStatusCode.OK_200
105 }))
106 }
107
108 // ---------------------------------------------------------------------------
109
110 view (options: OverrideCommandOptions & {
111 id: number | string
112 xForwardedFor?: string
113 }) {
114 const { id, xForwardedFor } = options
115 const path = '/api/v1/videos/' + id + '/views'
116
117 return this.postBodyRequest({
118 ...options,
119
120 path,
121 xForwardedFor,
122 implicitToken: false,
123 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
124 })
125 }
126
127 rate (options: OverrideCommandOptions & {
128 id: number | string
129 rating: UserVideoRateType
130 }) {
131 const { id, rating } = options
132 const path = '/api/v1/videos/' + id + '/rate'
133
134 return this.putBodyRequest({
135 ...options,
136
137 path,
138 fields: { rating },
139 implicitToken: true,
140 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
141 })
142 }
143
144 // ---------------------------------------------------------------------------
145
146 get (options: OverrideCommandOptions & {
147 id: number | string
148 }) {
149 const path = '/api/v1/videos/' + options.id
150
151 return this.getRequestBody<VideoDetails>({
152 ...options,
153
154 path,
155 implicitToken: false,
156 defaultExpectedStatus: HttpStatusCode.OK_200
157 })
158 }
159
160 getWithToken (options: OverrideCommandOptions & {
161 id: number | string
162 }) {
163 return this.get({
164 ...options,
165
166 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
167 })
168 }
169
170 async getId (options: OverrideCommandOptions & {
171 uuid: number | string
172 }) {
173 const { uuid } = options
174
175 if (validator.isUUID('' + uuid) === false) return uuid as number
176
177 const { id } = await this.get({ ...options, id: uuid })
178
179 return id
180 }
181
182 async listFiles (options: OverrideCommandOptions & {
183 id: number | string
184 }) {
185 const video = await this.get(options)
186
187 const files = video.files || []
188 const hlsFiles = video.streamingPlaylists[0]?.files || []
189
190 return files.concat(hlsFiles)
191 }
192
193 // ---------------------------------------------------------------------------
194
195 listMyVideos (options: OverrideCommandOptions & {
196 start?: number
197 count?: number
198 sort?: string
199 search?: string
200 isLive?: boolean
201 channelId?: number
202 } = {}) {
203 const path = '/api/v1/users/me/videos'
204
205 return this.getRequestBody<ResultList<Video>>({
206 ...options,
207
208 path,
209 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
210 implicitToken: true,
211 defaultExpectedStatus: HttpStatusCode.OK_200
212 })
213 }
214
215 // ---------------------------------------------------------------------------
216
217 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
218 const path = '/api/v1/videos'
219
220 const query = this.buildListQuery(options)
221
222 return this.getRequestBody<ResultList<Video>>({
223 ...options,
224
225 path,
226 query: { sort: 'name', ...query },
227 implicitToken: false,
228 defaultExpectedStatus: HttpStatusCode.OK_200
229 })
230 }
231
232 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
233 return this.list({
234 ...options,
235
236 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
237 })
238 }
239
240 listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
241 handle: string
242 }) {
243 const { handle, search } = options
244 const path = '/api/v1/accounts/' + handle + '/videos'
245
246 return this.getRequestBody<ResultList<Video>>({
247 ...options,
248
249 path,
250 query: { search, ...this.buildListQuery(options) },
251 implicitToken: true,
252 defaultExpectedStatus: HttpStatusCode.OK_200
253 })
254 }
255
256 listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
257 handle: string
258 }) {
259 const { handle } = options
260 const path = '/api/v1/video-channels/' + handle + '/videos'
261
262 return this.getRequestBody<ResultList<Video>>({
263 ...options,
264
265 path,
266 query: this.buildListQuery(options),
267 implicitToken: true,
268 defaultExpectedStatus: HttpStatusCode.OK_200
269 })
270 }
271
272 // ---------------------------------------------------------------------------
273
274 async find (options: OverrideCommandOptions & {
275 name: string
276 }) {
277 const { data } = await this.list(options)
278
279 return data.find(v => v.name === options.name)
280 }
281
282 // ---------------------------------------------------------------------------
283
284 update (options: OverrideCommandOptions & {
285 id: number | string
286 attributes?: VideoEdit
287 }) {
288 const { id, attributes = {} } = options
289 const path = '/api/v1/videos/' + id
290
291 // Upload request
292 if (attributes.thumbnailfile || attributes.previewfile) {
293 const attaches: any = {}
294 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
295 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
296
297 return this.putUploadRequest({
298 ...options,
299
300 path,
301 fields: options.attributes,
302 attaches: {
303 thumbnailfile: attributes.thumbnailfile,
304 previewfile: attributes.previewfile
305 },
306 implicitToken: true,
307 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
308 })
309 }
310
311 return this.putBodyRequest({
312 ...options,
313
314 path,
315 fields: options.attributes,
316 implicitToken: true,
317 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
318 })
319 }
320
321 remove (options: OverrideCommandOptions & {
322 id: number | string
323 }) {
324 const path = '/api/v1/videos/' + options.id
325
326 return unwrapBody(this.deleteRequest({
327 ...options,
328
329 path,
330 implicitToken: true,
331 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
332 }))
333 }
334
335 async removeAll () {
336 const { data } = await this.list()
337
338 for (const v of data) {
339 await this.remove({ id: v.id })
340 }
341 }
342
343 // ---------------------------------------------------------------------------
344
345 async upload (options: OverrideCommandOptions & {
346 attributes?: VideoEdit
347 mode?: 'legacy' | 'resumable' // default legacy
348 } = {}) {
349 const { mode = 'legacy' } = options
350 let defaultChannelId = 1
351
352 try {
353 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
354 defaultChannelId = videoChannels[0].id
355 } catch (e) { /* empty */ }
356
357 // Override default attributes
358 const attributes = {
359 name: 'my super video',
360 category: 5,
361 licence: 4,
362 language: 'zh',
363 channelId: defaultChannelId,
364 nsfw: true,
365 waitTranscoding: false,
366 description: 'my super description',
367 support: 'my super support text',
368 tags: [ 'tag' ],
369 privacy: VideoPrivacy.PUBLIC,
370 commentsEnabled: true,
371 downloadEnabled: true,
372 fixture: 'video_short.webm',
373
374 ...options.attributes
375 }
376
377 const created = mode === 'legacy'
378 ? await this.buildLegacyUpload({ ...options, attributes })
379 : await this.buildResumeUpload({ ...options, attributes })
380
381 // Wait torrent generation
382 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
383 if (expectedStatus === HttpStatusCode.OK_200) {
384 let video: VideoDetails
385
386 do {
387 video = await this.getWithToken({ ...options, id: created.uuid })
388
389 await wait(50)
390 } while (!video.files[0].torrentUrl)
391 }
392
393 return created
394 }
395
396 async buildLegacyUpload (options: OverrideCommandOptions & {
397 attributes: VideoEdit
398 }): Promise<VideoCreateResult> {
399 const path = '/api/v1/videos/upload'
400
401 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
402 ...options,
403
404 path,
405 fields: this.buildUploadFields(options.attributes),
406 attaches: this.buildUploadAttaches(options.attributes),
407 implicitToken: true,
408 defaultExpectedStatus: HttpStatusCode.OK_200
409 })).then(body => body.video || body as any)
410 }
411
412 async buildResumeUpload (options: OverrideCommandOptions & {
413 attributes: VideoEdit
414 }): Promise<VideoCreateResult> {
415 const { attributes, expectedStatus } = options
416
417 let size = 0
418 let videoFilePath: string
419 let mimetype = 'video/mp4'
420
421 if (attributes.fixture) {
422 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
423 size = (await stat(videoFilePath)).size
424
425 if (videoFilePath.endsWith('.mkv')) {
426 mimetype = 'video/x-matroska'
427 } else if (videoFilePath.endsWith('.webm')) {
428 mimetype = 'video/webm'
429 }
430 }
431
432 // Do not check status automatically, we'll check it manually
433 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
434 const initStatus = initializeSessionRes.status
435
436 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
437 const locationHeader = initializeSessionRes.header['location']
438 expect(locationHeader).to.not.be.undefined
439
440 const pathUploadId = locationHeader.split('?')[1]
441
442 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
443
444 if (result.statusCode === HttpStatusCode.OK_200) {
445 await this.endResumableUpload({ ...options, expectedStatus: HttpStatusCode.NO_CONTENT_204, pathUploadId })
446 }
447
448 return result.body?.video || result.body as any
449 }
450
451 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
452 ? HttpStatusCode.CREATED_201
453 : expectedStatus
454
455 expect(initStatus).to.equal(expectedInitStatus)
456
457 return initializeSessionRes.body.video || initializeSessionRes.body
458 }
459
460 async prepareResumableUpload (options: OverrideCommandOptions & {
461 attributes: VideoEdit
462 size: number
463 mimetype: string
464
465 originalName?: string
466 lastModified?: number
467 }) {
468 const { attributes, originalName, lastModified, size, mimetype } = options
469
470 const path = '/api/v1/videos/upload-resumable'
471
472 return this.postUploadRequest({
473 ...options,
474
475 path,
476 headers: {
477 'X-Upload-Content-Type': mimetype,
478 'X-Upload-Content-Length': size.toString()
479 },
480 fields: {
481 filename: attributes.fixture,
482 originalName,
483 lastModified,
484
485 ...this.buildUploadFields(options.attributes)
486 },
487
488 // Fixture will be sent later
489 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
490 implicitToken: true,
491
492 defaultExpectedStatus: null
493 })
494 }
495
496 sendResumableChunks (options: OverrideCommandOptions & {
497 pathUploadId: string
498 videoFilePath: string
499 size: number
500 contentLength?: number
501 contentRangeBuilder?: (start: number, chunk: any) => string
502 }) {
503 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
504
505 const path = '/api/v1/videos/upload-resumable'
506 let start = 0
507
508 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
509 const url = this.server.url
510
511 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
512 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
513 readable.on('data', async function onData (chunk) {
514 readable.pause()
515
516 const headers = {
517 'Authorization': 'Bearer ' + token,
518 'Content-Type': 'application/octet-stream',
519 'Content-Range': contentRangeBuilder
520 ? contentRangeBuilder(start, chunk)
521 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
522 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
523 }
524
525 const res = await got<{ video: VideoCreateResult }>({
526 url,
527 method: 'put',
528 headers,
529 path: path + '?' + pathUploadId,
530 body: chunk,
531 responseType: 'json',
532 throwHttpErrors: false
533 })
534
535 start += chunk.length
536
537 if (res.statusCode === expectedStatus) {
538 return resolve(res)
539 }
540
541 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
542 readable.off('data', onData)
543 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
544 }
545
546 readable.resume()
547 })
548 })
549 }
550
551 endResumableUpload (options: OverrideCommandOptions & {
552 pathUploadId: string
553 }) {
554 return this.deleteRequest({
555 ...options,
556
557 path: '/api/v1/videos/upload-resumable',
558 rawQuery: options.pathUploadId,
559 implicitToken: true,
560 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
561 })
562 }
563
564 quickUpload (options: OverrideCommandOptions & {
565 name: string
566 nsfw?: boolean
567 privacy?: VideoPrivacy
568 fixture?: string
569 }) {
570 const attributes: VideoEdit = { name: options.name }
571 if (options.nsfw) attributes.nsfw = options.nsfw
572 if (options.privacy) attributes.privacy = options.privacy
573 if (options.fixture) attributes.fixture = options.fixture
574
575 return this.upload({ ...options, attributes })
576 }
577
578 async randomUpload (options: OverrideCommandOptions & {
579 wait?: boolean // default true
580 additionalParams?: VideoEdit & { prefixName?: string }
581 } = {}) {
582 const { wait = true, additionalParams } = options
583 const prefixName = additionalParams?.prefixName || ''
584 const name = prefixName + buildUUID()
585
586 const attributes = { name, ...additionalParams }
587
588 const result = await this.upload({ ...options, attributes })
589
590 if (wait) await waitJobs([ this.server ])
591
592 return { ...result, name }
593 }
594
595 // ---------------------------------------------------------------------------
596
597 removeHLSFiles (options: OverrideCommandOptions & {
598 videoId: number | string
599 }) {
600 const path = '/api/v1/videos/' + options.videoId + '/hls'
601
602 return this.deleteRequest({
603 ...options,
604
605 path,
606 implicitToken: true,
607 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
608 })
609 }
610
611 removeWebTorrentFiles (options: OverrideCommandOptions & {
612 videoId: number | string
613 }) {
614 const path = '/api/v1/videos/' + options.videoId + '/webtorrent'
615
616 return this.deleteRequest({
617 ...options,
618
619 path,
620 implicitToken: true,
621 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
622 })
623 }
624
625 runTranscoding (options: OverrideCommandOptions & {
626 videoId: number | string
627 transcodingType: 'hls' | 'webtorrent'
628 }) {
629 const path = '/api/v1/videos/' + options.videoId + '/transcoding'
630
631 const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
632
633 return this.postBodyRequest({
634 ...options,
635
636 path,
637 fields,
638 implicitToken: true,
639 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
640 })
641 }
642
643 // ---------------------------------------------------------------------------
644
645 private buildListQuery (options: VideosCommonQuery) {
646 return pick(options, [
647 'start',
648 'count',
649 'sort',
650 'nsfw',
651 'isLive',
652 'categoryOneOf',
653 'licenceOneOf',
654 'languageOneOf',
655 'tagsOneOf',
656 'tagsAllOf',
657 'isLocal',
658 'include',
659 'skipCount'
660 ])
661 }
662
663 private buildUploadFields (attributes: VideoEdit) {
664 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
665 }
666
667 private buildUploadAttaches (attributes: VideoEdit) {
668 const attaches: { [ name: string ]: string } = {}
669
670 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
671 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
672 }
673
674 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
675
676 return attaches
677 }
678}