aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/author-interface.ts45
-rw-r--r--server/models/video/author.ts171
-rw-r--r--server/models/video/video-channel-interface.ts38
-rw-r--r--server/models/video/video-channel.ts100
-rw-r--r--server/models/video/video-interface.ts60
-rw-r--r--server/models/video/video.ts378
6 files changed, 300 insertions, 492 deletions
diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts
deleted file mode 100644
index fc69ff3c2..000000000
--- a/server/models/video/author-interface.ts
+++ /dev/null
@@ -1,45 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { PodInstance } from '../pod/pod-interface'
5import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model'
6import { VideoChannelInstance } from './video-channel-interface'
7
8export namespace AuthorMethods {
9 export type Load = (id: number) => Promise<AuthorInstance>
10 export type LoadByUUID = (uuid: string) => Promise<AuthorInstance>
11 export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise<AuthorInstance>
12 export type ListOwned = () => Promise<AuthorInstance[]>
13
14 export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData
15 export type IsOwned = (this: AuthorInstance) => boolean
16}
17
18export interface AuthorClass {
19 loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
20 load: AuthorMethods.Load
21 loadByUUID: AuthorMethods.LoadByUUID
22 listOwned: AuthorMethods.ListOwned
23}
24
25export interface AuthorAttributes {
26 name: string
27 uuid?: string
28
29 podId?: number
30 userId?: number
31}
32
33export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> {
34 isOwned: AuthorMethods.IsOwned
35 toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
36
37 id: number
38 createdAt: Date
39 updatedAt: Date
40
41 Pod: PodInstance
42 VideoChannels: VideoChannelInstance[]
43}
44
45export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {}
diff --git a/server/models/video/author.ts b/server/models/video/author.ts
deleted file mode 100644
index 43f84c3ea..000000000
--- a/server/models/video/author.ts
+++ /dev/null
@@ -1,171 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3import { isUserUsernameValid } from '../../helpers'
4import { removeVideoAuthorToFriends } from '../../lib'
5
6import { addMethodsToModel } from '../utils'
7import {
8 AuthorInstance,
9 AuthorAttributes,
10
11 AuthorMethods
12} from './author-interface'
13
14let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
15let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
16let load: AuthorMethods.Load
17let loadByUUID: AuthorMethods.LoadByUUID
18let listOwned: AuthorMethods.ListOwned
19let isOwned: AuthorMethods.IsOwned
20let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
21
22export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
23 Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author',
24 {
25 uuid: {
26 type: DataTypes.UUID,
27 defaultValue: DataTypes.UUIDV4,
28 allowNull: false,
29 validate: {
30 isUUID: 4
31 }
32 },
33 name: {
34 type: DataTypes.STRING,
35 allowNull: false,
36 validate: {
37 usernameValid: value => {
38 const res = isUserUsernameValid(value)
39 if (res === false) throw new Error('Username is not valid.')
40 }
41 }
42 }
43 },
44 {
45 indexes: [
46 {
47 fields: [ 'name' ]
48 },
49 {
50 fields: [ 'podId' ]
51 },
52 {
53 fields: [ 'userId' ],
54 unique: true
55 },
56 {
57 fields: [ 'name', 'podId' ],
58 unique: true
59 }
60 ],
61 hooks: { afterDestroy }
62 }
63 )
64
65 const classMethods = [
66 associate,
67 loadAuthorByPodAndUUID,
68 load,
69 loadByUUID,
70 listOwned
71 ]
72 const instanceMethods = [
73 isOwned,
74 toAddRemoteJSON
75 ]
76 addMethodsToModel(Author, classMethods, instanceMethods)
77
78 return Author
79}
80
81// ---------------------------------------------------------------------------
82
83function associate (models) {
84 Author.belongsTo(models.Pod, {
85 foreignKey: {
86 name: 'podId',
87 allowNull: true
88 },
89 onDelete: 'cascade'
90 })
91
92 Author.belongsTo(models.User, {
93 foreignKey: {
94 name: 'userId',
95 allowNull: true
96 },
97 onDelete: 'cascade'
98 })
99
100 Author.hasMany(models.VideoChannel, {
101 foreignKey: {
102 name: 'authorId',
103 allowNull: false
104 },
105 onDelete: 'cascade',
106 hooks: true
107 })
108}
109
110function afterDestroy (author: AuthorInstance) {
111 if (author.isOwned()) {
112 const removeVideoAuthorToFriendsParams = {
113 uuid: author.uuid
114 }
115
116 return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams)
117 }
118
119 return undefined
120}
121
122toAddRemoteJSON = function (this: AuthorInstance) {
123 const json = {
124 uuid: this.uuid,
125 name: this.name
126 }
127
128 return json
129}
130
131isOwned = function (this: AuthorInstance) {
132 return this.podId === null
133}
134
135// ------------------------------ STATICS ------------------------------
136
137listOwned = function () {
138 const query: Sequelize.FindOptions<AuthorAttributes> = {
139 where: {
140 podId: null
141 }
142 }
143
144 return Author.findAll(query)
145}
146
147load = function (id: number) {
148 return Author.findById(id)
149}
150
151loadByUUID = function (uuid: string) {
152 const query: Sequelize.FindOptions<AuthorAttributes> = {
153 where: {
154 uuid
155 }
156 }
157
158 return Author.findOne(query)
159}
160
161loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
162 const query: Sequelize.FindOptions<AuthorAttributes> = {
163 where: {
164 podId,
165 uuid
166 },
167 transaction
168 }
169
170 return Author.find(query)
171}
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts
index b8d3e0f42..477f97cd4 100644
--- a/server/models/video/video-channel-interface.ts
+++ b/server/models/video/video-channel-interface.ts
@@ -1,42 +1,42 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared' 4import { ResultList } from '../../../shared'
5 5
6// Don't use barrel, import just what we need 6// Don't use barrel, import just what we need
7import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' 7import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
8import { AuthorInstance } from './author-interface'
9import { VideoInstance } from './video-interface' 8import { VideoInstance } from './video-interface'
9import { AccountInstance } from '../account/account-interface'
10import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
10 11
11export namespace VideoChannelMethods { 12export namespace VideoChannelMethods {
12 export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel 13 export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
13 export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData 14 export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject
14 export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData
15 export type IsOwned = (this: VideoChannelInstance) => boolean 15 export type IsOwned = (this: VideoChannelInstance) => boolean
16 16
17 export type CountByAuthor = (authorId: number) => Promise<number> 17 export type CountByAccount = (accountId: number) => Promise<number>
18 export type ListOwned = () => Promise<VideoChannelInstance[]> 18 export type ListOwned = () => Promise<VideoChannelInstance[]>
19 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> > 19 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
20 export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise<VideoChannelInstance> 20 export type LoadByIdAndAccount = (id: number, accountId: number) => Promise<VideoChannelInstance>
21 export type ListByAuthor = (authorId: number) => Promise< ResultList<VideoChannelInstance> > 21 export type ListByAccount = (accountId: number) => Promise< ResultList<VideoChannelInstance> >
22 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoChannelInstance> 22 export type LoadAndPopulateAccount = (id: number) => Promise<VideoChannelInstance>
23 export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise<VideoChannelInstance> 23 export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise<VideoChannelInstance>
24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> 24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> 25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
26 export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise<VideoChannelInstance> 26 export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
27} 27}
28 28
29export interface VideoChannelClass { 29export interface VideoChannelClass {
30 countByAuthor: VideoChannelMethods.CountByAuthor 30 countByAccount: VideoChannelMethods.CountByAccount
31 listForApi: VideoChannelMethods.ListForApi 31 listForApi: VideoChannelMethods.ListForApi
32 listByAuthor: VideoChannelMethods.ListByAuthor 32 listByAccount: VideoChannelMethods.ListByAccount
33 listOwned: VideoChannelMethods.ListOwned 33 listOwned: VideoChannelMethods.ListOwned
34 loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor 34 loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
35 loadByUUID: VideoChannelMethods.LoadByUUID 35 loadByUUID: VideoChannelMethods.LoadByUUID
36 loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID 36 loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
37 loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor 37 loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
38 loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor 38 loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
39 loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos 39 loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
40} 40}
41 41
42export interface VideoChannelAttributes { 42export interface VideoChannelAttributes {
@@ -45,8 +45,9 @@ export interface VideoChannelAttributes {
45 name: string 45 name: string
46 description: string 46 description: string
47 remote: boolean 47 remote: boolean
48 url: string
48 49
49 Author?: AuthorInstance 50 Account?: AccountInstance
50 Videos?: VideoInstance[] 51 Videos?: VideoInstance[]
51} 52}
52 53
@@ -57,8 +58,7 @@ export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAtt
57 58
58 isOwned: VideoChannelMethods.IsOwned 59 isOwned: VideoChannelMethods.IsOwned
59 toFormattedJSON: VideoChannelMethods.ToFormattedJSON 60 toFormattedJSON: VideoChannelMethods.ToFormattedJSON
60 toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON 61 toActivityPubObject: VideoChannelMethods.ToActivityPubObject
61 toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
62} 62}
63 63
64export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {} 64export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 46c2db63f..c17828f3e 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -13,19 +13,18 @@ import {
13 13
14let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> 14let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
15let toFormattedJSON: VideoChannelMethods.ToFormattedJSON 15let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
16let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON 16let toActivityPubObject: VideoChannelMethods.ToActivityPubObject
17let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
18let isOwned: VideoChannelMethods.IsOwned 17let isOwned: VideoChannelMethods.IsOwned
19let countByAuthor: VideoChannelMethods.CountByAuthor 18let countByAccount: VideoChannelMethods.CountByAccount
20let listOwned: VideoChannelMethods.ListOwned 19let listOwned: VideoChannelMethods.ListOwned
21let listForApi: VideoChannelMethods.ListForApi 20let listForApi: VideoChannelMethods.ListForApi
22let listByAuthor: VideoChannelMethods.ListByAuthor 21let listByAccount: VideoChannelMethods.ListByAccount
23let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor 22let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
24let loadByUUID: VideoChannelMethods.LoadByUUID 23let loadByUUID: VideoChannelMethods.LoadByUUID
25let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor 24let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
26let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor 25let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
27let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID 26let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
28let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos 27let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
29 28
30export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 29export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
31 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', 30 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
@@ -62,12 +61,19 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
62 type: DataTypes.BOOLEAN, 61 type: DataTypes.BOOLEAN,
63 allowNull: false, 62 allowNull: false,
64 defaultValue: false 63 defaultValue: false
64 },
65 url: {
66 type: DataTypes.STRING,
67 allowNull: false,
68 validate: {
69 isUrl: true
70 }
65 } 71 }
66 }, 72 },
67 { 73 {
68 indexes: [ 74 indexes: [
69 { 75 {
70 fields: [ 'authorId' ] 76 fields: [ 'accountId' ]
71 } 77 }
72 ], 78 ],
73 hooks: { 79 hooks: {
@@ -80,21 +86,20 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
80 associate, 86 associate,
81 87
82 listForApi, 88 listForApi,
83 listByAuthor, 89 listByAccount,
84 listOwned, 90 listOwned,
85 loadByIdAndAuthor, 91 loadByIdAndAccount,
86 loadAndPopulateAuthor, 92 loadAndPopulateAccount,
87 loadByUUIDAndPopulateAuthor, 93 loadByUUIDAndPopulateAccount,
88 loadByUUID, 94 loadByUUID,
89 loadByHostAndUUID, 95 loadByHostAndUUID,
90 loadAndPopulateAuthorAndVideos, 96 loadAndPopulateAccountAndVideos,
91 countByAuthor 97 countByAccount
92 ] 98 ]
93 const instanceMethods = [ 99 const instanceMethods = [
94 isOwned, 100 isOwned,
95 toFormattedJSON, 101 toFormattedJSON,
96 toAddRemoteJSON, 102 toActivityPubObject,
97 toUpdateRemoteJSON
98 ] 103 ]
99 addMethodsToModel(VideoChannel, classMethods, instanceMethods) 104 addMethodsToModel(VideoChannel, classMethods, instanceMethods)
100 105
@@ -118,10 +123,10 @@ toFormattedJSON = function (this: VideoChannelInstance) {
118 updatedAt: this.updatedAt 123 updatedAt: this.updatedAt
119 } 124 }
120 125
121 if (this.Author !== undefined) { 126 if (this.Account !== undefined) {
122 json['owner'] = { 127 json['owner'] = {
123 name: this.Author.name, 128 name: this.Account.name,
124 uuid: this.Author.uuid 129 uuid: this.Account.uuid
125 } 130 }
126 } 131 }
127 132
@@ -132,27 +137,14 @@ toFormattedJSON = function (this: VideoChannelInstance) {
132 return json 137 return json
133} 138}
134 139
135toAddRemoteJSON = function (this: VideoChannelInstance) { 140toActivityPubObject = function (this: VideoChannelInstance) {
136 const json = {
137 uuid: this.uuid,
138 name: this.name,
139 description: this.description,
140 createdAt: this.createdAt,
141 updatedAt: this.updatedAt,
142 ownerUUID: this.Author.uuid
143 }
144
145 return json
146}
147
148toUpdateRemoteJSON = function (this: VideoChannelInstance) {
149 const json = { 141 const json = {
150 uuid: this.uuid, 142 uuid: this.uuid,
151 name: this.name, 143 name: this.name,
152 description: this.description, 144 description: this.description,
153 createdAt: this.createdAt, 145 createdAt: this.createdAt,
154 updatedAt: this.updatedAt, 146 updatedAt: this.updatedAt,
155 ownerUUID: this.Author.uuid 147 ownerUUID: this.Account.uuid
156 } 148 }
157 149
158 return json 150 return json
@@ -161,9 +153,9 @@ toUpdateRemoteJSON = function (this: VideoChannelInstance) {
161// ------------------------------ STATICS ------------------------------ 153// ------------------------------ STATICS ------------------------------
162 154
163function associate (models) { 155function associate (models) {
164 VideoChannel.belongsTo(models.Author, { 156 VideoChannel.belongsTo(models.Account, {
165 foreignKey: { 157 foreignKey: {
166 name: 'authorId', 158 name: 'accountId',
167 allowNull: false 159 allowNull: false
168 }, 160 },
169 onDelete: 'CASCADE' 161 onDelete: 'CASCADE'
@@ -190,10 +182,10 @@ function afterDestroy (videoChannel: VideoChannelInstance) {
190 return undefined 182 return undefined
191} 183}
192 184
193countByAuthor = function (authorId: number) { 185countByAccount = function (accountId: number) {
194 const query = { 186 const query = {
195 where: { 187 where: {
196 authorId 188 accountId
197 } 189 }
198 } 190 }
199 191
@@ -205,7 +197,7 @@ listOwned = function () {
205 where: { 197 where: {
206 remote: false 198 remote: false
207 }, 199 },
208 include: [ VideoChannel['sequelize'].models.Author ] 200 include: [ VideoChannel['sequelize'].models.Account ]
209 } 201 }
210 202
211 return VideoChannel.findAll(query) 203 return VideoChannel.findAll(query)
@@ -218,7 +210,7 @@ listForApi = function (start: number, count: number, sort: string) {
218 order: [ getSort(sort) ], 210 order: [ getSort(sort) ],
219 include: [ 211 include: [
220 { 212 {
221 model: VideoChannel['sequelize'].models.Author, 213 model: VideoChannel['sequelize'].models.Account,
222 required: true, 214 required: true,
223 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 215 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
224 } 216 }
@@ -230,14 +222,14 @@ listForApi = function (start: number, count: number, sort: string) {
230 }) 222 })
231} 223}
232 224
233listByAuthor = function (authorId: number) { 225listByAccount = function (accountId: number) {
234 const query = { 226 const query = {
235 order: [ getSort('createdAt') ], 227 order: [ getSort('createdAt') ],
236 include: [ 228 include: [
237 { 229 {
238 model: VideoChannel['sequelize'].models.Author, 230 model: VideoChannel['sequelize'].models.Account,
239 where: { 231 where: {
240 id: authorId 232 id: accountId
241 }, 233 },
242 required: true, 234 required: true,
243 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 235 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
@@ -269,7 +261,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
269 }, 261 },
270 include: [ 262 include: [
271 { 263 {
272 model: VideoChannel['sequelize'].models.Author, 264 model: VideoChannel['sequelize'].models.Account,
273 include: [ 265 include: [
274 { 266 {
275 model: VideoChannel['sequelize'].models.Pod, 267 model: VideoChannel['sequelize'].models.Pod,
@@ -288,15 +280,15 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
288 return VideoChannel.findOne(query) 280 return VideoChannel.findOne(query)
289} 281}
290 282
291loadByIdAndAuthor = function (id: number, authorId: number) { 283loadByIdAndAccount = function (id: number, accountId: number) {
292 const options = { 284 const options = {
293 where: { 285 where: {
294 id, 286 id,
295 authorId 287 accountId
296 }, 288 },
297 include: [ 289 include: [
298 { 290 {
299 model: VideoChannel['sequelize'].models.Author, 291 model: VideoChannel['sequelize'].models.Account,
300 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 292 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
301 } 293 }
302 ] 294 ]
@@ -305,11 +297,11 @@ loadByIdAndAuthor = function (id: number, authorId: number) {
305 return VideoChannel.findOne(options) 297 return VideoChannel.findOne(options)
306} 298}
307 299
308loadAndPopulateAuthor = function (id: number) { 300loadAndPopulateAccount = function (id: number) {
309 const options = { 301 const options = {
310 include: [ 302 include: [
311 { 303 {
312 model: VideoChannel['sequelize'].models.Author, 304 model: VideoChannel['sequelize'].models.Account,
313 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 305 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
314 } 306 }
315 ] 307 ]
@@ -318,14 +310,14 @@ loadAndPopulateAuthor = function (id: number) {
318 return VideoChannel.findById(id, options) 310 return VideoChannel.findById(id, options)
319} 311}
320 312
321loadByUUIDAndPopulateAuthor = function (uuid: string) { 313loadByUUIDAndPopulateAccount = function (uuid: string) {
322 const options = { 314 const options = {
323 where: { 315 where: {
324 uuid 316 uuid
325 }, 317 },
326 include: [ 318 include: [
327 { 319 {
328 model: VideoChannel['sequelize'].models.Author, 320 model: VideoChannel['sequelize'].models.Account,
329 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 321 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
330 } 322 }
331 ] 323 ]
@@ -334,11 +326,11 @@ loadByUUIDAndPopulateAuthor = function (uuid: string) {
334 return VideoChannel.findOne(options) 326 return VideoChannel.findOne(options)
335} 327}
336 328
337loadAndPopulateAuthorAndVideos = function (id: number) { 329loadAndPopulateAccountAndVideos = function (id: number) {
338 const options = { 330 const options = {
339 include: [ 331 include: [
340 { 332 {
341 model: VideoChannel['sequelize'].models.Author, 333 model: VideoChannel['sequelize'].models.Account,
342 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 334 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
343 }, 335 },
344 VideoChannel['sequelize'].models.Video 336 VideoChannel['sequelize'].models.Video
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index cfe65f9aa..e62e25a82 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -1,5 +1,5 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3
4import { TagAttributes, TagInstance } from './tag-interface' 4import { TagAttributes, TagInstance } from './tag-interface'
5import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' 5import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
@@ -13,6 +13,7 @@ import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/
13import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' 13import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
14import { ResultList } from '../../../shared/models/result-list.model' 14import { ResultList } from '../../../shared/models/result-list.model'
15import { VideoChannelInstance } from './video-channel-interface' 15import { VideoChannelInstance } from './video-channel-interface'
16import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
16 17
17export namespace VideoMethods { 18export namespace VideoMethods {
18 export type GetThumbnailName = (this: VideoInstance) => string 19 export type GetThumbnailName = (this: VideoInstance) => string
@@ -29,8 +30,7 @@ export namespace VideoMethods {
29 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string 30 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
30 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> 31 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
31 32
32 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData> 33 export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject
33 export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData
34 34
35 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void> 35 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
36 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void> 36 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
@@ -40,31 +40,35 @@ export namespace VideoMethods {
40 export type GetPreviewPath = (this: VideoInstance) => string 40 export type GetPreviewPath = (this: VideoInstance) => string
41 export type GetDescriptionPath = (this: VideoInstance) => string 41 export type GetDescriptionPath = (this: VideoInstance) => string
42 export type GetTruncatedDescription = (this: VideoInstance) => string 42 export type GetTruncatedDescription = (this: VideoInstance) => string
43 export type GetCategoryLabel = (this: VideoInstance) => string
44 export type GetLicenceLabel = (this: VideoInstance) => string
45 export type GetLanguageLabel = (this: VideoInstance) => string
43 46
44 // Return thumbnail name 47 // Return thumbnail name
45 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> 48 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
46 49
47 export type List = () => Promise<VideoInstance[]> 50 export type List = () => Bluebird<VideoInstance[]>
48 export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]> 51 export type ListOwnedAndPopulateAccountAndTags = () => Bluebird<VideoInstance[]>
49 export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]> 52 export type ListOwnedByAccount = (account: string) => Bluebird<VideoInstance[]>
50 53
51 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> > 54 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
52 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> > 55 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
53 export type SearchAndPopulateAuthorAndPodAndTags = ( 56 export type SearchAndPopulateAccountAndPodAndTags = (
54 value: string, 57 value: string,
55 field: string, 58 field: string,
56 start: number, 59 start: number,
57 count: number, 60 count: number,
58 sort: string 61 sort: string
59 ) => Promise< ResultList<VideoInstance> > 62 ) => Bluebird< ResultList<VideoInstance> >
60 63
61 export type Load = (id: number) => Promise<VideoInstance> 64 export type Load = (id: number) => Bluebird<VideoInstance>
62 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance> 65 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
63 export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance> 66 export type LoadByUrl = (url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
64 export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance> 67 export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
65 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance> 68 export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
66 export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance> 69 export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance>
67 export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance> 70 export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance>
71 export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance>
68 72
69 export type RemoveThumbnail = (this: VideoInstance) => Promise<void> 73 export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
70 export type RemovePreview = (this: VideoInstance) => Promise<void> 74 export type RemovePreview = (this: VideoInstance) => Promise<void>
@@ -77,16 +81,17 @@ export interface VideoClass {
77 list: VideoMethods.List 81 list: VideoMethods.List
78 listForApi: VideoMethods.ListForApi 82 listForApi: VideoMethods.ListForApi
79 listUserVideosForApi: VideoMethods.ListUserVideosForApi 83 listUserVideosForApi: VideoMethods.ListUserVideosForApi
80 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 84 listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
81 listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 85 listOwnedByAccount: VideoMethods.ListOwnedByAccount
82 load: VideoMethods.Load 86 load: VideoMethods.Load
83 loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor 87 loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
84 loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 88 loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
85 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 89 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
86 loadByUUID: VideoMethods.LoadByUUID 90 loadByUUID: VideoMethods.LoadByUUID
91 loadByUrl: VideoMethods.LoadByUrl
87 loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID 92 loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
88 loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 93 loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
89 searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 94 searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
90} 95}
91 96
92export interface VideoAttributes { 97export interface VideoAttributes {
@@ -104,7 +109,9 @@ export interface VideoAttributes {
104 likes?: number 109 likes?: number
105 dislikes?: number 110 dislikes?: number
106 remote: boolean 111 remote: boolean
112 url: string
107 113
114 parentId?: number
108 channelId?: number 115 channelId?: number
109 116
110 VideoChannel?: VideoChannelInstance 117 VideoChannel?: VideoChannelInstance
@@ -132,16 +139,18 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
132 removePreview: VideoMethods.RemovePreview 139 removePreview: VideoMethods.RemovePreview
133 removeThumbnail: VideoMethods.RemoveThumbnail 140 removeThumbnail: VideoMethods.RemoveThumbnail
134 removeTorrent: VideoMethods.RemoveTorrent 141 removeTorrent: VideoMethods.RemoveTorrent
135 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 142 toActivityPubObject: VideoMethods.ToActivityPubObject
136 toFormattedJSON: VideoMethods.ToFormattedJSON 143 toFormattedJSON: VideoMethods.ToFormattedJSON
137 toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON 144 toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
138 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
139 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 145 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
140 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile 146 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
141 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight 147 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
142 getEmbedPath: VideoMethods.GetEmbedPath 148 getEmbedPath: VideoMethods.GetEmbedPath
143 getDescriptionPath: VideoMethods.GetDescriptionPath 149 getDescriptionPath: VideoMethods.GetDescriptionPath
144 getTruncatedDescription: VideoMethods.GetTruncatedDescription 150 getTruncatedDescription: VideoMethods.GetTruncatedDescription
151 getCategoryLabel: VideoMethods.GetCategoryLabel
152 getLicenceLabel: VideoMethods.GetLicenceLabel
153 getLanguageLabel: VideoMethods.GetLanguageLabel
145 154
146 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> 155 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
147 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string> 156 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
@@ -149,3 +158,4 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
149} 158}
150 159
151export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} 160export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
161
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 02dde1726..94af1ece5 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -5,7 +5,6 @@ import { map, maxBy, truncate } from 'lodash'
5import * as parseTorrent from 'parse-torrent' 5import * as parseTorrent from 'parse-torrent'
6import { join } from 'path' 6import { join } from 'path'
7import * as Sequelize from 'sequelize' 7import * as Sequelize from 'sequelize'
8import * as Promise from 'bluebird'
9 8
10import { TagInstance } from './tag-interface' 9import { TagInstance } from './tag-interface'
11import { 10import {
@@ -52,6 +51,7 @@ import {
52 51
53 VideoMethods 52 VideoMethods
54} from './video-interface' 53} from './video-interface'
54import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
55 55
56let Video: Sequelize.Model<VideoInstance, VideoAttributes> 56let Video: Sequelize.Model<VideoInstance, VideoAttributes>
57let getOriginalFile: VideoMethods.GetOriginalFile 57let getOriginalFile: VideoMethods.GetOriginalFile
@@ -64,8 +64,7 @@ let getTorrentFileName: VideoMethods.GetTorrentFileName
64let isOwned: VideoMethods.IsOwned 64let isOwned: VideoMethods.IsOwned
65let toFormattedJSON: VideoMethods.ToFormattedJSON 65let toFormattedJSON: VideoMethods.ToFormattedJSON
66let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON 66let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
67let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 67let toActivityPubObject: VideoMethods.ToActivityPubObject
68let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
69let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 68let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
70let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile 69let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
71let createPreview: VideoMethods.CreatePreview 70let createPreview: VideoMethods.CreatePreview
@@ -76,21 +75,25 @@ let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
76let getEmbedPath: VideoMethods.GetEmbedPath 75let getEmbedPath: VideoMethods.GetEmbedPath
77let getDescriptionPath: VideoMethods.GetDescriptionPath 76let getDescriptionPath: VideoMethods.GetDescriptionPath
78let getTruncatedDescription: VideoMethods.GetTruncatedDescription 77let getTruncatedDescription: VideoMethods.GetTruncatedDescription
78let getCategoryLabel: VideoMethods.GetCategoryLabel
79let getLicenceLabel: VideoMethods.GetLicenceLabel
80let getLanguageLabel: VideoMethods.GetLanguageLabel
79 81
80let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 82let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
81let list: VideoMethods.List 83let list: VideoMethods.List
82let listForApi: VideoMethods.ListForApi 84let listForApi: VideoMethods.ListForApi
83let listUserVideosForApi: VideoMethods.ListUserVideosForApi 85let listUserVideosForApi: VideoMethods.ListUserVideosForApi
84let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 86let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
85let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 87let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
86let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 88let listOwnedByAccount: VideoMethods.ListOwnedByAccount
87let load: VideoMethods.Load 89let load: VideoMethods.Load
88let loadByUUID: VideoMethods.LoadByUUID 90let loadByUUID: VideoMethods.LoadByUUID
91let loadByUrl: VideoMethods.LoadByUrl
89let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID 92let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
90let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor 93let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
91let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 94let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
92let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 95let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
93let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 96let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
94let removeThumbnail: VideoMethods.RemoveThumbnail 97let removeThumbnail: VideoMethods.RemoveThumbnail
95let removePreview: VideoMethods.RemovePreview 98let removePreview: VideoMethods.RemovePreview
96let removeFile: VideoMethods.RemoveFile 99let removeFile: VideoMethods.RemoveFile
@@ -219,6 +222,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
219 type: DataTypes.BOOLEAN, 222 type: DataTypes.BOOLEAN,
220 allowNull: false, 223 allowNull: false,
221 defaultValue: false 224 defaultValue: false
225 },
226 url: {
227 type: DataTypes.STRING,
228 allowNull: false,
229 validate: {
230 isUrl: true
231 }
222 } 232 }
223 }, 233 },
224 { 234 {
@@ -243,6 +253,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
243 }, 253 },
244 { 254 {
245 fields: [ 'channelId' ] 255 fields: [ 'channelId' ]
256 },
257 {
258 fields: [ 'parentId' ]
246 } 259 }
247 ], 260 ],
248 hooks: { 261 hooks: {
@@ -258,16 +271,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
258 list, 271 list,
259 listForApi, 272 listForApi,
260 listUserVideosForApi, 273 listUserVideosForApi,
261 listOwnedAndPopulateAuthorAndTags, 274 listOwnedAndPopulateAccountAndTags,
262 listOwnedByAuthor, 275 listOwnedByAccount,
263 load, 276 load,
264 loadAndPopulateAuthor, 277 loadAndPopulateAccount,
265 loadAndPopulateAuthorAndPodAndTags, 278 loadAndPopulateAccountAndPodAndTags,
266 loadByHostAndUUID, 279 loadByHostAndUUID,
267 loadByUUID, 280 loadByUUID,
268 loadLocalVideoByUUID, 281 loadLocalVideoByUUID,
269 loadByUUIDAndPopulateAuthorAndPodAndTags, 282 loadByUUIDAndPopulateAccountAndPodAndTags,
270 searchAndPopulateAuthorAndPodAndTags 283 searchAndPopulateAccountAndPodAndTags
271 ] 284 ]
272 const instanceMethods = [ 285 const instanceMethods = [
273 createPreview, 286 createPreview,
@@ -286,16 +299,18 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
286 removePreview, 299 removePreview,
287 removeThumbnail, 300 removeThumbnail,
288 removeTorrent, 301 removeTorrent,
289 toAddRemoteJSON, 302 toActivityPubObject,
290 toFormattedJSON, 303 toFormattedJSON,
291 toFormattedDetailsJSON, 304 toFormattedDetailsJSON,
292 toUpdateRemoteJSON,
293 optimizeOriginalVideofile, 305 optimizeOriginalVideofile,
294 transcodeOriginalVideofile, 306 transcodeOriginalVideofile,
295 getOriginalFileHeight, 307 getOriginalFileHeight,
296 getEmbedPath, 308 getEmbedPath,
297 getTruncatedDescription, 309 getTruncatedDescription,
298 getDescriptionPath 310 getDescriptionPath,
311 getCategoryLabel,
312 getLicenceLabel,
313 getLanguageLabel
299 ] 314 ]
300 addMethodsToModel(Video, classMethods, instanceMethods) 315 addMethodsToModel(Video, classMethods, instanceMethods)
301 316
@@ -313,6 +328,14 @@ function associate (models) {
313 onDelete: 'cascade' 328 onDelete: 'cascade'
314 }) 329 })
315 330
331 Video.belongsTo(models.VideoChannel, {
332 foreignKey: {
333 name: 'parentId',
334 allowNull: true
335 },
336 onDelete: 'cascade'
337 })
338
316 Video.belongsToMany(models.Tag, { 339 Video.belongsToMany(models.Tag, {
317 foreignKey: 'videoId', 340 foreignKey: 'videoId',
318 through: models.VideoTag, 341 through: models.VideoTag,
@@ -423,7 +446,7 @@ getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance)
423 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 446 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
424} 447}
425 448
426createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { 449createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
427 const options = { 450 const options = {
428 announceList: [ 451 announceList: [
429 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] 452 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
@@ -433,18 +456,15 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil
433 ] 456 ]
434 } 457 }
435 458
436 return createTorrentPromise(this.getVideoFilePath(videoFile), options) 459 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
437 .then(torrent => {
438 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
439 logger.info('Creating torrent %s.', filePath)
440 460
441 return writeFilePromise(filePath, torrent).then(() => torrent) 461 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
442 }) 462 logger.info('Creating torrent %s.', filePath)
443 .then(torrent => {
444 const parsedTorrent = parseTorrent(torrent)
445 463
446 videoFile.infoHash = parsedTorrent.infoHash 464 await writeFilePromise(filePath, torrent)
447 }) 465
466 const parsedTorrent = parseTorrent(torrent)
467 videoFile.infoHash = parsedTorrent.infoHash
448} 468}
449 469
450getEmbedPath = function (this: VideoInstance) { 470getEmbedPath = function (this: VideoInstance) {
@@ -462,40 +482,28 @@ getPreviewPath = function (this: VideoInstance) {
462toFormattedJSON = function (this: VideoInstance) { 482toFormattedJSON = function (this: VideoInstance) {
463 let podHost 483 let podHost
464 484
465 if (this.VideoChannel.Author.Pod) { 485 if (this.VideoChannel.Account.Pod) {
466 podHost = this.VideoChannel.Author.Pod.host 486 podHost = this.VideoChannel.Account.Pod.host
467 } else { 487 } else {
468 // It means it's our video 488 // It means it's our video
469 podHost = CONFIG.WEBSERVER.HOST 489 podHost = CONFIG.WEBSERVER.HOST
470 } 490 }
471 491
472 // Maybe our pod is not up to date and there are new categories since our version
473 let categoryLabel = VIDEO_CATEGORIES[this.category]
474 if (!categoryLabel) categoryLabel = 'Misc'
475
476 // Maybe our pod is not up to date and there are new licences since our version
477 let licenceLabel = VIDEO_LICENCES[this.licence]
478 if (!licenceLabel) licenceLabel = 'Unknown'
479
480 // Language is an optional attribute
481 let languageLabel = VIDEO_LANGUAGES[this.language]
482 if (!languageLabel) languageLabel = 'Unknown'
483
484 const json = { 492 const json = {
485 id: this.id, 493 id: this.id,
486 uuid: this.uuid, 494 uuid: this.uuid,
487 name: this.name, 495 name: this.name,
488 category: this.category, 496 category: this.category,
489 categoryLabel, 497 categoryLabel: this.getCategoryLabel(),
490 licence: this.licence, 498 licence: this.licence,
491 licenceLabel, 499 licenceLabel: this.getLicenceLabel(),
492 language: this.language, 500 language: this.language,
493 languageLabel, 501 languageLabel: this.getLanguageLabel(),
494 nsfw: this.nsfw, 502 nsfw: this.nsfw,
495 description: this.getTruncatedDescription(), 503 description: this.getTruncatedDescription(),
496 podHost, 504 podHost,
497 isLocal: this.isOwned(), 505 isLocal: this.isOwned(),
498 author: this.VideoChannel.Author.name, 506 account: this.VideoChannel.Account.name,
499 duration: this.duration, 507 duration: this.duration,
500 views: this.views, 508 views: this.views,
501 likes: this.likes, 509 likes: this.likes,
@@ -552,75 +560,75 @@ toFormattedDetailsJSON = function (this: VideoInstance) {
552 return Object.assign(formattedJson, detailsJson) 560 return Object.assign(formattedJson, detailsJson)
553} 561}
554 562
555toAddRemoteJSON = function (this: VideoInstance) { 563toActivityPubObject = function (this: VideoInstance) {
556 // Get thumbnail data to send to the other pod 564 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
557 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
558 565
559 return readFileBufferPromise(thumbnailPath).then(thumbnailData => { 566 const tag = this.Tags.map(t => ({
560 const remoteVideo = { 567 type: 'Hashtag',
561 uuid: this.uuid, 568 name: t.name
562 name: this.name, 569 }))
563 category: this.category, 570
564 licence: this.licence, 571 const url = []
565 language: this.language, 572 for (const file of this.VideoFiles) {
566 nsfw: this.nsfw, 573 url.push({
567 truncatedDescription: this.getTruncatedDescription(), 574 type: 'Link',
568 channelUUID: this.VideoChannel.uuid, 575 mimeType: 'video/' + file.extname,
569 duration: this.duration, 576 url: getVideoFileUrl(this, file, baseUrlHttp),
570 thumbnailData: thumbnailData.toString('binary'), 577 width: file.resolution,
571 tags: map<TagInstance, string>(this.Tags, 'name'), 578 size: file.size
572 createdAt: this.createdAt, 579 })
573 updatedAt: this.updatedAt,
574 views: this.views,
575 likes: this.likes,
576 dislikes: this.dislikes,
577 privacy: this.privacy,
578 files: []
579 }
580 580
581 this.VideoFiles.forEach(videoFile => { 581 url.push({
582 remoteVideo.files.push({ 582 type: 'Link',
583 infoHash: videoFile.infoHash, 583 mimeType: 'application/x-bittorrent',
584 resolution: videoFile.resolution, 584 url: getTorrentUrl(this, file, baseUrlHttp),
585 extname: videoFile.extname, 585 width: file.resolution
586 size: videoFile.size
587 })
588 }) 586 })
589 587
590 return remoteVideo 588 url.push({
591 }) 589 type: 'Link',
592} 590 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
591 url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
592 width: file.resolution
593 })
594 }
593 595
594toUpdateRemoteJSON = function (this: VideoInstance) { 596 const videoObject: VideoTorrentObject = {
595 const json = { 597 type: 'Video',
596 uuid: this.uuid,
597 name: this.name, 598 name: this.name,
598 category: this.category, 599 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
599 licence: this.licence, 600 duration: 'PT' + this.duration + 'S',
600 language: this.language, 601 uuid: this.uuid,
601 nsfw: this.nsfw, 602 tag,
602 truncatedDescription: this.getTruncatedDescription(), 603 category: {
603 duration: this.duration, 604 id: this.category,
604 tags: map<TagInstance, string>(this.Tags, 'name'), 605 label: this.getCategoryLabel()
605 createdAt: this.createdAt, 606 },
606 updatedAt: this.updatedAt, 607 licence: {
608 id: this.licence,
609 name: this.getLicenceLabel()
610 },
611 language: {
612 id: this.language,
613 name: this.getLanguageLabel()
614 },
607 views: this.views, 615 views: this.views,
608 likes: this.likes, 616 nsfw: this.nsfw,
609 dislikes: this.dislikes, 617 published: this.createdAt,
610 privacy: this.privacy, 618 updated: this.updatedAt,
611 files: [] 619 mediaType: 'text/markdown',
620 content: this.getTruncatedDescription(),
621 icon: {
622 type: 'Image',
623 url: getThumbnailUrl(this, baseUrlHttp),
624 mediaType: 'image/jpeg',
625 width: THUMBNAILS_SIZE.width,
626 height: THUMBNAILS_SIZE.height
627 },
628 url
612 } 629 }
613 630
614 this.VideoFiles.forEach(videoFile => { 631 return videoObject
615 json.files.push({
616 infoHash: videoFile.infoHash,
617 resolution: videoFile.resolution,
618 extname: videoFile.extname,
619 size: videoFile.size
620 })
621 })
622
623 return json
624} 632}
625 633
626getTruncatedDescription = function (this: VideoInstance) { 634getTruncatedDescription = function (this: VideoInstance) {
@@ -631,7 +639,7 @@ getTruncatedDescription = function (this: VideoInstance) {
631 return truncate(this.description, options) 639 return truncate(this.description, options)
632} 640}
633 641
634optimizeOriginalVideofile = function (this: VideoInstance) { 642optimizeOriginalVideofile = async function (this: VideoInstance) {
635 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 643 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
636 const newExtname = '.mp4' 644 const newExtname = '.mp4'
637 const inputVideoFile = this.getOriginalFile() 645 const inputVideoFile = this.getOriginalFile()
@@ -643,40 +651,32 @@ optimizeOriginalVideofile = function (this: VideoInstance) {
643 outputPath: videoOutputPath 651 outputPath: videoOutputPath
644 } 652 }
645 653
646 return transcode(transcodeOptions) 654 try {
647 .then(() => { 655 // Could be very long!
648 return unlinkPromise(videoInputPath) 656 await transcode(transcodeOptions)
649 })
650 .then(() => {
651 // Important to do this before getVideoFilename() to take in account the new file extension
652 inputVideoFile.set('extname', newExtname)
653 657
654 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) 658 await unlinkPromise(videoInputPath)
655 })
656 .then(() => {
657 return statPromise(this.getVideoFilePath(inputVideoFile))
658 })
659 .then(stats => {
660 return inputVideoFile.set('size', stats.size)
661 })
662 .then(() => {
663 return this.createTorrentAndSetInfoHash(inputVideoFile)
664 })
665 .then(() => {
666 return inputVideoFile.save()
667 })
668 .then(() => {
669 return undefined
670 })
671 .catch(err => {
672 // Auto destruction...
673 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
674 659
675 throw err 660 // Important to do this before getVideoFilename() to take in account the new file extension
676 }) 661 inputVideoFile.set('extname', newExtname)
662
663 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
664 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
665
666 inputVideoFile.set('size', stats.size)
667
668 await this.createTorrentAndSetInfoHash(inputVideoFile)
669 await inputVideoFile.save()
670
671 } catch (err) {
672 // Auto destruction...
673 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
674
675 throw err
676 }
677} 677}
678 678
679transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { 679transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
680 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 680 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
681 const extname = '.mp4' 681 const extname = '.mp4'
682 682
@@ -696,25 +696,18 @@ transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoRes
696 outputPath: videoOutputPath, 696 outputPath: videoOutputPath,
697 resolution 697 resolution
698 } 698 }
699 return transcode(transcodeOptions)
700 .then(() => {
701 return statPromise(videoOutputPath)
702 })
703 .then(stats => {
704 newVideoFile.set('size', stats.size)
705 699
706 return undefined 700 await transcode(transcodeOptions)
707 }) 701
708 .then(() => { 702 const stats = await statPromise(videoOutputPath)
709 return this.createTorrentAndSetInfoHash(newVideoFile) 703
710 }) 704 newVideoFile.set('size', stats.size)
711 .then(() => { 705
712 return newVideoFile.save() 706 await this.createTorrentAndSetInfoHash(newVideoFile)
713 }) 707
714 .then(() => { 708 await newVideoFile.save()
715 return this.VideoFiles.push(newVideoFile) 709
716 }) 710 this.VideoFiles.push(newVideoFile)
717 .then(() => undefined)
718} 711}
719 712
720getOriginalFileHeight = function (this: VideoInstance) { 713getOriginalFileHeight = function (this: VideoInstance) {
@@ -727,6 +720,31 @@ getDescriptionPath = function (this: VideoInstance) {
727 return `/api/${API_VERSION}/videos/${this.uuid}/description` 720 return `/api/${API_VERSION}/videos/${this.uuid}/description`
728} 721}
729 722
723getCategoryLabel = function (this: VideoInstance) {
724 let categoryLabel = VIDEO_CATEGORIES[this.category]
725
726 // Maybe our pod is not up to date and there are new categories since our version
727 if (!categoryLabel) categoryLabel = 'Misc'
728
729 return categoryLabel
730}
731
732getLicenceLabel = function (this: VideoInstance) {
733 let licenceLabel = VIDEO_LICENCES[this.licence]
734 // Maybe our pod is not up to date and there are new licences since our version
735 if (!licenceLabel) licenceLabel = 'Unknown'
736
737 return licenceLabel
738}
739
740getLanguageLabel = function (this: VideoInstance) {
741 // Language is an optional attribute
742 let languageLabel = VIDEO_LANGUAGES[this.language]
743 if (!languageLabel) languageLabel = 'Unknown'
744
745 return languageLabel
746}
747
730removeThumbnail = function (this: VideoInstance) { 748removeThumbnail = function (this: VideoInstance) {
731 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) 749 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
732 return unlinkPromise(thumbnailPath) 750 return unlinkPromise(thumbnailPath)
@@ -779,7 +797,7 @@ listUserVideosForApi = function (userId: number, start: number, count: number, s
779 required: true, 797 required: true,
780 include: [ 798 include: [
781 { 799 {
782 model: Video['sequelize'].models.Author, 800 model: Video['sequelize'].models.Account,
783 where: { 801 where: {
784 userId 802 userId
785 }, 803 },
@@ -810,7 +828,7 @@ listForApi = function (start: number, count: number, sort: string) {
810 model: Video['sequelize'].models.VideoChannel, 828 model: Video['sequelize'].models.VideoChannel,
811 include: [ 829 include: [
812 { 830 {
813 model: Video['sequelize'].models.Author, 831 model: Video['sequelize'].models.Account,
814 include: [ 832 include: [
815 { 833 {
816 model: Video['sequelize'].models.Pod, 834 model: Video['sequelize'].models.Pod,
@@ -846,7 +864,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
846 model: Video['sequelize'].models.VideoChannel, 864 model: Video['sequelize'].models.VideoChannel,
847 include: [ 865 include: [
848 { 866 {
849 model: Video['sequelize'].models.Author, 867 model: Video['sequelize'].models.Account,
850 include: [ 868 include: [
851 { 869 {
852 model: Video['sequelize'].models.Pod, 870 model: Video['sequelize'].models.Pod,
@@ -867,7 +885,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
867 return Video.findOne(query) 885 return Video.findOne(query)
868} 886}
869 887
870listOwnedAndPopulateAuthorAndTags = function () { 888listOwnedAndPopulateAccountAndTags = function () {
871 const query = { 889 const query = {
872 where: { 890 where: {
873 remote: false 891 remote: false
@@ -876,7 +894,7 @@ listOwnedAndPopulateAuthorAndTags = function () {
876 Video['sequelize'].models.VideoFile, 894 Video['sequelize'].models.VideoFile,
877 { 895 {
878 model: Video['sequelize'].models.VideoChannel, 896 model: Video['sequelize'].models.VideoChannel,
879 include: [ Video['sequelize'].models.Author ] 897 include: [ Video['sequelize'].models.Account ]
880 }, 898 },
881 Video['sequelize'].models.Tag 899 Video['sequelize'].models.Tag
882 ] 900 ]
@@ -885,7 +903,7 @@ listOwnedAndPopulateAuthorAndTags = function () {
885 return Video.findAll(query) 903 return Video.findAll(query)
886} 904}
887 905
888listOwnedByAuthor = function (author: string) { 906listOwnedByAccount = function (account: string) {
889 const query = { 907 const query = {
890 where: { 908 where: {
891 remote: false 909 remote: false
@@ -898,9 +916,9 @@ listOwnedByAuthor = function (author: string) {
898 model: Video['sequelize'].models.VideoChannel, 916 model: Video['sequelize'].models.VideoChannel,
899 include: [ 917 include: [
900 { 918 {
901 model: Video['sequelize'].models.Author, 919 model: Video['sequelize'].models.Account,
902 where: { 920 where: {
903 name: author 921 name: account
904 } 922 }
905 } 923 }
906 ] 924 ]
@@ -942,13 +960,13 @@ loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
942 return Video.findOne(query) 960 return Video.findOne(query)
943} 961}
944 962
945loadAndPopulateAuthor = function (id: number) { 963loadAndPopulateAccount = function (id: number) {
946 const options = { 964 const options = {
947 include: [ 965 include: [
948 Video['sequelize'].models.VideoFile, 966 Video['sequelize'].models.VideoFile,
949 { 967 {
950 model: Video['sequelize'].models.VideoChannel, 968 model: Video['sequelize'].models.VideoChannel,
951 include: [ Video['sequelize'].models.Author ] 969 include: [ Video['sequelize'].models.Account ]
952 } 970 }
953 ] 971 ]
954 } 972 }
@@ -956,14 +974,14 @@ loadAndPopulateAuthor = function (id: number) {
956 return Video.findById(id, options) 974 return Video.findById(id, options)
957} 975}
958 976
959loadAndPopulateAuthorAndPodAndTags = function (id: number) { 977loadAndPopulateAccountAndPodAndTags = function (id: number) {
960 const options = { 978 const options = {
961 include: [ 979 include: [
962 { 980 {
963 model: Video['sequelize'].models.VideoChannel, 981 model: Video['sequelize'].models.VideoChannel,
964 include: [ 982 include: [
965 { 983 {
966 model: Video['sequelize'].models.Author, 984 model: Video['sequelize'].models.Account,
967 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 985 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
968 } 986 }
969 ] 987 ]
@@ -976,7 +994,7 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
976 return Video.findById(id, options) 994 return Video.findById(id, options)
977} 995}
978 996
979loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { 997loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
980 const options = { 998 const options = {
981 where: { 999 where: {
982 uuid 1000 uuid
@@ -986,7 +1004,7 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
986 model: Video['sequelize'].models.VideoChannel, 1004 model: Video['sequelize'].models.VideoChannel,
987 include: [ 1005 include: [
988 { 1006 {
989 model: Video['sequelize'].models.Author, 1007 model: Video['sequelize'].models.Account,
990 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 1008 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
991 } 1009 }
992 ] 1010 ]
@@ -999,20 +1017,20 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
999 return Video.findOne(options) 1017 return Video.findOne(options)
1000} 1018}
1001 1019
1002searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { 1020searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1003 const podInclude: Sequelize.IncludeOptions = { 1021 const podInclude: Sequelize.IncludeOptions = {
1004 model: Video['sequelize'].models.Pod, 1022 model: Video['sequelize'].models.Pod,
1005 required: false 1023 required: false
1006 } 1024 }
1007 1025
1008 const authorInclude: Sequelize.IncludeOptions = { 1026 const accountInclude: Sequelize.IncludeOptions = {
1009 model: Video['sequelize'].models.Author, 1027 model: Video['sequelize'].models.Account,
1010 include: [ podInclude ] 1028 include: [ podInclude ]
1011 } 1029 }
1012 1030
1013 const videoChannelInclude: Sequelize.IncludeOptions = { 1031 const videoChannelInclude: Sequelize.IncludeOptions = {
1014 model: Video['sequelize'].models.VideoChannel, 1032 model: Video['sequelize'].models.VideoChannel,
1015 include: [ authorInclude ], 1033 include: [ accountInclude ],
1016 required: true 1034 required: true
1017 } 1035 }
1018 1036
@@ -1045,8 +1063,8 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
1045 } 1063 }
1046 } 1064 }
1047 podInclude.required = true 1065 podInclude.required = true
1048 } else if (field === 'author') { 1066 } else if (field === 'account') {
1049 authorInclude.where = { 1067 accountInclude.where = {
1050 name: { 1068 name: {
1051 [Sequelize.Op.iLike]: '%' + value + '%' 1069 [Sequelize.Op.iLike]: '%' + value + '%'
1052 } 1070 }
@@ -1090,13 +1108,17 @@ function getBaseUrls (video: VideoInstance) {
1090 baseUrlHttp = CONFIG.WEBSERVER.URL 1108 baseUrlHttp = CONFIG.WEBSERVER.URL
1091 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 1109 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1092 } else { 1110 } else {
1093 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host 1111 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host
1094 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host 1112 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host
1095 } 1113 }
1096 1114
1097 return { baseUrlHttp, baseUrlWs } 1115 return { baseUrlHttp, baseUrlWs }
1098} 1116}
1099 1117
1118function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
1119 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
1120}
1121
1100function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { 1122function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1101 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile) 1123 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1102} 1124}