aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-follow-interface.ts23
-rw-r--r--server/models/account/account-follow.ts56
-rw-r--r--server/models/account/account-interface.ts74
-rw-r--r--server/models/account/account-video-rate-interface.ts26
-rw-r--r--server/models/account/account-video-rate.ts (renamed from server/models/user/user-video-rate.ts)36
-rw-r--r--server/models/account/account.ts444
-rw-r--r--server/models/account/index.ts4
-rw-r--r--server/models/account/user-interface.ts (renamed from server/models/user/user-interface.ts)20
-rw-r--r--server/models/account/user.ts (renamed from server/models/user/user.ts)27
-rw-r--r--server/models/index.ts2
-rw-r--r--server/models/job/job-interface.ts6
-rw-r--r--server/models/job/job.ts12
-rw-r--r--server/models/oauth/oauth-token-interface.ts2
-rw-r--r--server/models/pod/pod-interface.ts2
-rw-r--r--server/models/pod/pod.ts12
-rw-r--r--server/models/user/index.ts2
-rw-r--r--server/models/user/user-video-rate-interface.ts26
-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
23 files changed, 981 insertions, 585 deletions
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts
new file mode 100644
index 000000000..3be383649
--- /dev/null
+++ b/server/models/account/account-follow-interface.ts
@@ -0,0 +1,23 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5
6export namespace AccountFollowMethods {
7}
8
9export interface AccountFollowClass {
10}
11
12export interface AccountFollowAttributes {
13 accountId: number
14 targetAccountId: number
15}
16
17export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
18 id: number
19 createdAt: Date
20 updatedAt: Date
21}
22
23export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
new file mode 100644
index 000000000..9bf03b253
--- /dev/null
+++ b/server/models/account/account-follow.ts
@@ -0,0 +1,56 @@
1import * as Sequelize from 'sequelize'
2
3import { addMethodsToModel } from '../utils'
4import {
5 AccountFollowInstance,
6 AccountFollowAttributes,
7
8 AccountFollowMethods
9} from './account-follow-interface'
10
11let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes>
12
13export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
14 AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
15 { },
16 {
17 indexes: [
18 {
19 fields: [ 'accountId' ],
20 unique: true
21 },
22 {
23 fields: [ 'targetAccountId' ],
24 unique: true
25 }
26 ]
27 }
28 )
29
30 const classMethods = [
31 associate
32 ]
33 addMethodsToModel(AccountFollow, classMethods)
34
35 return AccountFollow
36}
37
38// ------------------------------ STATICS ------------------------------
39
40function associate (models) {
41 AccountFollow.belongsTo(models.Account, {
42 foreignKey: {
43 name: 'accountId',
44 allowNull: false
45 },
46 onDelete: 'CASCADE'
47 })
48
49 AccountFollow.belongsTo(models.Account, {
50 foreignKey: {
51 name: 'targetAccountId',
52 allowNull: false
53 },
54 onDelete: 'CASCADE'
55 })
56}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
new file mode 100644
index 000000000..2ef3e2246
--- /dev/null
+++ b/server/models/account/account-interface.ts
@@ -0,0 +1,74 @@
1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird'
3
4import { PodInstance } from '../pod/pod-interface'
5import { VideoChannelInstance } from '../video/video-channel-interface'
6import { ActivityPubActor } from '../../../shared'
7import { ResultList } from '../../../shared/models/result-list.model'
8
9export namespace AccountMethods {
10 export type Load = (id: number) => Bluebird<AccountInstance>
11 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
12 export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
13 export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
14 export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance>
15 export type ListOwned = () => Bluebird<AccountInstance[]>
16 export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
17 export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
18
19 export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
20 export type IsOwned = (this: AccountInstance) => boolean
21 export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
22 export type GetFollowingUrl = (this: AccountInstance) => string
23 export type GetFollowersUrl = (this: AccountInstance) => string
24 export type GetPublicKeyUrl = (this: AccountInstance) => string
25}
26
27export interface AccountClass {
28 loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
29 load: AccountMethods.Load
30 loadByUUID: AccountMethods.LoadByUUID
31 loadByUrl: AccountMethods.LoadByUrl
32 loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
33 listOwned: AccountMethods.ListOwned
34 listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
35 listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
36}
37
38export interface AccountAttributes {
39 name: string
40 url: string
41 publicKey: string
42 privateKey: string
43 followersCount: number
44 followingCount: number
45 inboxUrl: string
46 outboxUrl: string
47 sharedInboxUrl: string
48 followersUrl: string
49 followingUrl: string
50
51 uuid?: string
52
53 podId?: number
54 userId?: number
55 applicationId?: number
56}
57
58export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
59 isOwned: AccountMethods.IsOwned
60 toActivityPubObject: AccountMethods.ToActivityPubObject
61 getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
62 getFollowingUrl: AccountMethods.GetFollowingUrl
63 getFollowersUrl: AccountMethods.GetFollowersUrl
64 getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
65
66 id: number
67 createdAt: Date
68 updatedAt: Date
69
70 Pod: PodInstance
71 VideoChannels: VideoChannelInstance[]
72}
73
74export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts
new file mode 100644
index 000000000..82cbe38cc
--- /dev/null
+++ b/server/models/account/account-video-rate-interface.ts
@@ -0,0 +1,26 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5
6export namespace AccountVideoRateMethods {
7 export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance>
8}
9
10export interface AccountVideoRateClass {
11 load: AccountVideoRateMethods.Load
12}
13
14export interface AccountVideoRateAttributes {
15 type: VideoRateType
16 accountId: number
17 videoId: number
18}
19
20export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> {
21 id: number
22 createdAt: Date
23 updatedAt: Date
24}
25
26export interface AccountVideoRateModel extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {}
diff --git a/server/models/user/user-video-rate.ts b/server/models/account/account-video-rate.ts
index 7d6dd7281..7f7c97606 100644
--- a/server/models/user/user-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -1,5 +1,5 @@
1/* 1/*
2 User rates per video. 2 Account rates per video.
3*/ 3*/
4import { values } from 'lodash' 4import { values } from 'lodash'
5import * as Sequelize from 'sequelize' 5import * as Sequelize from 'sequelize'
@@ -8,17 +8,17 @@ import { VIDEO_RATE_TYPES } from '../../initializers'
8 8
9import { addMethodsToModel } from '../utils' 9import { addMethodsToModel } from '../utils'
10import { 10import {
11 UserVideoRateInstance, 11 AccountVideoRateInstance,
12 UserVideoRateAttributes, 12 AccountVideoRateAttributes,
13 13
14 UserVideoRateMethods 14 AccountVideoRateMethods
15} from './user-video-rate-interface' 15} from './account-video-rate-interface'
16 16
17let UserVideoRate: Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> 17let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
18let load: UserVideoRateMethods.Load 18let load: AccountVideoRateMethods.Load
19 19
20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
21 UserVideoRate = sequelize.define<UserVideoRateInstance, UserVideoRateAttributes>('UserVideoRate', 21 AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
22 { 22 {
23 type: { 23 type: {
24 type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)), 24 type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
@@ -28,7 +28,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
28 { 28 {
29 indexes: [ 29 indexes: [
30 { 30 {
31 fields: [ 'videoId', 'userId', 'type' ], 31 fields: [ 'videoId', 'accountId', 'type' ],
32 unique: true 32 unique: true
33 } 33 }
34 ] 34 ]
@@ -40,15 +40,15 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
40 40
41 load 41 load
42 ] 42 ]
43 addMethodsToModel(UserVideoRate, classMethods) 43 addMethodsToModel(AccountVideoRate, classMethods)
44 44
45 return UserVideoRate 45 return AccountVideoRate
46} 46}
47 47
48// ------------------------------ STATICS ------------------------------ 48// ------------------------------ STATICS ------------------------------
49 49
50function associate (models) { 50function associate (models) {
51 UserVideoRate.belongsTo(models.Video, { 51 AccountVideoRate.belongsTo(models.Video, {
52 foreignKey: { 52 foreignKey: {
53 name: 'videoId', 53 name: 'videoId',
54 allowNull: false 54 allowNull: false
@@ -56,23 +56,23 @@ function associate (models) {
56 onDelete: 'CASCADE' 56 onDelete: 'CASCADE'
57 }) 57 })
58 58
59 UserVideoRate.belongsTo(models.User, { 59 AccountVideoRate.belongsTo(models.Account, {
60 foreignKey: { 60 foreignKey: {
61 name: 'userId', 61 name: 'accountId',
62 allowNull: false 62 allowNull: false
63 }, 63 },
64 onDelete: 'CASCADE' 64 onDelete: 'CASCADE'
65 }) 65 })
66} 66}
67 67
68load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) { 68load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) {
69 const options: Sequelize.FindOptions<UserVideoRateAttributes> = { 69 const options: Sequelize.FindOptions<AccountVideoRateAttributes> = {
70 where: { 70 where: {
71 userId, 71 accountId,
72 videoId 72 videoId
73 } 73 }
74 } 74 }
75 if (transaction) options.transaction = transaction 75 if (transaction) options.transaction = transaction
76 76
77 return UserVideoRate.findOne(options) 77 return AccountVideoRate.findOne(options)
78} 78}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
new file mode 100644
index 000000000..00c0aefd4
--- /dev/null
+++ b/server/models/account/account.ts
@@ -0,0 +1,444 @@
1import * as Sequelize from 'sequelize'
2
3import {
4 isUserUsernameValid,
5 isAccountPublicKeyValid,
6 isAccountUrlValid,
7 isAccountPrivateKeyValid,
8 isAccountFollowersCountValid,
9 isAccountFollowingCountValid,
10 isAccountInboxValid,
11 isAccountOutboxValid,
12 isAccountSharedInboxValid,
13 isAccountFollowersValid,
14 isAccountFollowingValid,
15 activityPubContextify
16} from '../../helpers'
17
18import { addMethodsToModel } from '../utils'
19import {
20 AccountInstance,
21 AccountAttributes,
22
23 AccountMethods
24} from './account-interface'
25
26let Account: Sequelize.Model<AccountInstance, AccountAttributes>
27let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
28let load: AccountMethods.Load
29let loadByUUID: AccountMethods.LoadByUUID
30let loadByUrl: AccountMethods.LoadByUrl
31let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
32let listOwned: AccountMethods.ListOwned
33let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
34let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
35let isOwned: AccountMethods.IsOwned
36let toActivityPubObject: AccountMethods.ToActivityPubObject
37let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
38let getFollowingUrl: AccountMethods.GetFollowingUrl
39let getFollowersUrl: AccountMethods.GetFollowersUrl
40let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
41
42export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
43 Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
44 {
45 uuid: {
46 type: DataTypes.UUID,
47 defaultValue: DataTypes.UUIDV4,
48 allowNull: false,
49 validate: {
50 isUUID: 4
51 }
52 },
53 name: {
54 type: DataTypes.STRING,
55 allowNull: false,
56 validate: {
57 usernameValid: value => {
58 const res = isUserUsernameValid(value)
59 if (res === false) throw new Error('Username is not valid.')
60 }
61 }
62 },
63 url: {
64 type: DataTypes.STRING,
65 allowNull: false,
66 validate: {
67 urlValid: value => {
68 const res = isAccountUrlValid(value)
69 if (res === false) throw new Error('URL is not valid.')
70 }
71 }
72 },
73 publicKey: {
74 type: DataTypes.STRING,
75 allowNull: false,
76 validate: {
77 publicKeyValid: value => {
78 const res = isAccountPublicKeyValid(value)
79 if (res === false) throw new Error('Public key is not valid.')
80 }
81 }
82 },
83 privateKey: {
84 type: DataTypes.STRING,
85 allowNull: false,
86 validate: {
87 privateKeyValid: value => {
88 const res = isAccountPrivateKeyValid(value)
89 if (res === false) throw new Error('Private key is not valid.')
90 }
91 }
92 },
93 followersCount: {
94 type: DataTypes.INTEGER,
95 allowNull: false,
96 validate: {
97 followersCountValid: value => {
98 const res = isAccountFollowersCountValid(value)
99 if (res === false) throw new Error('Followers count is not valid.')
100 }
101 }
102 },
103 followingCount: {
104 type: DataTypes.INTEGER,
105 allowNull: false,
106 validate: {
107 followersCountValid: value => {
108 const res = isAccountFollowingCountValid(value)
109 if (res === false) throw new Error('Following count is not valid.')
110 }
111 }
112 },
113 inboxUrl: {
114 type: DataTypes.STRING,
115 allowNull: false,
116 validate: {
117 inboxUrlValid: value => {
118 const res = isAccountInboxValid(value)
119 if (res === false) throw new Error('Inbox URL is not valid.')
120 }
121 }
122 },
123 outboxUrl: {
124 type: DataTypes.STRING,
125 allowNull: false,
126 validate: {
127 outboxUrlValid: value => {
128 const res = isAccountOutboxValid(value)
129 if (res === false) throw new Error('Outbox URL is not valid.')
130 }
131 }
132 },
133 sharedInboxUrl: {
134 type: DataTypes.STRING,
135 allowNull: false,
136 validate: {
137 sharedInboxUrlValid: value => {
138 const res = isAccountSharedInboxValid(value)
139 if (res === false) throw new Error('Shared inbox URL is not valid.')
140 }
141 }
142 },
143 followersUrl: {
144 type: DataTypes.STRING,
145 allowNull: false,
146 validate: {
147 followersUrlValid: value => {
148 const res = isAccountFollowersValid(value)
149 if (res === false) throw new Error('Followers URL is not valid.')
150 }
151 }
152 },
153 followingUrl: {
154 type: DataTypes.STRING,
155 allowNull: false,
156 validate: {
157 followingUrlValid: value => {
158 const res = isAccountFollowingValid(value)
159 if (res === false) throw new Error('Following URL is not valid.')
160 }
161 }
162 }
163 },
164 {
165 indexes: [
166 {
167 fields: [ 'name' ]
168 },
169 {
170 fields: [ 'podId' ]
171 },
172 {
173 fields: [ 'userId' ],
174 unique: true
175 },
176 {
177 fields: [ 'applicationId' ],
178 unique: true
179 },
180 {
181 fields: [ 'name', 'podId' ],
182 unique: true
183 }
184 ],
185 hooks: { afterDestroy }
186 }
187 )
188
189 const classMethods = [
190 associate,
191 loadAccountByPodAndUUID,
192 load,
193 loadByUUID,
194 loadLocalAccountByName,
195 listOwned,
196 listFollowerUrlsForApi,
197 listFollowingUrlsForApi
198 ]
199 const instanceMethods = [
200 isOwned,
201 toActivityPubObject,
202 getFollowerSharedInboxUrls,
203 getFollowingUrl,
204 getFollowersUrl,
205 getPublicKeyUrl
206 ]
207 addMethodsToModel(Account, classMethods, instanceMethods)
208
209 return Account
210}
211
212// ---------------------------------------------------------------------------
213
214function associate (models) {
215 Account.belongsTo(models.Pod, {
216 foreignKey: {
217 name: 'podId',
218 allowNull: true
219 },
220 onDelete: 'cascade'
221 })
222
223 Account.belongsTo(models.User, {
224 foreignKey: {
225 name: 'userId',
226 allowNull: true
227 },
228 onDelete: 'cascade'
229 })
230
231 Account.belongsTo(models.Application, {
232 foreignKey: {
233 name: 'userId',
234 allowNull: true
235 },
236 onDelete: 'cascade'
237 })
238
239 Account.hasMany(models.VideoChannel, {
240 foreignKey: {
241 name: 'accountId',
242 allowNull: false
243 },
244 onDelete: 'cascade',
245 hooks: true
246 })
247
248 Account.hasMany(models.AccountFollower, {
249 foreignKey: {
250 name: 'accountId',
251 allowNull: false
252 },
253 onDelete: 'cascade'
254 })
255
256 Account.hasMany(models.AccountFollower, {
257 foreignKey: {
258 name: 'targetAccountId',
259 allowNull: false
260 },
261 onDelete: 'cascade'
262 })
263}
264
265function afterDestroy (account: AccountInstance) {
266 if (account.isOwned()) {
267 const removeVideoAccountToFriendsParams = {
268 uuid: account.uuid
269 }
270
271 return removeVideoAccountToFriends(removeVideoAccountToFriendsParams)
272 }
273
274 return undefined
275}
276
277toActivityPubObject = function (this: AccountInstance) {
278 const type = this.podId ? 'Application' : 'Person'
279
280 const json = {
281 type,
282 id: this.url,
283 following: this.getFollowingUrl(),
284 followers: this.getFollowersUrl(),
285 inbox: this.inboxUrl,
286 outbox: this.outboxUrl,
287 preferredUsername: this.name,
288 url: this.url,
289 name: this.name,
290 endpoints: {
291 sharedInbox: this.sharedInboxUrl
292 },
293 uuid: this.uuid,
294 publicKey: {
295 id: this.getPublicKeyUrl(),
296 owner: this.url,
297 publicKeyPem: this.publicKey
298 }
299 }
300
301 return activityPubContextify(json)
302}
303
304isOwned = function (this: AccountInstance) {
305 return this.podId === null
306}
307
308getFollowerSharedInboxUrls = function (this: AccountInstance) {
309 const query: Sequelize.FindOptions<AccountAttributes> = {
310 attributes: [ 'sharedInboxUrl' ],
311 include: [
312 {
313 model: Account['sequelize'].models.AccountFollower,
314 where: {
315 targetAccountId: this.id
316 }
317 }
318 ]
319 }
320
321 return Account.findAll(query)
322 .then(accounts => accounts.map(a => a.sharedInboxUrl))
323}
324
325getFollowingUrl = function (this: AccountInstance) {
326 return this.url + '/followers'
327}
328
329getFollowersUrl = function (this: AccountInstance) {
330 return this.url + '/followers'
331}
332
333getPublicKeyUrl = function (this: AccountInstance) {
334 return this.url + '#main-key'
335}
336
337// ------------------------------ STATICS ------------------------------
338
339listOwned = function () {
340 const query: Sequelize.FindOptions<AccountAttributes> = {
341 where: {
342 podId: null
343 }
344 }
345
346 return Account.findAll(query)
347}
348
349listFollowerUrlsForApi = function (name: string, start: number, count: number) {
350 return createListFollowForApiQuery('followers', name, start, count)
351}
352
353listFollowingUrlsForApi = function (name: string, start: number, count: number) {
354 return createListFollowForApiQuery('following', name, start, count)
355}
356
357load = function (id: number) {
358 return Account.findById(id)
359}
360
361loadByUUID = function (uuid: string) {
362 const query: Sequelize.FindOptions<AccountAttributes> = {
363 where: {
364 uuid
365 }
366 }
367
368 return Account.findOne(query)
369}
370
371loadLocalAccountByName = function (name: string) {
372 const query: Sequelize.FindOptions<AccountAttributes> = {
373 where: {
374 name,
375 userId: {
376 [Sequelize.Op.ne]: null
377 }
378 }
379 }
380
381 return Account.findOne(query)
382}
383
384loadByUrl = function (url: string) {
385 const query: Sequelize.FindOptions<AccountAttributes> = {
386 where: {
387 url
388 }
389 }
390
391 return Account.findOne(query)
392}
393
394loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
395 const query: Sequelize.FindOptions<AccountAttributes> = {
396 where: {
397 podId,
398 uuid
399 },
400 transaction
401 }
402
403 return Account.find(query)
404}
405
406// ------------------------------ UTILS ------------------------------
407
408async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count: number) {
409 let firstJoin: string
410 let secondJoin: string
411
412 if (type === 'followers') {
413 firstJoin = 'targetAccountId'
414 secondJoin = 'accountId'
415 } else {
416 firstJoin = 'accountId'
417 secondJoin = 'targetAccountId'
418 }
419
420 const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ]
421 const tasks: Promise<any>[] = []
422
423 for (const selection of selections) {
424 const query = 'SELECT ' + selection + ' FROM "Account" ' +
425 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
426 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' +
427 'WHERE "Account"."name" = \'$name\' ' +
428 'LIMIT ' + start + ', ' + count
429
430 const options = {
431 bind: { name },
432 type: Sequelize.QueryTypes.SELECT
433 }
434 tasks.push(Account['sequelize'].query(query, options))
435 }
436
437 const [ followers, [ { total } ]] = await Promise.all(tasks)
438 const urls: string[] = followers.map(f => f.url)
439
440 return {
441 data: urls,
442 total: parseInt(total, 10)
443 }
444}
diff --git a/server/models/account/index.ts b/server/models/account/index.ts
new file mode 100644
index 000000000..179f66974
--- /dev/null
+++ b/server/models/account/index.ts
@@ -0,0 +1,4 @@
1export * from './account-interface'
2export * from './account-follow-interface'
3export * from './account-video-rate-interface'
4export * from './user-interface'
diff --git a/server/models/user/user-interface.ts b/server/models/account/user-interface.ts
index 49c75aa3b..1a04fb750 100644
--- a/server/models/user/user-interface.ts
+++ b/server/models/account/user-interface.ts
@@ -1,10 +1,10 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3
4// Don't use barrel, import just what we need 4// Don't use barrel, import just what we need
5import { AccountInstance } from './account-interface'
5import { User as FormattedUser } from '../../../shared/models/users/user.model' 6import { User as FormattedUser } from '../../../shared/models/users/user.model'
6import { ResultList } from '../../../shared/models/result-list.model' 7import { ResultList } from '../../../shared/models/result-list.model'
7import { AuthorInstance } from '../video/author-interface'
8import { UserRight } from '../../../shared/models/users/user-right.enum' 8import { UserRight } from '../../../shared/models/users/user-right.enum'
9import { UserRole } from '../../../shared/models/users/user-role' 9import { UserRole } from '../../../shared/models/users/user-role'
10 10
@@ -15,18 +15,18 @@ export namespace UserMethods {
15 export type ToFormattedJSON = (this: UserInstance) => FormattedUser 15 export type ToFormattedJSON = (this: UserInstance) => FormattedUser
16 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean> 16 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
17 17
18 export type CountTotal = () => Promise<number> 18 export type CountTotal = () => Bluebird<number>
19 19
20 export type GetByUsername = (username: string) => Promise<UserInstance> 20 export type GetByUsername = (username: string) => Bluebird<UserInstance>
21 21
22 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> > 22 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
23 23
24 export type LoadById = (id: number) => Promise<UserInstance> 24 export type LoadById = (id: number) => Bluebird<UserInstance>
25 25
26 export type LoadByUsername = (username: string) => Promise<UserInstance> 26 export type LoadByUsername = (username: string) => Bluebird<UserInstance>
27 export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance> 27 export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
28 28
29 export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance> 29 export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
30} 30}
31 31
32export interface UserClass { 32export interface UserClass {
@@ -53,7 +53,7 @@ export interface UserAttributes {
53 role: UserRole 53 role: UserRole
54 videoQuota: number 54 videoQuota: number
55 55
56 Author?: AuthorInstance 56 Account?: AccountInstance
57} 57}
58 58
59export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { 59export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
diff --git a/server/models/user/user.ts b/server/models/account/user.ts
index b974418d4..1401762c5 100644
--- a/server/models/user/user.ts
+++ b/server/models/account/user.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3 2
4import { getSort, addMethodsToModel } from '../utils' 3import { getSort, addMethodsToModel } from '../utils'
5import { 4import {
@@ -166,13 +165,13 @@ toFormattedJSON = function (this: UserInstance) {
166 videoQuota: this.videoQuota, 165 videoQuota: this.videoQuota,
167 createdAt: this.createdAt, 166 createdAt: this.createdAt,
168 author: { 167 author: {
169 id: this.Author.id, 168 id: this.Account.id,
170 uuid: this.Author.uuid 169 uuid: this.Account.uuid
171 } 170 }
172 } 171 }
173 172
174 if (Array.isArray(this.Author.VideoChannels) === true) { 173 if (Array.isArray(this.Account.VideoChannels) === true) {
175 const videoChannels = this.Author.VideoChannels 174 const videoChannels = this.Account.VideoChannels
176 .map(c => c.toFormattedJSON()) 175 .map(c => c.toFormattedJSON())
177 .sort((v1, v2) => { 176 .sort((v1, v2) => {
178 if (v1.createdAt < v2.createdAt) return -1 177 if (v1.createdAt < v2.createdAt) return -1
@@ -198,7 +197,7 @@ isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.Fi
198// ------------------------------ STATICS ------------------------------ 197// ------------------------------ STATICS ------------------------------
199 198
200function associate (models) { 199function associate (models) {
201 User.hasOne(models.Author, { 200 User.hasOne(models.Account, {
202 foreignKey: 'userId', 201 foreignKey: 'userId',
203 onDelete: 'cascade' 202 onDelete: 'cascade'
204 }) 203 })
@@ -218,7 +217,7 @@ getByUsername = function (username: string) {
218 where: { 217 where: {
219 username: username 218 username: username
220 }, 219 },
221 include: [ { model: User['sequelize'].models.Author, required: true } ] 220 include: [ { model: User['sequelize'].models.Account, required: true } ]
222 } 221 }
223 222
224 return User.findOne(query) 223 return User.findOne(query)
@@ -229,7 +228,7 @@ listForApi = function (start: number, count: number, sort: string) {
229 offset: start, 228 offset: start,
230 limit: count, 229 limit: count,
231 order: [ getSort(sort) ], 230 order: [ getSort(sort) ],
232 include: [ { model: User['sequelize'].models.Author, required: true } ] 231 include: [ { model: User['sequelize'].models.Account, required: true } ]
233 } 232 }
234 233
235 return User.findAndCountAll(query).then(({ rows, count }) => { 234 return User.findAndCountAll(query).then(({ rows, count }) => {
@@ -242,7 +241,7 @@ listForApi = function (start: number, count: number, sort: string) {
242 241
243loadById = function (id: number) { 242loadById = function (id: number) {
244 const options = { 243 const options = {
245 include: [ { model: User['sequelize'].models.Author, required: true } ] 244 include: [ { model: User['sequelize'].models.Account, required: true } ]
246 } 245 }
247 246
248 return User.findById(id, options) 247 return User.findById(id, options)
@@ -253,7 +252,7 @@ loadByUsername = function (username: string) {
253 where: { 252 where: {
254 username 253 username
255 }, 254 },
256 include: [ { model: User['sequelize'].models.Author, required: true } ] 255 include: [ { model: User['sequelize'].models.Account, required: true } ]
257 } 256 }
258 257
259 return User.findOne(query) 258 return User.findOne(query)
@@ -266,7 +265,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
266 }, 265 },
267 include: [ 266 include: [
268 { 267 {
269 model: User['sequelize'].models.Author, 268 model: User['sequelize'].models.Account,
270 required: true, 269 required: true,
271 include: [ User['sequelize'].models.VideoChannel ] 270 include: [ User['sequelize'].models.VideoChannel ]
272 } 271 }
@@ -278,7 +277,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
278 277
279loadByUsernameOrEmail = function (username: string, email: string) { 278loadByUsernameOrEmail = function (username: string, email: string) {
280 const query = { 279 const query = {
281 include: [ { model: User['sequelize'].models.Author, required: true } ], 280 include: [ { model: User['sequelize'].models.Account, required: true } ],
282 where: { 281 where: {
283 [Sequelize.Op.or]: [ { username }, { email } ] 282 [Sequelize.Op.or]: [ { username }, { email } ]
284 } 283 }
@@ -296,8 +295,8 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
296 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + 295 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
297 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + 296 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
298 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' + 297 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
299 'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' + 298 'INNER JOIN "Accounts" ON "VideoChannels"."authorId" = "Accounts"."id" ' +
300 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' + 299 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
301 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' 300 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
302 301
303 const options = { 302 const options = {
diff --git a/server/models/index.ts b/server/models/index.ts
index b392a8a77..29479e067 100644
--- a/server/models/index.ts
+++ b/server/models/index.ts
@@ -3,5 +3,5 @@ export * from './job'
3export * from './oauth' 3export * from './oauth'
4export * from './pod' 4export * from './pod'
5export * from './request' 5export * from './request'
6export * from './user' 6export * from './account'
7export * from './video' 7export * from './video'
diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts
index ba5622977..163930a4f 100644
--- a/server/models/job/job-interface.ts
+++ b/server/models/job/job-interface.ts
@@ -1,14 +1,14 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { JobState } from '../../../shared/models/job.model' 4import { JobCategory, JobState } from '../../../shared/models/job.model'
5 5
6export namespace JobMethods { 6export namespace JobMethods {
7 export type ListWithLimit = (limit: number, state: JobState) => Promise<JobInstance[]> 7 export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise<JobInstance[]>
8} 8}
9 9
10export interface JobClass { 10export interface JobClass {
11 listWithLimit: JobMethods.ListWithLimit 11 listWithLimitByCategory: JobMethods.ListWithLimitByCategory
12} 12}
13 13
14export interface JobAttributes { 14export interface JobAttributes {
diff --git a/server/models/job/job.ts b/server/models/job/job.ts
index 968f9d71d..ce1203e5a 100644
--- a/server/models/job/job.ts
+++ b/server/models/job/job.ts
@@ -1,7 +1,7 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3 3
4import { JOB_STATES } from '../../initializers' 4import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
5 5
6import { addMethodsToModel } from '../utils' 6import { addMethodsToModel } from '../utils'
7import { 7import {
@@ -13,7 +13,7 @@ import {
13import { JobState } from '../../../shared/models/job.model' 13import { JobState } from '../../../shared/models/job.model'
14 14
15let Job: Sequelize.Model<JobInstance, JobAttributes> 15let Job: Sequelize.Model<JobInstance, JobAttributes>
16let listWithLimit: JobMethods.ListWithLimit 16let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
17 17
18export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 18export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
19 Job = sequelize.define<JobInstance, JobAttributes>('Job', 19 Job = sequelize.define<JobInstance, JobAttributes>('Job',
@@ -22,6 +22,10 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
22 type: DataTypes.ENUM(values(JOB_STATES)), 22 type: DataTypes.ENUM(values(JOB_STATES)),
23 allowNull: false 23 allowNull: false
24 }, 24 },
25 category: {
26 type: DataTypes.ENUM(values(JOB_CATEGORIES)),
27 allowNull: false
28 },
25 handlerName: { 29 handlerName: {
26 type: DataTypes.STRING, 30 type: DataTypes.STRING,
27 allowNull: false 31 allowNull: false
@@ -40,7 +44,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
40 } 44 }
41 ) 45 )
42 46
43 const classMethods = [ listWithLimit ] 47 const classMethods = [ listWithLimitByCategory ]
44 addMethodsToModel(Job, classMethods) 48 addMethodsToModel(Job, classMethods)
45 49
46 return Job 50 return Job
@@ -48,7 +52,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
48 52
49// --------------------------------------------------------------------------- 53// ---------------------------------------------------------------------------
50 54
51listWithLimit = function (limit: number, state: JobState) { 55listWithLimitByCategory = function (limit: number, state: JobState) {
52 const query = { 56 const query = {
53 order: [ 57 order: [
54 [ 'id', 'ASC' ] 58 [ 'id', 'ASC' ]
diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts
index 0c947bde8..ef97893c4 100644
--- a/server/models/oauth/oauth-token-interface.ts
+++ b/server/models/oauth/oauth-token-interface.ts
@@ -1,7 +1,7 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { UserModel } from '../user/user-interface' 4import { UserModel } from '../account/user-interface'
5 5
6export type OAuthTokenInfo = { 6export type OAuthTokenInfo = {
7 refreshToken: string 7 refreshToken: string
diff --git a/server/models/pod/pod-interface.ts b/server/models/pod/pod-interface.ts
index 7e095d424..6c5aab3fa 100644
--- a/server/models/pod/pod-interface.ts
+++ b/server/models/pod/pod-interface.ts
@@ -48,9 +48,7 @@ export interface PodClass {
48export interface PodAttributes { 48export interface PodAttributes {
49 id?: number 49 id?: number
50 host?: string 50 host?: string
51 publicKey?: string
52 score?: number | Sequelize.literal // Sequelize literal for 'score +' + value 51 score?: number | Sequelize.literal // Sequelize literal for 'score +' + value
53 email?: string
54} 52}
55 53
56export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> { 54export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> {
diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts
index 6b33336b8..7c8b49bf8 100644
--- a/server/models/pod/pod.ts
+++ b/server/models/pod/pod.ts
@@ -39,10 +39,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
39 } 39 }
40 } 40 }
41 }, 41 },
42 publicKey: {
43 type: DataTypes.STRING(5000),
44 allowNull: false
45 },
46 score: { 42 score: {
47 type: DataTypes.INTEGER, 43 type: DataTypes.INTEGER,
48 defaultValue: FRIEND_SCORE.BASE, 44 defaultValue: FRIEND_SCORE.BASE,
@@ -51,13 +47,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
51 isInt: true, 47 isInt: true,
52 max: FRIEND_SCORE.MAX 48 max: FRIEND_SCORE.MAX
53 } 49 }
54 },
55 email: {
56 type: DataTypes.STRING(400),
57 allowNull: false,
58 validate: {
59 isEmail: true
60 }
61 } 50 }
62 }, 51 },
63 { 52 {
@@ -100,7 +89,6 @@ toFormattedJSON = function (this: PodInstance) {
100 const json = { 89 const json = {
101 id: this.id, 90 id: this.id,
102 host: this.host, 91 host: this.host,
103 email: this.email,
104 score: this.score as number, 92 score: this.score as number,
105 createdAt: this.createdAt 93 createdAt: this.createdAt
106 } 94 }
diff --git a/server/models/user/index.ts b/server/models/user/index.ts
deleted file mode 100644
index ed3689518..000000000
--- a/server/models/user/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './user-video-rate-interface'
2export * from './user-interface'
diff --git a/server/models/user/user-video-rate-interface.ts b/server/models/user/user-video-rate-interface.ts
deleted file mode 100644
index ea0fdc4d9..000000000
--- a/server/models/user/user-video-rate-interface.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5
6export namespace UserVideoRateMethods {
7 export type Load = (userId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<UserVideoRateInstance>
8}
9
10export interface UserVideoRateClass {
11 load: UserVideoRateMethods.Load
12}
13
14export interface UserVideoRateAttributes {
15 type: VideoRateType
16 userId: number
17 videoId: number
18}
19
20export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance<UserVideoRateAttributes> {
21 id: number
22 createdAt: Date
23 updatedAt: Date
24}
25
26export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> {}
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}