aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils/videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-21 15:51:30 +0200
committerChocobozzz <me@florianbigard.com>2021-07-21 15:51:30 +0200
commita24bd1ed41b43790bab6ba789580bb4e85f07d85 (patch)
treea54b0f6c921ba83a6e909cd0ced325b2d4b8863c /shared/extra-utils/videos
parent5f26f13b3c16ac5ae0a3b0a7142d84a9528cf565 (diff)
parentc63830f15403ac4e750829f27d8bbbdc9a59282c (diff)
downloadPeerTube-a24bd1ed41b43790bab6ba789580bb4e85f07d85.tar.gz
PeerTube-a24bd1ed41b43790bab6ba789580bb4e85f07d85.tar.zst
PeerTube-a24bd1ed41b43790bab6ba789580bb4e85f07d85.zip
Merge branch 'next' into develop
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.ts147
-rw-r--r--shared/extra-utils/videos/playlists-command.ts279
-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.ts75
-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.ts598
-rw-r--r--shared/extra-utils/videos/videos.ts761
29 files changed, 1918 insertions, 1988 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..f8eb3f885
--- /dev/null
+++ b/shared/extra-utils/videos/channels-command.ts
@@ -0,0 +1,156 @@
1import { pick } from 'lodash'
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..502964b1a 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 checkLiveCleanup (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) {
@@ -206,33 +101,11 @@ async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resoluti
206 expect(files).to.contain('segments-sha256.json') 101 expect(files).to.contain('segments-sha256.json')
207} 102}
208 103
209async function getPlaylistsCount (server: ServerInfo, videoUUID: string) {
210 const basePath = buildServerDirectory(server, 'streaming-playlists')
211 const hlsPath = join(basePath, 'hls', videoUUID)
212
213 const files = await readdir(hlsPath)
214
215 return files.filter(f => f.endsWith('.m3u8')).length
216}
217
218// ---------------------------------------------------------------------------
219
220export { 104export {
221 getLive, 105 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, 106 waitFfmpegUntilError,
107 testFfmpegStreamError,
108 stopFfmpeg,
235 waitUntilLivePublishedOnAllServers, 109 waitUntilLivePublishedOnAllServers,
236 sendRTMPStream, 110 checkLiveCleanup
237 testFfmpegStreamError
238} 111}
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
new file mode 100644
index 000000000..6f329800e
--- /dev/null
+++ b/shared/extra-utils/videos/playlists-command.ts
@@ -0,0 +1,279 @@
1import { omit, pick } from 'lodash'
2import {
3 BooleanBothQuery,
4 HttpStatusCode,
5 ResultList,
6 VideoExistInPlaylist,
7 VideoPlaylist,
8 VideoPlaylistCreate,
9 VideoPlaylistCreateResult,
10 VideoPlaylistElement,
11 VideoPlaylistElementCreate,
12 VideoPlaylistElementCreateResult,
13 VideoPlaylistElementUpdate,
14 VideoPlaylistReorder,
15 VideoPlaylistType,
16 VideoPlaylistUpdate
17} from '@shared/models'
18import { unwrapBody } from '../requests'
19import { AbstractCommand, OverrideCommandOptions } from '../shared'
20
21export class PlaylistsCommand extends AbstractCommand {
22
23 list (options: OverrideCommandOptions & {
24 start?: number
25 count?: number
26 sort?: string
27 }) {
28 const path = '/api/v1/video-playlists'
29 const query = pick(options, [ 'start', 'count', 'sort' ])
30
31 return this.getRequestBody<ResultList<VideoPlaylist>>({
32 ...options,
33
34 path,
35 query,
36 implicitToken: false,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 listByChannel (options: OverrideCommandOptions & {
42 handle: string
43 start?: number
44 count?: number
45 sort?: string
46 }) {
47 const path = '/api/v1/video-channels/' + options.handle + '/video-playlists'
48 const query = pick(options, [ 'start', 'count', 'sort' ])
49
50 return this.getRequestBody<ResultList<VideoPlaylist>>({
51 ...options,
52
53 path,
54 query,
55 implicitToken: false,
56 defaultExpectedStatus: HttpStatusCode.OK_200
57 })
58 }
59
60 listByAccount (options: OverrideCommandOptions & {
61 handle: string
62 start?: number
63 count?: number
64 sort?: string
65 search?: string
66 playlistType?: VideoPlaylistType
67 }) {
68 const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
69 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
70
71 return this.getRequestBody<ResultList<VideoPlaylist>>({
72 ...options,
73
74 path,
75 query,
76 implicitToken: false,
77 defaultExpectedStatus: HttpStatusCode.OK_200
78 })
79 }
80
81 get (options: OverrideCommandOptions & {
82 playlistId: number | string
83 }) {
84 const { playlistId } = options
85 const path = '/api/v1/video-playlists/' + playlistId
86
87 return this.getRequestBody<VideoPlaylist>({
88 ...options,
89
90 path,
91 implicitToken: false,
92 defaultExpectedStatus: HttpStatusCode.OK_200
93 })
94 }
95
96 listVideos (options: OverrideCommandOptions & {
97 playlistId: number | string
98 start?: number
99 count?: number
100 query?: { nsfw?: BooleanBothQuery }
101 }) {
102 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
103 const query = options.query ?? {}
104
105 return this.getRequestBody<ResultList<VideoPlaylistElement>>({
106 ...options,
107
108 path,
109 query: {
110 ...query,
111 start: options.start,
112 count: options.count
113 },
114 implicitToken: true,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 })
117 }
118
119 delete (options: OverrideCommandOptions & {
120 playlistId: number | string
121 }) {
122 const path = '/api/v1/video-playlists/' + options.playlistId
123
124 return this.deleteRequest({
125 ...options,
126
127 path,
128 implicitToken: true,
129 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
130 })
131 }
132
133 async create (options: OverrideCommandOptions & {
134 attributes: VideoPlaylistCreate
135 }) {
136 const path = '/api/v1/video-playlists'
137
138 const fields = omit(options.attributes, 'thumbnailfile')
139
140 const attaches = options.attributes.thumbnailfile
141 ? { thumbnailfile: options.attributes.thumbnailfile }
142 : {}
143
144 const body = await unwrapBody<{ videoPlaylist: VideoPlaylistCreateResult }>(this.postUploadRequest({
145 ...options,
146
147 path,
148 fields,
149 attaches,
150 implicitToken: true,
151 defaultExpectedStatus: HttpStatusCode.OK_200
152 }))
153
154 return body.videoPlaylist
155 }
156
157 update (options: OverrideCommandOptions & {
158 attributes: VideoPlaylistUpdate
159 playlistId: number | string
160 }) {
161 const path = '/api/v1/video-playlists/' + options.playlistId
162
163 const fields = omit(options.attributes, 'thumbnailfile')
164
165 const attaches = options.attributes.thumbnailfile
166 ? { thumbnailfile: options.attributes.thumbnailfile }
167 : {}
168
169 return this.putUploadRequest({
170 ...options,
171
172 path,
173 fields,
174 attaches,
175 implicitToken: true,
176 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
177 })
178 }
179
180 async addElement (options: OverrideCommandOptions & {
181 playlistId: number | string
182 attributes: VideoPlaylistElementCreate | { videoId: string }
183 }) {
184 const attributes = {
185 ...options.attributes,
186
187 videoId: await this.server.videos.getId({ ...options, uuid: options.attributes.videoId })
188 }
189
190 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
191
192 const body = await unwrapBody<{ videoPlaylistElement: VideoPlaylistElementCreateResult }>(this.postBodyRequest({
193 ...options,
194
195 path,
196 fields: attributes,
197 implicitToken: true,
198 defaultExpectedStatus: HttpStatusCode.OK_200
199 }))
200
201 return body.videoPlaylistElement
202 }
203
204 updateElement (options: OverrideCommandOptions & {
205 playlistId: number | string
206 elementId: number | string
207 attributes: VideoPlaylistElementUpdate
208 }) {
209 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
210
211 return this.putBodyRequest({
212 ...options,
213
214 path,
215 fields: options.attributes,
216 implicitToken: true,
217 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
218 })
219 }
220
221 removeElement (options: OverrideCommandOptions & {
222 playlistId: number | string
223 elementId: number
224 }) {
225 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
226
227 return this.deleteRequest({
228 ...options,
229
230 path,
231 implicitToken: true,
232 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
233 })
234 }
235
236 reorderElements (options: OverrideCommandOptions & {
237 playlistId: number | string
238 attributes: VideoPlaylistReorder
239 }) {
240 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
241
242 return this.postBodyRequest({
243 ...options,
244
245 path,
246 fields: options.attributes,
247 implicitToken: true,
248 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
249 })
250 }
251
252 getPrivacies (options: OverrideCommandOptions = {}) {
253 const path = '/api/v1/video-playlists/privacies'
254
255 return this.getRequestBody<{ [ id: number ]: string }>({
256 ...options,
257
258 path,
259 implicitToken: false,
260 defaultExpectedStatus: HttpStatusCode.OK_200
261 })
262 }
263
264 videosExist (options: OverrideCommandOptions & {
265 videoIds: number[]
266 }) {
267 const { videoIds } = options
268 const path = '/api/v1/users/me/video-playlists/videos-exist'
269
270 return this.getRequestBody<VideoExistInPlaylist>({
271 ...options,
272
273 path,
274 query: { videoIds },
275 implicitToken: true,
276 defaultExpectedStatus: HttpStatusCode.OK_200
277 })
278 }
279}
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..1ae3fefc1
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists.ts
@@ -0,0 +1,75 @@
1import { expect } from 'chai'
2import { sha256 } from '@server/helpers/core-utils'
3import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
4import { PeerTubeServer } from '../server'
5
6async function checkSegmentHash (options: {
7 server: PeerTubeServer
8 baseUrlPlaylist: string
9 baseUrlSegment: string
10 videoUUID: string
11 resolution: number
12 hlsPlaylist: VideoStreamingPlaylist
13}) {
14 const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
15 const command = server.streamingPlaylists
16
17 const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8` })
18
19 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
20
21 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
22
23 const length = parseInt(matches[1], 10)
24 const offset = parseInt(matches[2], 10)
25 const range = `${offset}-${offset + length - 1}`
26
27 const segmentBody = await command.getSegment({
28 url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
29 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
30 range: `bytes=${range}`
31 })
32
33 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
34 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
35}
36
37async function checkLiveSegmentHash (options: {
38 server: PeerTubeServer
39 baseUrlSegment: string
40 videoUUID: string
41 segmentName: string
42 hlsPlaylist: VideoStreamingPlaylist
43}) {
44 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
45 const command = server.streamingPlaylists
46
47 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
48 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
49
50 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
51}
52
53async function checkResolutionsInMasterPlaylist (options: {
54 server: PeerTubeServer
55 playlistUrl: string
56 resolutions: number[]
57}) {
58 const { server, playlistUrl, resolutions } = options
59
60 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
61
62 for (const resolution of resolutions) {
63 const reg = new RegExp(
64 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
65 )
66
67 expect(masterPlaylist).to.match(reg)
68 }
69}
70
71export {
72 checkSegmentHash,
73 checkLiveSegmentHash,
74 checkResolutionsInMasterPlaylist
75}
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..98465e8f6
--- /dev/null
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -0,0 +1,598 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { createReadStream, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got'
6import { omit, pick } from 'lodash'
7import validator from 'validator'
8import { buildUUID } from '@server/helpers/uuid'
9import { loadLanguages } from '@server/initializers/constants'
10import {
11 HttpStatusCode,
12 ResultList,
13 UserVideoRateType,
14 Video,
15 VideoCreate,
16 VideoCreateResult,
17 VideoDetails,
18 VideoFileMetadata,
19 VideoPrivacy,
20 VideosCommonQuery,
21 VideosWithSearchCommonQuery
22} from '@shared/models'
23import { buildAbsoluteFixturePath, wait } from '../miscs'
24import { unwrapBody } from '../requests'
25import { PeerTubeServer, waitJobs } from '../server'
26import { AbstractCommand, OverrideCommandOptions } from '../shared'
27
28export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
29 fixture?: string
30 thumbnailfile?: string
31 previewfile?: string
32}
33
34export class VideosCommand extends AbstractCommand {
35
36 constructor (server: PeerTubeServer) {
37 super(server)
38
39 loadLanguages()
40 }
41
42 getCategories (options: OverrideCommandOptions = {}) {
43 const path = '/api/v1/videos/categories'
44
45 return this.getRequestBody<{ [id: number]: string }>({
46 ...options,
47 path,
48
49 implicitToken: false,
50 defaultExpectedStatus: HttpStatusCode.OK_200
51 })
52 }
53
54 getLicences (options: OverrideCommandOptions = {}) {
55 const path = '/api/v1/videos/licences'
56
57 return this.getRequestBody<{ [id: number]: string }>({
58 ...options,
59 path,
60
61 implicitToken: false,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 })
64 }
65
66 getLanguages (options: OverrideCommandOptions = {}) {
67 const path = '/api/v1/videos/languages'
68
69 return this.getRequestBody<{ [id: string]: string }>({
70 ...options,
71 path,
72
73 implicitToken: false,
74 defaultExpectedStatus: HttpStatusCode.OK_200
75 })
76 }
77
78 getPrivacies (options: OverrideCommandOptions = {}) {
79 const path = '/api/v1/videos/privacies'
80
81 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
82 ...options,
83 path,
84
85 implicitToken: false,
86 defaultExpectedStatus: HttpStatusCode.OK_200
87 })
88 }
89
90 // ---------------------------------------------------------------------------
91
92 getDescription (options: OverrideCommandOptions & {
93 descriptionPath: string
94 }) {
95 return this.getRequestBody<{ description: string }>({
96 ...options,
97 path: options.descriptionPath,
98
99 implicitToken: false,
100 defaultExpectedStatus: HttpStatusCode.OK_200
101 })
102 }
103
104 getFileMetadata (options: OverrideCommandOptions & {
105 url: string
106 }) {
107 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
108 ...options,
109
110 url: options.url,
111 implicitToken: false,
112 defaultExpectedStatus: HttpStatusCode.OK_200
113 }))
114 }
115
116 // ---------------------------------------------------------------------------
117
118 view (options: OverrideCommandOptions & {
119 id: number | string
120 xForwardedFor?: string
121 }) {
122 const { id, xForwardedFor } = options
123 const path = '/api/v1/videos/' + id + '/views'
124
125 return this.postBodyRequest({
126 ...options,
127
128 path,
129 xForwardedFor,
130 implicitToken: false,
131 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
132 })
133 }
134
135 rate (options: OverrideCommandOptions & {
136 id: number | string
137 rating: UserVideoRateType
138 }) {
139 const { id, rating } = options
140 const path = '/api/v1/videos/' + id + '/rate'
141
142 return this.putBodyRequest({
143 ...options,
144
145 path,
146 fields: { rating },
147 implicitToken: true,
148 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
149 })
150 }
151
152 // ---------------------------------------------------------------------------
153
154 get (options: OverrideCommandOptions & {
155 id: number | string
156 }) {
157 const path = '/api/v1/videos/' + options.id
158
159 return this.getRequestBody<VideoDetails>({
160 ...options,
161
162 path,
163 implicitToken: false,
164 defaultExpectedStatus: HttpStatusCode.OK_200
165 })
166 }
167
168 getWithToken (options: OverrideCommandOptions & {
169 id: number | string
170 }) {
171 return this.get({
172 ...options,
173
174 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
175 })
176 }
177
178 async getId (options: OverrideCommandOptions & {
179 uuid: number | string
180 }) {
181 const { uuid } = options
182
183 if (validator.isUUID('' + uuid) === false) return uuid as number
184
185 const { id } = await this.get({ ...options, id: uuid })
186
187 return id
188 }
189
190 // ---------------------------------------------------------------------------
191
192 listMyVideos (options: OverrideCommandOptions & {
193 start?: number
194 count?: number
195 sort?: string
196 search?: string
197 isLive?: boolean
198 } = {}) {
199 const path = '/api/v1/users/me/videos'
200
201 return this.getRequestBody<ResultList<Video>>({
202 ...options,
203
204 path,
205 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
206 implicitToken: true,
207 defaultExpectedStatus: HttpStatusCode.OK_200
208 })
209 }
210
211 // ---------------------------------------------------------------------------
212
213 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
214 const path = '/api/v1/videos'
215
216 const query = this.buildListQuery(options)
217
218 return this.getRequestBody<ResultList<Video>>({
219 ...options,
220
221 path,
222 query: { sort: 'name', ...query },
223 implicitToken: false,
224 defaultExpectedStatus: HttpStatusCode.OK_200
225 })
226 }
227
228 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
229 return this.list({
230 ...options,
231
232 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
233 })
234 }
235
236 listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
237 handle: string
238 }) {
239 const { handle, search } = options
240 const path = '/api/v1/accounts/' + handle + '/videos'
241
242 return this.getRequestBody<ResultList<Video>>({
243 ...options,
244
245 path,
246 query: { search, ...this.buildListQuery(options) },
247 implicitToken: true,
248 defaultExpectedStatus: HttpStatusCode.OK_200
249 })
250 }
251
252 listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
253 handle: string
254 }) {
255 const { handle } = options
256 const path = '/api/v1/video-channels/' + handle + '/videos'
257
258 return this.getRequestBody<ResultList<Video>>({
259 ...options,
260
261 path,
262 query: this.buildListQuery(options),
263 implicitToken: true,
264 defaultExpectedStatus: HttpStatusCode.OK_200
265 })
266 }
267
268 // ---------------------------------------------------------------------------
269
270 async find (options: OverrideCommandOptions & {
271 name: string
272 }) {
273 const { data } = await this.list(options)
274
275 return data.find(v => v.name === options.name)
276 }
277
278 // ---------------------------------------------------------------------------
279
280 update (options: OverrideCommandOptions & {
281 id: number | string
282 attributes?: VideoEdit
283 }) {
284 const { id, attributes = {} } = options
285 const path = '/api/v1/videos/' + id
286
287 // Upload request
288 if (attributes.thumbnailfile || attributes.previewfile) {
289 const attaches: any = {}
290 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
291 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
292
293 return this.putUploadRequest({
294 ...options,
295
296 path,
297 fields: options.attributes,
298 attaches: {
299 thumbnailfile: attributes.thumbnailfile,
300 previewfile: attributes.previewfile
301 },
302 implicitToken: true,
303 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
304 })
305 }
306
307 return this.putBodyRequest({
308 ...options,
309
310 path,
311 fields: options.attributes,
312 implicitToken: true,
313 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
314 })
315 }
316
317 remove (options: OverrideCommandOptions & {
318 id: number | string
319 }) {
320 const path = '/api/v1/videos/' + options.id
321
322 return unwrapBody(this.deleteRequest({
323 ...options,
324
325 path,
326 implicitToken: true,
327 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
328 }))
329 }
330
331 async removeAll () {
332 const { data } = await this.list()
333
334 for (const v of data) {
335 await this.remove({ id: v.id })
336 }
337 }
338
339 // ---------------------------------------------------------------------------
340
341 async upload (options: OverrideCommandOptions & {
342 attributes?: VideoEdit
343 mode?: 'legacy' | 'resumable' // default legacy
344 } = {}) {
345 const { mode = 'legacy' } = options
346 let defaultChannelId = 1
347
348 try {
349 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
350 defaultChannelId = videoChannels[0].id
351 } catch (e) { /* empty */ }
352
353 // Override default attributes
354 const attributes = {
355 name: 'my super video',
356 category: 5,
357 licence: 4,
358 language: 'zh',
359 channelId: defaultChannelId,
360 nsfw: true,
361 waitTranscoding: false,
362 description: 'my super description',
363 support: 'my super support text',
364 tags: [ 'tag' ],
365 privacy: VideoPrivacy.PUBLIC,
366 commentsEnabled: true,
367 downloadEnabled: true,
368 fixture: 'video_short.webm',
369
370 ...options.attributes
371 }
372
373 const created = mode === 'legacy'
374 ? await this.buildLegacyUpload({ ...options, attributes })
375 : await this.buildResumeUpload({ ...options, attributes })
376
377 // Wait torrent generation
378 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
379 if (expectedStatus === HttpStatusCode.OK_200) {
380 let video: VideoDetails
381
382 do {
383 video = await this.getWithToken({ ...options, id: created.uuid })
384
385 await wait(50)
386 } while (!video.files[0].torrentUrl)
387 }
388
389 return created
390 }
391
392 async buildLegacyUpload (options: OverrideCommandOptions & {
393 attributes: VideoEdit
394 }): Promise<VideoCreateResult> {
395 const path = '/api/v1/videos/upload'
396
397 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
398 ...options,
399
400 path,
401 fields: this.buildUploadFields(options.attributes),
402 attaches: this.buildUploadAttaches(options.attributes),
403 implicitToken: true,
404 defaultExpectedStatus: HttpStatusCode.OK_200
405 })).then(body => body.video || body as any)
406 }
407
408 async buildResumeUpload (options: OverrideCommandOptions & {
409 attributes: VideoEdit
410 }): Promise<VideoCreateResult> {
411 const { attributes, expectedStatus } = options
412
413 let size = 0
414 let videoFilePath: string
415 let mimetype = 'video/mp4'
416
417 if (attributes.fixture) {
418 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
419 size = (await stat(videoFilePath)).size
420
421 if (videoFilePath.endsWith('.mkv')) {
422 mimetype = 'video/x-matroska'
423 } else if (videoFilePath.endsWith('.webm')) {
424 mimetype = 'video/webm'
425 }
426 }
427
428 // Do not check status automatically, we'll check it manually
429 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
430 const initStatus = initializeSessionRes.status
431
432 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
433 const locationHeader = initializeSessionRes.header['location']
434 expect(locationHeader).to.not.be.undefined
435
436 const pathUploadId = locationHeader.split('?')[1]
437
438 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
439
440 return result.body?.video || result.body as any
441 }
442
443 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
444 ? HttpStatusCode.CREATED_201
445 : expectedStatus
446
447 expect(initStatus).to.equal(expectedInitStatus)
448
449 return initializeSessionRes.body.video || initializeSessionRes.body
450 }
451
452 async prepareResumableUpload (options: OverrideCommandOptions & {
453 attributes: VideoEdit
454 size: number
455 mimetype: string
456 }) {
457 const { attributes, size, mimetype } = options
458
459 const path = '/api/v1/videos/upload-resumable'
460
461 return this.postUploadRequest({
462 ...options,
463
464 path,
465 headers: {
466 'X-Upload-Content-Type': mimetype,
467 'X-Upload-Content-Length': size.toString()
468 },
469 fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
470 // Fixture will be sent later
471 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
472 implicitToken: true,
473
474 defaultExpectedStatus: null
475 })
476 }
477
478 sendResumableChunks (options: OverrideCommandOptions & {
479 pathUploadId: string
480 videoFilePath: string
481 size: number
482 contentLength?: number
483 contentRangeBuilder?: (start: number, chunk: any) => string
484 }) {
485 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
486
487 const path = '/api/v1/videos/upload-resumable'
488 let start = 0
489
490 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
491 const url = this.server.url
492
493 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
494 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
495 readable.on('data', async function onData (chunk) {
496 readable.pause()
497
498 const headers = {
499 'Authorization': 'Bearer ' + token,
500 'Content-Type': 'application/octet-stream',
501 'Content-Range': contentRangeBuilder
502 ? contentRangeBuilder(start, chunk)
503 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
504 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
505 }
506
507 const res = await got<{ video: VideoCreateResult }>({
508 url,
509 method: 'put',
510 headers,
511 path: path + '?' + pathUploadId,
512 body: chunk,
513 responseType: 'json',
514 throwHttpErrors: false
515 })
516
517 start += chunk.length
518
519 if (res.statusCode === expectedStatus) {
520 return resolve(res)
521 }
522
523 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
524 readable.off('data', onData)
525 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
526 }
527
528 readable.resume()
529 })
530 })
531 }
532
533 quickUpload (options: OverrideCommandOptions & {
534 name: string
535 nsfw?: boolean
536 privacy?: VideoPrivacy
537 fixture?: string
538 }) {
539 const attributes: VideoEdit = { name: options.name }
540 if (options.nsfw) attributes.nsfw = options.nsfw
541 if (options.privacy) attributes.privacy = options.privacy
542 if (options.fixture) attributes.fixture = options.fixture
543
544 return this.upload({ ...options, attributes })
545 }
546
547 async randomUpload (options: OverrideCommandOptions & {
548 wait?: boolean // default true
549 additionalParams?: VideoEdit & { prefixName?: string }
550 } = {}) {
551 const { wait = true, additionalParams } = options
552 const prefixName = additionalParams?.prefixName || ''
553 const name = prefixName + buildUUID()
554
555 const attributes = { name, ...additionalParams }
556
557 const result = await this.upload({ ...options, attributes })
558
559 if (wait) await waitJobs([ this.server ])
560
561 return { ...result, name }
562 }
563
564 // ---------------------------------------------------------------------------
565
566 private buildListQuery (options: VideosCommonQuery) {
567 return pick(options, [
568 'start',
569 'count',
570 'sort',
571 'nsfw',
572 'isLive',
573 'categoryOneOf',
574 'licenceOneOf',
575 'languageOneOf',
576 'tagsOneOf',
577 'tagsAllOf',
578 'filter',
579 'skipCount'
580 ])
581 }
582
583 private buildUploadFields (attributes: VideoEdit) {
584 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
585 }
586
587 private buildUploadAttaches (attributes: VideoEdit) {
588 const attaches: { [ name: string ]: string } = {}
589
590 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
591 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
592 }
593
594 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
595
596 return attaches
597 }
598}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 469ea4d63..9a9bfb3cf 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -1,348 +1,20 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra' 4import { pathExists, readdir } from 'fs-extra'
5import got, { Response as GotResponse } from 'got/dist/source'
6import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 5import { join } from 'path'
8import * as request from 'supertest'
9import validator from 'validator'
10import { getLowercaseExtension } from '@server/helpers/core-utils' 6import { getLowercaseExtension } from '@server/helpers/core-utils'
11import { buildUUID } from '@server/helpers/uuid' 7import { HttpStatusCode } from '@shared/models'
12import { HttpStatusCode } from '@shared/core-utils' 8import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
13import { VideosCommonQuery } from '@shared/models' 9import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
14import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' 10import { makeRawRequest } from '../requests/requests'
15import { VideoDetails, VideoPrivacy } from '../../models/videos' 11import { waitJobs } from '../server'
16import { 12import { PeerTubeServer } from '../server/server'
17 buildAbsoluteFixturePath, 13import { VideoEdit } from './videos-command'
18 buildServerDirectory,
19 dateIsValid,
20 immutableAssign,
21 testImage,
22 wait,
23 webtorrentAdd
24} from '../miscs/miscs'
25import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
26import { waitJobs } from '../server/jobs'
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
105async function getVideoIdFromUUID (url: string, uuid: string) {
106 const res = await getVideo(url, uuid)
107
108 return res.body.id
109}
110
111function getVideoFileMetadataUrl (url: string) {
112 return request(url)
113 .get('/')
114 .set('Accept', 'application/json')
115 .expect(HttpStatusCode.OK_200)
116 .expect('Content-Type', /json/)
117}
118
119function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
120 const path = '/api/v1/videos/' + id + '/views'
121
122 const req = request(url)
123 .post(path)
124 .set('Accept', 'application/json')
125
126 if (xForwardedFor) {
127 req.set('X-Forwarded-For', xForwardedFor)
128 }
129
130 return req.expect(expectedStatus)
131}
132
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
335async function removeAllVideos (server: ServerInfo) {
336 const resVideos = await getVideosList(server.url)
337
338 for (const v of resVideos.body.data) {
339 await removeVideo(server.url, server.accessToken, v.id)
340 }
341}
342 14
343async function checkVideoFilesWereRemoved ( 15async function checkVideoFilesWereRemoved (
344 videoUUID: string, 16 videoUUID: string,
345 serverNumber: number, 17 server: PeerTubeServer,
346 directories = [ 18 directories = [
347 'redundancy', 19 'redundancy',
348 'videos', 20 'videos',
@@ -355,7 +27,7 @@ async function checkVideoFilesWereRemoved (
355 ] 27 ]
356) { 28) {
357 for (const directory of directories) { 29 for (const directory of directories) {
358 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory) 30 const directoryPath = server.servers.buildDirectory(directory)
359 31
360 const directoryExists = await pathExists(directoryPath) 32 const directoryExists = await pathExists(directoryPath)
361 if (directoryExists === false) continue 33 if (directoryExists === false) continue
@@ -367,280 +39,20 @@ async function checkVideoFilesWereRemoved (
367 } 39 }
368} 40}
369 41
370async function uploadVideo (
371 url: string,
372 accessToken: string,
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 }
416
417 return res
418}
419
420function checkUploadVideoParam ( 42function checkUploadVideoParam (
421 url: string, 43 server: PeerTubeServer,
422 token: string, 44 token: string,
423 attributes: Partial<VideoAttributes>, 45 attributes: Partial<VideoEdit>,
424 specialStatus = HttpStatusCode.OK_200, 46 expectedStatus = HttpStatusCode.OK_200,
425 mode: 'legacy' | 'resumable' = 'legacy' 47 mode: 'legacy' | 'resumable' = 'legacy'
426) { 48) {
427 return mode === 'legacy' 49 return mode === 'legacy'
428 ? buildLegacyUpload(url, token, attributes, specialStatus) 50 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
429 : buildResumeUpload(url, token, attributes, specialStatus) 51 : 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} 52}
641 53
642async function completeVideoCheck ( 54async function completeVideoCheck (
643 url: string, 55 server: PeerTubeServer,
644 video: any, 56 video: any,
645 attributes: { 57 attributes: {
646 name: string 58 name: string
@@ -682,7 +94,7 @@ async function completeVideoCheck (
682 if (!attributes.likes) attributes.likes = 0 94 if (!attributes.likes) attributes.likes = 0
683 if (!attributes.dislikes) attributes.dislikes = 0 95 if (!attributes.dislikes) attributes.dislikes = 0
684 96
685 const host = new URL(url).host 97 const host = new URL(server.url).host
686 const originHost = attributes.account.host 98 const originHost = attributes.account.host
687 99
688 expect(video.name).to.equal(attributes.name) 100 expect(video.name).to.equal(attributes.name)
@@ -719,8 +131,7 @@ async function completeVideoCheck (
719 expect(video.originallyPublishedAt).to.be.null 131 expect(video.originallyPublishedAt).to.be.null
720 } 132 }
721 133
722 const res = await getVideo(url, video.uuid) 134 const videoDetails = await server.videos.get({ id: video.uuid })
723 const videoDetails: VideoDetails = res.body
724 135
725 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 136 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
726 expect(videoDetails.tags).to.deep.equal(attributes.tags) 137 expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -776,149 +187,33 @@ async function completeVideoCheck (
776 } 187 }
777 188
778 expect(videoDetails.thumbnailPath).to.exist 189 expect(videoDetails.thumbnailPath).to.exist
779 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) 190 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
780 191
781 if (attributes.previewfile) { 192 if (attributes.previewfile) {
782 expect(videoDetails.previewPath).to.exist 193 expect(videoDetails.previewPath).to.exist
783 await testImage(url, attributes.previewfile, videoDetails.previewPath) 194 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
784 } 195 }
785} 196}
786 197
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 198// serverNumber starts from 1
819async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) { 199async function uploadRandomVideoOnServers (
200 servers: PeerTubeServer[],
201 serverNumber: number,
202 additionalParams?: VideoEdit & { prefixName?: string }
203) {
820 const server = servers.find(s => s.serverNumber === serverNumber) 204 const server = servers.find(s => s.serverNumber === serverNumber)
821 const res = await uploadRandomVideo(server, false, additionalParams) 205 const res = await server.videos.randomUpload({ wait: false, additionalParams })
822 206
823 await waitJobs(servers) 207 await waitJobs(servers)
824 208
825 return res 209 return res
826} 210}
827 211
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// --------------------------------------------------------------------------- 212// ---------------------------------------------------------------------------
841 213
842export { 214export {
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, 215 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, 216 completeVideoCheck,
873 checkVideoFilesWereRemoved, 217 uploadRandomVideoOnServers,
874 getPlaylistVideos, 218 checkVideoFilesWereRemoved
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} 219}