aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-15 10:02:54 +0200
committerChocobozzz <me@florianbigard.com>2021-07-20 15:27:18 +0200
commitd23dd9fbfc4d26026352c10f81d2795ceaf2908a (patch)
treeda82286d423c5e834a1ee2dcd5970076b8263cf1 /shared
parent7926c5f9b3ffcabb1ffb0dcfa5e48b8e0b88fbc0 (diff)
downloadPeerTube-d23dd9fbfc4d26026352c10f81d2795ceaf2908a.tar.gz
PeerTube-d23dd9fbfc4d26026352c10f81d2795ceaf2908a.tar.zst
PeerTube-d23dd9fbfc4d26026352c10f81d2795ceaf2908a.zip
Introduce videos command
Diffstat (limited to 'shared')
-rw-r--r--shared/extra-utils/miscs/webtorrent.ts16
-rw-r--r--shared/extra-utils/requests/requests.ts25
-rw-r--r--shared/extra-utils/server/servers.ts5
-rw-r--r--shared/extra-utils/shared/abstract-command.ts41
-rw-r--r--shared/extra-utils/videos/index.ts1
-rw-r--r--shared/extra-utils/videos/live-command.ts7
-rw-r--r--shared/extra-utils/videos/playlists-command.ts17
-rw-r--r--shared/extra-utils/videos/videos-command.ts583
-rw-r--r--shared/extra-utils/videos/videos.ts714
-rw-r--r--shared/models/search/videos-common-query.model.ts2
-rw-r--r--shared/models/videos/video-update.model.ts1
11 files changed, 682 insertions, 730 deletions
diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts
index 82548946d..63e648309 100644
--- a/shared/extra-utils/miscs/webtorrent.ts
+++ b/shared/extra-utils/miscs/webtorrent.ts
@@ -1,4 +1,8 @@
1import { readFile } from 'fs-extra'
2import * as parseTorrent from 'parse-torrent'
3import { join } from 'path'
1import * as WebTorrent from 'webtorrent' 4import * as WebTorrent from 'webtorrent'
5import { ServerInfo } from '../server'
2 6
3let webtorrent: WebTorrent.Instance 7let webtorrent: WebTorrent.Instance
4 8
@@ -11,6 +15,16 @@ function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
11 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res)) 15 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
12} 16}
13 17
18async function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
19 const torrentName = videoUUID + '-' + resolution + '.torrent'
20 const torrentPath = server.serversCommand.buildDirectory(join('torrents', torrentName))
21
22 const data = await readFile(torrentPath)
23
24 return parseTorrent(data)
25}
26
14export { 27export {
15 webtorrentAdd 28 webtorrentAdd,
29 parseTorrentVideo
16} 30}
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
index c5ee63e05..3f1ac6650 100644
--- a/shared/extra-utils/requests/requests.ts
+++ b/shared/extra-utils/requests/requests.ts
@@ -67,11 +67,17 @@ function makeUploadRequest (options: {
67 method?: 'POST' | 'PUT' 67 method?: 'POST' | 'PUT'
68 path: string 68 path: string
69 token?: string 69 token?: string
70
70 fields: { [ fieldName: string ]: any } 71 fields: { [ fieldName: string ]: any }
71 attaches?: { [ attachName: string ]: any | any[] } 72 attaches?: { [ attachName: string ]: any | any[] }
73
74 headers?: { [ name: string ]: string }
75
72 statusCodeExpected?: HttpStatusCode 76 statusCodeExpected?: HttpStatusCode
73}) { 77}) {
74 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 78 if (options.statusCodeExpected === undefined) {
79 options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
80 }
75 81
76 let req: request.Test 82 let req: request.Test
77 if (options.method === 'PUT') { 83 if (options.method === 'PUT') {
@@ -84,6 +90,10 @@ function makeUploadRequest (options: {
84 90
85 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 91 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
86 92
93 Object.keys(options.headers || {}).forEach(name => {
94 req.set(name, options.headers[name])
95 })
96
87 Object.keys(options.fields).forEach(field => { 97 Object.keys(options.fields).forEach(field => {
88 const value = options.fields[field] 98 const value = options.fields[field]
89 99
@@ -107,7 +117,11 @@ function makeUploadRequest (options: {
107 } 117 }
108 }) 118 })
109 119
110 return req.expect(options.statusCodeExpected) 120 if (options.statusCodeExpected) {
121 req.expect(options.statusCodeExpected)
122 }
123
124 return req
111} 125}
112 126
113function makePostBodyRequest (options: { 127function makePostBodyRequest (options: {
@@ -115,7 +129,9 @@ function makePostBodyRequest (options: {
115 path: string 129 path: string
116 token?: string 130 token?: string
117 fields?: { [ fieldName: string ]: any } 131 fields?: { [ fieldName: string ]: any }
132 headers?: { [ name: string ]: string }
118 type?: string 133 type?: string
134 xForwardedFor?: string
119 statusCodeExpected?: HttpStatusCode 135 statusCodeExpected?: HttpStatusCode
120}) { 136}) {
121 if (!options.fields) options.fields = {} 137 if (!options.fields) options.fields = {}
@@ -126,8 +142,13 @@ function makePostBodyRequest (options: {
126 .set('Accept', 'application/json') 142 .set('Accept', 'application/json')
127 143
128 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 144 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
145 if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
129 if (options.type) req.type(options.type) 146 if (options.type) req.type(options.type)
130 147
148 Object.keys(options.headers || {}).forEach(name => {
149 req.set(name, options.headers[name])
150 })
151
131 return req.send(options.fields) 152 return req.send(options.fields)
132 .expect(options.statusCodeExpected) 153 .expect(options.statusCodeExpected)
133} 154}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index b6d597c5d..fda5c3d6d 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -27,7 +27,8 @@ import {
27 LiveCommand, 27 LiveCommand,
28 PlaylistsCommand, 28 PlaylistsCommand,
29 ServicesCommand, 29 ServicesCommand,
30 StreamingPlaylistsCommand 30 StreamingPlaylistsCommand,
31 VideosCommand
31} from '../videos' 32} from '../videos'
32import { CommentsCommand } from '../videos/comments-command' 33import { CommentsCommand } from '../videos/comments-command'
33import { ConfigCommand } from './config-command' 34import { ConfigCommand } from './config-command'
@@ -128,6 +129,7 @@ interface ServerInfo {
128 serversCommand?: ServersCommand 129 serversCommand?: ServersCommand
129 loginCommand?: LoginCommand 130 loginCommand?: LoginCommand
130 usersCommand?: UsersCommand 131 usersCommand?: UsersCommand
132 videosCommand?: VideosCommand
131} 133}
132 134
133function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) { 135function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
@@ -361,6 +363,7 @@ function assignCommands (server: ServerInfo) {
361 server.serversCommand = new ServersCommand(server) 363 server.serversCommand = new ServersCommand(server)
362 server.loginCommand = new LoginCommand(server) 364 server.loginCommand = new LoginCommand(server)
363 server.usersCommand = new UsersCommand(server) 365 server.usersCommand = new UsersCommand(server)
366 server.videosCommand = new VideosCommand(server)
364} 367}
365 368
366async function reRunServer (server: ServerInfo, configOverride?: any) { 369async function reRunServer (server: ServerInfo, configOverride?: any) {
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
index af9ecd926..5fddcf639 100644
--- a/shared/extra-utils/shared/abstract-command.ts
+++ b/shared/extra-utils/shared/abstract-command.ts
@@ -1,5 +1,4 @@
1import { isAbsolute, join } from 'path' 1import { isAbsolute, join } from 'path'
2import { HttpStatusCode } from '@shared/core-utils'
3import { root } from '../miscs/tests' 2import { root } from '../miscs/tests'
4import { 3import {
5 makeDeleteRequest, 4 makeDeleteRequest,
@@ -38,22 +37,12 @@ interface InternalGetCommandOptions extends InternalCommonCommandOptions {
38 37
39abstract class AbstractCommand { 38abstract class AbstractCommand {
40 39
41 private expectedStatus: HttpStatusCode
42
43 constructor ( 40 constructor (
44 protected server: ServerInfo 41 protected server: ServerInfo
45 ) { 42 ) {
46 43
47 } 44 }
48 45
49 setServer (server: ServerInfo) {
50 this.server = server
51 }
52
53 setExpectedStatus (status: HttpStatusCode) {
54 this.expectedStatus = status
55 }
56
57 protected getRequestBody <T> (options: InternalGetCommandOptions) { 46 protected getRequestBody <T> (options: InternalGetCommandOptions) {
58 return unwrapBody<T>(this.getRequest(options)) 47 return unwrapBody<T>(this.getRequest(options))
59 } 48 }
@@ -111,43 +100,51 @@ abstract class AbstractCommand {
111 100
112 protected postBodyRequest (options: InternalCommonCommandOptions & { 101 protected postBodyRequest (options: InternalCommonCommandOptions & {
113 fields?: { [ fieldName: string ]: any } 102 fields?: { [ fieldName: string ]: any }
103 headers?: { [ name: string ]: string }
114 type?: string 104 type?: string
105 xForwardedFor?: string
115 }) { 106 }) {
116 const { type, fields } = options 107 const { type, fields, xForwardedFor, headers } = options
117 108
118 return makePostBodyRequest({ 109 return makePostBodyRequest({
119 ...this.buildCommonRequestOptions(options), 110 ...this.buildCommonRequestOptions(options),
120 111
121 fields, 112 fields,
122 type 113 xForwardedFor,
114 type,
115 headers
123 }) 116 })
124 } 117 }
125 118
126 protected postUploadRequest (options: InternalCommonCommandOptions & { 119 protected postUploadRequest (options: InternalCommonCommandOptions & {
127 fields?: { [ fieldName: string ]: any } 120 fields?: { [ fieldName: string ]: any }
128 attaches?: any 121 attaches?: { [ fieldName: string ]: any }
122 headers?: { [ name: string ]: string }
129 }) { 123 }) {
130 const { fields, attaches } = options 124 const { fields, attaches, headers } = options
131 125
132 return makeUploadRequest({ 126 return makeUploadRequest({
133 ...this.buildCommonRequestOptions(options), 127 ...this.buildCommonRequestOptions(options),
134 128
135 method: 'POST', 129 method: 'POST',
136 fields, 130 fields,
137 attaches 131 attaches,
132 headers
138 }) 133 })
139 } 134 }
140 135
141 protected putUploadRequest (options: InternalCommonCommandOptions & { 136 protected putUploadRequest (options: InternalCommonCommandOptions & {
142 fields?: { [ fieldName: string ]: any } 137 fields?: { [ fieldName: string ]: any }
143 attaches?: any 138 attaches?: { [ fieldName: string ]: any }
139 headers?: { [ name: string ]: string }
144 }) { 140 }) {
145 const { fields, attaches } = options 141 const { fields, attaches, headers } = options
146 142
147 return makeUploadRequest({ 143 return makeUploadRequest({
148 ...this.buildCommonRequestOptions(options), 144 ...this.buildCommonRequestOptions(options),
149 145
150 method: 'PUT', 146 method: 'PUT',
147 headers,
151 fields, 148 fields,
152 attaches 149 attaches
153 }) 150 })
@@ -172,7 +169,7 @@ abstract class AbstractCommand {
172 }) 169 })
173 } 170 }
174 171
175 private buildCommonRequestOptions (options: InternalCommonCommandOptions) { 172 protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
176 const { url, path } = options 173 const { url, path } = options
177 174
178 return { 175 return {
@@ -184,7 +181,7 @@ abstract class AbstractCommand {
184 } 181 }
185 } 182 }
186 183
187 private buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) { 184 protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
188 const { token } = options 185 const { token } = options
189 186
190 const fallbackToken = options.implicitToken 187 const fallbackToken = options.implicitToken
@@ -194,10 +191,10 @@ abstract class AbstractCommand {
194 return token !== undefined ? token : fallbackToken 191 return token !== undefined ? token : fallbackToken
195 } 192 }
196 193
197 private buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) { 194 protected buildStatusCodeExpected (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
198 const { expectedStatus, defaultExpectedStatus } = options 195 const { expectedStatus, defaultExpectedStatus } = options
199 196
200 return expectedStatus ?? this.expectedStatus ?? defaultExpectedStatus 197 return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
201 } 198 }
202} 199}
203 200
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
index 652d82842..26e663f46 100644
--- a/shared/extra-utils/videos/index.ts
+++ b/shared/extra-utils/videos/index.ts
@@ -15,4 +15,5 @@ export * from './services-command'
15export * from './streaming-playlists-command' 15export * from './streaming-playlists-command'
16export * from './streaming-playlists' 16export * from './streaming-playlists'
17export * from './comments-command' 17export * from './comments-command'
18export * from './videos-command'
18export * from './videos' 19export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
index a494e60fa..5adf601cc 100644
--- a/shared/extra-utils/videos/live-command.ts
+++ b/shared/extra-utils/videos/live-command.ts
@@ -9,7 +9,6 @@ import { wait } from '../miscs'
9import { unwrapBody } from '../requests' 9import { unwrapBody } from '../requests'
10import { AbstractCommand, OverrideCommandOptions } from '../shared' 10import { AbstractCommand, OverrideCommandOptions } from '../shared'
11import { sendRTMPStream, testFfmpegStreamError } from './live' 11import { sendRTMPStream, testFfmpegStreamError } from './live'
12import { getVideoWithToken } from './videos'
13 12
14export class LiveCommand extends AbstractCommand { 13export class LiveCommand extends AbstractCommand {
15 14
@@ -124,8 +123,7 @@ export class LiveCommand extends AbstractCommand {
124 let video: VideoDetails 123 let video: VideoDetails
125 124
126 do { 125 do {
127 const res = await getVideoWithToken(this.server.url, options.token ?? this.server.accessToken, options.videoId) 126 video = await this.server.videosCommand.getWithToken({ token: options.token, id: options.videoId })
128 video = res.body
129 127
130 await wait(500) 128 await wait(500)
131 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED) 129 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
@@ -149,8 +147,7 @@ export class LiveCommand extends AbstractCommand {
149 let video: VideoDetails 147 let video: VideoDetails
150 148
151 do { 149 do {
152 const res = await getVideoWithToken(this.server.url, options.token ?? this.server.accessToken, options.videoId) 150 video = await this.server.videosCommand.getWithToken({ token: options.token, id: options.videoId })
153 video = res.body
154 151
155 await wait(500) 152 await wait(500)
156 } while (video.state.id !== options.state) 153 } while (video.state.id !== options.state)
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
index f77decc1a..75c8f2433 100644
--- a/shared/extra-utils/videos/playlists-command.ts
+++ b/shared/extra-utils/videos/playlists-command.ts
@@ -1,23 +1,22 @@
1import { omit, pick } from 'lodash' 1import { omit, pick } from 'lodash'
2import { HttpStatusCode } from '@shared/core-utils'
2import { 3import {
3 BooleanBothQuery, 4 BooleanBothQuery,
4 ResultList, 5 ResultList,
5 VideoExistInPlaylist, 6 VideoExistInPlaylist,
6 VideoPlaylist, 7 VideoPlaylist,
8 VideoPlaylistCreate,
7 VideoPlaylistCreateResult, 9 VideoPlaylistCreateResult,
8 VideoPlaylistElement, 10 VideoPlaylistElement,
11 VideoPlaylistElementCreate,
9 VideoPlaylistElementCreateResult, 12 VideoPlaylistElementCreateResult,
10 VideoPlaylistReorder 13 VideoPlaylistElementUpdate,
14 VideoPlaylistReorder,
15 VideoPlaylistType,
16 VideoPlaylistUpdate
11} from '@shared/models' 17} from '@shared/models'
12import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
13import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
14import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
15import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
16import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
17import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
18import { unwrapBody } from '../requests' 18import { unwrapBody } from '../requests'
19import { AbstractCommand, OverrideCommandOptions } from '../shared' 19import { AbstractCommand, OverrideCommandOptions } from '../shared'
20import { videoUUIDToId } from './videos'
21 20
22export class PlaylistsCommand extends AbstractCommand { 21export class PlaylistsCommand extends AbstractCommand {
23 22
@@ -185,7 +184,7 @@ export class PlaylistsCommand extends AbstractCommand {
185 const attributes = { 184 const attributes = {
186 ...options.attributes, 185 ...options.attributes,
187 186
188 videoId: await videoUUIDToId(this.server.url, options.attributes.videoId) 187 videoId: await this.server.videosCommand.getId({ ...options, uuid: options.attributes.videoId })
189 } 188 }
190 189
191 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos' 190 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
new file mode 100644
index 000000000..574705474
--- /dev/null
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -0,0 +1,583 @@
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, pick } from 'lodash'
7import validator from 'validator'
8import { buildUUID } from '@server/helpers/uuid'
9import { loadLanguages } from '@server/initializers/constants'
10import { HttpStatusCode } from '@shared/core-utils'
11import {
12 ResultList,
13 UserVideoRateType,
14 Video,
15 VideoCreate,
16 VideoCreateResult,
17 VideoDetails,
18 VideoFileMetadata,
19 VideoPrivacy,
20 VideosCommonQuery,
21 VideosWithSearchCommonQuery
22} from '@shared/models'
23import { buildAbsoluteFixturePath, wait } from '../miscs'
24import { unwrapBody } from '../requests'
25import { ServerInfo, waitJobs } from '../server'
26import { AbstractCommand, OverrideCommandOptions } from '../shared'
27
28export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
29 fixture?: string
30 thumbnailfile?: string
31 previewfile?: string
32}
33
34export class VideosCommand extends AbstractCommand {
35
36 constructor (server: ServerInfo) {
37 super(server)
38
39 loadLanguages()
40 }
41
42 getCategories (options: OverrideCommandOptions = {}) {
43 const path = '/api/v1/videos/categories'
44
45 return this.getRequestBody<{ [id: number]: string }>({
46 ...options,
47 path,
48
49 implicitToken: false,
50 defaultExpectedStatus: HttpStatusCode.OK_200
51 })
52 }
53
54 getLicences (options: OverrideCommandOptions = {}) {
55 const path = '/api/v1/videos/licences'
56
57 return this.getRequestBody<{ [id: number]: string }>({
58 ...options,
59 path,
60
61 implicitToken: false,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 })
64 }
65
66 getLanguages (options: OverrideCommandOptions = {}) {
67 const path = '/api/v1/videos/languages'
68
69 return this.getRequestBody<{ [id: string]: string }>({
70 ...options,
71 path,
72
73 implicitToken: false,
74 defaultExpectedStatus: HttpStatusCode.OK_200
75 })
76 }
77
78 getPrivacies (options: OverrideCommandOptions = {}) {
79 const path = '/api/v1/videos/privacies'
80
81 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
82 ...options,
83 path,
84
85 implicitToken: false,
86 defaultExpectedStatus: HttpStatusCode.OK_200
87 })
88 }
89
90 // ---------------------------------------------------------------------------
91
92 getDescription (options: OverrideCommandOptions & {
93 descriptionPath: string
94 }) {
95 return this.getRequestBody<{ description: string }>({
96 ...options,
97 path: options.descriptionPath,
98
99 implicitToken: false,
100 defaultExpectedStatus: HttpStatusCode.OK_200
101 })
102 }
103
104 getFileMetadata (options: OverrideCommandOptions & {
105 url: string
106 }) {
107 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
108 ...options,
109
110 url: options.url,
111 implicitToken: false,
112 defaultExpectedStatus: HttpStatusCode.OK_200
113 }))
114 }
115
116 // ---------------------------------------------------------------------------
117
118 view (options: OverrideCommandOptions & {
119 id: number | string
120 xForwardedFor?: string
121 }) {
122 const { id, xForwardedFor } = options
123 const path = '/api/v1/videos/' + id + '/views'
124
125 return this.postBodyRequest({
126 ...options,
127
128 path,
129 xForwardedFor,
130 implicitToken: false,
131 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
132 })
133 }
134
135 rate (options: OverrideCommandOptions & {
136 id: number | string
137 rating: UserVideoRateType
138 }) {
139 const { id, rating } = options
140 const path = '/api/v1/videos/' + id + '/rate'
141
142 return this.putBodyRequest({
143 ...options,
144
145 path,
146 fields: { rating },
147 implicitToken: true,
148 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
149 })
150 }
151
152 // ---------------------------------------------------------------------------
153
154 get (options: OverrideCommandOptions & {
155 id: number | string
156 }) {
157 const path = '/api/v1/videos/' + options.id
158
159 return this.getRequestBody<VideoDetails>({
160 ...options,
161
162 path,
163 implicitToken: false,
164 defaultExpectedStatus: HttpStatusCode.OK_200
165 })
166 }
167
168 getWithToken (options: OverrideCommandOptions & {
169 id: number | string
170 }) {
171 return this.get({
172 ...options,
173
174 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
175 })
176 }
177
178 async getId (options: OverrideCommandOptions & {
179 uuid: number | string
180 }) {
181 const { uuid } = options
182
183 if (validator.isUUID('' + uuid) === false) return uuid as number
184
185 const { id } = await this.get({ ...options, id: uuid })
186
187 return id
188 }
189
190 // ---------------------------------------------------------------------------
191
192 listMyVideos (options: OverrideCommandOptions & {
193 start?: number
194 count?: number
195 sort?: string
196 search?: string
197 isLive?: boolean
198 } = {}) {
199 const path = '/api/v1/users/me/videos'
200
201 return this.getRequestBody<ResultList<Video>>({
202 ...options,
203
204 path,
205 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
206 implicitToken: true,
207 defaultExpectedStatus: HttpStatusCode.OK_200
208 })
209 }
210
211 // ---------------------------------------------------------------------------
212
213 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
214 const path = '/api/v1/videos'
215
216 const query = this.buildListQuery(options)
217
218 return this.getRequestBody<ResultList<Video>>({
219 ...options,
220
221 path,
222 query: { sort: 'name', ...query },
223 implicitToken: false,
224 defaultExpectedStatus: HttpStatusCode.OK_200
225 })
226 }
227
228 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
229 return this.list({
230 ...options,
231
232 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
233 })
234 }
235
236 listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
237 accountName: string
238 }) {
239 const { accountName, search } = options
240 const path = '/api/v1/accounts/' + accountName + '/videos'
241
242 return this.getRequestBody<ResultList<Video>>({
243 ...options,
244
245 path,
246 query: { search, ...this.buildListQuery(options) },
247 implicitToken: true,
248 defaultExpectedStatus: HttpStatusCode.OK_200
249 })
250 }
251
252 listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
253 videoChannelName: string
254 }) {
255 const { videoChannelName } = options
256 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
257
258 return this.getRequestBody<ResultList<Video>>({
259 ...options,
260
261 path,
262 query: this.buildListQuery(options),
263 implicitToken: true,
264 defaultExpectedStatus: HttpStatusCode.OK_200
265 })
266 }
267
268 // ---------------------------------------------------------------------------
269
270 update (options: OverrideCommandOptions & {
271 id: number | string
272 attributes?: VideoEdit
273 }) {
274 const { id, attributes = {} } = options
275 const path = '/api/v1/videos/' + id
276
277 // Upload request
278 if (attributes.thumbnailfile || attributes.previewfile) {
279 const attaches: any = {}
280 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
281 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
282
283 return this.putUploadRequest({
284 ...options,
285
286 path,
287 fields: options.attributes,
288 attaches: {
289 thumbnailfile: attributes.thumbnailfile,
290 previewfile: attributes.previewfile
291 },
292 implicitToken: true,
293 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
294 })
295 }
296
297 return this.putBodyRequest({
298 ...options,
299
300 path,
301 fields: options.attributes,
302 implicitToken: true,
303 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
304 })
305 }
306
307 remove (options: OverrideCommandOptions & {
308 id: number | string
309 }) {
310 const path = '/api/v1/videos/' + options.id
311
312 return this.deleteRequest({
313 ...options,
314
315 path,
316 implicitToken: true,
317 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
318 })
319 }
320
321 async removeAll () {
322 const { data } = await this.list()
323
324 for (const v of data) {
325 await this.remove({ id: v.id })
326 }
327 }
328
329 // ---------------------------------------------------------------------------
330
331 async upload (options: OverrideCommandOptions & {
332 attributes?: VideoEdit
333 mode?: 'legacy' | 'resumable' // default legacy
334 } = {}) {
335 const { mode = 'legacy', expectedStatus } = options
336 let defaultChannelId = 1
337
338 try {
339 const { videoChannels } = await this.server.usersCommand.getMyInfo({ token: options.token })
340 defaultChannelId = videoChannels[0].id
341 } catch (e) { /* empty */ }
342
343 // Override default attributes
344 const attributes = {
345 name: 'my super video',
346 category: 5,
347 licence: 4,
348 language: 'zh',
349 channelId: defaultChannelId,
350 nsfw: true,
351 waitTranscoding: false,
352 description: 'my super description',
353 support: 'my super support text',
354 tags: [ 'tag' ],
355 privacy: VideoPrivacy.PUBLIC,
356 commentsEnabled: true,
357 downloadEnabled: true,
358 fixture: 'video_short.webm',
359
360 ...options.attributes
361 }
362
363 const res = mode === 'legacy'
364 ? await this.buildLegacyUpload({ ...options, attributes })
365 : await this.buildResumeUpload({ ...options, attributes })
366
367 // Wait torrent generation
368 if (expectedStatus === HttpStatusCode.OK_200) {
369 let video: VideoDetails
370
371 do {
372 video = await this.getWithToken({ ...options, id: video.uuid })
373
374 await wait(50)
375 } while (!video.files[0].torrentUrl)
376 }
377
378 return res
379 }
380
381 async buildLegacyUpload (options: OverrideCommandOptions & {
382 attributes: VideoEdit
383 }): Promise<VideoCreateResult> {
384 const path = '/api/v1/videos/upload'
385
386 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
387 ...options,
388
389 path,
390 fields: this.buildUploadFields(options.attributes),
391 attaches: this.buildUploadAttaches(options.attributes),
392 implicitToken: true,
393 defaultExpectedStatus: HttpStatusCode.OK_200
394 })).then(body => body.video || body as any)
395 }
396
397 async buildResumeUpload (options: OverrideCommandOptions & {
398 attributes: VideoEdit
399 }) {
400 const { attributes, expectedStatus } = options
401
402 let size = 0
403 let videoFilePath: string
404 let mimetype = 'video/mp4'
405
406 if (attributes.fixture) {
407 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
408 size = (await stat(videoFilePath)).size
409
410 if (videoFilePath.endsWith('.mkv')) {
411 mimetype = 'video/x-matroska'
412 } else if (videoFilePath.endsWith('.webm')) {
413 mimetype = 'video/webm'
414 }
415 }
416
417 const initializeSessionRes = await this.prepareResumableUpload({ ...options, attributes, size, mimetype })
418 const initStatus = initializeSessionRes.status
419
420 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
421 const locationHeader = initializeSessionRes.header['location']
422 expect(locationHeader).to.not.be.undefined
423
424 const pathUploadId = locationHeader.split('?')[1]
425
426 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
427
428 return result.body.video
429 }
430
431 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
432 ? HttpStatusCode.CREATED_201
433 : expectedStatus
434
435 expect(initStatus).to.equal(expectedInitStatus)
436
437 return initializeSessionRes.body.video as VideoCreateResult
438 }
439
440 async prepareResumableUpload (options: OverrideCommandOptions & {
441 attributes: VideoEdit
442 size: number
443 mimetype: string
444 }) {
445 const { attributes, size, mimetype } = options
446
447 const path = '/api/v1/videos/upload-resumable'
448
449 return this.postUploadRequest({
450 ...options,
451
452 path,
453 headers: {
454 'X-Upload-Content-Type': mimetype,
455 'X-Upload-Content-Length': size.toString()
456 },
457 fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
458 implicitToken: true,
459 defaultExpectedStatus: null
460 })
461 }
462
463 sendResumableChunks (options: OverrideCommandOptions & {
464 pathUploadId: string
465 videoFilePath: string
466 size: number
467 contentLength?: number
468 contentRangeBuilder?: (start: number, chunk: any) => string
469 }) {
470 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
471
472 const path = '/api/v1/videos/upload-resumable'
473 let start = 0
474
475 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
476 const url = this.server.url
477
478 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
479 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
480 readable.on('data', async function onData (chunk) {
481 readable.pause()
482
483 const headers = {
484 'Authorization': 'Bearer ' + token,
485 'Content-Type': 'application/octet-stream',
486 'Content-Range': contentRangeBuilder
487 ? contentRangeBuilder(start, chunk)
488 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
489 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
490 }
491
492 const res = await got<{ video: VideoCreateResult }>({
493 url,
494 method: 'put',
495 headers,
496 path: path + '?' + pathUploadId,
497 body: chunk,
498 responseType: 'json',
499 throwHttpErrors: false
500 })
501
502 start += chunk.length
503
504 if (res.statusCode === expectedStatus) {
505 return resolve(res)
506 }
507
508 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
509 readable.off('data', onData)
510 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
511 }
512
513 readable.resume()
514 })
515 })
516 }
517
518 quickUpload (options: OverrideCommandOptions & {
519 name: string
520 nsfw?: boolean
521 privacy?: VideoPrivacy
522 fixture?: string
523 }) {
524 const attributes: VideoEdit = { name: options.name }
525 if (options.nsfw) attributes.nsfw = options.nsfw
526 if (options.privacy) attributes.privacy = options.privacy
527 if (options.fixture) attributes.fixture = options.fixture
528
529 return this.upload({ ...options, attributes })
530 }
531
532 async randomUpload (options: OverrideCommandOptions & {
533 wait?: boolean // default true
534 additionalParams?: VideoEdit & { prefixName: string }
535 } = {}) {
536 const { wait = true, additionalParams } = options
537 const prefixName = additionalParams?.prefixName || ''
538 const name = prefixName + buildUUID()
539
540 const attributes = { name, additionalParams }
541
542 if (wait) await waitJobs([ this.server ])
543
544 const result = await this.upload({ ...options, attributes })
545
546 return { ...result, name }
547 }
548
549 // ---------------------------------------------------------------------------
550
551 private buildListQuery (options: VideosCommonQuery) {
552 return pick(options, [
553 'start',
554 'count',
555 'sort',
556 'nsfw',
557 'isLive',
558 'categoryOneOf',
559 'licenceOneOf',
560 'languageOneOf',
561 'tagsOneOf',
562 'tagsAllOf',
563 'filter',
564 'skipCount'
565 ])
566 }
567
568 private buildUploadFields (attributes: VideoEdit) {
569 return omit(attributes, [ 'thumbnailfile', 'previewfile' ])
570 }
571
572 private buildUploadAttaches (attributes: VideoEdit) {
573 const attaches: { [ name: string ]: string } = {}
574
575 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
576 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
577 }
578
579 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
580
581 return attaches
582 }
583}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 5e20f8010..19f0df8b8 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -1,306 +1,16 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra' 4import { pathExists, readdir } from 'fs-extra'
5import got, { Response as GotResponse } from 'got/dist/source'
6import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 5import { join } from 'path'
8import * as request from 'supertest'
9import validator from 'validator'
10import { getLowercaseExtension } from '@server/helpers/core-utils' 6import { getLowercaseExtension } from '@server/helpers/core-utils'
11import { buildUUID } from '@server/helpers/uuid'
12import { HttpStatusCode } from '@shared/core-utils' 7import { HttpStatusCode } from '@shared/core-utils'
13import { BooleanBothQuery, VideosCommonQuery } from '@shared/models' 8import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
14import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' 9import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
15import { VideoDetails, VideoPrivacy } from '../../models/videos' 10import { makeRawRequest } from '../requests/requests'
16import { buildAbsoluteFixturePath, dateIsValid, testImage, wait, webtorrentAdd } from '../miscs' 11import { waitJobs } from '../server'
17import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
18import { waitJobs } from '../server/jobs'
19import { ServerInfo } from '../server/servers' 12import { ServerInfo } from '../server/servers'
20import { xxxgetMyUserInformation } from '../users' 13import { VideoEdit } from './videos-command'
21
22loadLanguages()
23
24type VideoAttributes = {
25 name?: string
26 category?: number
27 licence?: number
28 language?: string
29 nsfw?: boolean
30 commentsEnabled?: boolean
31 downloadEnabled?: boolean
32 waitTranscoding?: boolean
33 description?: string
34 originallyPublishedAt?: string
35 tags?: string[]
36 channelId?: number
37 privacy?: VideoPrivacy
38 fixture?: string
39 support?: string
40 thumbnailfile?: string
41 previewfile?: string
42 scheduleUpdate?: {
43 updateAt: string
44 privacy?: VideoPrivacy
45 }
46}
47
48function getVideoCategories (url: string) {
49 const path = '/api/v1/videos/categories'
50
51 return makeGetRequest({
52 url,
53 path,
54 statusCodeExpected: HttpStatusCode.OK_200
55 })
56}
57
58function getVideoLicences (url: string) {
59 const path = '/api/v1/videos/licences'
60
61 return makeGetRequest({
62 url,
63 path,
64 statusCodeExpected: HttpStatusCode.OK_200
65 })
66}
67
68function getVideoLanguages (url: string) {
69 const path = '/api/v1/videos/languages'
70
71 return makeGetRequest({
72 url,
73 path,
74 statusCodeExpected: HttpStatusCode.OK_200
75 })
76}
77
78function getVideoPrivacies (url: string) {
79 const path = '/api/v1/videos/privacies'
80
81 return makeGetRequest({
82 url,
83 path,
84 statusCodeExpected: HttpStatusCode.OK_200
85 })
86}
87
88function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
89 const path = '/api/v1/videos/' + id
90
91 return request(url)
92 .get(path)
93 .set('Accept', 'application/json')
94 .expect(expectedStatus)
95}
96
97async function getVideoIdFromUUID (url: string, uuid: string) {
98 const res = await getVideo(url, uuid)
99
100 return res.body.id
101}
102
103function getVideoFileMetadataUrl (url: string) {
104 return request(url)
105 .get('/')
106 .set('Accept', 'application/json')
107 .expect(HttpStatusCode.OK_200)
108 .expect('Content-Type', /json/)
109}
110
111function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
112 const path = '/api/v1/videos/' + id + '/views'
113
114 const req = request(url)
115 .post(path)
116 .set('Accept', 'application/json')
117
118 if (xForwardedFor) {
119 req.set('X-Forwarded-For', xForwardedFor)
120 }
121
122 return req.expect(expectedStatus)
123}
124
125function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
126 const path = '/api/v1/videos/' + id
127
128 return request(url)
129 .get(path)
130 .set('Authorization', 'Bearer ' + token)
131 .set('Accept', 'application/json')
132 .expect(expectedStatus)
133}
134
135function getVideoDescription (url: string, descriptionPath: string) {
136 return request(url)
137 .get(descriptionPath)
138 .set('Accept', 'application/json')
139 .expect(HttpStatusCode.OK_200)
140 .expect('Content-Type', /json/)
141}
142
143function getVideosList (url: string) {
144 const path = '/api/v1/videos'
145
146 return request(url)
147 .get(path)
148 .query({ sort: 'name' })
149 .set('Accept', 'application/json')
150 .expect(HttpStatusCode.OK_200)
151 .expect('Content-Type', /json/)
152}
153
154function getVideosListWithToken (url: string, token: string, query: { nsfw?: BooleanBothQuery } = {}) {
155 const path = '/api/v1/videos'
156
157 return request(url)
158 .get(path)
159 .set('Authorization', 'Bearer ' + token)
160 .query({ sort: 'name', ...query })
161 .set('Accept', 'application/json')
162 .expect(HttpStatusCode.OK_200)
163 .expect('Content-Type', /json/)
164}
165
166function getLocalVideos (url: string) {
167 const path = '/api/v1/videos'
168
169 return request(url)
170 .get(path)
171 .query({ sort: 'name', filter: 'local' })
172 .set('Accept', 'application/json')
173 .expect(HttpStatusCode.OK_200)
174 .expect('Content-Type', /json/)
175}
176
177function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
178 const path = '/api/v1/users/me/videos'
179
180 const req = request(url)
181 .get(path)
182 .query({ start: start })
183 .query({ count: count })
184 .query({ search: search })
185
186 if (sort) req.query({ sort })
187
188 return req.set('Accept', 'application/json')
189 .set('Authorization', 'Bearer ' + accessToken)
190 .expect(HttpStatusCode.OK_200)
191 .expect('Content-Type', /json/)
192}
193
194function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
195 const path = '/api/v1/users/me/videos'
196
197 return makeGetRequest({
198 url,
199 path,
200 token: accessToken,
201 query,
202 statusCodeExpected: HttpStatusCode.OK_200
203 })
204}
205
206function getAccountVideos (
207 url: string,
208 accessToken: string,
209 accountName: string,
210 start: number,
211 count: number,
212 sort?: string,
213 query: {
214 nsfw?: BooleanBothQuery
215 search?: string
216 } = {}
217) {
218 const path = '/api/v1/accounts/' + accountName + '/videos'
219
220 return makeGetRequest({
221 url,
222 path,
223 query: { ...query, start, count, sort },
224 token: accessToken,
225 statusCodeExpected: HttpStatusCode.OK_200
226 })
227}
228
229function getVideoChannelVideos (
230 url: string,
231 accessToken: string,
232 videoChannelName: string,
233 start: number,
234 count: number,
235 sort?: string,
236 query: { nsfw?: BooleanBothQuery } = {}
237) {
238 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
239
240 return makeGetRequest({
241 url,
242 path,
243 query: { ...query, start, count, sort },
244 token: accessToken,
245 statusCodeExpected: HttpStatusCode.OK_200
246 })
247}
248
249function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
250 const path = '/api/v1/videos'
251
252 const req = request(url)
253 .get(path)
254 .query({ start: start })
255 .query({ count: count })
256
257 if (sort) req.query({ sort })
258 if (skipCount) req.query({ skipCount })
259
260 return req.set('Accept', 'application/json')
261 .expect(HttpStatusCode.OK_200)
262 .expect('Content-Type', /json/)
263}
264
265function getVideosListSort (url: string, sort: string) {
266 const path = '/api/v1/videos'
267
268 return request(url)
269 .get(path)
270 .query({ sort: sort })
271 .set('Accept', 'application/json')
272 .expect(HttpStatusCode.OK_200)
273 .expect('Content-Type', /json/)
274}
275
276function getVideosWithFilters (url: string, query: VideosCommonQuery) {
277 const path = '/api/v1/videos'
278
279 return request(url)
280 .get(path)
281 .query(query)
282 .set('Accept', 'application/json')
283 .expect(HttpStatusCode.OK_200)
284 .expect('Content-Type', /json/)
285}
286
287function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
288 const path = '/api/v1/videos'
289
290 return request(url)
291 .delete(path + '/' + id)
292 .set('Accept', 'application/json')
293 .set('Authorization', 'Bearer ' + token)
294 .expect(expectedStatus)
295}
296
297async function removeAllVideos (server: ServerInfo) {
298 const resVideos = await getVideosList(server.url)
299
300 for (const v of resVideos.body.data) {
301 await removeVideo(server.url, server.accessToken, v.id)
302 }
303}
304 14
305async function checkVideoFilesWereRemoved ( 15async function checkVideoFilesWereRemoved (
306 videoUUID: string, 16 videoUUID: string,
@@ -329,280 +39,20 @@ async function checkVideoFilesWereRemoved (
329 } 39 }
330} 40}
331 41
332async function uploadVideo (
333 url: string,
334 accessToken: string,
335 videoAttributesArg: VideoAttributes,
336 specialStatus = HttpStatusCode.OK_200,
337 mode: 'legacy' | 'resumable' = 'legacy'
338) {
339 let defaultChannelId = '1'
340
341 try {
342 const res = await xxxgetMyUserInformation(url, accessToken)
343 defaultChannelId = res.body.videoChannels[0].id
344 } catch (e) { /* empty */ }
345
346 // Override default attributes
347 const attributes = Object.assign({
348 name: 'my super video',
349 category: 5,
350 licence: 4,
351 language: 'zh',
352 channelId: defaultChannelId,
353 nsfw: true,
354 waitTranscoding: false,
355 description: 'my super description',
356 support: 'my super support text',
357 tags: [ 'tag' ],
358 privacy: VideoPrivacy.PUBLIC,
359 commentsEnabled: true,
360 downloadEnabled: true,
361 fixture: 'video_short.webm'
362 }, videoAttributesArg)
363
364 const res = mode === 'legacy'
365 ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
366 : await buildResumeUpload(url, accessToken, attributes, specialStatus)
367
368 // Wait torrent generation
369 if (specialStatus === HttpStatusCode.OK_200) {
370 let video: VideoDetails
371 do {
372 const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
373 video = resVideo.body
374
375 await wait(50)
376 } while (!video.files[0].torrentUrl)
377 }
378
379 return res
380}
381
382function checkUploadVideoParam ( 42function checkUploadVideoParam (
383 url: string, 43 server: ServerInfo,
384 token: string, 44 token: string,
385 attributes: Partial<VideoAttributes>, 45 attributes: Partial<VideoEdit>,
386 specialStatus = HttpStatusCode.OK_200, 46 expectedStatus = HttpStatusCode.OK_200,
387 mode: 'legacy' | 'resumable' = 'legacy' 47 mode: 'legacy' | 'resumable' = 'legacy'
388) { 48) {
389 return mode === 'legacy' 49 return mode === 'legacy'
390 ? buildLegacyUpload(url, token, attributes, specialStatus) 50 ? server.videosCommand.buildLegacyUpload({ token, attributes, expectedStatus })
391 : buildResumeUpload(url, token, attributes, specialStatus) 51 : server.videosCommand.buildResumeUpload({ token, attributes, expectedStatus })
392}
393
394async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
395 const path = '/api/v1/videos/upload'
396 const req = request(url)
397 .post(path)
398 .set('Accept', 'application/json')
399 .set('Authorization', 'Bearer ' + token)
400
401 buildUploadReq(req, attributes)
402
403 if (attributes.fixture !== undefined) {
404 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
405 }
406
407 return req.expect(specialStatus)
408}
409
410async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
411 let size = 0
412 let videoFilePath: string
413 let mimetype = 'video/mp4'
414
415 if (attributes.fixture) {
416 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
417 size = (await stat(videoFilePath)).size
418
419 if (videoFilePath.endsWith('.mkv')) {
420 mimetype = 'video/x-matroska'
421 } else if (videoFilePath.endsWith('.webm')) {
422 mimetype = 'video/webm'
423 }
424 }
425
426 const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
427 const initStatus = initializeSessionRes.status
428
429 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
430 const locationHeader = initializeSessionRes.header['location']
431 expect(locationHeader).to.not.be.undefined
432
433 const pathUploadId = locationHeader.split('?')[1]
434
435 return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
436 }
437
438 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
439 ? HttpStatusCode.CREATED_201
440 : specialStatus
441
442 expect(initStatus).to.equal(expectedInitStatus)
443
444 return initializeSessionRes
445}
446
447async function prepareResumableUpload (options: {
448 url: string
449 token: string
450 attributes: VideoAttributes
451 size: number
452 mimetype: string
453}) {
454 const { url, token, attributes, size, mimetype } = options
455
456 const path = '/api/v1/videos/upload-resumable'
457
458 const req = request(url)
459 .post(path)
460 .set('Authorization', 'Bearer ' + token)
461 .set('X-Upload-Content-Type', mimetype)
462 .set('X-Upload-Content-Length', size.toString())
463
464 buildUploadReq(req, attributes)
465
466 if (attributes.fixture) {
467 req.field('filename', attributes.fixture)
468 }
469
470 return req
471}
472
473function sendResumableChunks (options: {
474 url: string
475 token: string
476 pathUploadId: string
477 videoFilePath: string
478 size: number
479 specialStatus?: HttpStatusCode
480 contentLength?: number
481 contentRangeBuilder?: (start: number, chunk: any) => string
482}) {
483 const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
484
485 const expectedStatus = specialStatus || HttpStatusCode.OK_200
486
487 const path = '/api/v1/videos/upload-resumable'
488 let start = 0
489
490 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
491 return new Promise<GotResponse>((resolve, reject) => {
492 readable.on('data', async function onData (chunk) {
493 readable.pause()
494
495 const headers = {
496 'Authorization': 'Bearer ' + token,
497 'Content-Type': 'application/octet-stream',
498 'Content-Range': contentRangeBuilder
499 ? contentRangeBuilder(start, chunk)
500 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
501 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
502 }
503
504 const res = await got({
505 url,
506 method: 'put',
507 headers,
508 path: path + '?' + pathUploadId,
509 body: chunk,
510 responseType: 'json',
511 throwHttpErrors: false
512 })
513
514 start += chunk.length
515
516 if (res.statusCode === expectedStatus) {
517 return resolve(res)
518 }
519
520 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
521 readable.off('data', onData)
522 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
523 }
524
525 readable.resume()
526 })
527 })
528}
529
530function updateVideo (
531 url: string,
532 accessToken: string,
533 id: number | string,
534 attributes: VideoAttributes,
535 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
536) {
537 const path = '/api/v1/videos/' + id
538 const body = {}
539
540 if (attributes.name) body['name'] = attributes.name
541 if (attributes.category) body['category'] = attributes.category
542 if (attributes.licence) body['licence'] = attributes.licence
543 if (attributes.language) body['language'] = attributes.language
544 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
545 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
546 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
547 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
548 if (attributes.description) body['description'] = attributes.description
549 if (attributes.tags) body['tags'] = attributes.tags
550 if (attributes.privacy) body['privacy'] = attributes.privacy
551 if (attributes.channelId) body['channelId'] = attributes.channelId
552 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
553
554 // Upload request
555 if (attributes.thumbnailfile || attributes.previewfile) {
556 const attaches: any = {}
557 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
558 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
559
560 return makeUploadRequest({
561 url,
562 method: 'PUT',
563 path,
564 token: accessToken,
565 fields: body,
566 attaches,
567 statusCodeExpected
568 })
569 }
570
571 return makePutBodyRequest({
572 url,
573 path,
574 fields: body,
575 token: accessToken,
576 statusCodeExpected
577 })
578}
579
580function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
581 const path = '/api/v1/videos/' + id + '/rate'
582
583 return request(url)
584 .put(path)
585 .set('Accept', 'application/json')
586 .set('Authorization', 'Bearer ' + accessToken)
587 .send({ rating })
588 .expect(specialStatus)
589}
590
591function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
592 return new Promise<any>((res, rej) => {
593 const torrentName = videoUUID + '-' + resolution + '.torrent'
594 const torrentPath = server.serversCommand.buildDirectory(join('torrents', torrentName))
595
596 readFile(torrentPath, (err, data) => {
597 if (err) return rej(err)
598
599 return res(parseTorrent(data))
600 })
601 })
602} 52}
603 53
604async function completeVideoCheck ( 54async function completeVideoCheck (
605 url: string, 55 server: ServerInfo,
606 video: any, 56 video: any,
607 attributes: { 57 attributes: {
608 name: string 58 name: string
@@ -644,7 +94,7 @@ async function completeVideoCheck (
644 if (!attributes.likes) attributes.likes = 0 94 if (!attributes.likes) attributes.likes = 0
645 if (!attributes.dislikes) attributes.dislikes = 0 95 if (!attributes.dislikes) attributes.dislikes = 0
646 96
647 const host = new URL(url).host 97 const host = new URL(server.url).host
648 const originHost = attributes.account.host 98 const originHost = attributes.account.host
649 99
650 expect(video.name).to.equal(attributes.name) 100 expect(video.name).to.equal(attributes.name)
@@ -681,8 +131,7 @@ async function completeVideoCheck (
681 expect(video.originallyPublishedAt).to.be.null 131 expect(video.originallyPublishedAt).to.be.null
682 } 132 }
683 133
684 const res = await getVideo(url, video.uuid) 134 const videoDetails = await server.videosCommand.get({ id: video.uuid })
685 const videoDetails: VideoDetails = res.body
686 135
687 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 136 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
688 expect(videoDetails.tags).to.deep.equal(attributes.tags) 137 expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -738,148 +187,33 @@ async function completeVideoCheck (
738 } 187 }
739 188
740 expect(videoDetails.thumbnailPath).to.exist 189 expect(videoDetails.thumbnailPath).to.exist
741 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) 190 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
742 191
743 if (attributes.previewfile) { 192 if (attributes.previewfile) {
744 expect(videoDetails.previewPath).to.exist 193 expect(videoDetails.previewPath).to.exist
745 await testImage(url, attributes.previewfile, videoDetails.previewPath) 194 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
746 } 195 }
747} 196}
748 197
749async function videoUUIDToId (url: string, id: number | string) {
750 if (validator.isUUID('' + id) === false) return id
751
752 const res = await getVideo(url, id)
753 return res.body.id
754}
755
756async function uploadVideoAndGetId (options: {
757 server: ServerInfo
758 videoName: string
759 nsfw?: boolean
760 privacy?: VideoPrivacy
761 token?: string
762 fixture?: string
763}) {
764 const videoAttrs: any = { name: options.videoName }
765 if (options.nsfw) videoAttrs.nsfw = options.nsfw
766 if (options.privacy) videoAttrs.privacy = options.privacy
767 if (options.fixture) videoAttrs.fixture = options.fixture
768
769 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
770
771 return res.body.video as { id: number, uuid: string, shortUUID: string }
772}
773
774async function getLocalIdByUUID (url: string, uuid: string) {
775 const res = await getVideo(url, uuid)
776
777 return res.body.id
778}
779
780// serverNumber starts from 1 198// serverNumber starts from 1
781async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) { 199async function uploadRandomVideoOnServers (
200 servers: ServerInfo[],
201 serverNumber: number,
202 additionalParams?: VideoEdit & { prefixName?: string }
203) {
782 const server = servers.find(s => s.serverNumber === serverNumber) 204 const server = servers.find(s => s.serverNumber === serverNumber)
783 const res = await uploadRandomVideo(server, false, additionalParams) 205 const res = await server.videosCommand.randomUpload({ wait: false, ...additionalParams })
784 206
785 await waitJobs(servers) 207 await waitJobs(servers)
786 208
787 return res 209 return res
788} 210}
789 211
790async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
791 const prefixName = additionalParams.prefixName || ''
792 const name = prefixName + buildUUID()
793
794 const data = Object.assign({ name }, additionalParams)
795 const res = await uploadVideo(server.url, server.accessToken, data)
796
797 if (wait) await waitJobs([ server ])
798
799 return { uuid: res.body.video.uuid, name }
800}
801
802// --------------------------------------------------------------------------- 212// ---------------------------------------------------------------------------
803 213
804export { 214export {
805 getVideoDescription,
806 getVideoCategories,
807 uploadRandomVideo,
808 getVideoLicences,
809 videoUUIDToId,
810 getVideoPrivacies,
811 getVideoLanguages,
812 getMyVideos,
813 getAccountVideos,
814 getVideoChannelVideos,
815 getVideo,
816 getVideoFileMetadataUrl,
817 getVideoWithToken,
818 getVideosList,
819 removeAllVideos,
820 checkUploadVideoParam, 215 checkUploadVideoParam,
821 getVideosListPagination,
822 getVideosListSort,
823 removeVideo,
824 getVideosListWithToken,
825 uploadVideo,
826 sendResumableChunks,
827 getVideosWithFilters,
828 uploadRandomVideoOnServers,
829 updateVideo,
830 rateVideo,
831 viewVideo,
832 parseTorrentVideo,
833 getLocalVideos,
834 completeVideoCheck, 216 completeVideoCheck,
835 checkVideoFilesWereRemoved, 217 uploadRandomVideoOnServers,
836 getMyVideosWithFilter, 218 checkVideoFilesWereRemoved
837 uploadVideoAndGetId,
838 getLocalIdByUUID,
839 getVideoIdFromUUID,
840 prepareResumableUpload
841}
842
843// ---------------------------------------------------------------------------
844
845function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
846
847 for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
848 if (attributes[key] !== undefined) {
849 req.field(key, attributes[key])
850 }
851 }
852
853 for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
854 if (attributes[key] !== undefined) {
855 req.field(key, JSON.stringify(attributes[key]))
856 }
857 }
858
859 for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
860 if (attributes[key] !== undefined) {
861 req.field(key, attributes[key].toString())
862 }
863 }
864
865 const tags = attributes.tags || []
866 for (let i = 0; i < tags.length; i++) {
867 req.field('tags[' + i + ']', attributes.tags[i])
868 }
869
870 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
871 if (attributes[key] !== undefined) {
872 req.attach(key, buildAbsoluteFixturePath(attributes[key]))
873 }
874 }
875
876 if (attributes.scheduleUpdate) {
877 if (attributes.scheduleUpdate.updateAt) {
878 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
879 }
880
881 if (attributes.scheduleUpdate.privacy) {
882 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
883 }
884 }
885} 219}
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
index bd02489ea..179266338 100644
--- a/shared/models/search/videos-common-query.model.ts
+++ b/shared/models/search/videos-common-query.model.ts
@@ -21,6 +21,8 @@ export interface VideosCommonQuery {
21 tagsAllOf?: string[] 21 tagsAllOf?: string[]
22 22
23 filter?: VideoFilter 23 filter?: VideoFilter
24
25 skipCount?: boolean
24} 26}
25 27
26export interface VideosWithSearchCommonQuery extends VideosCommonQuery { 28export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts
index e21ccae04..86653b959 100644
--- a/shared/models/videos/video-update.model.ts
+++ b/shared/models/videos/video-update.model.ts
@@ -1,5 +1,6 @@
1import { VideoPrivacy } from './video-privacy.enum' 1import { VideoPrivacy } from './video-privacy.enum'
2import { VideoScheduleUpdate } from './video-schedule-update.model' 2import { VideoScheduleUpdate } from './video-schedule-update.model'
3
3export interface VideoUpdate { 4export interface VideoUpdate {
4 name?: string 5 name?: string
5 category?: number 6 category?: number