aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils/videos
diff options
context:
space:
mode:
Diffstat (limited to 'shared/extra-utils/videos')
-rw-r--r--shared/extra-utils/videos/blacklist-command.ts76
-rw-r--r--shared/extra-utils/videos/captions-command.ts65
-rw-r--r--shared/extra-utils/videos/captions.ts17
-rw-r--r--shared/extra-utils/videos/change-ownership-command.ts68
-rw-r--r--shared/extra-utils/videos/channels-command.ts156
-rw-r--r--shared/extra-utils/videos/channels.ts18
-rw-r--r--shared/extra-utils/videos/comments-command.ts152
-rw-r--r--shared/extra-utils/videos/history-command.ts58
-rw-r--r--shared/extra-utils/videos/imports-command.ts47
-rw-r--r--shared/extra-utils/videos/index.ts19
-rw-r--r--shared/extra-utils/videos/live-command.ts154
-rw-r--r--shared/extra-utils/videos/live.ts159
-rw-r--r--shared/extra-utils/videos/playlists-command.ts280
-rw-r--r--shared/extra-utils/videos/playlists.ts25
-rw-r--r--shared/extra-utils/videos/services-command.ts29
-rw-r--r--shared/extra-utils/videos/services.ts24
-rw-r--r--shared/extra-utils/videos/streaming-playlists-command.ts44
-rw-r--r--shared/extra-utils/videos/streaming-playlists.ts78
-rw-r--r--shared/extra-utils/videos/video-blacklist.ts79
-rw-r--r--shared/extra-utils/videos/video-captions.ts72
-rw-r--r--shared/extra-utils/videos/video-change-ownership.ts72
-rw-r--r--shared/extra-utils/videos/video-channels.ts192
-rw-r--r--shared/extra-utils/videos/video-comments.ts138
-rw-r--r--shared/extra-utils/videos/video-history.ts49
-rw-r--r--shared/extra-utils/videos/video-imports.ts90
-rw-r--r--shared/extra-utils/videos/video-playlists.ts320
-rw-r--r--shared/extra-utils/videos/video-streaming-playlists.ts82
-rw-r--r--shared/extra-utils/videos/videos-command.ts599
-rw-r--r--shared/extra-utils/videos/videos.ts816
29 files changed, 1976 insertions, 2002 deletions
diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts
new file mode 100644
index 000000000..3a2ef89ba
--- /dev/null
+++ b/shared/extra-utils/videos/blacklist-command.ts
@@ -0,0 +1,76 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoBlacklist, VideoBlacklistType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class BlacklistCommand extends AbstractCommand {
7
8 add (options: OverrideCommandOptions & {
9 videoId: number | string
10 reason?: string
11 unfederate?: boolean
12 }) {
13 const { videoId, reason, unfederate } = options
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return this.postBodyRequest({
17 ...options,
18
19 path,
20 fields: { reason, unfederate },
21 implicitToken: true,
22 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
23 })
24 }
25
26 update (options: OverrideCommandOptions & {
27 videoId: number | string
28 reason?: string
29 }) {
30 const { videoId, reason } = options
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return this.putBodyRequest({
34 ...options,
35
36 path,
37 fields: { reason },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 videoId: number | string
45 }) {
46 const { videoId } = options
47 const path = '/api/v1/videos/' + videoId + '/blacklist'
48
49 return this.deleteRequest({
50 ...options,
51
52 path,
53 implicitToken: true,
54 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
55 })
56 }
57
58 list (options: OverrideCommandOptions & {
59 sort?: string
60 type?: VideoBlacklistType
61 } = {}) {
62 const { sort, type } = options
63 const path = '/api/v1/videos/blacklist/'
64
65 const query = { sort, type }
66
67 return this.getRequestBody<ResultList<VideoBlacklist>>({
68 ...options,
69
70 path,
71 query,
72 implicitToken: true,
73 defaultExpectedStatus: HttpStatusCode.OK_200
74 })
75 }
76}
diff --git a/shared/extra-utils/videos/captions-command.ts b/shared/extra-utils/videos/captions-command.ts
new file mode 100644
index 000000000..a65ea99e3
--- /dev/null
+++ b/shared/extra-utils/videos/captions-command.ts
@@ -0,0 +1,65 @@
1import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models'
2import { buildAbsoluteFixturePath } from '../miscs'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class CaptionsCommand extends AbstractCommand {
6
7 add (options: OverrideCommandOptions & {
8 videoId: string | number
9 language: string
10 fixture: string
11 mimeType?: string
12 }) {
13 const { videoId, language, fixture, mimeType } = options
14
15 const path = '/api/v1/videos/' + videoId + '/captions/' + language
16
17 const captionfile = buildAbsoluteFixturePath(fixture)
18 const captionfileAttach = mimeType
19 ? [ captionfile, { contentType: mimeType } ]
20 : captionfile
21
22 return this.putUploadRequest({
23 ...options,
24
25 path,
26 fields: {},
27 attaches: {
28 captionfile: captionfileAttach
29 },
30 implicitToken: true,
31 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
32 })
33 }
34
35 list (options: OverrideCommandOptions & {
36 videoId: string | number
37 }) {
38 const { videoId } = options
39 const path = '/api/v1/videos/' + videoId + '/captions'
40
41 return this.getRequestBody<ResultList<VideoCaption>>({
42 ...options,
43
44 path,
45 implicitToken: false,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 delete (options: OverrideCommandOptions & {
51 videoId: string | number
52 language: string
53 }) {
54 const { videoId, language } = options
55 const path = '/api/v1/videos/' + videoId + '/captions/' + language
56
57 return this.deleteRequest({
58 ...options,
59
60 path,
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65}
diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts
new file mode 100644
index 000000000..ff8a43366
--- /dev/null
+++ b/shared/extra-utils/videos/captions.ts
@@ -0,0 +1,17 @@
1import { expect } from 'chai'
2import * as request from 'supertest'
3import { HttpStatusCode } from '@shared/models'
4
5async function testCaptionFile (url: string, captionPath: string, containsString: string) {
6 const res = await request(url)
7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200)
9
10 expect(res.text).to.contain(containsString)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 testCaptionFile
17}
diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts
new file mode 100644
index 000000000..ad4c726ef
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts
new file mode 100644
index 000000000..255e1d62d
--- /dev/null
+++ b/shared/extra-utils/videos/channels-command.ts
@@ -0,0 +1,156 @@
1import { pick } from '@shared/core-utils'
2import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
3import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { unwrapBody } from '../requests'
6import { AbstractCommand, OverrideCommandOptions } from '../shared'
7
8export class ChannelsCommand extends AbstractCommand {
9
10 list (options: OverrideCommandOptions & {
11 start?: number
12 count?: number
13 sort?: string
14 withStats?: boolean
15 } = {}) {
16 const path = '/api/v1/video-channels'
17
18 return this.getRequestBody<ResultList<VideoChannel>>({
19 ...options,
20
21 path,
22 query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
23 implicitToken: false,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 listByAccount (options: OverrideCommandOptions & {
29 accountName: string
30 start?: number
31 count?: number
32 sort?: string
33 withStats?: boolean
34 search?: string
35 }) {
36 const { accountName, sort = 'createdAt' } = options
37 const path = '/api/v1/accounts/' + accountName + '/video-channels'
38
39 return this.getRequestBody<ResultList<VideoChannel>>({
40 ...options,
41
42 path,
43 query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
44 implicitToken: false,
45 defaultExpectedStatus: HttpStatusCode.OK_200
46 })
47 }
48
49 async create (options: OverrideCommandOptions & {
50 attributes: VideoChannelCreate
51 }) {
52 const path = '/api/v1/video-channels/'
53
54 // Default attributes
55 const defaultAttributes = {
56 displayName: 'my super video channel',
57 description: 'my super channel description',
58 support: 'my super channel support'
59 }
60 const attributes = { ...defaultAttributes, ...options.attributes }
61
62 const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
63 ...options,
64
65 path,
66 fields: attributes,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 }))
70
71 return body.videoChannel
72 }
73
74 update (options: OverrideCommandOptions & {
75 channelName: string
76 attributes: VideoChannelUpdate
77 }) {
78 const { channelName, attributes } = options
79 const path = '/api/v1/video-channels/' + channelName
80
81 return this.putBodyRequest({
82 ...options,
83
84 path,
85 fields: attributes,
86 implicitToken: true,
87 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
88 })
89 }
90
91 delete (options: OverrideCommandOptions & {
92 channelName: string
93 }) {
94 const path = '/api/v1/video-channels/' + options.channelName
95
96 return this.deleteRequest({
97 ...options,
98
99 path,
100 implicitToken: true,
101 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
102 })
103 }
104
105 get (options: OverrideCommandOptions & {
106 channelName: string
107 }) {
108 const path = '/api/v1/video-channels/' + options.channelName
109
110 return this.getRequestBody<VideoChannel>({
111 ...options,
112
113 path,
114 implicitToken: false,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 })
117 }
118
119 updateImage (options: OverrideCommandOptions & {
120 fixture: string
121 channelName: string | number
122 type: 'avatar' | 'banner'
123 }) {
124 const { channelName, fixture, type } = options
125
126 const path = `/api/v1/video-channels/${channelName}/${type}/pick`
127
128 return this.updateImageRequest({
129 ...options,
130
131 path,
132 fixture,
133 fieldname: type + 'file',
134
135 implicitToken: true,
136 defaultExpectedStatus: HttpStatusCode.OK_200
137 })
138 }
139
140 deleteImage (options: OverrideCommandOptions & {
141 channelName: string | number
142 type: 'avatar' | 'banner'
143 }) {
144 const { channelName, type } = options
145
146 const path = `/api/v1/video-channels/${channelName}/${type}`
147
148 return this.deleteRequest({
149 ...options,
150
151 path,
152 implicitToken: true,
153 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
154 })
155 }
156}
diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts
new file mode 100644
index 000000000..756c47453
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/comments-command.ts b/shared/extra-utils/videos/comments-command.ts
new file mode 100644
index 000000000..f0d163a07
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/history-command.ts b/shared/extra-utils/videos/history-command.ts
new file mode 100644
index 000000000..13b7150c1
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/imports-command.ts b/shared/extra-utils/videos/imports-command.ts
new file mode 100644
index 000000000..e4944694d
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
new file mode 100644
index 000000000..26e663f46
--- /dev/null
+++ b/shared/extra-utils/videos/index.ts
@@ -0,0 +1,19 @@
1export * from './blacklist-command'
2export * from './captions-command'
3export * from './captions'
4export * from './change-ownership-command'
5export * from './channels'
6export * from './channels-command'
7export * from './comments-command'
8export * from './history-command'
9export * from './imports-command'
10export * from './live-command'
11export * from './live'
12export * from './playlists-command'
13export * from './playlists'
14export * from './services-command'
15export * from './streaming-playlists-command'
16export * from './streaming-playlists'
17export * from './comments-command'
18export * from './videos-command'
19export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
new file mode 100644
index 000000000..bf9486a05
--- /dev/null
+++ b/shared/extra-utils/videos/live-command.ts
@@ -0,0 +1,154 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readdir } from 'fs-extra'
4import { omit } from 'lodash'
5import { join } from 'path'
6import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
7import { wait } from '../miscs'
8import { unwrapBody } from '../requests'
9import { AbstractCommand, OverrideCommandOptions } from '../shared'
10import { sendRTMPStream, testFfmpegStreamError } from './live'
11
12export class LiveCommand extends AbstractCommand {
13
14 get (options: OverrideCommandOptions & {
15 videoId: number | string
16 }) {
17 const path = '/api/v1/videos/live'
18
19 return this.getRequestBody<LiveVideo>({
20 ...options,
21
22 path: path + '/' + options.videoId,
23 implicitToken: true,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 update (options: OverrideCommandOptions & {
29 videoId: number | string
30 fields: LiveVideoUpdate
31 }) {
32 const { videoId, fields } = options
33 const path = '/api/v1/videos/live'
34
35 return this.putBodyRequest({
36 ...options,
37
38 path: path + '/' + videoId,
39 fields,
40 implicitToken: true,
41 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
42 })
43 }
44
45 async create (options: OverrideCommandOptions & {
46 fields: LiveVideoCreate
47 }) {
48 const { fields } = options
49 const path = '/api/v1/videos/live'
50
51 const attaches: any = {}
52 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
53 if (fields.previewfile) attaches.previewfile = fields.previewfile
54
55 const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
56 ...options,
57
58 path,
59 attaches,
60 fields: omit(fields, 'thumbnailfile', 'previewfile'),
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 }))
64
65 return body.video
66 }
67
68 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
69 videoId: number | string
70 fixtureName?: string
71 }) {
72 const { videoId, fixtureName } = options
73 const videoLive = await this.get({ videoId })
74
75 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
76 }
77
78 async runAndTestStreamError (options: OverrideCommandOptions & {
79 videoId: number | string
80 shouldHaveError: boolean
81 }) {
82 const command = await this.sendRTMPStreamInVideo(options)
83
84 return testFfmpegStreamError(command, options.shouldHaveError)
85 }
86
87 waitUntilPublished (options: OverrideCommandOptions & {
88 videoId: number | string
89 }) {
90 const { videoId } = options
91 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
92 }
93
94 waitUntilWaiting (options: OverrideCommandOptions & {
95 videoId: number | string
96 }) {
97 const { videoId } = options
98 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
99 }
100
101 waitUntilEnded (options: OverrideCommandOptions & {
102 videoId: number | string
103 }) {
104 const { videoId } = options
105 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
106 }
107
108 waitUntilSegmentGeneration (options: OverrideCommandOptions & {
109 videoUUID: string
110 resolution: number
111 segment: number
112 }) {
113 const { resolution, segment, videoUUID } = options
114 const segmentName = `${resolution}-00000${segment}.ts`
115
116 return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
117 }
118
119 async waitUntilSaved (options: OverrideCommandOptions & {
120 videoId: number | string
121 }) {
122 let video: VideoDetails
123
124 do {
125 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
126
127 await wait(500)
128 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
129 }
130
131 async countPlaylists (options: OverrideCommandOptions & {
132 videoUUID: string
133 }) {
134 const basePath = this.server.servers.buildDirectory('streaming-playlists')
135 const hlsPath = join(basePath, 'hls', options.videoUUID)
136
137 const files = await readdir(hlsPath)
138
139 return files.filter(f => f.endsWith('.m3u8')).length
140 }
141
142 private async waitUntilState (options: OverrideCommandOptions & {
143 videoId: number | string
144 state: VideoState
145 }) {
146 let video: VideoDetails
147
148 do {
149 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
150
151 await wait(500)
152 } while (video.state.id !== options.state)
153 }
154}
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
index c0384769b..94f5f5b59 100644
--- a/shared/extra-utils/videos/live.ts
+++ b/shared/extra-utils/videos/live.ts
@@ -3,69 +3,9 @@
3import { expect } from 'chai' 3import { expect } from 'chai'
4import * as ffmpeg from 'fluent-ffmpeg' 4import * as ffmpeg from 'fluent-ffmpeg'
5import { pathExists, readdir } from 'fs-extra' 5import { pathExists, readdir } from 'fs-extra'
6import { omit } from 'lodash'
7import { join } from 'path' 6import { join } from 'path'
8import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' 7import { buildAbsoluteFixturePath, wait } from '../miscs'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { PeerTubeServer } from '../server/server'
10import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs'
11import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
12import { ServerInfo, waitUntilLog } from '../server/servers'
13import { getVideoWithToken } from './videos'
14
15function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
16 const path = '/api/v1/videos/live'
17
18 return makeGetRequest({
19 url,
20 token,
21 path: path + '/' + videoId,
22 statusCodeExpected
23 })
24}
25
26function updateLive (
27 url: string,
28 token: string,
29 videoId: number | string,
30 fields: LiveVideoUpdate,
31 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
32) {
33 const path = '/api/v1/videos/live'
34
35 return makePutBodyRequest({
36 url,
37 token,
38 path: path + '/' + videoId,
39 fields,
40 statusCodeExpected
41 })
42}
43
44function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) {
45 const path = '/api/v1/videos/live'
46
47 const attaches: any = {}
48 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
49 if (fields.previewfile) attaches.previewfile = fields.previewfile
50
51 const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
52
53 return makeUploadRequest({
54 url,
55 path,
56 token,
57 attaches,
58 fields: updatedFields,
59 statusCodeExpected
60 })
61}
62
63async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) {
64 const res = await getLive(url, token, videoId)
65 const videoLive = res.body as LiveVideo
66
67 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
68}
69 9
70function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') { 10function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
71 const fixture = buildAbsoluteFixturePath(fixtureName) 11 const fixture = buildAbsoluteFixturePath(fixtureName)
@@ -109,12 +49,6 @@ function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 1
109 }) 49 })
110} 50}
111 51
112async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) {
113 const command = await sendRTMPStreamInVideo(url, token, videoId)
114
115 return testFfmpegStreamError(command, shouldHaveError)
116}
117
118async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) { 52async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) {
119 let error: Error 53 let error: Error
120 54
@@ -136,53 +70,14 @@ async function stopFfmpeg (command: ffmpeg.FfmpegCommand) {
136 await wait(500) 70 await wait(500)
137} 71}
138 72
139function waitUntilLivePublished (url: string, token: string, videoId: number | string) { 73async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
140 return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED)
141}
142
143function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) {
144 return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE)
145}
146
147function waitUntilLiveEnded (url: string, token: string, videoId: number | string) {
148 return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED)
149}
150
151function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) {
152 const segmentName = `${resolutionNum}-00000${segmentNum}.ts`
153 return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false)
154}
155
156async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) {
157 let video: VideoDetails
158
159 do {
160 const res = await getVideoWithToken(url, token, videoId)
161 video = res.body
162
163 await wait(500)
164 } while (video.state.id !== state)
165}
166
167async function waitUntilLiveSaved (url: string, token: string, videoId: number | string) {
168 let video: VideoDetails
169
170 do {
171 const res = await getVideoWithToken(url, token, videoId)
172 video = res.body
173
174 await wait(500)
175 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
176}
177
178async function waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) {
179 for (const server of servers) { 74 for (const server of servers) {
180 await waitUntilLivePublished(server.url, server.accessToken, videoId) 75 await server.live.waitUntilPublished({ videoId })
181 } 76 }
182} 77}
183 78
184async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) { 79async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
185 const basePath = buildServerDirectory(server, 'streaming-playlists') 80 const basePath = server.servers.buildDirectory('streaming-playlists')
186 const hlsPath = join(basePath, 'hls', videoUUID) 81 const hlsPath = join(basePath, 'hls', videoUUID)
187 82
188 if (resolutions.length === 0) { 83 if (resolutions.length === 0) {
@@ -198,41 +93,25 @@ async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resoluti
198 expect(files).to.have.lengthOf(resolutions.length * 2 + 2) 93 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
199 94
200 for (const resolution of resolutions) { 95 for (const resolution of resolutions) {
201 expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) 96 const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
202 expect(files).to.contain(`${resolution}.m3u8`) 97 expect(fragmentedFile).to.exist
203 }
204
205 expect(files).to.contain('master.m3u8')
206 expect(files).to.contain('segments-sha256.json')
207}
208 98
209async function getPlaylistsCount (server: ServerInfo, videoUUID: string) { 99 const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
210 const basePath = buildServerDirectory(server, 'streaming-playlists') 100 expect(playlistFile).to.exist
211 const hlsPath = join(basePath, 'hls', videoUUID) 101 }
212 102
213 const files = await readdir(hlsPath) 103 const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
104 expect(masterPlaylistFile).to.exist
214 105
215 return files.filter(f => f.endsWith('.m3u8')).length 106 const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
107 expect(shaFile).to.exist
216} 108}
217 109
218// ---------------------------------------------------------------------------
219
220export { 110export {
221 getLive, 111 sendRTMPStream,
222 getPlaylistsCount,
223 waitUntilLiveSaved,
224 waitUntilLivePublished,
225 updateLive,
226 createLive,
227 runAndTestFfmpegStreamError,
228 checkLiveCleanup,
229 waitUntilLiveSegmentGeneration,
230 stopFfmpeg,
231 waitUntilLiveWaiting,
232 sendRTMPStreamInVideo,
233 waitUntilLiveEnded,
234 waitFfmpegUntilError, 112 waitFfmpegUntilError,
113 testFfmpegStreamError,
114 stopFfmpeg,
235 waitUntilLivePublishedOnAllServers, 115 waitUntilLivePublishedOnAllServers,
236 sendRTMPStream, 116 checkLiveCleanupAfterSave
237 testFfmpegStreamError
238} 117}
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
new file mode 100644
index 000000000..ce23900d3
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/playlists.ts b/shared/extra-utils/videos/playlists.ts
new file mode 100644
index 000000000..3dde52bb9
--- /dev/null
+++ b/shared/extra-utils/videos/playlists.ts
@@ -0,0 +1,25 @@
1import { expect } from 'chai'
2import { readdir } from 'fs-extra'
3import { join } from 'path'
4import { root } from '../miscs'
5
6async function checkPlaylistFilesWereRemoved (
7 playlistUUID: string,
8 internalServerNumber: number,
9 directories = [ 'thumbnails' ]
10) {
11 const testDirectory = 'test' + internalServerNumber
12
13 for (const directory of directories) {
14 const directoryPath = join(root(), testDirectory, directory)
15
16 const files = await readdir(directoryPath)
17 for (const file of files) {
18 expect(file).to.not.contain(playlistUUID)
19 }
20 }
21}
22
23export {
24 checkPlaylistFilesWereRemoved
25}
diff --git a/shared/extra-utils/videos/services-command.ts b/shared/extra-utils/videos/services-command.ts
new file mode 100644
index 000000000..06760df42
--- /dev/null
+++ b/shared/extra-utils/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/extra-utils/videos/services.ts b/shared/extra-utils/videos/services.ts
deleted file mode 100644
index e13a788bd..000000000
--- a/shared/extra-utils/videos/services.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
5 const path = '/services/oembed'
6 const query = {
7 url: oembedUrl,
8 format,
9 maxheight: maxHeight,
10 maxwidth: maxWidth
11 }
12
13 return request(url)
14 .get(path)
15 .query(query)
16 .set('Accept', 'application/json')
17 .expect(HttpStatusCode.OK_200)
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 getOEmbed
24}
diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
new file mode 100644
index 000000000..9662685da
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists-command.ts
@@ -0,0 +1,44 @@
1import { HttpStatusCode } from '@shared/models'
2import { unwrapBody, unwrapText } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class StreamingPlaylistsCommand extends AbstractCommand {
6
7 get (options: OverrideCommandOptions & {
8 url: string
9 }) {
10 return unwrapText(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 unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({
37 ...options,
38
39 url: options.url,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 }))
43 }
44}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
new file mode 100644
index 000000000..a224b8f5f
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists.ts
@@ -0,0 +1,78 @@
1import { expect } from 'chai'
2import { basename } from 'path'
3import { sha256 } from '@server/helpers/core-utils'
4import { removeFragmentedMP4Ext } from '@shared/core-utils'
5import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
6import { PeerTubeServer } from '../server'
7
8async function checkSegmentHash (options: {
9 server: PeerTubeServer
10 baseUrlPlaylist: string
11 baseUrlSegment: string
12 videoUUID: string
13 resolution: number
14 hlsPlaylist: VideoStreamingPlaylist
15}) {
16 const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
17 const command = server.streamingPlaylists
18
19 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
20 const videoName = basename(file.fileUrl)
21
22 const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
23
24 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
25
26 const length = parseInt(matches[1], 10)
27 const offset = parseInt(matches[2], 10)
28 const range = `${offset}-${offset + length - 1}`
29
30 const segmentBody = await command.getSegment({
31 url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
32 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
33 range: `bytes=${range}`
34 })
35
36 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
37 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
38}
39
40async function checkLiveSegmentHash (options: {
41 server: PeerTubeServer
42 baseUrlSegment: string
43 videoUUID: string
44 segmentName: string
45 hlsPlaylist: VideoStreamingPlaylist
46}) {
47 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
48 const command = server.streamingPlaylists
49
50 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
51 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
52
53 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
54}
55
56async function checkResolutionsInMasterPlaylist (options: {
57 server: PeerTubeServer
58 playlistUrl: string
59 resolutions: number[]
60}) {
61 const { server, playlistUrl, resolutions } = options
62
63 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
64
65 for (const resolution of resolutions) {
66 const reg = new RegExp(
67 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
68 )
69
70 expect(masterPlaylist).to.match(reg)
71 }
72}
73
74export {
75 checkSegmentHash,
76 checkLiveSegmentHash,
77 checkResolutionsInMasterPlaylist
78}
diff --git a/shared/extra-utils/videos/video-blacklist.ts b/shared/extra-utils/videos/video-blacklist.ts
deleted file mode 100644
index aa1548537..000000000
--- a/shared/extra-utils/videos/video-blacklist.ts
+++ /dev/null
@@ -1,79 +0,0 @@
1import * as request from 'supertest'
2import { VideoBlacklistType } from '../../models/videos'
3import { makeGetRequest } from '..'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function addVideoToBlacklist (
7 url: string,
8 token: string,
9 videoId: number | string,
10 reason?: string,
11 unfederate?: boolean,
12 specialStatus = HttpStatusCode.NO_CONTENT_204
13) {
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return request(url)
17 .post(path)
18 .send({ reason, unfederate })
19 .set('Accept', 'application/json')
20 .set('Authorization', 'Bearer ' + token)
21 .expect(specialStatus)
22}
23
24function updateVideoBlacklist (
25 url: string,
26 token: string,
27 videoId: number,
28 reason?: string,
29 specialStatus = HttpStatusCode.NO_CONTENT_204
30) {
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return request(url)
34 .put(path)
35 .send({ reason })
36 .set('Accept', 'application/json')
37 .set('Authorization', 'Bearer ' + token)
38 .expect(specialStatus)
39}
40
41function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
42 const path = '/api/v1/videos/' + videoId + '/blacklist'
43
44 return request(url)
45 .delete(path)
46 .set('Accept', 'application/json')
47 .set('Authorization', 'Bearer ' + token)
48 .expect(specialStatus)
49}
50
51function getBlacklistedVideosList (parameters: {
52 url: string
53 token: string
54 sort?: string
55 type?: VideoBlacklistType
56 specialStatus?: HttpStatusCode
57}) {
58 const { url, token, sort, type, specialStatus = HttpStatusCode.OK_200 } = parameters
59 const path = '/api/v1/videos/blacklist/'
60
61 const query = { sort, type }
62
63 return makeGetRequest({
64 url,
65 path,
66 query,
67 token,
68 statusCodeExpected: specialStatus
69 })
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 addVideoToBlacklist,
76 removeVideoFromBlacklist,
77 getBlacklistedVideosList,
78 updateVideoBlacklist
79}
diff --git a/shared/extra-utils/videos/video-captions.ts b/shared/extra-utils/videos/video-captions.ts
deleted file mode 100644
index 62eec7b90..000000000
--- a/shared/extra-utils/videos/video-captions.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7const expect = chai.expect
8
9function createVideoCaption (args: {
10 url: string
11 accessToken: string
12 videoId: string | number
13 language: string
14 fixture: string
15 mimeType?: string
16 statusCodeExpected?: number
17}) {
18 const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
19
20 const captionfile = buildAbsoluteFixturePath(args.fixture)
21 const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
22
23 return makeUploadRequest({
24 method: 'PUT',
25 url: args.url,
26 path,
27 token: args.accessToken,
28 fields: {},
29 attaches: {
30 captionfile: captionfileAttach
31 },
32 statusCodeExpected: args.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
33 })
34}
35
36function listVideoCaptions (url: string, videoId: string | number) {
37 const path = '/api/v1/videos/' + videoId + '/captions'
38
39 return makeGetRequest({
40 url,
41 path,
42 statusCodeExpected: HttpStatusCode.OK_200
43 })
44}
45
46function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
47 const path = '/api/v1/videos/' + videoId + '/captions/' + language
48
49 return makeDeleteRequest({
50 url,
51 token,
52 path,
53 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
54 })
55}
56
57async function testCaptionFile (url: string, captionPath: string, containsString: string) {
58 const res = await request(url)
59 .get(captionPath)
60 .expect(HttpStatusCode.OK_200)
61
62 expect(res.text).to.contain(containsString)
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 createVideoCaption,
69 listVideoCaptions,
70 testCaptionFile,
71 deleteVideoCaption
72}
diff --git a/shared/extra-utils/videos/video-change-ownership.ts b/shared/extra-utils/videos/video-change-ownership.ts
deleted file mode 100644
index ef82a7636..000000000
--- a/shared/extra-utils/videos/video-change-ownership.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function changeVideoOwnership (
5 url: string,
6 token: string,
7 videoId: number | string,
8 username,
9 expectedStatus = HttpStatusCode.NO_CONTENT_204
10) {
11 const path = '/api/v1/videos/' + videoId + '/give-ownership'
12
13 return request(url)
14 .post(path)
15 .set('Accept', 'application/json')
16 .set('Authorization', 'Bearer ' + token)
17 .send({ username })
18 .expect(expectedStatus)
19}
20
21function getVideoChangeOwnershipList (url: string, token: string) {
22 const path = '/api/v1/videos/ownership'
23
24 return request(url)
25 .get(path)
26 .query({ sort: '-createdAt' })
27 .set('Accept', 'application/json')
28 .set('Authorization', 'Bearer ' + token)
29 .expect(HttpStatusCode.OK_200)
30 .expect('Content-Type', /json/)
31}
32
33function acceptChangeOwnership (
34 url: string,
35 token: string,
36 ownershipId: string,
37 channelId: number,
38 expectedStatus = HttpStatusCode.NO_CONTENT_204
39) {
40 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
41
42 return request(url)
43 .post(path)
44 .set('Accept', 'application/json')
45 .set('Authorization', 'Bearer ' + token)
46 .send({ channelId })
47 .expect(expectedStatus)
48}
49
50function refuseChangeOwnership (
51 url: string,
52 token: string,
53 ownershipId: string,
54 expectedStatus = HttpStatusCode.NO_CONTENT_204
55) {
56 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
57
58 return request(url)
59 .post(path)
60 .set('Accept', 'application/json')
61 .set('Authorization', 'Bearer ' + token)
62 .expect(expectedStatus)
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 changeVideoOwnership,
69 getVideoChangeOwnershipList,
70 acceptChangeOwnership,
71 refuseChangeOwnership
72}
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts
deleted file mode 100644
index 0aab93e52..000000000
--- a/shared/extra-utils/videos/video-channels.ts
+++ /dev/null
@@ -1,192 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import * as request from 'supertest'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
6import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
7import { ServerInfo } from '../server/servers'
8import { MyUser, User } from '../../models/users/user.model'
9import { getMyUserInformation } from '../users/users'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
11
12function getVideoChannelsList (url: string, start: number, count: number, sort?: string, withStats?: boolean) {
13 const path = '/api/v1/video-channels'
14
15 const req = request(url)
16 .get(path)
17 .query({ start: start })
18 .query({ count: count })
19
20 if (sort) req.query({ sort })
21 if (withStats) req.query({ withStats })
22
23 return req.set('Accept', 'application/json')
24 .expect(HttpStatusCode.OK_200)
25 .expect('Content-Type', /json/)
26}
27
28function getAccountVideoChannelsList (parameters: {
29 url: string
30 accountName: string
31 start?: number
32 count?: number
33 sort?: string
34 specialStatus?: HttpStatusCode
35 withStats?: boolean
36 search?: string
37}) {
38 const {
39 url,
40 accountName,
41 start,
42 count,
43 sort = 'createdAt',
44 specialStatus = HttpStatusCode.OK_200,
45 withStats = false,
46 search
47 } = parameters
48
49 const path = '/api/v1/accounts/' + accountName + '/video-channels'
50
51 return makeGetRequest({
52 url,
53 path,
54 query: {
55 start,
56 count,
57 sort,
58 withStats,
59 search
60 },
61 statusCodeExpected: specialStatus
62 })
63}
64
65function addVideoChannel (
66 url: string,
67 token: string,
68 videoChannelAttributesArg: VideoChannelCreate,
69 expectedStatus = HttpStatusCode.OK_200
70) {
71 const path = '/api/v1/video-channels/'
72
73 // Default attributes
74 let attributes = {
75 displayName: 'my super video channel',
76 description: 'my super channel description',
77 support: 'my super channel support'
78 }
79 attributes = Object.assign(attributes, videoChannelAttributesArg)
80
81 return request(url)
82 .post(path)
83 .send(attributes)
84 .set('Accept', 'application/json')
85 .set('Authorization', 'Bearer ' + token)
86 .expect(expectedStatus)
87}
88
89function updateVideoChannel (
90 url: string,
91 token: string,
92 channelName: string,
93 attributes: VideoChannelUpdate,
94 expectedStatus = HttpStatusCode.NO_CONTENT_204
95) {
96 const body: any = {}
97 const path = '/api/v1/video-channels/' + channelName
98
99 if (attributes.displayName) body.displayName = attributes.displayName
100 if (attributes.description) body.description = attributes.description
101 if (attributes.support) body.support = attributes.support
102 if (attributes.bulkVideosSupportUpdate) body.bulkVideosSupportUpdate = attributes.bulkVideosSupportUpdate
103
104 return request(url)
105 .put(path)
106 .send(body)
107 .set('Accept', 'application/json')
108 .set('Authorization', 'Bearer ' + token)
109 .expect(expectedStatus)
110}
111
112function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
113 const path = '/api/v1/video-channels/' + channelName
114
115 return request(url)
116 .delete(path)
117 .set('Accept', 'application/json')
118 .set('Authorization', 'Bearer ' + token)
119 .expect(expectedStatus)
120}
121
122function getVideoChannel (url: string, channelName: string) {
123 const path = '/api/v1/video-channels/' + channelName
124
125 return request(url)
126 .get(path)
127 .set('Accept', 'application/json')
128 .expect(HttpStatusCode.OK_200)
129 .expect('Content-Type', /json/)
130}
131
132function updateVideoChannelImage (options: {
133 url: string
134 accessToken: string
135 fixture: string
136 videoChannelName: string | number
137 type: 'avatar' | 'banner'
138}) {
139 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}/pick`
140
141 return updateImageRequest({ ...options, path, fieldname: options.type + 'file' })
142}
143
144function deleteVideoChannelImage (options: {
145 url: string
146 accessToken: string
147 videoChannelName: string | number
148 type: 'avatar' | 'banner'
149}) {
150 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}`
151
152 return makeDeleteRequest({
153 url: options.url,
154 token: options.accessToken,
155 path,
156 statusCodeExpected: 204
157 })
158}
159
160function setDefaultVideoChannel (servers: ServerInfo[]) {
161 const tasks: Promise<any>[] = []
162
163 for (const server of servers) {
164 const p = getMyUserInformation(server.url, server.accessToken)
165 .then(res => { server.videoChannel = (res.body as User).videoChannels[0] })
166
167 tasks.push(p)
168 }
169
170 return Promise.all(tasks)
171}
172
173async function getDefaultVideoChannel (url: string, token: string) {
174 const res = await getMyUserInformation(url, token)
175
176 return (res.body as MyUser).videoChannels[0].id
177}
178
179// ---------------------------------------------------------------------------
180
181export {
182 updateVideoChannelImage,
183 getVideoChannelsList,
184 getAccountVideoChannelsList,
185 addVideoChannel,
186 updateVideoChannel,
187 deleteVideoChannel,
188 getVideoChannel,
189 setDefaultVideoChannel,
190 deleteVideoChannelImage,
191 getDefaultVideoChannel
192}
diff --git a/shared/extra-utils/videos/video-comments.ts b/shared/extra-utils/videos/video-comments.ts
deleted file mode 100644
index 71b9f875a..000000000
--- a/shared/extra-utils/videos/video-comments.ts
+++ /dev/null
@@ -1,138 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import * as request from 'supertest'
4import { makeDeleteRequest, makeGetRequest } from '../requests/requests'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getAdminVideoComments (options: {
8 url: string
9 token: string
10 start: number
11 count: number
12 sort?: string
13 isLocal?: boolean
14 search?: string
15 searchAccount?: string
16 searchVideo?: string
17}) {
18 const { url, token, start, count, sort, isLocal, search, searchAccount, searchVideo } = options
19 const path = '/api/v1/videos/comments'
20
21 const query = {
22 start,
23 count,
24 sort: sort || '-createdAt'
25 }
26
27 if (isLocal !== undefined) Object.assign(query, { isLocal })
28 if (search !== undefined) Object.assign(query, { search })
29 if (searchAccount !== undefined) Object.assign(query, { searchAccount })
30 if (searchVideo !== undefined) Object.assign(query, { searchVideo })
31
32 return makeGetRequest({
33 url,
34 path,
35 token,
36 query,
37 statusCodeExpected: HttpStatusCode.OK_200
38 })
39}
40
41function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
42 const path = '/api/v1/videos/' + videoId + '/comment-threads'
43
44 const req = request(url)
45 .get(path)
46 .query({ start: start })
47 .query({ count: count })
48
49 if (sort) req.query({ sort })
50 if (token) req.set('Authorization', 'Bearer ' + token)
51
52 return req.set('Accept', 'application/json')
53 .expect(HttpStatusCode.OK_200)
54 .expect('Content-Type', /json/)
55}
56
57function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
58 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
59
60 const req = request(url)
61 .get(path)
62 .set('Accept', 'application/json')
63
64 if (token) req.set('Authorization', 'Bearer ' + token)
65
66 return req.expect(HttpStatusCode.OK_200)
67 .expect('Content-Type', /json/)
68}
69
70function addVideoCommentThread (
71 url: string,
72 token: string,
73 videoId: number | string,
74 text: string,
75 expectedStatus = HttpStatusCode.OK_200
76) {
77 const path = '/api/v1/videos/' + videoId + '/comment-threads'
78
79 return request(url)
80 .post(path)
81 .send({ text })
82 .set('Accept', 'application/json')
83 .set('Authorization', 'Bearer ' + token)
84 .expect(expectedStatus)
85}
86
87function addVideoCommentReply (
88 url: string,
89 token: string,
90 videoId: number | string,
91 inReplyToCommentId: number,
92 text: string,
93 expectedStatus = HttpStatusCode.OK_200
94) {
95 const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
96
97 return request(url)
98 .post(path)
99 .send({ text })
100 .set('Accept', 'application/json')
101 .set('Authorization', 'Bearer ' + token)
102 .expect(expectedStatus)
103}
104
105async function findCommentId (url: string, videoId: number | string, text: string) {
106 const res = await getVideoCommentThreads(url, videoId, 0, 25, '-createdAt')
107
108 return res.body.data.find(c => c.text === text).id as number
109}
110
111function deleteVideoComment (
112 url: string,
113 token: string,
114 videoId: number | string,
115 commentId: number,
116 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
117) {
118 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
119
120 return makeDeleteRequest({
121 url,
122 path,
123 token,
124 statusCodeExpected
125 })
126}
127
128// ---------------------------------------------------------------------------
129
130export {
131 getVideoCommentThreads,
132 getAdminVideoComments,
133 getVideoThreadComments,
134 addVideoCommentThread,
135 addVideoCommentReply,
136 findCommentId,
137 deleteVideoComment
138}
diff --git a/shared/extra-utils/videos/video-history.ts b/shared/extra-utils/videos/video-history.ts
deleted file mode 100644
index b989e14dc..000000000
--- a/shared/extra-utils/videos/video-history.ts
+++ /dev/null
@@ -1,49 +0,0 @@
1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function userWatchVideo (
5 url: string,
6 token: string,
7 videoId: number | string,
8 currentTime: number,
9 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
10) {
11 const path = '/api/v1/videos/' + videoId + '/watching'
12 const fields = { currentTime }
13
14 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
15}
16
17function listMyVideosHistory (url: string, token: string, search?: string) {
18 const path = '/api/v1/users/me/history/videos'
19
20 return makeGetRequest({
21 url,
22 path,
23 token,
24 query: {
25 search
26 },
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
32 const path = '/api/v1/users/me/history/videos/remove'
33
34 return makePostBodyRequest({
35 url,
36 path,
37 token,
38 fields: beforeDate ? { beforeDate } : {},
39 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
40 })
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 userWatchVideo,
47 listMyVideosHistory,
48 removeMyVideosHistory
49}
diff --git a/shared/extra-utils/videos/video-imports.ts b/shared/extra-utils/videos/video-imports.ts
deleted file mode 100644
index 81c0163cb..000000000
--- a/shared/extra-utils/videos/video-imports.ts
+++ /dev/null
@@ -1,90 +0,0 @@
1
2import { VideoImportCreate } from '../../models/videos'
3import { makeGetRequest, makeUploadRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getYoutubeVideoUrl () {
7 return 'https://www.youtube.com/watch?v=msX3jv1XdvM'
8}
9
10function getYoutubeHDRVideoUrl () {
11 /**
12 * The video is used to check format-selection correctness wrt. HDR,
13 * which brings its own set of oddities outside of a MediaSource.
14 * FIXME: refactor once HDR is supported at playback
15 *
16 * The video needs to have the following format_ids:
17 * (which you can check by using `youtube-dl <url> -F`):
18 * - 303 (1080p webm vp9)
19 * - 299 (1080p mp4 avc1)
20 * - 335 (1080p webm vp9.2 HDR)
21 *
22 * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
23 * - 400 (1080p mp4 av01)
24 * - 315 (2160p webm vp9 HDR)
25 * - 337 (2160p webm vp9.2 HDR)
26 * - 401 (2160p mp4 av01 HDR)
27 */
28 return 'https://www.youtube.com/watch?v=qR5vOXbZsI4'
29}
30
31function getMagnetURI () {
32 // eslint-disable-next-line max-len
33 return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4'
34}
35
36function getBadVideoUrl () {
37 return 'https://download.cpy.re/peertube/bad_video.mp4'
38}
39
40function getGoodVideoUrl () {
41 return 'https://download.cpy.re/peertube/good_video.mp4'
42}
43
44function importVideo (
45 url: string,
46 token: string,
47 attributes: VideoImportCreate & { torrentfile?: string },
48 statusCodeExpected = HttpStatusCode.OK_200
49) {
50 const path = '/api/v1/videos/imports'
51
52 let attaches: any = {}
53 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
54
55 return makeUploadRequest({
56 url,
57 path,
58 token,
59 attaches,
60 fields: attributes,
61 statusCodeExpected
62 })
63}
64
65function getMyVideoImports (url: string, token: string, sort?: string) {
66 const path = '/api/v1/users/me/videos/imports'
67
68 const query = {}
69 if (sort) query['sort'] = sort
70
71 return makeGetRequest({
72 url,
73 query,
74 path,
75 token,
76 statusCodeExpected: HttpStatusCode.OK_200
77 })
78}
79
80// ---------------------------------------------------------------------------
81
82export {
83 getBadVideoUrl,
84 getYoutubeVideoUrl,
85 getYoutubeHDRVideoUrl,
86 importVideo,
87 getMagnetURI,
88 getMyVideoImports,
89 getGoodVideoUrl
90}
diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts
deleted file mode 100644
index c6f799e5d..000000000
--- a/shared/extra-utils/videos/video-playlists.ts
+++ /dev/null
@@ -1,320 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
2import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
3import { omit } from 'lodash'
4import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
5import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
6import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
7import { videoUUIDToId } from './videos'
8import { join } from 'path'
9import { root } from '..'
10import { readdir } from 'fs-extra'
11import { expect } from 'chai'
12import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
13import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
14
15function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
16 const path = '/api/v1/video-playlists'
17
18 const query = {
19 start,
20 count,
21 sort
22 }
23
24 return makeGetRequest({
25 url,
26 path,
27 query,
28 statusCodeExpected: HttpStatusCode.OK_200
29 })
30}
31
32function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
33 const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
34
35 const query = {
36 start,
37 count,
38 sort
39 }
40
41 return makeGetRequest({
42 url,
43 path,
44 query,
45 statusCodeExpected: HttpStatusCode.OK_200
46 })
47}
48
49function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string, search?: string) {
50 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
51
52 const query = {
53 start,
54 count,
55 sort,
56 search
57 }
58
59 return makeGetRequest({
60 url,
61 path,
62 query,
63 statusCodeExpected: HttpStatusCode.OK_200
64 })
65}
66
67function getAccountPlaylistsListWithToken (
68 url: string,
69 token: string,
70 accountName: string,
71 start: number,
72 count: number,
73 playlistType?: VideoPlaylistType,
74 sort?: string
75) {
76 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
77
78 const query = {
79 start,
80 count,
81 playlistType,
82 sort
83 }
84
85 return makeGetRequest({
86 url,
87 token,
88 path,
89 query,
90 statusCodeExpected: HttpStatusCode.OK_200
91 })
92}
93
94function getVideoPlaylist (url: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
95 const path = '/api/v1/video-playlists/' + playlistId
96
97 return makeGetRequest({
98 url,
99 path,
100 statusCodeExpected
101 })
102}
103
104function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
105 const path = '/api/v1/video-playlists/' + playlistId
106
107 return makeGetRequest({
108 url,
109 token,
110 path,
111 statusCodeExpected
112 })
113}
114
115function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
116 const path = '/api/v1/video-playlists/' + playlistId
117
118 return makeDeleteRequest({
119 url,
120 path,
121 token,
122 statusCodeExpected
123 })
124}
125
126function createVideoPlaylist (options: {
127 url: string
128 token: string
129 playlistAttrs: VideoPlaylistCreate
130 expectedStatus?: number
131}) {
132 const path = '/api/v1/video-playlists'
133
134 const fields = omit(options.playlistAttrs, 'thumbnailfile')
135
136 const attaches = options.playlistAttrs.thumbnailfile
137 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
138 : {}
139
140 return makeUploadRequest({
141 method: 'POST',
142 url: options.url,
143 path,
144 token: options.token,
145 fields,
146 attaches,
147 statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
148 })
149}
150
151function updateVideoPlaylist (options: {
152 url: string
153 token: string
154 playlistAttrs: VideoPlaylistUpdate
155 playlistId: number | string
156 expectedStatus?: number
157}) {
158 const path = '/api/v1/video-playlists/' + options.playlistId
159
160 const fields = omit(options.playlistAttrs, 'thumbnailfile')
161
162 const attaches = options.playlistAttrs.thumbnailfile
163 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
164 : {}
165
166 return makeUploadRequest({
167 method: 'PUT',
168 url: options.url,
169 path,
170 token: options.token,
171 fields,
172 attaches,
173 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
174 })
175}
176
177async function addVideoInPlaylist (options: {
178 url: string
179 token: string
180 playlistId: number | string
181 elementAttrs: VideoPlaylistElementCreate | { videoId: string }
182 expectedStatus?: number
183}) {
184 options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
185
186 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
187
188 return makePostBodyRequest({
189 url: options.url,
190 path,
191 token: options.token,
192 fields: options.elementAttrs,
193 statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
194 })
195}
196
197function updateVideoPlaylistElement (options: {
198 url: string
199 token: string
200 playlistId: number | string
201 playlistElementId: number | string
202 elementAttrs: VideoPlaylistElementUpdate
203 expectedStatus?: number
204}) {
205 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
206
207 return makePutBodyRequest({
208 url: options.url,
209 path,
210 token: options.token,
211 fields: options.elementAttrs,
212 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
213 })
214}
215
216function removeVideoFromPlaylist (options: {
217 url: string
218 token: string
219 playlistId: number | string
220 playlistElementId: number
221 expectedStatus?: number
222}) {
223 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
224
225 return makeDeleteRequest({
226 url: options.url,
227 path,
228 token: options.token,
229 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
230 })
231}
232
233function reorderVideosPlaylist (options: {
234 url: string
235 token: string
236 playlistId: number | string
237 elementAttrs: {
238 startPosition: number
239 insertAfterPosition: number
240 reorderLength?: number
241 }
242 expectedStatus?: number
243}) {
244 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
245
246 return makePostBodyRequest({
247 url: options.url,
248 path,
249 token: options.token,
250 fields: options.elementAttrs,
251 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
252 })
253}
254
255async function checkPlaylistFilesWereRemoved (
256 playlistUUID: string,
257 internalServerNumber: number,
258 directories = [ 'thumbnails' ]
259) {
260 const testDirectory = 'test' + internalServerNumber
261
262 for (const directory of directories) {
263 const directoryPath = join(root(), testDirectory, directory)
264
265 const files = await readdir(directoryPath)
266 for (const file of files) {
267 expect(file).to.not.contain(playlistUUID)
268 }
269 }
270}
271
272function getVideoPlaylistPrivacies (url: string) {
273 const path = '/api/v1/video-playlists/privacies'
274
275 return makeGetRequest({
276 url,
277 path,
278 statusCodeExpected: HttpStatusCode.OK_200
279 })
280}
281
282function doVideosExistInMyPlaylist (url: string, token: string, videoIds: number[]) {
283 const path = '/api/v1/users/me/video-playlists/videos-exist'
284
285 return makeGetRequest({
286 url,
287 token,
288 path,
289 query: { videoIds },
290 statusCodeExpected: HttpStatusCode.OK_200
291 })
292}
293
294// ---------------------------------------------------------------------------
295
296export {
297 getVideoPlaylistPrivacies,
298
299 getVideoPlaylistsList,
300 getVideoChannelPlaylistsList,
301 getAccountPlaylistsList,
302 getAccountPlaylistsListWithToken,
303
304 getVideoPlaylist,
305 getVideoPlaylistWithToken,
306
307 createVideoPlaylist,
308 updateVideoPlaylist,
309 deleteVideoPlaylist,
310
311 addVideoInPlaylist,
312 updateVideoPlaylistElement,
313 removeVideoFromPlaylist,
314
315 reorderVideosPlaylist,
316
317 checkPlaylistFilesWereRemoved,
318
319 doVideosExistInMyPlaylist
320}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
deleted file mode 100644
index 99c2e1880..000000000
--- a/shared/extra-utils/videos/video-streaming-playlists.ts
+++ /dev/null
@@ -1,82 +0,0 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getPlaylist (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
8 return makeRawRequest(url, statusCodeExpected)
9}
10
11function getSegment (url: string, statusCodeExpected = HttpStatusCode.OK_200, range?: string) {
12 return makeRawRequest(url, statusCodeExpected, range)
13}
14
15function getSegmentSha256 (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
16 return makeRawRequest(url, statusCodeExpected)
17}
18
19async function checkSegmentHash (
20 baseUrlPlaylist: string,
21 baseUrlSegment: string,
22 videoUUID: string,
23 resolution: number,
24 hlsPlaylist: VideoStreamingPlaylist
25) {
26 const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
27 const playlist = res.text
28
29 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
30
31 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
32
33 const length = parseInt(matches[1], 10)
34 const offset = parseInt(matches[2], 10)
35 const range = `${offset}-${offset + length - 1}`
36
37 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, HttpStatusCode.PARTIAL_CONTENT_206, `bytes=${range}`)
38
39 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
40
41 const sha256Server = resSha.body[videoName][range]
42 expect(sha256(res2.body)).to.equal(sha256Server)
43}
44
45async function checkLiveSegmentHash (
46 baseUrlSegment: string,
47 videoUUID: string,
48 segmentName: string,
49 hlsPlaylist: VideoStreamingPlaylist
50) {
51 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
52
53 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
54
55 const sha256Server = resSha.body[segmentName]
56 expect(sha256(res2.body)).to.equal(sha256Server)
57}
58
59async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
60 const res = await getPlaylist(playlistUrl)
61
62 const masterPlaylist = res.text
63
64 for (const resolution of resolutions) {
65 const reg = new RegExp(
66 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
67 )
68
69 expect(masterPlaylist).to.match(reg)
70 }
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 getPlaylist,
77 getSegment,
78 checkResolutionsInMasterPlaylist,
79 getSegmentSha256,
80 checkLiveSegmentHash,
81 checkSegmentHash
82}
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
new file mode 100644
index 000000000..33725bfdc
--- /dev/null
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -0,0 +1,599 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { createReadStream, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got'
6import { omit } from 'lodash'
7import validator from 'validator'
8import { buildUUID } from '@server/helpers/uuid'
9import { loadLanguages } from '@server/initializers/constants'
10import { pick } from '@shared/core-utils'
11import {
12 HttpStatusCode,
13 ResultList,
14 UserVideoRateType,
15 Video,
16 VideoCreate,
17 VideoCreateResult,
18 VideoDetails,
19 VideoFileMetadata,
20 VideoPrivacy,
21 VideosCommonQuery,
22 VideosWithSearchCommonQuery
23} from '@shared/models'
24import { buildAbsoluteFixturePath, wait } from '../miscs'
25import { unwrapBody } from '../requests'
26import { PeerTubeServer, waitJobs } from '../server'
27import { AbstractCommand, OverrideCommandOptions } from '../shared'
28
29export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
30 fixture?: string
31 thumbnailfile?: string
32 previewfile?: string
33}
34
35export class VideosCommand extends AbstractCommand {
36
37 constructor (server: PeerTubeServer) {
38 super(server)
39
40 loadLanguages()
41 }
42
43 getCategories (options: OverrideCommandOptions = {}) {
44 const path = '/api/v1/videos/categories'
45
46 return this.getRequestBody<{ [id: number]: string }>({
47 ...options,
48 path,
49
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 getLicences (options: OverrideCommandOptions = {}) {
56 const path = '/api/v1/videos/licences'
57
58 return this.getRequestBody<{ [id: number]: string }>({
59 ...options,
60 path,
61
62 implicitToken: false,
63 defaultExpectedStatus: HttpStatusCode.OK_200
64 })
65 }
66
67 getLanguages (options: OverrideCommandOptions = {}) {
68 const path = '/api/v1/videos/languages'
69
70 return this.getRequestBody<{ [id: string]: string }>({
71 ...options,
72 path,
73
74 implicitToken: false,
75 defaultExpectedStatus: HttpStatusCode.OK_200
76 })
77 }
78
79 getPrivacies (options: OverrideCommandOptions = {}) {
80 const path = '/api/v1/videos/privacies'
81
82 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
83 ...options,
84 path,
85
86 implicitToken: false,
87 defaultExpectedStatus: HttpStatusCode.OK_200
88 })
89 }
90
91 // ---------------------------------------------------------------------------
92
93 getDescription (options: OverrideCommandOptions & {
94 descriptionPath: string
95 }) {
96 return this.getRequestBody<{ description: string }>({
97 ...options,
98 path: options.descriptionPath,
99
100 implicitToken: false,
101 defaultExpectedStatus: HttpStatusCode.OK_200
102 })
103 }
104
105 getFileMetadata (options: OverrideCommandOptions & {
106 url: string
107 }) {
108 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
109 ...options,
110
111 url: options.url,
112 implicitToken: false,
113 defaultExpectedStatus: HttpStatusCode.OK_200
114 }))
115 }
116
117 // ---------------------------------------------------------------------------
118
119 view (options: OverrideCommandOptions & {
120 id: number | string
121 xForwardedFor?: string
122 }) {
123 const { id, xForwardedFor } = options
124 const path = '/api/v1/videos/' + id + '/views'
125
126 return this.postBodyRequest({
127 ...options,
128
129 path,
130 xForwardedFor,
131 implicitToken: false,
132 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
133 })
134 }
135
136 rate (options: OverrideCommandOptions & {
137 id: number | string
138 rating: UserVideoRateType
139 }) {
140 const { id, rating } = options
141 const path = '/api/v1/videos/' + id + '/rate'
142
143 return this.putBodyRequest({
144 ...options,
145
146 path,
147 fields: { rating },
148 implicitToken: true,
149 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
150 })
151 }
152
153 // ---------------------------------------------------------------------------
154
155 get (options: OverrideCommandOptions & {
156 id: number | string
157 }) {
158 const path = '/api/v1/videos/' + options.id
159
160 return this.getRequestBody<VideoDetails>({
161 ...options,
162
163 path,
164 implicitToken: false,
165 defaultExpectedStatus: HttpStatusCode.OK_200
166 })
167 }
168
169 getWithToken (options: OverrideCommandOptions & {
170 id: number | string
171 }) {
172 return this.get({
173 ...options,
174
175 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
176 })
177 }
178
179 async getId (options: OverrideCommandOptions & {
180 uuid: number | string
181 }) {
182 const { uuid } = options
183
184 if (validator.isUUID('' + uuid) === false) return uuid as number
185
186 const { id } = await this.get({ ...options, id: uuid })
187
188 return id
189 }
190
191 // ---------------------------------------------------------------------------
192
193 listMyVideos (options: OverrideCommandOptions & {
194 start?: number
195 count?: number
196 sort?: string
197 search?: string
198 isLive?: boolean
199 } = {}) {
200 const path = '/api/v1/users/me/videos'
201
202 return this.getRequestBody<ResultList<Video>>({
203 ...options,
204
205 path,
206 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
207 implicitToken: true,
208 defaultExpectedStatus: HttpStatusCode.OK_200
209 })
210 }
211
212 // ---------------------------------------------------------------------------
213
214 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
215 const path = '/api/v1/videos'
216
217 const query = this.buildListQuery(options)
218
219 return this.getRequestBody<ResultList<Video>>({
220 ...options,
221
222 path,
223 query: { sort: 'name', ...query },
224 implicitToken: false,
225 defaultExpectedStatus: HttpStatusCode.OK_200
226 })
227 }
228
229 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
230 return this.list({
231 ...options,
232
233 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
234 })
235 }
236
237 listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
238 handle: string
239 }) {
240 const { handle, search } = options
241 const path = '/api/v1/accounts/' + handle + '/videos'
242
243 return this.getRequestBody<ResultList<Video>>({
244 ...options,
245
246 path,
247 query: { search, ...this.buildListQuery(options) },
248 implicitToken: true,
249 defaultExpectedStatus: HttpStatusCode.OK_200
250 })
251 }
252
253 listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
254 handle: string
255 }) {
256 const { handle } = options
257 const path = '/api/v1/video-channels/' + handle + '/videos'
258
259 return this.getRequestBody<ResultList<Video>>({
260 ...options,
261
262 path,
263 query: this.buildListQuery(options),
264 implicitToken: true,
265 defaultExpectedStatus: HttpStatusCode.OK_200
266 })
267 }
268
269 // ---------------------------------------------------------------------------
270
271 async find (options: OverrideCommandOptions & {
272 name: string
273 }) {
274 const { data } = await this.list(options)
275
276 return data.find(v => v.name === options.name)
277 }
278
279 // ---------------------------------------------------------------------------
280
281 update (options: OverrideCommandOptions & {
282 id: number | string
283 attributes?: VideoEdit
284 }) {
285 const { id, attributes = {} } = options
286 const path = '/api/v1/videos/' + id
287
288 // Upload request
289 if (attributes.thumbnailfile || attributes.previewfile) {
290 const attaches: any = {}
291 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
292 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
293
294 return this.putUploadRequest({
295 ...options,
296
297 path,
298 fields: options.attributes,
299 attaches: {
300 thumbnailfile: attributes.thumbnailfile,
301 previewfile: attributes.previewfile
302 },
303 implicitToken: true,
304 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
305 })
306 }
307
308 return this.putBodyRequest({
309 ...options,
310
311 path,
312 fields: options.attributes,
313 implicitToken: true,
314 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
315 })
316 }
317
318 remove (options: OverrideCommandOptions & {
319 id: number | string
320 }) {
321 const path = '/api/v1/videos/' + options.id
322
323 return unwrapBody(this.deleteRequest({
324 ...options,
325
326 path,
327 implicitToken: true,
328 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
329 }))
330 }
331
332 async removeAll () {
333 const { data } = await this.list()
334
335 for (const v of data) {
336 await this.remove({ id: v.id })
337 }
338 }
339
340 // ---------------------------------------------------------------------------
341
342 async upload (options: OverrideCommandOptions & {
343 attributes?: VideoEdit
344 mode?: 'legacy' | 'resumable' // default legacy
345 } = {}) {
346 const { mode = 'legacy' } = options
347 let defaultChannelId = 1
348
349 try {
350 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
351 defaultChannelId = videoChannels[0].id
352 } catch (e) { /* empty */ }
353
354 // Override default attributes
355 const attributes = {
356 name: 'my super video',
357 category: 5,
358 licence: 4,
359 language: 'zh',
360 channelId: defaultChannelId,
361 nsfw: true,
362 waitTranscoding: false,
363 description: 'my super description',
364 support: 'my super support text',
365 tags: [ 'tag' ],
366 privacy: VideoPrivacy.PUBLIC,
367 commentsEnabled: true,
368 downloadEnabled: true,
369 fixture: 'video_short.webm',
370
371 ...options.attributes
372 }
373
374 const created = mode === 'legacy'
375 ? await this.buildLegacyUpload({ ...options, attributes })
376 : await this.buildResumeUpload({ ...options, attributes })
377
378 // Wait torrent generation
379 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
380 if (expectedStatus === HttpStatusCode.OK_200) {
381 let video: VideoDetails
382
383 do {
384 video = await this.getWithToken({ ...options, id: created.uuid })
385
386 await wait(50)
387 } while (!video.files[0].torrentUrl)
388 }
389
390 return created
391 }
392
393 async buildLegacyUpload (options: OverrideCommandOptions & {
394 attributes: VideoEdit
395 }): Promise<VideoCreateResult> {
396 const path = '/api/v1/videos/upload'
397
398 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
399 ...options,
400
401 path,
402 fields: this.buildUploadFields(options.attributes),
403 attaches: this.buildUploadAttaches(options.attributes),
404 implicitToken: true,
405 defaultExpectedStatus: HttpStatusCode.OK_200
406 })).then(body => body.video || body as any)
407 }
408
409 async buildResumeUpload (options: OverrideCommandOptions & {
410 attributes: VideoEdit
411 }): Promise<VideoCreateResult> {
412 const { attributes, expectedStatus } = options
413
414 let size = 0
415 let videoFilePath: string
416 let mimetype = 'video/mp4'
417
418 if (attributes.fixture) {
419 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
420 size = (await stat(videoFilePath)).size
421
422 if (videoFilePath.endsWith('.mkv')) {
423 mimetype = 'video/x-matroska'
424 } else if (videoFilePath.endsWith('.webm')) {
425 mimetype = 'video/webm'
426 }
427 }
428
429 // Do not check status automatically, we'll check it manually
430 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
431 const initStatus = initializeSessionRes.status
432
433 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
434 const locationHeader = initializeSessionRes.header['location']
435 expect(locationHeader).to.not.be.undefined
436
437 const pathUploadId = locationHeader.split('?')[1]
438
439 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
440
441 return result.body?.video || result.body as any
442 }
443
444 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
445 ? HttpStatusCode.CREATED_201
446 : expectedStatus
447
448 expect(initStatus).to.equal(expectedInitStatus)
449
450 return initializeSessionRes.body.video || initializeSessionRes.body
451 }
452
453 async prepareResumableUpload (options: OverrideCommandOptions & {
454 attributes: VideoEdit
455 size: number
456 mimetype: string
457 }) {
458 const { attributes, size, mimetype } = options
459
460 const path = '/api/v1/videos/upload-resumable'
461
462 return this.postUploadRequest({
463 ...options,
464
465 path,
466 headers: {
467 'X-Upload-Content-Type': mimetype,
468 'X-Upload-Content-Length': size.toString()
469 },
470 fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
471 // Fixture will be sent later
472 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
473 implicitToken: true,
474
475 defaultExpectedStatus: null
476 })
477 }
478
479 sendResumableChunks (options: OverrideCommandOptions & {
480 pathUploadId: string
481 videoFilePath: string
482 size: number
483 contentLength?: number
484 contentRangeBuilder?: (start: number, chunk: any) => string
485 }) {
486 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
487
488 const path = '/api/v1/videos/upload-resumable'
489 let start = 0
490
491 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
492 const url = this.server.url
493
494 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
495 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
496 readable.on('data', async function onData (chunk) {
497 readable.pause()
498
499 const headers = {
500 'Authorization': 'Bearer ' + token,
501 'Content-Type': 'application/octet-stream',
502 'Content-Range': contentRangeBuilder
503 ? contentRangeBuilder(start, chunk)
504 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
505 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
506 }
507
508 const res = await got<{ video: VideoCreateResult }>({
509 url,
510 method: 'put',
511 headers,
512 path: path + '?' + pathUploadId,
513 body: chunk,
514 responseType: 'json',
515 throwHttpErrors: false
516 })
517
518 start += chunk.length
519
520 if (res.statusCode === expectedStatus) {
521 return resolve(res)
522 }
523
524 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
525 readable.off('data', onData)
526 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
527 }
528
529 readable.resume()
530 })
531 })
532 }
533
534 quickUpload (options: OverrideCommandOptions & {
535 name: string
536 nsfw?: boolean
537 privacy?: VideoPrivacy
538 fixture?: string
539 }) {
540 const attributes: VideoEdit = { name: options.name }
541 if (options.nsfw) attributes.nsfw = options.nsfw
542 if (options.privacy) attributes.privacy = options.privacy
543 if (options.fixture) attributes.fixture = options.fixture
544
545 return this.upload({ ...options, attributes })
546 }
547
548 async randomUpload (options: OverrideCommandOptions & {
549 wait?: boolean // default true
550 additionalParams?: VideoEdit & { prefixName?: string }
551 } = {}) {
552 const { wait = true, additionalParams } = options
553 const prefixName = additionalParams?.prefixName || ''
554 const name = prefixName + buildUUID()
555
556 const attributes = { name, ...additionalParams }
557
558 const result = await this.upload({ ...options, attributes })
559
560 if (wait) await waitJobs([ this.server ])
561
562 return { ...result, name }
563 }
564
565 // ---------------------------------------------------------------------------
566
567 private buildListQuery (options: VideosCommonQuery) {
568 return pick(options, [
569 'start',
570 'count',
571 'sort',
572 'nsfw',
573 'isLive',
574 'categoryOneOf',
575 'licenceOneOf',
576 'languageOneOf',
577 'tagsOneOf',
578 'tagsAllOf',
579 'filter',
580 'skipCount'
581 ])
582 }
583
584 private buildUploadFields (attributes: VideoEdit) {
585 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
586 }
587
588 private buildUploadAttaches (attributes: VideoEdit) {
589 const attaches: { [ name: string ]: string } = {}
590
591 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
592 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
593 }
594
595 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
596
597 return attaches
598 }
599}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 469ea4d63..a1d2ba0fc 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -1,646 +1,92 @@
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' 5import { basename, join } from 'path'
6import * as parseTorrent from 'parse-torrent'
7import { 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' 7import { uuidRegex } from '@shared/core-utils'
12import { HttpStatusCode } from '@shared/core-utils' 8import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
13import { VideosCommonQuery } from '@shared/models' 9import { 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' 10import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
15import { VideoDetails, VideoPrivacy } from '../../models/videos' 11import { makeRawRequest } from '../requests/requests'
16import { 12import { waitJobs } from '../server'
17 buildAbsoluteFixturePath, 13import { PeerTubeServer } from '../server/server'
18 buildServerDirectory, 14import { VideoEdit } from './videos-command'
19 dateIsValid, 15
20 immutableAssign, 16async function checkVideoFilesWereRemoved (options: {
21 testImage, 17 server: PeerTubeServer
22 wait, 18 video: VideoDetails
23 webtorrentAdd 19 captions?: VideoCaption[]
24} from '../miscs/miscs' 20 onlyVideoFiles?: boolean // default false
25import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests' 21}) {
26import { waitJobs } from '../server/jobs' 22 const { video, server, captions = [], onlyVideoFiles = false } = options
27import { ServerInfo } from '../server/servers'
28import { getMyUserInformation } from '../users/users'
29
30loadLanguages()
31
32type VideoAttributes = {
33 name?: string
34 category?: number
35 licence?: number
36 language?: string
37 nsfw?: boolean
38 commentsEnabled?: boolean
39 downloadEnabled?: boolean
40 waitTranscoding?: boolean
41 description?: string
42 originallyPublishedAt?: string
43 tags?: string[]
44 channelId?: number
45 privacy?: VideoPrivacy
46 fixture?: string
47 support?: string
48 thumbnailfile?: string
49 previewfile?: string
50 scheduleUpdate?: {
51 updateAt: string
52 privacy?: VideoPrivacy
53 }
54}
55
56function getVideoCategories (url: string) {
57 const path = '/api/v1/videos/categories'
58
59 return makeGetRequest({
60 url,
61 path,
62 statusCodeExpected: HttpStatusCode.OK_200
63 })
64}
65
66function getVideoLicences (url: string) {
67 const path = '/api/v1/videos/licences'
68
69 return makeGetRequest({
70 url,
71 path,
72 statusCodeExpected: HttpStatusCode.OK_200
73 })
74}
75
76function getVideoLanguages (url: string) {
77 const path = '/api/v1/videos/languages'
78
79 return makeGetRequest({
80 url,
81 path,
82 statusCodeExpected: HttpStatusCode.OK_200
83 })
84}
85
86function getVideoPrivacies (url: string) {
87 const path = '/api/v1/videos/privacies'
88
89 return makeGetRequest({
90 url,
91 path,
92 statusCodeExpected: HttpStatusCode.OK_200
93 })
94}
95
96function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
97 const path = '/api/v1/videos/' + id
98
99 return request(url)
100 .get(path)
101 .set('Accept', 'application/json')
102 .expect(expectedStatus)
103}
104 23
105async function getVideoIdFromUUID (url: string, uuid: string) { 24 const webtorrentFiles = video.files || []
106 const res = await getVideo(url, uuid) 25 const hlsFiles = video.streamingPlaylists[0]?.files || []
107 26
108 return res.body.id 27 const thumbnailName = basename(video.thumbnailPath)
109} 28 const previewName = basename(video.previewPath)
110 29
111function getVideoFileMetadataUrl (url: string) { 30 const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
112 return request(url)
113 .get('/')
114 .set('Accept', 'application/json')
115 .expect(HttpStatusCode.OK_200)
116 .expect('Content-Type', /json/)
117}
118 31
119function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) { 32 const captionNames = captions.map(c => basename(c.captionPath))
120 const path = '/api/v1/videos/' + id + '/views'
121 33
122 const req = request(url) 34 const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl))
123 .post(path) 35 const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
124 .set('Accept', 'application/json')
125 36
126 if (xForwardedFor) { 37 let directories: { [ directory: string ]: string[] } = {
127 req.set('X-Forwarded-For', xForwardedFor) 38 videos: webtorrentFilenames,
39 redundancy: webtorrentFilenames,
40 [join('playlists', 'hls')]: hlsFilenames,
41 [join('redundancy', 'hls')]: hlsFilenames
128 } 42 }
129 43
130 return req.expect(expectedStatus) 44 if (onlyVideoFiles !== true) {
131} 45 directories = {
132 46 ...directories,
133function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
134 const path = '/api/v1/videos/' + id
135
136 return request(url)
137 .get(path)
138 .set('Authorization', 'Bearer ' + token)
139 .set('Accept', 'application/json')
140 .expect(expectedStatus)
141}
142
143function getVideoDescription (url: string, descriptionPath: string) {
144 return request(url)
145 .get(descriptionPath)
146 .set('Accept', 'application/json')
147 .expect(HttpStatusCode.OK_200)
148 .expect('Content-Type', /json/)
149}
150
151function getVideosList (url: string) {
152 const path = '/api/v1/videos'
153
154 return request(url)
155 .get(path)
156 .query({ sort: 'name' })
157 .set('Accept', 'application/json')
158 .expect(HttpStatusCode.OK_200)
159 .expect('Content-Type', /json/)
160}
161
162function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
163 const path = '/api/v1/videos'
164
165 return request(url)
166 .get(path)
167 .set('Authorization', 'Bearer ' + token)
168 .query(immutableAssign(query, { sort: 'name' }))
169 .set('Accept', 'application/json')
170 .expect(HttpStatusCode.OK_200)
171 .expect('Content-Type', /json/)
172}
173
174function getLocalVideos (url: string) {
175 const path = '/api/v1/videos'
176
177 return request(url)
178 .get(path)
179 .query({ sort: 'name', filter: 'local' })
180 .set('Accept', 'application/json')
181 .expect(HttpStatusCode.OK_200)
182 .expect('Content-Type', /json/)
183}
184
185function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
186 const path = '/api/v1/users/me/videos'
187
188 const req = request(url)
189 .get(path)
190 .query({ start: start })
191 .query({ count: count })
192 .query({ search: search })
193
194 if (sort) req.query({ sort })
195
196 return req.set('Accept', 'application/json')
197 .set('Authorization', 'Bearer ' + accessToken)
198 .expect(HttpStatusCode.OK_200)
199 .expect('Content-Type', /json/)
200}
201
202function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
203 const path = '/api/v1/users/me/videos'
204
205 return makeGetRequest({
206 url,
207 path,
208 token: accessToken,
209 query,
210 statusCodeExpected: HttpStatusCode.OK_200
211 })
212}
213
214function getAccountVideos (
215 url: string,
216 accessToken: string,
217 accountName: string,
218 start: number,
219 count: number,
220 sort?: string,
221 query: {
222 nsfw?: boolean
223 search?: string
224 } = {}
225) {
226 const path = '/api/v1/accounts/' + accountName + '/videos'
227
228 return makeGetRequest({
229 url,
230 path,
231 query: immutableAssign(query, {
232 start,
233 count,
234 sort
235 }),
236 token: accessToken,
237 statusCodeExpected: HttpStatusCode.OK_200
238 })
239}
240
241function getVideoChannelVideos (
242 url: string,
243 accessToken: string,
244 videoChannelName: string,
245 start: number,
246 count: number,
247 sort?: string,
248 query: { nsfw?: boolean } = {}
249) {
250 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
251
252 return makeGetRequest({
253 url,
254 path,
255 query: immutableAssign(query, {
256 start,
257 count,
258 sort
259 }),
260 token: accessToken,
261 statusCodeExpected: HttpStatusCode.OK_200
262 })
263}
264
265function getPlaylistVideos (
266 url: string,
267 accessToken: string,
268 playlistId: number | string,
269 start: number,
270 count: number,
271 query: { nsfw?: boolean } = {}
272) {
273 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
274
275 return makeGetRequest({
276 url,
277 path,
278 query: immutableAssign(query, {
279 start,
280 count
281 }),
282 token: accessToken,
283 statusCodeExpected: HttpStatusCode.OK_200
284 })
285}
286
287function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
288 const path = '/api/v1/videos'
289
290 const req = request(url)
291 .get(path)
292 .query({ start: start })
293 .query({ count: count })
294
295 if (sort) req.query({ sort })
296 if (skipCount) req.query({ skipCount })
297
298 return req.set('Accept', 'application/json')
299 .expect(HttpStatusCode.OK_200)
300 .expect('Content-Type', /json/)
301}
302
303function getVideosListSort (url: string, sort: string) {
304 const path = '/api/v1/videos'
305
306 return request(url)
307 .get(path)
308 .query({ sort: sort })
309 .set('Accept', 'application/json')
310 .expect(HttpStatusCode.OK_200)
311 .expect('Content-Type', /json/)
312}
313
314function getVideosWithFilters (url: string, query: VideosCommonQuery) {
315 const path = '/api/v1/videos'
316
317 return request(url)
318 .get(path)
319 .query(query)
320 .set('Accept', 'application/json')
321 .expect(HttpStatusCode.OK_200)
322 .expect('Content-Type', /json/)
323}
324
325function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
326 const path = '/api/v1/videos'
327
328 return request(url)
329 .delete(path + '/' + id)
330 .set('Accept', 'application/json')
331 .set('Authorization', 'Bearer ' + token)
332 .expect(expectedStatus)
333}
334 47
335async function removeAllVideos (server: ServerInfo) { 48 thumbnails: [ thumbnailName ],
336 const resVideos = await getVideosList(server.url) 49 previews: [ previewName ],
337 50 torrents: torrentNames,
338 for (const v of resVideos.body.data) { 51 captions: captionNames
339 await removeVideo(server.url, server.accessToken, v.id) 52 }
340 } 53 }
341}
342 54
343async function checkVideoFilesWereRemoved ( 55 for (const directory of Object.keys(directories)) {
344 videoUUID: string, 56 const directoryPath = server.servers.buildDirectory(directory)
345 serverNumber: number,
346 directories = [
347 'redundancy',
348 'videos',
349 'thumbnails',
350 'torrents',
351 'previews',
352 'captions',
353 join('playlists', 'hls'),
354 join('redundancy', 'hls')
355 ]
356) {
357 for (const directory of directories) {
358 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
359 57
360 const directoryExists = await pathExists(directoryPath) 58 const directoryExists = await pathExists(directoryPath)
361 if (directoryExists === false) continue 59 if (directoryExists === false) continue
362 60
363 const files = await readdir(directoryPath) 61 const existingFiles = await readdir(directoryPath)
364 for (const file of files) { 62 for (const existingFile of existingFiles) {
365 expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID) 63 for (const shouldNotExist of directories[directory]) {
64 expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
65 }
366 } 66 }
367 } 67 }
368} 68}
369 69
370async function uploadVideo ( 70async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
371 url: string, 71 for (const server of servers) {
372 accessToken: string, 72 server.store.videoDetails = await server.videos.get({ id: uuid })
373 videoAttributesArg: VideoAttributes,
374 specialStatus = HttpStatusCode.OK_200,
375 mode: 'legacy' | 'resumable' = 'legacy'
376) {
377 let defaultChannelId = '1'
378
379 try {
380 const res = await getMyUserInformation(url, accessToken)
381 defaultChannelId = res.body.videoChannels[0].id
382 } catch (e) { /* empty */ }
383
384 // Override default attributes
385 const attributes = Object.assign({
386 name: 'my super video',
387 category: 5,
388 licence: 4,
389 language: 'zh',
390 channelId: defaultChannelId,
391 nsfw: true,
392 waitTranscoding: false,
393 description: 'my super description',
394 support: 'my super support text',
395 tags: [ 'tag' ],
396 privacy: VideoPrivacy.PUBLIC,
397 commentsEnabled: true,
398 downloadEnabled: true,
399 fixture: 'video_short.webm'
400 }, videoAttributesArg)
401
402 const res = mode === 'legacy'
403 ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
404 : await buildResumeUpload(url, accessToken, attributes, specialStatus)
405
406 // Wait torrent generation
407 if (specialStatus === HttpStatusCode.OK_200) {
408 let video: VideoDetails
409 do {
410 const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
411 video = resVideo.body
412
413 await wait(50)
414 } while (!video.files[0].torrentUrl)
415 } 73 }
416
417 return res
418} 74}
419 75
420function checkUploadVideoParam ( 76function checkUploadVideoParam (
421 url: string, 77 server: PeerTubeServer,
422 token: string, 78 token: string,
423 attributes: Partial<VideoAttributes>, 79 attributes: Partial<VideoEdit>,
424 specialStatus = HttpStatusCode.OK_200, 80 expectedStatus = HttpStatusCode.OK_200,
425 mode: 'legacy' | 'resumable' = 'legacy' 81 mode: 'legacy' | 'resumable' = 'legacy'
426) { 82) {
427 return mode === 'legacy' 83 return mode === 'legacy'
428 ? buildLegacyUpload(url, token, attributes, specialStatus) 84 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
429 : buildResumeUpload(url, token, attributes, specialStatus) 85 : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
430}
431
432async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
433 const path = '/api/v1/videos/upload'
434 const req = request(url)
435 .post(path)
436 .set('Accept', 'application/json')
437 .set('Authorization', 'Bearer ' + token)
438
439 buildUploadReq(req, attributes)
440
441 if (attributes.fixture !== undefined) {
442 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
443 }
444
445 return req.expect(specialStatus)
446}
447
448async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
449 let size = 0
450 let videoFilePath: string
451 let mimetype = 'video/mp4'
452
453 if (attributes.fixture) {
454 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
455 size = (await stat(videoFilePath)).size
456
457 if (videoFilePath.endsWith('.mkv')) {
458 mimetype = 'video/x-matroska'
459 } else if (videoFilePath.endsWith('.webm')) {
460 mimetype = 'video/webm'
461 }
462 }
463
464 const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
465 const initStatus = initializeSessionRes.status
466
467 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
468 const locationHeader = initializeSessionRes.header['location']
469 expect(locationHeader).to.not.be.undefined
470
471 const pathUploadId = locationHeader.split('?')[1]
472
473 return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
474 }
475
476 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
477 ? HttpStatusCode.CREATED_201
478 : specialStatus
479
480 expect(initStatus).to.equal(expectedInitStatus)
481
482 return initializeSessionRes
483}
484
485async function prepareResumableUpload (options: {
486 url: string
487 token: string
488 attributes: VideoAttributes
489 size: number
490 mimetype: string
491}) {
492 const { url, token, attributes, size, mimetype } = options
493
494 const path = '/api/v1/videos/upload-resumable'
495
496 const req = request(url)
497 .post(path)
498 .set('Authorization', 'Bearer ' + token)
499 .set('X-Upload-Content-Type', mimetype)
500 .set('X-Upload-Content-Length', size.toString())
501
502 buildUploadReq(req, attributes)
503
504 if (attributes.fixture) {
505 req.field('filename', attributes.fixture)
506 }
507
508 return req
509}
510
511function sendResumableChunks (options: {
512 url: string
513 token: string
514 pathUploadId: string
515 videoFilePath: string
516 size: number
517 specialStatus?: HttpStatusCode
518 contentLength?: number
519 contentRangeBuilder?: (start: number, chunk: any) => string
520}) {
521 const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
522
523 const expectedStatus = specialStatus || HttpStatusCode.OK_200
524
525 const path = '/api/v1/videos/upload-resumable'
526 let start = 0
527
528 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
529 return new Promise<GotResponse>((resolve, reject) => {
530 readable.on('data', async function onData (chunk) {
531 readable.pause()
532
533 const headers = {
534 'Authorization': 'Bearer ' + token,
535 'Content-Type': 'application/octet-stream',
536 'Content-Range': contentRangeBuilder
537 ? contentRangeBuilder(start, chunk)
538 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
539 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
540 }
541
542 const res = await got({
543 url,
544 method: 'put',
545 headers,
546 path: path + '?' + pathUploadId,
547 body: chunk,
548 responseType: 'json',
549 throwHttpErrors: false
550 })
551
552 start += chunk.length
553
554 if (res.statusCode === expectedStatus) {
555 return resolve(res)
556 }
557
558 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
559 readable.off('data', onData)
560 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
561 }
562
563 readable.resume()
564 })
565 })
566}
567
568function updateVideo (
569 url: string,
570 accessToken: string,
571 id: number | string,
572 attributes: VideoAttributes,
573 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
574) {
575 const path = '/api/v1/videos/' + id
576 const body = {}
577
578 if (attributes.name) body['name'] = attributes.name
579 if (attributes.category) body['category'] = attributes.category
580 if (attributes.licence) body['licence'] = attributes.licence
581 if (attributes.language) body['language'] = attributes.language
582 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
583 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
584 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
585 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
586 if (attributes.description) body['description'] = attributes.description
587 if (attributes.tags) body['tags'] = attributes.tags
588 if (attributes.privacy) body['privacy'] = attributes.privacy
589 if (attributes.channelId) body['channelId'] = attributes.channelId
590 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
591
592 // Upload request
593 if (attributes.thumbnailfile || attributes.previewfile) {
594 const attaches: any = {}
595 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
596 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
597
598 return makeUploadRequest({
599 url,
600 method: 'PUT',
601 path,
602 token: accessToken,
603 fields: body,
604 attaches,
605 statusCodeExpected
606 })
607 }
608
609 return makePutBodyRequest({
610 url,
611 path,
612 fields: body,
613 token: accessToken,
614 statusCodeExpected
615 })
616}
617
618function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
619 const path = '/api/v1/videos/' + id + '/rate'
620
621 return request(url)
622 .put(path)
623 .set('Accept', 'application/json')
624 .set('Authorization', 'Bearer ' + accessToken)
625 .send({ rating })
626 .expect(specialStatus)
627}
628
629function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
630 return new Promise<any>((res, rej) => {
631 const torrentName = videoUUID + '-' + resolution + '.torrent'
632 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
633
634 readFile(torrentPath, (err, data) => {
635 if (err) return rej(err)
636
637 return res(parseTorrent(data))
638 })
639 })
640} 86}
641 87
642async function completeVideoCheck ( 88async function completeVideoCheck (
643 url: string, 89 server: PeerTubeServer,
644 video: any, 90 video: any,
645 attributes: { 91 attributes: {
646 name: string 92 name: string
@@ -682,7 +128,7 @@ async function completeVideoCheck (
682 if (!attributes.likes) attributes.likes = 0 128 if (!attributes.likes) attributes.likes = 0
683 if (!attributes.dislikes) attributes.dislikes = 0 129 if (!attributes.dislikes) attributes.dislikes = 0
684 130
685 const host = new URL(url).host 131 const host = new URL(server.url).host
686 const originHost = attributes.account.host 132 const originHost = attributes.account.host
687 133
688 expect(video.name).to.equal(attributes.name) 134 expect(video.name).to.equal(attributes.name)
@@ -719,8 +165,7 @@ async function completeVideoCheck (
719 expect(video.originallyPublishedAt).to.be.null 165 expect(video.originallyPublishedAt).to.be.null
720 } 166 }
721 167
722 const res = await getVideo(url, video.uuid) 168 const videoDetails = await server.videos.get({ id: video.uuid })
723 const videoDetails: VideoDetails = res.body
724 169
725 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 170 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
726 expect(videoDetails.tags).to.deep.equal(attributes.tags) 171 expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -745,18 +190,16 @@ async function completeVideoCheck (
745 190
746 expect(file.magnetUri).to.have.lengthOf.above(2) 191 expect(file.magnetUri).to.have.lengthOf.above(2)
747 192
748 expect(file.torrentDownloadUrl).to.equal(`http://${host}/download/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) 193 expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
749 expect(file.torrentUrl).to.equal(`http://${host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) 194 expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
750 195
751 expect(file.fileUrl).to.equal(`http://${originHost}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`) 196 expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
752 expect(file.fileDownloadUrl).to.equal(`http://${originHost}/download/videos/${videoDetails.uuid}-${file.resolution.id}${extension}`) 197 expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
753 198
754 await Promise.all([ 199 await Promise.all([
755 makeRawRequest(file.torrentUrl, 200), 200 makeRawRequest(file.torrentUrl, 200),
756 makeRawRequest(file.torrentDownloadUrl, 200), 201 makeRawRequest(file.torrentDownloadUrl, 200),
757 makeRawRequest(file.metadataUrl, 200), 202 makeRawRequest(file.metadataUrl, 200)
758 // Backward compatibility
759 makeRawRequest(`http://${originHost}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`, 200)
760 ]) 203 ])
761 204
762 expect(file.resolution.id).to.equal(attributeFile.resolution) 205 expect(file.resolution.id).to.equal(attributeFile.resolution)
@@ -776,149 +219,34 @@ async function completeVideoCheck (
776 } 219 }
777 220
778 expect(videoDetails.thumbnailPath).to.exist 221 expect(videoDetails.thumbnailPath).to.exist
779 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) 222 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
780 223
781 if (attributes.previewfile) { 224 if (attributes.previewfile) {
782 expect(videoDetails.previewPath).to.exist 225 expect(videoDetails.previewPath).to.exist
783 await testImage(url, attributes.previewfile, videoDetails.previewPath) 226 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
784 } 227 }
785} 228}
786 229
787async function videoUUIDToId (url: string, id: number | string) {
788 if (validator.isUUID('' + id) === false) return id
789
790 const res = await getVideo(url, id)
791 return res.body.id
792}
793
794async function uploadVideoAndGetId (options: {
795 server: ServerInfo
796 videoName: string
797 nsfw?: boolean
798 privacy?: VideoPrivacy
799 token?: string
800 fixture?: string
801}) {
802 const videoAttrs: any = { name: options.videoName }
803 if (options.nsfw) videoAttrs.nsfw = options.nsfw
804 if (options.privacy) videoAttrs.privacy = options.privacy
805 if (options.fixture) videoAttrs.fixture = options.fixture
806
807 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
808
809 return res.body.video as { id: number, uuid: string, shortUUID: string }
810}
811
812async function getLocalIdByUUID (url: string, uuid: string) {
813 const res = await getVideo(url, uuid)
814
815 return res.body.id
816}
817
818// serverNumber starts from 1 230// serverNumber starts from 1
819async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) { 231async function uploadRandomVideoOnServers (
232 servers: PeerTubeServer[],
233 serverNumber: number,
234 additionalParams?: VideoEdit & { prefixName?: string }
235) {
820 const server = servers.find(s => s.serverNumber === serverNumber) 236 const server = servers.find(s => s.serverNumber === serverNumber)
821 const res = await uploadRandomVideo(server, false, additionalParams) 237 const res = await server.videos.randomUpload({ wait: false, additionalParams })
822 238
823 await waitJobs(servers) 239 await waitJobs(servers)
824 240
825 return res 241 return res
826} 242}
827 243
828async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
829 const prefixName = additionalParams.prefixName || ''
830 const name = prefixName + buildUUID()
831
832 const data = Object.assign({ name }, additionalParams)
833 const res = await uploadVideo(server.url, server.accessToken, data)
834
835 if (wait) await waitJobs([ server ])
836
837 return { uuid: res.body.video.uuid, name }
838}
839
840// --------------------------------------------------------------------------- 244// ---------------------------------------------------------------------------
841 245
842export { 246export {
843 getVideoDescription,
844 getVideoCategories,
845 uploadRandomVideo,
846 getVideoLicences,
847 videoUUIDToId,
848 getVideoPrivacies,
849 getVideoLanguages,
850 getMyVideos,
851 getAccountVideos,
852 getVideoChannelVideos,
853 getVideo,
854 getVideoFileMetadataUrl,
855 getVideoWithToken,
856 getVideosList,
857 removeAllVideos,
858 checkUploadVideoParam, 247 checkUploadVideoParam,
859 getVideosListPagination,
860 getVideosListSort,
861 removeVideo,
862 getVideosListWithToken,
863 uploadVideo,
864 sendResumableChunks,
865 getVideosWithFilters,
866 uploadRandomVideoOnServers,
867 updateVideo,
868 rateVideo,
869 viewVideo,
870 parseTorrentVideo,
871 getLocalVideos,
872 completeVideoCheck, 248 completeVideoCheck,
249 uploadRandomVideoOnServers,
873 checkVideoFilesWereRemoved, 250 checkVideoFilesWereRemoved,
874 getPlaylistVideos, 251 saveVideoInServers
875 getMyVideosWithFilter,
876 uploadVideoAndGetId,
877 getLocalIdByUUID,
878 getVideoIdFromUUID,
879 prepareResumableUpload
880}
881
882// ---------------------------------------------------------------------------
883
884function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
885
886 for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
887 if (attributes[key] !== undefined) {
888 req.field(key, attributes[key])
889 }
890 }
891
892 for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
893 if (attributes[key] !== undefined) {
894 req.field(key, JSON.stringify(attributes[key]))
895 }
896 }
897
898 for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
899 if (attributes[key] !== undefined) {
900 req.field(key, attributes[key].toString())
901 }
902 }
903
904 const tags = attributes.tags || []
905 for (let i = 0; i < tags.length; i++) {
906 req.field('tags[' + i + ']', attributes.tags[i])
907 }
908
909 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
910 if (attributes[key] !== undefined) {
911 req.attach(key, buildAbsoluteFixturePath(attributes[key]))
912 }
913 }
914
915 if (attributes.scheduleUpdate) {
916 if (attributes.scheduleUpdate.updateAt) {
917 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
918 }
919
920 if (attributes.scheduleUpdate.privacy) {
921 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
922 }
923 }
924} 252}