diff options
author | Chocobozzz <me@florianbigard.com> | 2017-12-12 17:53:50 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2017-12-13 16:50:33 +0100 |
commit | 3fd3ab2d34d512b160a5e6084d7609be7b4f4452 (patch) | |
tree | e5ca358287fca6ecacce83defcf23af1e8e9f419 /server/models/video | |
parent | c893d4514e6ecbf282c7985fe5f82b8acd8a1137 (diff) | |
download | PeerTube-3fd3ab2d34d512b160a5e6084d7609be7b4f4452.tar.gz PeerTube-3fd3ab2d34d512b160a5e6084d7609be7b4f4452.tar.zst PeerTube-3fd3ab2d34d512b160a5e6084d7609be7b4f4452.zip |
Move models to typescript-sequelize
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/index.ts | 9 | ||||
-rw-r--r-- | server/models/video/tag-interface.ts | 20 | ||||
-rw-r--r-- | server/models/video/tag.ts | 105 | ||||
-rw-r--r-- | server/models/video/video-abuse-interface.ts | 41 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 210 | ||||
-rw-r--r-- | server/models/video/video-blacklist-interface.ts | 39 | ||||
-rw-r--r-- | server/models/video/video-blacklist.ts | 142 | ||||
-rw-r--r-- | server/models/video/video-channel-interface.ts | 64 | ||||
-rw-r--r-- | server/models/video/video-channel-share-interface.ts | 32 | ||||
-rw-r--r-- | server/models/video/video-channel-share.ts | 120 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 572 | ||||
-rw-r--r-- | server/models/video/video-file-interface.ts | 24 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 109 | ||||
-rw-r--r-- | server/models/video/video-interface.ts | 150 | ||||
-rw-r--r-- | server/models/video/video-share-interface.ts | 30 | ||||
-rw-r--r-- | server/models/video/video-share.ts | 118 | ||||
-rw-r--r-- | server/models/video/video-tag-interface.ts | 18 | ||||
-rw-r--r-- | server/models/video/video-tag.ts | 43 | ||||
-rw-r--r-- | server/models/video/video.ts | 1845 |
19 files changed, 1514 insertions, 2177 deletions
diff --git a/server/models/video/index.ts b/server/models/video/index.ts deleted file mode 100644 index e17bbfab4..000000000 --- a/server/models/video/index.ts +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | export * from './tag-interface' | ||
2 | export * from './video-abuse-interface' | ||
3 | export * from './video-blacklist-interface' | ||
4 | export * from './video-channel-interface' | ||
5 | export * from './video-tag-interface' | ||
6 | export * from './video-file-interface' | ||
7 | export * from './video-interface' | ||
8 | export * from './video-share-interface' | ||
9 | export * from './video-channel-share-interface' | ||
diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts deleted file mode 100644 index 08e5c3246..000000000 --- a/server/models/video/tag-interface.ts +++ /dev/null | |||
@@ -1,20 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | export namespace TagMethods { | ||
5 | export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction) => Promise<TagInstance[]> | ||
6 | } | ||
7 | |||
8 | export interface TagClass { | ||
9 | findOrCreateTags: TagMethods.FindOrCreateTags | ||
10 | } | ||
11 | |||
12 | export interface TagAttributes { | ||
13 | name: string | ||
14 | } | ||
15 | |||
16 | export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance<TagAttributes> { | ||
17 | id: number | ||
18 | } | ||
19 | |||
20 | export interface TagModel extends TagClass, Sequelize.Model<TagInstance, TagAttributes> {} | ||
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 0c0757fc8..0ae74d808 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -1,73 +1,60 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as Promise from 'bluebird' | 2 | import { Transaction } from 'sequelize' |
3 | 3 | import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | |
4 | import { addMethodsToModel } from '../utils' | 4 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
5 | import { | 5 | import { throwIfNotValid } from '../utils' |
6 | TagInstance, | 6 | import { VideoModel } from './video' |
7 | TagAttributes, | 7 | import { VideoTagModel } from './video-tag' |
8 | 8 | ||
9 | TagMethods | 9 | @Table({ |
10 | } from './tag-interface' | 10 | tableName: 'tag', |
11 | 11 | timestamps: false, | |
12 | let Tag: Sequelize.Model<TagInstance, TagAttributes> | 12 | indexes: [ |
13 | let findOrCreateTags: TagMethods.FindOrCreateTags | ||
14 | |||
15 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
16 | Tag = sequelize.define<TagInstance, TagAttributes>('Tag', | ||
17 | { | 13 | { |
18 | name: { | 14 | fields: [ 'name' ], |
19 | type: DataTypes.STRING, | 15 | unique: true |
20 | allowNull: false | ||
21 | } | ||
22 | }, | ||
23 | { | ||
24 | timestamps: false, | ||
25 | indexes: [ | ||
26 | { | ||
27 | fields: [ 'name' ], | ||
28 | unique: true | ||
29 | } | ||
30 | ] | ||
31 | } | 16 | } |
32 | ) | ||
33 | |||
34 | const classMethods = [ | ||
35 | associate, | ||
36 | |||
37 | findOrCreateTags | ||
38 | ] | 17 | ] |
39 | addMethodsToModel(Tag, classMethods) | 18 | }) |
19 | export class TagModel extends Model<TagModel> { | ||
40 | 20 | ||
41 | return Tag | 21 | @AllowNull(false) |
42 | } | 22 | @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag')) |
23 | @Column | ||
24 | name: string | ||
43 | 25 | ||
44 | // --------------------------------------------------------------------------- | 26 | @CreatedAt |
27 | createdAt: Date | ||
45 | 28 | ||
46 | function associate (models) { | 29 | @UpdatedAt |
47 | Tag.belongsToMany(models.Video, { | 30 | updatedAt: Date |
31 | |||
32 | @BelongsToMany(() => VideoModel, { | ||
48 | foreignKey: 'tagId', | 33 | foreignKey: 'tagId', |
49 | through: models.VideoTag, | 34 | through: () => VideoTagModel, |
50 | onDelete: 'CASCADE' | 35 | onDelete: 'CASCADE' |
51 | }) | 36 | }) |
52 | } | 37 | Videos: VideoModel[] |
53 | 38 | ||
54 | findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction) { | 39 | static findOrCreateTags (tags: string[], transaction: Transaction) { |
55 | const tasks: Promise<TagInstance>[] = [] | 40 | const tasks: Bluebird<TagModel>[] = [] |
56 | tags.forEach(tag => { | 41 | tags.forEach(tag => { |
57 | const query: Sequelize.FindOrInitializeOptions<TagAttributes> = { | 42 | const query = { |
58 | where: { | 43 | where: { |
59 | name: tag | 44 | name: tag |
60 | }, | 45 | }, |
61 | defaults: { | 46 | defaults: { |
62 | name: tag | 47 | name: tag |
48 | } | ||
63 | } | 49 | } |
64 | } | ||
65 | 50 | ||
66 | if (transaction) query.transaction = transaction | 51 | if (transaction) query['transaction'] = transaction |
67 | 52 | ||
68 | const promise = Tag.findOrCreate(query).then(([ tagInstance ]) => tagInstance) | 53 | const promise = TagModel.findOrCreate(query) |
69 | tasks.push(promise) | 54 | .then(([ tagInstance ]) => tagInstance) |
70 | }) | 55 | tasks.push(promise) |
56 | }) | ||
71 | 57 | ||
72 | return Promise.all(tasks) | 58 | return Promise.all(tasks) |
59 | } | ||
73 | } | 60 | } |
diff --git a/server/models/video/video-abuse-interface.ts b/server/models/video/video-abuse-interface.ts deleted file mode 100644 index feafc4a19..000000000 --- a/server/models/video/video-abuse-interface.ts +++ /dev/null | |||
@@ -1,41 +0,0 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { ResultList } from '../../../shared' | ||
4 | import { VideoAbuse as FormattedVideoAbuse } from '../../../shared/models/videos/video-abuse.model' | ||
5 | import { AccountInstance } from '../account/account-interface' | ||
6 | import { ServerInstance } from '../server/server-interface' | ||
7 | import { VideoInstance } from './video-interface' | ||
8 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' | ||
9 | |||
10 | export namespace VideoAbuseMethods { | ||
11 | export type ToFormattedJSON = (this: VideoAbuseInstance) => FormattedVideoAbuse | ||
12 | |||
13 | export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoAbuseInstance> > | ||
14 | export type ToActivityPubObject = () => VideoAbuseObject | ||
15 | } | ||
16 | |||
17 | export interface VideoAbuseClass { | ||
18 | listForApi: VideoAbuseMethods.ListForApi | ||
19 | toActivityPubObject: VideoAbuseMethods.ToActivityPubObject | ||
20 | } | ||
21 | |||
22 | export interface VideoAbuseAttributes { | ||
23 | reason: string | ||
24 | videoId: number | ||
25 | reporterAccountId: number | ||
26 | |||
27 | Account?: AccountInstance | ||
28 | Video?: VideoInstance | ||
29 | } | ||
30 | |||
31 | export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance<VideoAbuseAttributes> { | ||
32 | id: number | ||
33 | createdAt: Date | ||
34 | updatedAt: Date | ||
35 | |||
36 | Server: ServerInstance | ||
37 | |||
38 | toFormattedJSON: VideoAbuseMethods.ToFormattedJSON | ||
39 | } | ||
40 | |||
41 | export interface VideoAbuseModel extends VideoAbuseClass, Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes> {} | ||
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index d09f5f7a1..d0ee969fb 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,142 +1,116 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | 2 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | |
3 | import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' | ||
3 | import { CONFIG } from '../../initializers' | 4 | import { CONFIG } from '../../initializers' |
4 | import { isVideoAbuseReasonValid } from '../../helpers' | 5 | import { AccountModel } from '../account/account' |
5 | 6 | import { ServerModel } from '../server/server' | |
6 | import { addMethodsToModel, getSort } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
7 | import { | 8 | import { VideoModel } from './video' |
8 | VideoAbuseInstance, | 9 | |
9 | VideoAbuseAttributes, | 10 | @Table({ |
10 | 11 | tableName: 'videoAbuse', | |
11 | VideoAbuseMethods | 12 | indexes: [ |
12 | } from './video-abuse-interface' | ||
13 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' | ||
14 | |||
15 | let VideoAbuse: Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes> | ||
16 | let toFormattedJSON: VideoAbuseMethods.ToFormattedJSON | ||
17 | let listForApi: VideoAbuseMethods.ListForApi | ||
18 | let toActivityPubObject: VideoAbuseMethods.ToActivityPubObject | ||
19 | |||
20 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
21 | VideoAbuse = sequelize.define<VideoAbuseInstance, VideoAbuseAttributes>('VideoAbuse', | ||
22 | { | 13 | { |
23 | reason: { | 14 | fields: [ 'videoId' ] |
24 | type: DataTypes.STRING, | ||
25 | allowNull: false, | ||
26 | validate: { | ||
27 | reasonValid: value => { | ||
28 | const res = isVideoAbuseReasonValid(value) | ||
29 | if (res === false) throw new Error('Video abuse reason is not valid.') | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | }, | 15 | }, |
34 | { | 16 | { |
35 | indexes: [ | 17 | fields: [ 'reporterAccountId' ] |
36 | { | ||
37 | fields: [ 'videoId' ] | ||
38 | }, | ||
39 | { | ||
40 | fields: [ 'reporterAccountId' ] | ||
41 | } | ||
42 | ] | ||
43 | } | 18 | } |
44 | ) | ||
45 | |||
46 | const classMethods = [ | ||
47 | associate, | ||
48 | |||
49 | listForApi | ||
50 | ] | ||
51 | const instanceMethods = [ | ||
52 | toFormattedJSON, | ||
53 | toActivityPubObject | ||
54 | ] | 19 | ] |
55 | addMethodsToModel(VideoAbuse, classMethods, instanceMethods) | 20 | }) |
56 | 21 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | |
57 | return VideoAbuse | ||
58 | } | ||
59 | |||
60 | // ------------------------------ METHODS ------------------------------ | ||
61 | |||
62 | toFormattedJSON = function (this: VideoAbuseInstance) { | ||
63 | let reporterServerHost | ||
64 | |||
65 | if (this.Account.Server) { | ||
66 | reporterServerHost = this.Account.Server.host | ||
67 | } else { | ||
68 | // It means it's our video | ||
69 | reporterServerHost = CONFIG.WEBSERVER.HOST | ||
70 | } | ||
71 | |||
72 | const json = { | ||
73 | id: this.id, | ||
74 | reason: this.reason, | ||
75 | reporterUsername: this.Account.name, | ||
76 | reporterServerHost, | ||
77 | videoId: this.Video.id, | ||
78 | videoUUID: this.Video.uuid, | ||
79 | videoName: this.Video.name, | ||
80 | createdAt: this.createdAt | ||
81 | } | ||
82 | 22 | ||
83 | return json | 23 | @AllowNull(false) |
84 | } | 24 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) |
25 | @Column | ||
26 | reason: string | ||
85 | 27 | ||
86 | toActivityPubObject = function (this: VideoAbuseInstance) { | 28 | @CreatedAt |
87 | const videoAbuseObject: VideoAbuseObject = { | 29 | createdAt: Date |
88 | type: 'Flag' as 'Flag', | ||
89 | content: this.reason, | ||
90 | object: this.Video.url | ||
91 | } | ||
92 | 30 | ||
93 | return videoAbuseObject | 31 | @UpdatedAt |
94 | } | 32 | updatedAt: Date |
95 | 33 | ||
96 | // ------------------------------ STATICS ------------------------------ | 34 | @ForeignKey(() => AccountModel) |
35 | @Column | ||
36 | reporterAccountId: number | ||
97 | 37 | ||
98 | function associate (models) { | 38 | @BelongsTo(() => AccountModel, { |
99 | VideoAbuse.belongsTo(models.Account, { | ||
100 | foreignKey: { | 39 | foreignKey: { |
101 | name: 'reporterAccountId', | ||
102 | allowNull: false | 40 | allowNull: false |
103 | }, | 41 | }, |
104 | onDelete: 'CASCADE' | 42 | onDelete: 'cascade' |
105 | }) | 43 | }) |
44 | Account: AccountModel | ||
45 | |||
46 | @ForeignKey(() => VideoModel) | ||
47 | @Column | ||
48 | videoId: number | ||
106 | 49 | ||
107 | VideoAbuse.belongsTo(models.Video, { | 50 | @BelongsTo(() => VideoModel, { |
108 | foreignKey: { | 51 | foreignKey: { |
109 | name: 'videoId', | ||
110 | allowNull: false | 52 | allowNull: false |
111 | }, | 53 | }, |
112 | onDelete: 'CASCADE' | 54 | onDelete: 'cascade' |
113 | }) | 55 | }) |
114 | } | 56 | Video: VideoModel |
57 | |||
58 | static listForApi (start: number, count: number, sort: string) { | ||
59 | const query = { | ||
60 | offset: start, | ||
61 | limit: count, | ||
62 | order: [ getSort(sort) ], | ||
63 | include: [ | ||
64 | { | ||
65 | model: AccountModel, | ||
66 | required: true, | ||
67 | include: [ | ||
68 | { | ||
69 | model: ServerModel, | ||
70 | required: false | ||
71 | } | ||
72 | ] | ||
73 | }, | ||
74 | { | ||
75 | model: VideoModel, | ||
76 | required: true | ||
77 | } | ||
78 | ] | ||
79 | } | ||
115 | 80 | ||
116 | listForApi = function (start: number, count: number, sort: string) { | 81 | return VideoAbuseModel.findAndCountAll(query) |
117 | const query = { | 82 | .then(({ rows, count }) => { |
118 | offset: start, | 83 | return { total: count, data: rows } |
119 | limit: count, | 84 | }) |
120 | order: [ getSort(sort) ], | ||
121 | include: [ | ||
122 | { | ||
123 | model: VideoAbuse['sequelize'].models.Account, | ||
124 | required: true, | ||
125 | include: [ | ||
126 | { | ||
127 | model: VideoAbuse['sequelize'].models.Server, | ||
128 | required: false | ||
129 | } | ||
130 | ] | ||
131 | }, | ||
132 | { | ||
133 | model: VideoAbuse['sequelize'].models.Video, | ||
134 | required: true | ||
135 | } | ||
136 | ] | ||
137 | } | 85 | } |
138 | 86 | ||
139 | return VideoAbuse.findAndCountAll(query).then(({ rows, count }) => { | 87 | toFormattedJSON () { |
140 | return { total: count, data: rows } | 88 | let reporterServerHost |
141 | }) | 89 | |
90 | if (this.Account.Server) { | ||
91 | reporterServerHost = this.Account.Server.host | ||
92 | } else { | ||
93 | // It means it's our video | ||
94 | reporterServerHost = CONFIG.WEBSERVER.HOST | ||
95 | } | ||
96 | |||
97 | return { | ||
98 | id: this.id, | ||
99 | reason: this.reason, | ||
100 | reporterUsername: this.Account.name, | ||
101 | reporterServerHost, | ||
102 | videoId: this.Video.id, | ||
103 | videoUUID: this.Video.uuid, | ||
104 | videoName: this.Video.name, | ||
105 | createdAt: this.createdAt | ||
106 | } | ||
107 | } | ||
108 | |||
109 | toActivityPubObject (): VideoAbuseObject { | ||
110 | return { | ||
111 | type: 'Flag' as 'Flag', | ||
112 | content: this.reason, | ||
113 | object: this.Video.url | ||
114 | } | ||
115 | } | ||
142 | } | 116 | } |
diff --git a/server/models/video/video-blacklist-interface.ts b/server/models/video/video-blacklist-interface.ts deleted file mode 100644 index be2483d4c..000000000 --- a/server/models/video/video-blacklist-interface.ts +++ /dev/null | |||
@@ -1,39 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { SortType } from '../../helpers' | ||
5 | import { ResultList } from '../../../shared' | ||
6 | import { VideoInstance } from './video-interface' | ||
7 | |||
8 | // Don't use barrel, import just what we need | ||
9 | import { BlacklistedVideo as FormattedBlacklistedVideo } from '../../../shared/models/videos/video-blacklist.model' | ||
10 | |||
11 | export namespace BlacklistedVideoMethods { | ||
12 | export type ToFormattedJSON = (this: BlacklistedVideoInstance) => FormattedBlacklistedVideo | ||
13 | export type ListForApi = (start: number, count: number, sort: SortType) => Promise< ResultList<BlacklistedVideoInstance> > | ||
14 | export type LoadByVideoId = (id: number) => Promise<BlacklistedVideoInstance> | ||
15 | } | ||
16 | |||
17 | export interface BlacklistedVideoClass { | ||
18 | toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON | ||
19 | listForApi: BlacklistedVideoMethods.ListForApi | ||
20 | loadByVideoId: BlacklistedVideoMethods.LoadByVideoId | ||
21 | } | ||
22 | |||
23 | export interface BlacklistedVideoAttributes { | ||
24 | videoId: number | ||
25 | |||
26 | Video?: VideoInstance | ||
27 | } | ||
28 | |||
29 | export interface BlacklistedVideoInstance | ||
30 | extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance<BlacklistedVideoAttributes> { | ||
31 | id: number | ||
32 | createdAt: Date | ||
33 | updatedAt: Date | ||
34 | |||
35 | toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON | ||
36 | } | ||
37 | |||
38 | export interface BlacklistedVideoModel | ||
39 | extends BlacklistedVideoClass, Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> {} | ||
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index ae8286285..6db562719 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,104 +1,80 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | |||
3 | import { SortType } from '../../helpers' | 2 | import { SortType } from '../../helpers' |
4 | import { addMethodsToModel, getSortOnModel } from '../utils' | 3 | import { getSortOnModel } from '../utils' |
5 | import { VideoInstance } from './video-interface' | 4 | import { VideoModel } from './video' |
6 | import { | ||
7 | BlacklistedVideoInstance, | ||
8 | BlacklistedVideoAttributes, | ||
9 | |||
10 | BlacklistedVideoMethods | ||
11 | } from './video-blacklist-interface' | ||
12 | |||
13 | let BlacklistedVideo: Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> | ||
14 | let toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON | ||
15 | let listForApi: BlacklistedVideoMethods.ListForApi | ||
16 | let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId | ||
17 | 5 | ||
18 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 6 | @Table({ |
19 | BlacklistedVideo = sequelize.define<BlacklistedVideoInstance, BlacklistedVideoAttributes>('BlacklistedVideo', | 7 | tableName: 'videoBlacklist', |
20 | {}, | 8 | indexes: [ |
21 | { | 9 | { |
22 | indexes: [ | 10 | fields: [ 'videoId' ], |
23 | { | 11 | unique: true |
24 | fields: [ 'videoId' ], | ||
25 | unique: true | ||
26 | } | ||
27 | ] | ||
28 | } | 12 | } |
29 | ) | ||
30 | |||
31 | const classMethods = [ | ||
32 | associate, | ||
33 | |||
34 | listForApi, | ||
35 | loadByVideoId | ||
36 | ] | 13 | ] |
37 | const instanceMethods = [ | 14 | }) |
38 | toFormattedJSON | 15 | export class VideoBlacklistModel extends Model<VideoBlacklistModel> { |
39 | ] | ||
40 | addMethodsToModel(BlacklistedVideo, classMethods, instanceMethods) | ||
41 | |||
42 | return BlacklistedVideo | ||
43 | } | ||
44 | 16 | ||
45 | // ------------------------------ METHODS ------------------------------ | 17 | @CreatedAt |
18 | createdAt: Date | ||
46 | 19 | ||
47 | toFormattedJSON = function (this: BlacklistedVideoInstance) { | 20 | @UpdatedAt |
48 | let video: VideoInstance | 21 | updatedAt: Date |
49 | |||
50 | video = this.Video | ||
51 | |||
52 | return { | ||
53 | id: this.id, | ||
54 | videoId: this.videoId, | ||
55 | createdAt: this.createdAt, | ||
56 | updatedAt: this.updatedAt, | ||
57 | name: video.name, | ||
58 | uuid: video.uuid, | ||
59 | description: video.description, | ||
60 | duration: video.duration, | ||
61 | views: video.views, | ||
62 | likes: video.likes, | ||
63 | dislikes: video.dislikes, | ||
64 | nsfw: video.nsfw | ||
65 | } | ||
66 | } | ||
67 | 22 | ||
68 | // ------------------------------ STATICS ------------------------------ | 23 | @ForeignKey(() => VideoModel) |
24 | @Column | ||
25 | videoId: number | ||
69 | 26 | ||
70 | function associate (models) { | 27 | @BelongsTo(() => VideoModel, { |
71 | BlacklistedVideo.belongsTo(models.Video, { | ||
72 | foreignKey: { | 28 | foreignKey: { |
73 | name: 'videoId', | ||
74 | allowNull: false | 29 | allowNull: false |
75 | }, | 30 | }, |
76 | onDelete: 'CASCADE' | 31 | onDelete: 'cascade' |
77 | }) | 32 | }) |
78 | } | 33 | Video: VideoModel |
34 | |||
35 | static listForApi (start: number, count: number, sort: SortType) { | ||
36 | const query = { | ||
37 | offset: start, | ||
38 | limit: count, | ||
39 | order: [ getSortOnModel(sort.sortModel, sort.sortValue) ], | ||
40 | include: [ { model: VideoModel } ] | ||
41 | } | ||
79 | 42 | ||
80 | listForApi = function (start: number, count: number, sort: SortType) { | 43 | return VideoBlacklistModel.findAndCountAll(query) |
81 | const query = { | 44 | .then(({ rows, count }) => { |
82 | offset: start, | 45 | return { |
83 | limit: count, | 46 | data: rows, |
84 | order: [ getSortOnModel(sort.sortModel, sort.sortValue) ], | 47 | total: count |
85 | include: [ { model: BlacklistedVideo['sequelize'].models.Video } ] | 48 | } |
49 | }) | ||
86 | } | 50 | } |
87 | 51 | ||
88 | return BlacklistedVideo.findAndCountAll(query).then(({ rows, count }) => { | 52 | static loadByVideoId (id: number) { |
89 | return { | 53 | const query = { |
90 | data: rows, | 54 | where: { |
91 | total: count | 55 | videoId: id |
56 | } | ||
92 | } | 57 | } |
93 | }) | ||
94 | } | ||
95 | 58 | ||
96 | loadByVideoId = function (id: number) { | 59 | return VideoBlacklistModel.findOne(query) |
97 | const query = { | ||
98 | where: { | ||
99 | videoId: id | ||
100 | } | ||
101 | } | 60 | } |
102 | 61 | ||
103 | return BlacklistedVideo.findOne(query) | 62 | toFormattedJSON () { |
63 | const video = this.Video | ||
64 | |||
65 | return { | ||
66 | id: this.id, | ||
67 | videoId: this.videoId, | ||
68 | createdAt: this.createdAt, | ||
69 | updatedAt: this.updatedAt, | ||
70 | name: video.name, | ||
71 | uuid: video.uuid, | ||
72 | description: video.description, | ||
73 | duration: video.duration, | ||
74 | views: video.views, | ||
75 | likes: video.likes, | ||
76 | dislikes: video.dislikes, | ||
77 | nsfw: video.nsfw | ||
78 | } | ||
79 | } | ||
104 | } | 80 | } |
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts deleted file mode 100644 index 21f81e901..000000000 --- a/server/models/video/video-channel-interface.ts +++ /dev/null | |||
@@ -1,64 +0,0 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | import { ResultList } from '../../../shared' | ||
5 | import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' | ||
6 | import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' | ||
7 | import { AccountInstance } from '../account/account-interface' | ||
8 | import { VideoInstance } from './video-interface' | ||
9 | import { VideoChannelShareInstance } from './video-channel-share-interface' | ||
10 | |||
11 | export namespace VideoChannelMethods { | ||
12 | export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel | ||
13 | export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject | ||
14 | export type IsOwned = (this: VideoChannelInstance) => boolean | ||
15 | |||
16 | export type CountByAccount = (accountId: number) => Promise<number> | ||
17 | export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> > | ||
18 | export type LoadByIdAndAccount = (id: number, accountId: number) => Promise<VideoChannelInstance> | ||
19 | export type ListByAccount = (accountId: number) => Promise< ResultList<VideoChannelInstance> > | ||
20 | export type LoadAndPopulateAccount = (id: number) => Promise<VideoChannelInstance> | ||
21 | export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise<VideoChannelInstance> | ||
22 | export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | ||
23 | export type LoadByHostAndUUID = (uuid: string, serverHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | ||
24 | export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance> | ||
25 | export type LoadByUrl = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | ||
26 | export type LoadByUUIDOrUrl = (uuid: string, url: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | ||
27 | } | ||
28 | |||
29 | export interface VideoChannelClass { | ||
30 | countByAccount: VideoChannelMethods.CountByAccount | ||
31 | listForApi: VideoChannelMethods.ListForApi | ||
32 | listByAccount: VideoChannelMethods.ListByAccount | ||
33 | loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount | ||
34 | loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount | ||
35 | loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount | ||
36 | loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos | ||
37 | loadByUrl: VideoChannelMethods.LoadByUrl | ||
38 | loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl | ||
39 | } | ||
40 | |||
41 | export interface VideoChannelAttributes { | ||
42 | id?: number | ||
43 | uuid?: string | ||
44 | name: string | ||
45 | description: string | ||
46 | remote: boolean | ||
47 | url?: string | ||
48 | |||
49 | Account?: AccountInstance | ||
50 | Videos?: VideoInstance[] | ||
51 | VideoChannelShares?: VideoChannelShareInstance[] | ||
52 | } | ||
53 | |||
54 | export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance<VideoChannelAttributes> { | ||
55 | id: number | ||
56 | createdAt: Date | ||
57 | updatedAt: Date | ||
58 | |||
59 | isOwned: VideoChannelMethods.IsOwned | ||
60 | toFormattedJSON: VideoChannelMethods.ToFormattedJSON | ||
61 | toActivityPubObject: VideoChannelMethods.ToActivityPubObject | ||
62 | } | ||
63 | |||
64 | export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {} | ||
diff --git a/server/models/video/video-channel-share-interface.ts b/server/models/video/video-channel-share-interface.ts deleted file mode 100644 index 2fff41a1b..000000000 --- a/server/models/video/video-channel-share-interface.ts +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { AccountInstance } from '../account/account-interface' | ||
4 | import { VideoChannelInstance } from './video-channel-interface' | ||
5 | |||
6 | export namespace VideoChannelShareMethods { | ||
7 | export type LoadAccountsByShare = (videoChannelId: number, t: Sequelize.Transaction) => Bluebird<AccountInstance[]> | ||
8 | export type Load = (accountId: number, videoId: number, t: Sequelize.Transaction) => Bluebird<VideoChannelShareInstance> | ||
9 | } | ||
10 | |||
11 | export interface VideoChannelShareClass { | ||
12 | loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare | ||
13 | load: VideoChannelShareMethods.Load | ||
14 | } | ||
15 | |||
16 | export interface VideoChannelShareAttributes { | ||
17 | accountId: number | ||
18 | videoChannelId: number | ||
19 | } | ||
20 | |||
21 | export interface VideoChannelShareInstance | ||
22 | extends VideoChannelShareClass, VideoChannelShareAttributes, Sequelize.Instance<VideoChannelShareAttributes> { | ||
23 | id: number | ||
24 | createdAt: Date | ||
25 | updatedAt: Date | ||
26 | |||
27 | Account?: AccountInstance | ||
28 | VideoChannel?: VideoChannelInstance | ||
29 | } | ||
30 | |||
31 | export interface VideoChannelShareModel | ||
32 | extends VideoChannelShareClass, Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes> {} | ||
diff --git a/server/models/video/video-channel-share.ts b/server/models/video/video-channel-share.ts index 2e9b658a3..cdba32fcd 100644 --- a/server/models/video/video-channel-share.ts +++ b/server/models/video/video-channel-share.ts | |||
@@ -1,85 +1,79 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { AccountModel } from '../account/account' | ||
4 | import { VideoChannelModel } from './video-channel' | ||
2 | 5 | ||
3 | import { addMethodsToModel } from '../utils' | 6 | @Table({ |
4 | import { VideoChannelShareAttributes, VideoChannelShareInstance, VideoChannelShareMethods } from './video-channel-share-interface' | 7 | tableName: 'videoChannelShare', |
5 | 8 | indexes: [ | |
6 | let VideoChannelShare: Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes> | ||
7 | let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare | ||
8 | let load: VideoChannelShareMethods.Load | ||
9 | |||
10 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
11 | VideoChannelShare = sequelize.define<VideoChannelShareInstance, VideoChannelShareAttributes>('VideoChannelShare', | ||
12 | { }, | ||
13 | { | 9 | { |
14 | indexes: [ | 10 | fields: [ 'accountId' ] |
15 | { | 11 | }, |
16 | fields: [ 'accountId' ] | 12 | { |
17 | }, | 13 | fields: [ 'videoChannelId' ] |
18 | { | ||
19 | fields: [ 'videoChannelId' ] | ||
20 | } | ||
21 | ] | ||
22 | } | 14 | } |
23 | ) | ||
24 | |||
25 | const classMethods = [ | ||
26 | associate, | ||
27 | load, | ||
28 | loadAccountsByShare | ||
29 | ] | 15 | ] |
30 | addMethodsToModel(VideoChannelShare, classMethods) | 16 | }) |
17 | export class VideoChannelShareModel extends Model<VideoChannelShareModel> { | ||
18 | @CreatedAt | ||
19 | createdAt: Date | ||
31 | 20 | ||
32 | return VideoChannelShare | 21 | @UpdatedAt |
33 | } | 22 | updatedAt: Date |
34 | 23 | ||
35 | // ------------------------------ METHODS ------------------------------ | 24 | @ForeignKey(() => AccountModel) |
25 | @Column | ||
26 | accountId: number | ||
36 | 27 | ||
37 | function associate (models) { | 28 | @BelongsTo(() => AccountModel, { |
38 | VideoChannelShare.belongsTo(models.Account, { | ||
39 | foreignKey: { | 29 | foreignKey: { |
40 | name: 'accountId', | ||
41 | allowNull: false | 30 | allowNull: false |
42 | }, | 31 | }, |
43 | onDelete: 'cascade' | 32 | onDelete: 'cascade' |
44 | }) | 33 | }) |
34 | Account: AccountModel | ||
45 | 35 | ||
46 | VideoChannelShare.belongsTo(models.VideoChannel, { | 36 | @ForeignKey(() => VideoChannelModel) |
37 | @Column | ||
38 | videoChannelId: number | ||
39 | |||
40 | @BelongsTo(() => VideoChannelModel, { | ||
47 | foreignKey: { | 41 | foreignKey: { |
48 | name: 'videoChannelId', | 42 | allowNull: false |
49 | allowNull: true | ||
50 | }, | 43 | }, |
51 | onDelete: 'cascade' | 44 | onDelete: 'cascade' |
52 | }) | 45 | }) |
53 | } | 46 | VideoChannel: VideoChannelModel |
54 | |||
55 | load = function (accountId: number, videoChannelId: number, t: Sequelize.Transaction) { | ||
56 | return VideoChannelShare.findOne({ | ||
57 | where: { | ||
58 | accountId, | ||
59 | videoChannelId | ||
60 | }, | ||
61 | include: [ | ||
62 | VideoChannelShare['sequelize'].models.Account, | ||
63 | VideoChannelShare['sequelize'].models.VideoChannel | ||
64 | ], | ||
65 | transaction: t | ||
66 | }) | ||
67 | } | ||
68 | 47 | ||
69 | loadAccountsByShare = function (videoChannelId: number, t: Sequelize.Transaction) { | 48 | static load (accountId: number, videoChannelId: number, t: Sequelize.Transaction) { |
70 | const query = { | 49 | return VideoChannelShareModel.findOne({ |
71 | where: { | 50 | where: { |
72 | videoChannelId | 51 | accountId, |
73 | }, | 52 | videoChannelId |
74 | include: [ | 53 | }, |
75 | { | 54 | include: [ |
76 | model: VideoChannelShare['sequelize'].models.Account, | 55 | AccountModel, |
77 | required: true | 56 | VideoChannelModel |
78 | } | 57 | ], |
79 | ], | 58 | transaction: t |
80 | transaction: t | 59 | }) |
81 | } | 60 | } |
82 | 61 | ||
83 | return VideoChannelShare.findAll(query) | 62 | static loadAccountsByShare (videoChannelId: number, t: Sequelize.Transaction) { |
84 | .then(res => res.map(r => r.Account)) | 63 | const query = { |
64 | where: { | ||
65 | videoChannelId | ||
66 | }, | ||
67 | include: [ | ||
68 | { | ||
69 | model: AccountModel, | ||
70 | required: true | ||
71 | } | ||
72 | ], | ||
73 | transaction: t | ||
74 | } | ||
75 | |||
76 | return VideoChannelShareModel.findAll(query) | ||
77 | .then(res => res.map(r => r.Account)) | ||
78 | } | ||
85 | } | 79 | } |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 54f12dce3..9b545a4ef 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,371 +1,341 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers' | 2 | import { |
3 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 3 | AfterDestroy, |
4 | import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' | 4 | AllowNull, |
5 | 5 | BelongsTo, | |
6 | import { addMethodsToModel, getSort } from '../utils' | 6 | Column, |
7 | import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' | 7 | CreatedAt, |
8 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' | 8 | DataType, |
9 | import { activityPubCollection } from '../../helpers/activitypub' | 9 | Default, |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 10 | ForeignKey, |
11 | 11 | HasMany, | |
12 | let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> | 12 | Is, |
13 | let toFormattedJSON: VideoChannelMethods.ToFormattedJSON | 13 | IsUUID, |
14 | let toActivityPubObject: VideoChannelMethods.ToActivityPubObject | 14 | Model, |
15 | let isOwned: VideoChannelMethods.IsOwned | 15 | Table, |
16 | let countByAccount: VideoChannelMethods.CountByAccount | 16 | UpdatedAt |
17 | let listForApi: VideoChannelMethods.ListForApi | 17 | } from 'sequelize-typescript' |
18 | let listByAccount: VideoChannelMethods.ListByAccount | 18 | import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' |
19 | let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount | 19 | import { activityPubCollection } from '../../helpers' |
20 | let loadByUUID: VideoChannelMethods.LoadByUUID | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' |
21 | let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount | 21 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' |
22 | let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount | 22 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
23 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID | 23 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub' |
24 | let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos | 24 | import { sendDeleteVideoChannel } from '../../lib/activitypub/send' |
25 | let loadByUrl: VideoChannelMethods.LoadByUrl | 25 | import { AccountModel } from '../account/account' |
26 | let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl | 26 | import { ServerModel } from '../server/server' |
27 | 27 | import { getSort, throwIfNotValid } from '../utils' | |
28 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 28 | import { VideoModel } from './video' |
29 | VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', | 29 | import { VideoChannelShareModel } from './video-channel-share' |
30 | |||
31 | @Table({ | ||
32 | tableName: 'videoChannel', | ||
33 | indexes: [ | ||
30 | { | 34 | { |
31 | uuid: { | 35 | fields: [ 'accountId' ] |
32 | type: DataTypes.UUID, | ||
33 | defaultValue: DataTypes.UUIDV4, | ||
34 | allowNull: false, | ||
35 | validate: { | ||
36 | isUUID: 4 | ||
37 | } | ||
38 | }, | ||
39 | name: { | ||
40 | type: DataTypes.STRING, | ||
41 | allowNull: false, | ||
42 | validate: { | ||
43 | nameValid: value => { | ||
44 | const res = isVideoChannelNameValid(value) | ||
45 | if (res === false) throw new Error('Video channel name is not valid.') | ||
46 | } | ||
47 | } | ||
48 | }, | ||
49 | description: { | ||
50 | type: DataTypes.STRING, | ||
51 | allowNull: true, | ||
52 | validate: { | ||
53 | descriptionValid: value => { | ||
54 | const res = isVideoChannelDescriptionValid(value) | ||
55 | if (res === false) throw new Error('Video channel description is not valid.') | ||
56 | } | ||
57 | } | ||
58 | }, | ||
59 | remote: { | ||
60 | type: DataTypes.BOOLEAN, | ||
61 | allowNull: false, | ||
62 | defaultValue: false | ||
63 | }, | ||
64 | url: { | ||
65 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max), | ||
66 | allowNull: false, | ||
67 | validate: { | ||
68 | urlValid: value => { | ||
69 | const res = isActivityPubUrlValid(value) | ||
70 | if (res === false) throw new Error('Video channel URL is not valid.') | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | }, | ||
75 | { | ||
76 | indexes: [ | ||
77 | { | ||
78 | fields: [ 'accountId' ] | ||
79 | } | ||
80 | ], | ||
81 | hooks: { | ||
82 | afterDestroy | ||
83 | } | ||
84 | } | 36 | } |
85 | ) | ||
86 | |||
87 | const classMethods = [ | ||
88 | associate, | ||
89 | |||
90 | listForApi, | ||
91 | listByAccount, | ||
92 | loadByIdAndAccount, | ||
93 | loadAndPopulateAccount, | ||
94 | loadByUUIDAndPopulateAccount, | ||
95 | loadByUUID, | ||
96 | loadByHostAndUUID, | ||
97 | loadAndPopulateAccountAndVideos, | ||
98 | countByAccount, | ||
99 | loadByUrl, | ||
100 | loadByUUIDOrUrl | ||
101 | ] | 37 | ] |
102 | const instanceMethods = [ | 38 | }) |
103 | isOwned, | 39 | export class VideoChannelModel extends Model<VideoChannelModel> { |
104 | toFormattedJSON, | ||
105 | toActivityPubObject | ||
106 | ] | ||
107 | addMethodsToModel(VideoChannel, classMethods, instanceMethods) | ||
108 | 40 | ||
109 | return VideoChannel | 41 | @AllowNull(false) |
110 | } | 42 | @Default(DataType.UUIDV4) |
43 | @IsUUID(4) | ||
44 | @Column(DataType.UUID) | ||
45 | uuid: string | ||
111 | 46 | ||
112 | // ------------------------------ METHODS ------------------------------ | 47 | @AllowNull(false) |
48 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) | ||
49 | @Column | ||
50 | name: string | ||
113 | 51 | ||
114 | isOwned = function (this: VideoChannelInstance) { | 52 | @AllowNull(true) |
115 | return this.remote === false | 53 | @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description')) |
116 | } | 54 | @Column |
117 | 55 | description: string | |
118 | toFormattedJSON = function (this: VideoChannelInstance) { | ||
119 | const json = { | ||
120 | id: this.id, | ||
121 | uuid: this.uuid, | ||
122 | name: this.name, | ||
123 | description: this.description, | ||
124 | isLocal: this.isOwned(), | ||
125 | createdAt: this.createdAt, | ||
126 | updatedAt: this.updatedAt | ||
127 | } | ||
128 | 56 | ||
129 | if (this.Account !== undefined) { | 57 | @AllowNull(false) |
130 | json['owner'] = { | 58 | @Column |
131 | name: this.Account.name, | 59 | remote: boolean |
132 | uuid: this.Account.uuid | ||
133 | } | ||
134 | } | ||
135 | 60 | ||
136 | if (Array.isArray(this.Videos)) { | 61 | @AllowNull(false) |
137 | json['videos'] = this.Videos.map(v => v.toFormattedJSON()) | 62 | @Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) |
138 | } | 63 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max)) |
64 | url: string | ||
139 | 65 | ||
140 | return json | 66 | @CreatedAt |
141 | } | 67 | createdAt: Date |
142 | 68 | ||
143 | toActivityPubObject = function (this: VideoChannelInstance) { | 69 | @UpdatedAt |
144 | let sharesObject | 70 | updatedAt: Date |
145 | if (Array.isArray(this.VideoChannelShares)) { | ||
146 | const shares: string[] = [] | ||
147 | 71 | ||
148 | for (const videoChannelShare of this.VideoChannelShares) { | 72 | @ForeignKey(() => AccountModel) |
149 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) | 73 | @Column |
150 | shares.push(shareUrl) | 74 | accountId: number |
151 | } | ||
152 | 75 | ||
153 | sharesObject = activityPubCollection(shares) | 76 | @BelongsTo(() => AccountModel, { |
154 | } | 77 | foreignKey: { |
155 | 78 | allowNull: false | |
156 | const json = { | 79 | }, |
157 | type: 'VideoChannel' as 'VideoChannel', | 80 | onDelete: 'CASCADE' |
158 | id: this.url, | 81 | }) |
159 | uuid: this.uuid, | 82 | Account: AccountModel |
160 | content: this.description, | ||
161 | name: this.name, | ||
162 | published: this.createdAt.toISOString(), | ||
163 | updated: this.updatedAt.toISOString(), | ||
164 | shares: sharesObject | ||
165 | } | ||
166 | |||
167 | return json | ||
168 | } | ||
169 | |||
170 | // ------------------------------ STATICS ------------------------------ | ||
171 | 83 | ||
172 | function associate (models) { | 84 | @HasMany(() => VideoModel, { |
173 | VideoChannel.belongsTo(models.Account, { | ||
174 | foreignKey: { | 85 | foreignKey: { |
175 | name: 'accountId', | 86 | name: 'channelId', |
176 | allowNull: false | 87 | allowNull: false |
177 | }, | 88 | }, |
178 | onDelete: 'CASCADE' | 89 | onDelete: 'CASCADE' |
179 | }) | 90 | }) |
91 | Videos: VideoModel[] | ||
180 | 92 | ||
181 | VideoChannel.hasMany(models.Video, { | 93 | @HasMany(() => VideoChannelShareModel, { |
182 | foreignKey: { | 94 | foreignKey: { |
183 | name: 'channelId', | 95 | name: 'channelId', |
184 | allowNull: false | 96 | allowNull: false |
185 | }, | 97 | }, |
186 | onDelete: 'CASCADE' | 98 | onDelete: 'CASCADE' |
187 | }) | 99 | }) |
188 | } | 100 | VideoChannelShares: VideoChannelShareModel[] |
189 | 101 | ||
190 | function afterDestroy (videoChannel: VideoChannelInstance) { | 102 | @AfterDestroy |
191 | if (videoChannel.isOwned()) { | 103 | static sendDeleteIfOwned (instance: VideoChannelModel) { |
192 | return sendDeleteVideoChannel(videoChannel, undefined) | 104 | if (instance.isOwned()) { |
193 | } | 105 | return sendDeleteVideoChannel(instance, undefined) |
106 | } | ||
194 | 107 | ||
195 | return undefined | 108 | return undefined |
196 | } | 109 | } |
197 | 110 | ||
198 | countByAccount = function (accountId: number) { | 111 | static countByAccount (accountId: number) { |
199 | const query = { | 112 | const query = { |
200 | where: { | 113 | where: { |
201 | accountId | 114 | accountId |
115 | } | ||
202 | } | 116 | } |
117 | |||
118 | return VideoChannelModel.count(query) | ||
203 | } | 119 | } |
204 | 120 | ||
205 | return VideoChannel.count(query) | 121 | static listForApi (start: number, count: number, sort: string) { |
206 | } | 122 | const query = { |
123 | offset: start, | ||
124 | limit: count, | ||
125 | order: [ getSort(sort) ], | ||
126 | include: [ | ||
127 | { | ||
128 | model: AccountModel, | ||
129 | required: true, | ||
130 | include: [ { model: ServerModel, required: false } ] | ||
131 | } | ||
132 | ] | ||
133 | } | ||
207 | 134 | ||
208 | listForApi = function (start: number, count: number, sort: string) { | 135 | return VideoChannelModel.findAndCountAll(query) |
209 | const query = { | 136 | .then(({ rows, count }) => { |
210 | offset: start, | 137 | return { total: count, data: rows } |
211 | limit: count, | 138 | }) |
212 | order: [ getSort(sort) ], | ||
213 | include: [ | ||
214 | { | ||
215 | model: VideoChannel['sequelize'].models.Account, | ||
216 | required: true, | ||
217 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
218 | } | ||
219 | ] | ||
220 | } | 139 | } |
221 | 140 | ||
222 | return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { | 141 | static listByAccount (accountId: number) { |
223 | return { total: count, data: rows } | 142 | const query = { |
224 | }) | 143 | order: [ getSort('createdAt') ], |
225 | } | 144 | include: [ |
145 | { | ||
146 | model: AccountModel, | ||
147 | where: { | ||
148 | id: accountId | ||
149 | }, | ||
150 | required: true, | ||
151 | include: [ { model: ServerModel, required: false } ] | ||
152 | } | ||
153 | ] | ||
154 | } | ||
226 | 155 | ||
227 | listByAccount = function (accountId: number) { | 156 | return VideoChannelModel.findAndCountAll(query) |
228 | const query = { | 157 | .then(({ rows, count }) => { |
229 | order: [ getSort('createdAt') ], | 158 | return { total: count, data: rows } |
230 | include: [ | 159 | }) |
231 | { | ||
232 | model: VideoChannel['sequelize'].models.Account, | ||
233 | where: { | ||
234 | id: accountId | ||
235 | }, | ||
236 | required: true, | ||
237 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
238 | } | ||
239 | ] | ||
240 | } | 160 | } |
241 | 161 | ||
242 | return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { | 162 | static loadByUUID (uuid: string, t?: Sequelize.Transaction) { |
243 | return { total: count, data: rows } | 163 | const query: IFindOptions<VideoChannelModel> = { |
244 | }) | 164 | where: { |
245 | } | 165 | uuid |
166 | } | ||
167 | } | ||
168 | |||
169 | if (t !== undefined) query.transaction = t | ||
246 | 170 | ||
247 | loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { | 171 | return VideoChannelModel.findOne(query) |
248 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 172 | } |
249 | where: { | 173 | |
250 | uuid | 174 | static loadByUrl (url: string, t?: Sequelize.Transaction) { |
175 | const query: IFindOptions<VideoChannelModel> = { | ||
176 | where: { | ||
177 | url | ||
178 | }, | ||
179 | include: [ AccountModel ] | ||
251 | } | 180 | } |
181 | |||
182 | if (t !== undefined) query.transaction = t | ||
183 | |||
184 | return VideoChannelModel.findOne(query) | ||
252 | } | 185 | } |
253 | 186 | ||
254 | if (t !== undefined) query.transaction = t | 187 | static loadByUUIDOrUrl (uuid: string, url: string, t?: Sequelize.Transaction) { |
188 | const query: IFindOptions<VideoChannelModel> = { | ||
189 | where: { | ||
190 | [ Sequelize.Op.or ]: [ | ||
191 | { uuid }, | ||
192 | { url } | ||
193 | ] | ||
194 | } | ||
195 | } | ||
255 | 196 | ||
256 | return VideoChannel.findOne(query) | 197 | if (t !== undefined) query.transaction = t |
257 | } | ||
258 | 198 | ||
259 | loadByUrl = function (url: string, t?: Sequelize.Transaction) { | 199 | return VideoChannelModel.findOne(query) |
260 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | ||
261 | where: { | ||
262 | url | ||
263 | }, | ||
264 | include: [ VideoChannel['sequelize'].models.Account ] | ||
265 | } | 200 | } |
266 | 201 | ||
267 | if (t !== undefined) query.transaction = t | 202 | static loadByHostAndUUID (fromHost: string, uuid: string, t?: Sequelize.Transaction) { |
203 | const query: IFindOptions<VideoChannelModel> = { | ||
204 | where: { | ||
205 | uuid | ||
206 | }, | ||
207 | include: [ | ||
208 | { | ||
209 | model: AccountModel, | ||
210 | include: [ | ||
211 | { | ||
212 | model: ServerModel, | ||
213 | required: true, | ||
214 | where: { | ||
215 | host: fromHost | ||
216 | } | ||
217 | } | ||
218 | ] | ||
219 | } | ||
220 | ] | ||
221 | } | ||
268 | 222 | ||
269 | return VideoChannel.findOne(query) | 223 | if (t !== undefined) query.transaction = t |
270 | } | 224 | |
225 | return VideoChannelModel.findOne(query) | ||
226 | } | ||
271 | 227 | ||
272 | loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) { | 228 | static loadByIdAndAccount (id: number, accountId: number) { |
273 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 229 | const options = { |
274 | where: { | 230 | where: { |
275 | [Sequelize.Op.or]: [ | 231 | id, |
276 | { uuid }, | 232 | accountId |
277 | { url } | 233 | }, |
234 | include: [ | ||
235 | { | ||
236 | model: AccountModel, | ||
237 | include: [ { model: ServerModel, required: false } ] | ||
238 | } | ||
278 | ] | 239 | ] |
279 | } | 240 | } |
241 | |||
242 | return VideoChannelModel.findOne(options) | ||
280 | } | 243 | } |
281 | 244 | ||
282 | if (t !== undefined) query.transaction = t | 245 | static loadAndPopulateAccount (id: number) { |
246 | const options = { | ||
247 | include: [ | ||
248 | { | ||
249 | model: AccountModel, | ||
250 | include: [ { model: ServerModel, required: false } ] | ||
251 | } | ||
252 | ] | ||
253 | } | ||
283 | 254 | ||
284 | return VideoChannel.findOne(query) | 255 | return VideoChannelModel.findById(id, options) |
285 | } | 256 | } |
286 | 257 | ||
287 | loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { | 258 | static loadByUUIDAndPopulateAccount (uuid: string) { |
288 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 259 | const options = { |
289 | where: { | 260 | where: { |
290 | uuid | 261 | uuid |
291 | }, | 262 | }, |
292 | include: [ | 263 | include: [ |
293 | { | 264 | { |
294 | model: VideoChannel['sequelize'].models.Account, | 265 | model: AccountModel, |
295 | include: [ | 266 | include: [ { model: ServerModel, required: false } ] |
296 | { | 267 | } |
297 | model: VideoChannel['sequelize'].models.Server, | 268 | ] |
298 | required: true, | 269 | } |
299 | where: { | 270 | |
300 | host: fromHost | 271 | return VideoChannelModel.findOne(options) |
301 | } | ||
302 | } | ||
303 | ] | ||
304 | } | ||
305 | ] | ||
306 | } | 272 | } |
307 | 273 | ||
308 | if (t !== undefined) query.transaction = t | 274 | static loadAndPopulateAccountAndVideos (id: number) { |
275 | const options = { | ||
276 | include: [ | ||
277 | { | ||
278 | model: AccountModel, | ||
279 | include: [ { model: ServerModel, required: false } ] | ||
280 | }, | ||
281 | VideoModel | ||
282 | ] | ||
283 | } | ||
309 | 284 | ||
310 | return VideoChannel.findOne(query) | 285 | return VideoChannelModel.findById(id, options) |
311 | } | 286 | } |
312 | 287 | ||
313 | loadByIdAndAccount = function (id: number, accountId: number) { | 288 | isOwned () { |
314 | const options = { | 289 | return this.remote === false |
315 | where: { | ||
316 | id, | ||
317 | accountId | ||
318 | }, | ||
319 | include: [ | ||
320 | { | ||
321 | model: VideoChannel['sequelize'].models.Account, | ||
322 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
323 | } | ||
324 | ] | ||
325 | } | 290 | } |
326 | 291 | ||
327 | return VideoChannel.findOne(options) | 292 | toFormattedJSON () { |
328 | } | 293 | const json = { |
294 | id: this.id, | ||
295 | uuid: this.uuid, | ||
296 | name: this.name, | ||
297 | description: this.description, | ||
298 | isLocal: this.isOwned(), | ||
299 | createdAt: this.createdAt, | ||
300 | updatedAt: this.updatedAt | ||
301 | } | ||
329 | 302 | ||
330 | loadAndPopulateAccount = function (id: number) { | 303 | if (this.Account !== undefined) { |
331 | const options = { | 304 | json[ 'owner' ] = { |
332 | include: [ | 305 | name: this.Account.name, |
333 | { | 306 | uuid: this.Account.uuid |
334 | model: VideoChannel['sequelize'].models.Account, | ||
335 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
336 | } | 307 | } |
337 | ] | 308 | } |
309 | |||
310 | if (Array.isArray(this.Videos)) { | ||
311 | json[ 'videos' ] = this.Videos.map(v => v.toFormattedJSON()) | ||
312 | } | ||
313 | |||
314 | return json | ||
338 | } | 315 | } |
339 | 316 | ||
340 | return VideoChannel.findById(id, options) | 317 | toActivityPubObject () { |
341 | } | 318 | let sharesObject |
319 | if (Array.isArray(this.VideoChannelShares)) { | ||
320 | const shares: string[] = [] | ||
342 | 321 | ||
343 | loadByUUIDAndPopulateAccount = function (uuid: string) { | 322 | for (const videoChannelShare of this.VideoChannelShares) { |
344 | const options = { | 323 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) |
345 | where: { | 324 | shares.push(shareUrl) |
346 | uuid | ||
347 | }, | ||
348 | include: [ | ||
349 | { | ||
350 | model: VideoChannel['sequelize'].models.Account, | ||
351 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
352 | } | 325 | } |
353 | ] | ||
354 | } | ||
355 | 326 | ||
356 | return VideoChannel.findOne(options) | 327 | sharesObject = activityPubCollection(shares) |
357 | } | 328 | } |
358 | 329 | ||
359 | loadAndPopulateAccountAndVideos = function (id: number) { | 330 | return { |
360 | const options = { | 331 | type: 'VideoChannel' as 'VideoChannel', |
361 | include: [ | 332 | id: this.url, |
362 | { | 333 | uuid: this.uuid, |
363 | model: VideoChannel['sequelize'].models.Account, | 334 | content: this.description, |
364 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | 335 | name: this.name, |
365 | }, | 336 | published: this.createdAt.toISOString(), |
366 | VideoChannel['sequelize'].models.Video | 337 | updated: this.updatedAt.toISOString(), |
367 | ] | 338 | shares: sharesObject |
339 | } | ||
368 | } | 340 | } |
369 | |||
370 | return VideoChannel.findById(id, options) | ||
371 | } | 341 | } |
diff --git a/server/models/video/video-file-interface.ts b/server/models/video/video-file-interface.ts deleted file mode 100644 index c9fb8b8ae..000000000 --- a/server/models/video/video-file-interface.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace VideoFileMethods { | ||
4 | } | ||
5 | |||
6 | export interface VideoFileClass { | ||
7 | } | ||
8 | |||
9 | export interface VideoFileAttributes { | ||
10 | resolution: number | ||
11 | size: number | ||
12 | infoHash?: string | ||
13 | extname: string | ||
14 | |||
15 | videoId?: number | ||
16 | } | ||
17 | |||
18 | export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance<VideoFileAttributes> { | ||
19 | id: number | ||
20 | createdAt: Date | ||
21 | updatedAt: Date | ||
22 | } | ||
23 | |||
24 | export interface VideoFileModel extends VideoFileClass, Sequelize.Model<VideoFileInstance, VideoFileAttributes> {} | ||
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 600141994..df4067a4e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,81 +1,56 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' | 3 | import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' |
4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 4 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
5 | import { throwIfNotValid } from '../utils' | ||
6 | import { VideoModel } from './video' | ||
5 | 7 | ||
6 | import { addMethodsToModel } from '../utils' | 8 | @Table({ |
7 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' | 9 | tableName: 'videoFile', |
8 | 10 | indexes: [ | |
9 | let VideoFile: Sequelize.Model<VideoFileInstance, VideoFileAttributes> | ||
10 | |||
11 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
12 | VideoFile = sequelize.define<VideoFileInstance, VideoFileAttributes>('VideoFile', | ||
13 | { | 11 | { |
14 | resolution: { | 12 | fields: [ 'videoId' ] |
15 | type: DataTypes.INTEGER, | ||
16 | allowNull: false, | ||
17 | validate: { | ||
18 | resolutionValid: value => { | ||
19 | const res = isVideoFileResolutionValid(value) | ||
20 | if (res === false) throw new Error('Video file resolution is not valid.') | ||
21 | } | ||
22 | } | ||
23 | }, | ||
24 | size: { | ||
25 | type: DataTypes.BIGINT, | ||
26 | allowNull: false, | ||
27 | validate: { | ||
28 | sizeValid: value => { | ||
29 | const res = isVideoFileSizeValid(value) | ||
30 | if (res === false) throw new Error('Video file size is not valid.') | ||
31 | } | ||
32 | } | ||
33 | }, | ||
34 | extname: { | ||
35 | type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), | ||
36 | allowNull: false | ||
37 | }, | ||
38 | infoHash: { | ||
39 | type: DataTypes.STRING, | ||
40 | allowNull: false, | ||
41 | validate: { | ||
42 | infoHashValid: value => { | ||
43 | const res = isVideoFileInfoHashValid(value) | ||
44 | if (res === false) throw new Error('Video file info hash is not valid.') | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | }, | 13 | }, |
49 | { | 14 | { |
50 | indexes: [ | 15 | fields: [ 'infoHash' ] |
51 | { | ||
52 | fields: [ 'videoId' ] | ||
53 | }, | ||
54 | { | ||
55 | fields: [ 'infoHash' ] | ||
56 | } | ||
57 | ] | ||
58 | } | 16 | } |
59 | ) | ||
60 | |||
61 | const classMethods = [ | ||
62 | associate | ||
63 | ] | 17 | ] |
64 | addMethodsToModel(VideoFile, classMethods) | 18 | }) |
65 | 19 | export class VideoFileModel extends Model<VideoFileModel> { | |
66 | return VideoFile | 20 | @CreatedAt |
67 | } | 21 | createdAt: Date |
68 | 22 | ||
69 | // ------------------------------ STATICS ------------------------------ | 23 | @UpdatedAt |
70 | 24 | updatedAt: Date | |
71 | function associate (models) { | 25 | |
72 | VideoFile.belongsTo(models.Video, { | 26 | @AllowNull(false) |
27 | @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution')) | ||
28 | @Column | ||
29 | resolution: number | ||
30 | |||
31 | @AllowNull(false) | ||
32 | @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size')) | ||
33 | @Column(DataType.BIGINT) | ||
34 | size: number | ||
35 | |||
36 | @AllowNull(false) | ||
37 | @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) | ||
38 | extname: string | ||
39 | |||
40 | @AllowNull(false) | ||
41 | @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) | ||
42 | @Column | ||
43 | infoHash: string | ||
44 | |||
45 | @ForeignKey(() => VideoModel) | ||
46 | @Column | ||
47 | videoId: number | ||
48 | |||
49 | @BelongsTo(() => VideoModel, { | ||
73 | foreignKey: { | 50 | foreignKey: { |
74 | name: 'videoId', | ||
75 | allowNull: false | 51 | allowNull: false |
76 | }, | 52 | }, |
77 | onDelete: 'CASCADE' | 53 | onDelete: 'CASCADE' |
78 | }) | 54 | }) |
55 | Video: VideoModel | ||
79 | } | 56 | } |
80 | |||
81 | // ------------------------------ METHODS ------------------------------ | ||
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts deleted file mode 100644 index 2a63350af..000000000 --- a/server/models/video/video-interface.ts +++ /dev/null | |||
@@ -1,150 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' | ||
4 | import { ResultList } from '../../../shared/models/result-list.model' | ||
5 | import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model' | ||
6 | import { AccountVideoRateInstance } from '../account/account-video-rate-interface' | ||
7 | |||
8 | import { TagAttributes, TagInstance } from './tag-interface' | ||
9 | import { VideoChannelInstance } from './video-channel-interface' | ||
10 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' | ||
11 | import { VideoShareInstance } from './video-share-interface' | ||
12 | |||
13 | export namespace VideoMethods { | ||
14 | export type GetThumbnailName = (this: VideoInstance) => string | ||
15 | export type GetPreviewName = (this: VideoInstance) => string | ||
16 | export type IsOwned = (this: VideoInstance) => boolean | ||
17 | export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo | ||
18 | export type ToFormattedDetailsJSON = (this: VideoInstance) => FormattedDetailsVideo | ||
19 | |||
20 | export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance | ||
21 | export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string | ||
22 | export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string | ||
23 | export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string> | ||
24 | export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string> | ||
25 | export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string | ||
26 | export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> | ||
27 | |||
28 | export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject | ||
29 | |||
30 | export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void> | ||
31 | export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void> | ||
32 | export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number> | ||
33 | export type GetEmbedPath = (this: VideoInstance) => string | ||
34 | export type GetThumbnailPath = (this: VideoInstance) => string | ||
35 | export type GetPreviewPath = (this: VideoInstance) => string | ||
36 | export type GetDescriptionPath = (this: VideoInstance) => string | ||
37 | export type GetTruncatedDescription = (this: VideoInstance) => string | ||
38 | export type GetCategoryLabel = (this: VideoInstance) => string | ||
39 | export type GetLicenceLabel = (this: VideoInstance) => string | ||
40 | export type GetLanguageLabel = (this: VideoInstance) => string | ||
41 | |||
42 | export type List = () => Bluebird<VideoInstance[]> | ||
43 | |||
44 | export type ListAllAndSharedByAccountForOutbox = ( | ||
45 | accountId: number, | ||
46 | start: number, | ||
47 | count: number | ||
48 | ) => Bluebird< ResultList<VideoInstance> > | ||
49 | export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> > | ||
50 | export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> > | ||
51 | export type SearchAndPopulateAccountAndServerAndTags = ( | ||
52 | value: string, | ||
53 | start: number, | ||
54 | count: number, | ||
55 | sort: string | ||
56 | ) => Bluebird< ResultList<VideoInstance> > | ||
57 | |||
58 | export type Load = (id: number) => Bluebird<VideoInstance> | ||
59 | export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance> | ||
60 | export type LoadByUrlAndPopulateAccount = (url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance> | ||
61 | export type LoadAndPopulateAccountAndServerAndTags = (id: number) => Bluebird<VideoInstance> | ||
62 | export type LoadByUUIDAndPopulateAccountAndServerAndTags = (uuid: string) => Bluebird<VideoInstance> | ||
63 | export type LoadByUUIDOrURL = (uuid: string, url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance> | ||
64 | |||
65 | export type RemoveThumbnail = (this: VideoInstance) => Promise<void> | ||
66 | export type RemovePreview = (this: VideoInstance) => Promise<void> | ||
67 | export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> | ||
68 | export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> | ||
69 | } | ||
70 | |||
71 | export interface VideoClass { | ||
72 | list: VideoMethods.List | ||
73 | listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox | ||
74 | listForApi: VideoMethods.ListForApi | ||
75 | listUserVideosForApi: VideoMethods.ListUserVideosForApi | ||
76 | load: VideoMethods.Load | ||
77 | loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags | ||
78 | loadByUUID: VideoMethods.LoadByUUID | ||
79 | loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount | ||
80 | loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL | ||
81 | loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags | ||
82 | searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags | ||
83 | } | ||
84 | |||
85 | export interface VideoAttributes { | ||
86 | id?: number | ||
87 | uuid?: string | ||
88 | name: string | ||
89 | category: number | ||
90 | licence: number | ||
91 | language: number | ||
92 | nsfw: boolean | ||
93 | description: string | ||
94 | duration: number | ||
95 | privacy: number | ||
96 | views?: number | ||
97 | likes?: number | ||
98 | dislikes?: number | ||
99 | remote: boolean | ||
100 | url?: string | ||
101 | |||
102 | createdAt?: Date | ||
103 | updatedAt?: Date | ||
104 | |||
105 | parentId?: number | ||
106 | channelId?: number | ||
107 | |||
108 | VideoChannel?: VideoChannelInstance | ||
109 | Tags?: TagInstance[] | ||
110 | VideoFiles?: VideoFileInstance[] | ||
111 | VideoShares?: VideoShareInstance[] | ||
112 | AccountVideoRates?: AccountVideoRateInstance[] | ||
113 | } | ||
114 | |||
115 | export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { | ||
116 | createPreview: VideoMethods.CreatePreview | ||
117 | createThumbnail: VideoMethods.CreateThumbnail | ||
118 | createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | ||
119 | getOriginalFile: VideoMethods.GetOriginalFile | ||
120 | getPreviewName: VideoMethods.GetPreviewName | ||
121 | getPreviewPath: VideoMethods.GetPreviewPath | ||
122 | getThumbnailName: VideoMethods.GetThumbnailName | ||
123 | getThumbnailPath: VideoMethods.GetThumbnailPath | ||
124 | getTorrentFileName: VideoMethods.GetTorrentFileName | ||
125 | getVideoFilename: VideoMethods.GetVideoFilename | ||
126 | getVideoFilePath: VideoMethods.GetVideoFilePath | ||
127 | isOwned: VideoMethods.IsOwned | ||
128 | removeFile: VideoMethods.RemoveFile | ||
129 | removePreview: VideoMethods.RemovePreview | ||
130 | removeThumbnail: VideoMethods.RemoveThumbnail | ||
131 | removeTorrent: VideoMethods.RemoveTorrent | ||
132 | toActivityPubObject: VideoMethods.ToActivityPubObject | ||
133 | toFormattedJSON: VideoMethods.ToFormattedJSON | ||
134 | toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON | ||
135 | optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile | ||
136 | transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile | ||
137 | getOriginalFileHeight: VideoMethods.GetOriginalFileHeight | ||
138 | getEmbedPath: VideoMethods.GetEmbedPath | ||
139 | getDescriptionPath: VideoMethods.GetDescriptionPath | ||
140 | getTruncatedDescription: VideoMethods.GetTruncatedDescription | ||
141 | getCategoryLabel: VideoMethods.GetCategoryLabel | ||
142 | getLicenceLabel: VideoMethods.GetLicenceLabel | ||
143 | getLanguageLabel: VideoMethods.GetLanguageLabel | ||
144 | |||
145 | setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> | ||
146 | addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string> | ||
147 | setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string> | ||
148 | } | ||
149 | |||
150 | export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} | ||
diff --git a/server/models/video/video-share-interface.ts b/server/models/video/video-share-interface.ts deleted file mode 100644 index 3946303f1..000000000 --- a/server/models/video/video-share-interface.ts +++ /dev/null | |||
@@ -1,30 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { AccountInstance } from '../account/account-interface' | ||
4 | import { VideoInstance } from './video-interface' | ||
5 | |||
6 | export namespace VideoShareMethods { | ||
7 | export type LoadAccountsByShare = (videoId: number, t: Sequelize.Transaction) => Bluebird<AccountInstance[]> | ||
8 | export type Load = (accountId: number, videoId: number, t: Sequelize.Transaction) => Bluebird<VideoShareInstance> | ||
9 | } | ||
10 | |||
11 | export interface VideoShareClass { | ||
12 | loadAccountsByShare: VideoShareMethods.LoadAccountsByShare | ||
13 | load: VideoShareMethods.Load | ||
14 | } | ||
15 | |||
16 | export interface VideoShareAttributes { | ||
17 | accountId: number | ||
18 | videoId: number | ||
19 | } | ||
20 | |||
21 | export interface VideoShareInstance extends VideoShareClass, VideoShareAttributes, Sequelize.Instance<VideoShareAttributes> { | ||
22 | id: number | ||
23 | createdAt: Date | ||
24 | updatedAt: Date | ||
25 | |||
26 | Account?: AccountInstance | ||
27 | Video?: VideoInstance | ||
28 | } | ||
29 | |||
30 | export interface VideoShareModel extends VideoShareClass, Sequelize.Model<VideoShareInstance, VideoShareAttributes> {} | ||
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 37e405fa9..01b6d3d34 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -1,84 +1,78 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { AccountModel } from '../account/account' | ||
4 | import { VideoModel } from './video' | ||
2 | 5 | ||
3 | import { addMethodsToModel } from '../utils' | 6 | @Table({ |
4 | import { VideoShareAttributes, VideoShareInstance, VideoShareMethods } from './video-share-interface' | 7 | tableName: 'videoShare', |
5 | 8 | indexes: [ | |
6 | let VideoShare: Sequelize.Model<VideoShareInstance, VideoShareAttributes> | ||
7 | let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare | ||
8 | let load: VideoShareMethods.Load | ||
9 | |||
10 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
11 | VideoShare = sequelize.define<VideoShareInstance, VideoShareAttributes>('VideoShare', | ||
12 | { }, | ||
13 | { | 9 | { |
14 | indexes: [ | 10 | fields: [ 'accountId' ] |
15 | { | 11 | }, |
16 | fields: [ 'accountId' ] | 12 | { |
17 | }, | 13 | fields: [ 'videoId' ] |
18 | { | ||
19 | fields: [ 'videoId' ] | ||
20 | } | ||
21 | ] | ||
22 | } | 14 | } |
23 | ) | ||
24 | |||
25 | const classMethods = [ | ||
26 | associate, | ||
27 | loadAccountsByShare, | ||
28 | load | ||
29 | ] | 15 | ] |
30 | addMethodsToModel(VideoShare, classMethods) | 16 | }) |
17 | export class VideoShareModel extends Model<VideoShareModel> { | ||
18 | @CreatedAt | ||
19 | createdAt: Date | ||
31 | 20 | ||
32 | return VideoShare | 21 | @UpdatedAt |
33 | } | 22 | updatedAt: Date |
34 | 23 | ||
35 | // ------------------------------ METHODS ------------------------------ | 24 | @ForeignKey(() => AccountModel) |
25 | @Column | ||
26 | accountId: number | ||
36 | 27 | ||
37 | function associate (models) { | 28 | @BelongsTo(() => AccountModel, { |
38 | VideoShare.belongsTo(models.Account, { | ||
39 | foreignKey: { | 29 | foreignKey: { |
40 | name: 'accountId', | ||
41 | allowNull: false | 30 | allowNull: false |
42 | }, | 31 | }, |
43 | onDelete: 'cascade' | 32 | onDelete: 'cascade' |
44 | }) | 33 | }) |
34 | Account: AccountModel | ||
45 | 35 | ||
46 | VideoShare.belongsTo(models.Video, { | 36 | @ForeignKey(() => VideoModel) |
37 | @Column | ||
38 | videoId: number | ||
39 | |||
40 | @BelongsTo(() => VideoModel, { | ||
47 | foreignKey: { | 41 | foreignKey: { |
48 | name: 'videoId', | 42 | allowNull: false |
49 | allowNull: true | ||
50 | }, | 43 | }, |
51 | onDelete: 'cascade' | 44 | onDelete: 'cascade' |
52 | }) | 45 | }) |
53 | } | 46 | Video: VideoModel |
54 | |||
55 | load = function (accountId: number, videoId: number, t: Sequelize.Transaction) { | ||
56 | return VideoShare.findOne({ | ||
57 | where: { | ||
58 | accountId, | ||
59 | videoId | ||
60 | }, | ||
61 | include: [ | ||
62 | VideoShare['sequelize'].models.Account | ||
63 | ], | ||
64 | transaction: t | ||
65 | }) | ||
66 | } | ||
67 | 47 | ||
68 | loadAccountsByShare = function (videoId: number, t: Sequelize.Transaction) { | 48 | static load (accountId: number, videoId: number, t: Sequelize.Transaction) { |
69 | const query = { | 49 | return VideoShareModel.findOne({ |
70 | where: { | 50 | where: { |
71 | videoId | 51 | accountId, |
72 | }, | 52 | videoId |
73 | include: [ | 53 | }, |
74 | { | 54 | include: [ |
75 | model: VideoShare['sequelize'].models.Account, | 55 | AccountModel |
76 | required: true | 56 | ], |
77 | } | 57 | transaction: t |
78 | ], | 58 | }) |
79 | transaction: t | ||
80 | } | 59 | } |
81 | 60 | ||
82 | return VideoShare.findAll(query) | 61 | static loadAccountsByShare (videoId: number, t: Sequelize.Transaction) { |
83 | .then(res => res.map(r => r.Account)) | 62 | const query = { |
63 | where: { | ||
64 | videoId | ||
65 | }, | ||
66 | include: [ | ||
67 | { | ||
68 | model: AccountModel, | ||
69 | required: true | ||
70 | } | ||
71 | ], | ||
72 | transaction: t | ||
73 | } | ||
74 | |||
75 | return VideoShareModel.findAll(query) | ||
76 | .then(res => res.map(r => r.Account)) | ||
77 | } | ||
84 | } | 78 | } |
diff --git a/server/models/video/video-tag-interface.ts b/server/models/video/video-tag-interface.ts deleted file mode 100644 index f928cecff..000000000 --- a/server/models/video/video-tag-interface.ts +++ /dev/null | |||
@@ -1,18 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace VideoTagMethods { | ||
4 | } | ||
5 | |||
6 | export interface VideoTagClass { | ||
7 | } | ||
8 | |||
9 | export interface VideoTagAttributes { | ||
10 | } | ||
11 | |||
12 | export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance<VideoTagAttributes> { | ||
13 | id: number | ||
14 | createdAt: Date | ||
15 | updatedAt: Date | ||
16 | } | ||
17 | |||
18 | export interface VideoTagModel extends VideoTagClass, Sequelize.Model<VideoTagInstance, VideoTagAttributes> {} | ||
diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts index ac45374f8..ca15e3426 100644 --- a/server/models/video/video-tag.ts +++ b/server/models/video/video-tag.ts | |||
@@ -1,23 +1,30 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { TagModel } from './tag' | ||
3 | import { VideoModel } from './video' | ||
2 | 4 | ||
3 | import { | 5 | @Table({ |
4 | VideoTagInstance, | 6 | tableName: 'videoTag', |
5 | VideoTagAttributes | 7 | indexes: [ |
6 | } from './video-tag-interface' | 8 | { |
9 | fields: [ 'videoId' ] | ||
10 | }, | ||
11 | { | ||
12 | fields: [ 'tagId' ] | ||
13 | } | ||
14 | ] | ||
15 | }) | ||
16 | export class VideoTagModel extends Model<VideoTagModel> { | ||
17 | @CreatedAt | ||
18 | createdAt: Date | ||
7 | 19 | ||
8 | let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes> | 20 | @UpdatedAt |
21 | updatedAt: Date | ||
9 | 22 | ||
10 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 23 | @ForeignKey(() => VideoModel) |
11 | VideoTag = sequelize.define<VideoTagInstance, VideoTagAttributes>('VideoTag', {}, { | 24 | @Column |
12 | indexes: [ | 25 | videoId: number |
13 | { | ||
14 | fields: [ 'videoId' ] | ||
15 | }, | ||
16 | { | ||
17 | fields: [ 'tagId' ] | ||
18 | } | ||
19 | ] | ||
20 | }) | ||
21 | 26 | ||
22 | return VideoTag | 27 | @ForeignKey(() => TagModel) |
28 | @Column | ||
29 | tagId: number | ||
23 | } | 30 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index d46fdeebe..9e26f9bbe 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -4,21 +4,52 @@ import * as magnetUtil from 'magnet-uri' | |||
4 | import * as parseTorrent from 'parse-torrent' | 4 | import * as parseTorrent from 'parse-torrent' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import * as Sequelize from 'sequelize' | 6 | import * as Sequelize from 'sequelize' |
7 | import { | ||
8 | AfterDestroy, | ||
9 | AllowNull, | ||
10 | BelongsTo, | ||
11 | BelongsToMany, | ||
12 | Column, | ||
13 | CreatedAt, | ||
14 | DataType, | ||
15 | Default, | ||
16 | ForeignKey, | ||
17 | HasMany, | ||
18 | IFindOptions, | ||
19 | Is, | ||
20 | IsInt, | ||
21 | IsUUID, | ||
22 | Min, | ||
23 | Model, | ||
24 | Table, | ||
25 | UpdatedAt | ||
26 | } from 'sequelize-typescript' | ||
27 | import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions' | ||
7 | import { VideoPrivacy, VideoResolution } from '../../../shared' | 28 | import { VideoPrivacy, VideoResolution } from '../../../shared' |
8 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' | 29 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
9 | import { activityPubCollection } from '../../helpers/activitypub' | 30 | import { |
10 | import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils' | 31 | activityPubCollection, |
11 | import { isVideoCategoryValid, isVideoLanguageValid, isVideoPrivacyValid } from '../../helpers/custom-validators/videos' | 32 | createTorrentPromise, |
12 | import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' | 33 | generateImageFromVideoFile, |
34 | getVideoFileHeight, | ||
35 | logger, | ||
36 | renamePromise, | ||
37 | statPromise, | ||
38 | transcode, | ||
39 | unlinkPromise, | ||
40 | writeFilePromise | ||
41 | } from '../../helpers' | ||
42 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' | ||
13 | import { | 43 | import { |
14 | isActivityPubUrlValid, | 44 | isVideoCategoryValid, |
15 | isVideoDescriptionValid, | 45 | isVideoDescriptionValid, |
16 | isVideoDurationValid, | 46 | isVideoDurationValid, |
47 | isVideoLanguageValid, | ||
17 | isVideoLicenceValid, | 48 | isVideoLicenceValid, |
18 | isVideoNameValid, | 49 | isVideoNameValid, |
19 | isVideoNSFWValid | 50 | isVideoNSFWValid, |
20 | } from '../../helpers/index' | 51 | isVideoPrivacyValid |
21 | import { logger } from '../../helpers/logger' | 52 | } from '../../helpers/custom-validators/videos' |
22 | import { | 53 | import { |
23 | API_VERSION, | 54 | API_VERSION, |
24 | CONFIG, | 55 | CONFIG, |
@@ -31,1169 +62,1025 @@ import { | |||
31 | VIDEO_LANGUAGES, | 62 | VIDEO_LANGUAGES, |
32 | VIDEO_LICENCES, | 63 | VIDEO_LICENCES, |
33 | VIDEO_PRIVACIES | 64 | VIDEO_PRIVACIES |
34 | } from '../../initializers/constants' | 65 | } from '../../initializers' |
35 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' | 66 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub' |
36 | import { sendDeleteVideo } from '../../lib/index' | 67 | import { sendDeleteVideo } from '../../lib/index' |
37 | import { addMethodsToModel, getSort } from '../utils' | 68 | import { AccountModel } from '../account/account' |
38 | import { TagInstance } from './tag-interface' | 69 | import { AccountVideoRateModel } from '../account/account-video-rate' |
39 | import { VideoFileInstance, VideoFileModel } from './video-file-interface' | 70 | import { ServerModel } from '../server/server' |
40 | import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' | 71 | import { getSort, throwIfNotValid } from '../utils' |
41 | 72 | import { TagModel } from './tag' | |
42 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> | 73 | import { VideoAbuseModel } from './video-abuse' |
43 | let getOriginalFile: VideoMethods.GetOriginalFile | 74 | import { VideoChannelModel } from './video-channel' |
44 | let getVideoFilename: VideoMethods.GetVideoFilename | 75 | import { VideoFileModel } from './video-file' |
45 | let getThumbnailName: VideoMethods.GetThumbnailName | 76 | import { VideoShareModel } from './video-share' |
46 | let getThumbnailPath: VideoMethods.GetThumbnailPath | 77 | import { VideoTagModel } from './video-tag' |
47 | let getPreviewName: VideoMethods.GetPreviewName | 78 | |
48 | let getPreviewPath: VideoMethods.GetPreviewPath | 79 | @Table({ |
49 | let getTorrentFileName: VideoMethods.GetTorrentFileName | 80 | tableName: 'video', |
50 | let isOwned: VideoMethods.IsOwned | 81 | indexes: [ |
51 | let toFormattedJSON: VideoMethods.ToFormattedJSON | ||
52 | let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON | ||
53 | let toActivityPubObject: VideoMethods.ToActivityPubObject | ||
54 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile | ||
55 | let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile | ||
56 | let createPreview: VideoMethods.CreatePreview | ||
57 | let createThumbnail: VideoMethods.CreateThumbnail | ||
58 | let getVideoFilePath: VideoMethods.GetVideoFilePath | ||
59 | let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | ||
60 | let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight | ||
61 | let getEmbedPath: VideoMethods.GetEmbedPath | ||
62 | let getDescriptionPath: VideoMethods.GetDescriptionPath | ||
63 | let getTruncatedDescription: VideoMethods.GetTruncatedDescription | ||
64 | let getCategoryLabel: VideoMethods.GetCategoryLabel | ||
65 | let getLicenceLabel: VideoMethods.GetLicenceLabel | ||
66 | let getLanguageLabel: VideoMethods.GetLanguageLabel | ||
67 | |||
68 | let list: VideoMethods.List | ||
69 | let listForApi: VideoMethods.ListForApi | ||
70 | let listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox | ||
71 | let listUserVideosForApi: VideoMethods.ListUserVideosForApi | ||
72 | let load: VideoMethods.Load | ||
73 | let loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount | ||
74 | let loadByUUID: VideoMethods.LoadByUUID | ||
75 | let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL | ||
76 | let loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags | ||
77 | let loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags | ||
78 | let searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags | ||
79 | let removeThumbnail: VideoMethods.RemoveThumbnail | ||
80 | let removePreview: VideoMethods.RemovePreview | ||
81 | let removeFile: VideoMethods.RemoveFile | ||
82 | let removeTorrent: VideoMethods.RemoveTorrent | ||
83 | |||
84 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
85 | Video = sequelize.define<VideoInstance, VideoAttributes>('Video', | ||
86 | { | 82 | { |
87 | uuid: { | 83 | fields: [ 'name' ] |
88 | type: DataTypes.UUID, | ||
89 | defaultValue: DataTypes.UUIDV4, | ||
90 | allowNull: false, | ||
91 | validate: { | ||
92 | isUUID: 4 | ||
93 | } | ||
94 | }, | ||
95 | name: { | ||
96 | type: DataTypes.STRING, | ||
97 | allowNull: false, | ||
98 | validate: { | ||
99 | nameValid: value => { | ||
100 | const res = isVideoNameValid(value) | ||
101 | if (res === false) throw new Error('Video name is not valid.') | ||
102 | } | ||
103 | } | ||
104 | }, | ||
105 | category: { | ||
106 | type: DataTypes.INTEGER, | ||
107 | allowNull: true, | ||
108 | defaultValue: null, | ||
109 | validate: { | ||
110 | categoryValid: value => { | ||
111 | const res = isVideoCategoryValid(value) | ||
112 | if (res === false) throw new Error('Video category is not valid.') | ||
113 | } | ||
114 | } | ||
115 | }, | ||
116 | licence: { | ||
117 | type: DataTypes.INTEGER, | ||
118 | allowNull: true, | ||
119 | defaultValue: null, | ||
120 | validate: { | ||
121 | licenceValid: value => { | ||
122 | const res = isVideoLicenceValid(value) | ||
123 | if (res === false) throw new Error('Video licence is not valid.') | ||
124 | } | ||
125 | } | ||
126 | }, | ||
127 | language: { | ||
128 | type: DataTypes.INTEGER, | ||
129 | allowNull: true, | ||
130 | defaultValue: null, | ||
131 | validate: { | ||
132 | languageValid: value => { | ||
133 | const res = isVideoLanguageValid(value) | ||
134 | if (res === false) throw new Error('Video language is not valid.') | ||
135 | } | ||
136 | } | ||
137 | }, | ||
138 | privacy: { | ||
139 | type: DataTypes.INTEGER, | ||
140 | allowNull: false, | ||
141 | validate: { | ||
142 | privacyValid: value => { | ||
143 | const res = isVideoPrivacyValid(value) | ||
144 | if (res === false) throw new Error('Video privacy is not valid.') | ||
145 | } | ||
146 | } | ||
147 | }, | ||
148 | nsfw: { | ||
149 | type: DataTypes.BOOLEAN, | ||
150 | allowNull: false, | ||
151 | validate: { | ||
152 | nsfwValid: value => { | ||
153 | const res = isVideoNSFWValid(value) | ||
154 | if (res === false) throw new Error('Video nsfw attribute is not valid.') | ||
155 | } | ||
156 | } | ||
157 | }, | ||
158 | description: { | ||
159 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | ||
160 | allowNull: true, | ||
161 | defaultValue: null, | ||
162 | validate: { | ||
163 | descriptionValid: value => { | ||
164 | const res = isVideoDescriptionValid(value) | ||
165 | if (res === false) throw new Error('Video description is not valid.') | ||
166 | } | ||
167 | } | ||
168 | }, | ||
169 | duration: { | ||
170 | type: DataTypes.INTEGER, | ||
171 | allowNull: false, | ||
172 | validate: { | ||
173 | durationValid: value => { | ||
174 | const res = isVideoDurationValid(value) | ||
175 | if (res === false) throw new Error('Video duration is not valid.') | ||
176 | } | ||
177 | } | ||
178 | }, | ||
179 | views: { | ||
180 | type: DataTypes.INTEGER, | ||
181 | allowNull: false, | ||
182 | defaultValue: 0, | ||
183 | validate: { | ||
184 | min: 0, | ||
185 | isInt: true | ||
186 | } | ||
187 | }, | ||
188 | likes: { | ||
189 | type: DataTypes.INTEGER, | ||
190 | allowNull: false, | ||
191 | defaultValue: 0, | ||
192 | validate: { | ||
193 | min: 0, | ||
194 | isInt: true | ||
195 | } | ||
196 | }, | ||
197 | dislikes: { | ||
198 | type: DataTypes.INTEGER, | ||
199 | allowNull: false, | ||
200 | defaultValue: 0, | ||
201 | validate: { | ||
202 | min: 0, | ||
203 | isInt: true | ||
204 | } | ||
205 | }, | ||
206 | remote: { | ||
207 | type: DataTypes.BOOLEAN, | ||
208 | allowNull: false, | ||
209 | defaultValue: false | ||
210 | }, | ||
211 | url: { | ||
212 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max), | ||
213 | allowNull: false, | ||
214 | validate: { | ||
215 | urlValid: value => { | ||
216 | const res = isActivityPubUrlValid(value) | ||
217 | if (res === false) throw new Error('Video URL is not valid.') | ||
218 | } | ||
219 | } | ||
220 | } | ||
221 | }, | 84 | }, |
222 | { | 85 | { |
223 | indexes: [ | 86 | fields: [ 'createdAt' ] |
224 | { | 87 | }, |
225 | fields: [ 'name' ] | 88 | { |
226 | }, | 89 | fields: [ 'duration' ] |
227 | { | 90 | }, |
228 | fields: [ 'createdAt' ] | 91 | { |
229 | }, | 92 | fields: [ 'views' ] |
230 | { | 93 | }, |
231 | fields: [ 'duration' ] | 94 | { |
232 | }, | 95 | fields: [ 'likes' ] |
233 | { | 96 | }, |
234 | fields: [ 'views' ] | 97 | { |
235 | }, | 98 | fields: [ 'uuid' ] |
236 | { | 99 | }, |
237 | fields: [ 'likes' ] | 100 | { |
238 | }, | 101 | fields: [ 'channelId' ] |
239 | { | ||
240 | fields: [ 'uuid' ] | ||
241 | }, | ||
242 | { | ||
243 | fields: [ 'channelId' ] | ||
244 | } | ||
245 | ], | ||
246 | hooks: { | ||
247 | afterDestroy | ||
248 | } | ||
249 | } | 102 | } |
250 | ) | ||
251 | |||
252 | const classMethods = [ | ||
253 | associate, | ||
254 | |||
255 | list, | ||
256 | listAllAndSharedByAccountForOutbox, | ||
257 | listForApi, | ||
258 | listUserVideosForApi, | ||
259 | load, | ||
260 | loadByUrlAndPopulateAccount, | ||
261 | loadAndPopulateAccountAndServerAndTags, | ||
262 | loadByUUIDOrURL, | ||
263 | loadByUUID, | ||
264 | loadByUUIDAndPopulateAccountAndServerAndTags, | ||
265 | searchAndPopulateAccountAndServerAndTags | ||
266 | ] | ||
267 | const instanceMethods = [ | ||
268 | createPreview, | ||
269 | createThumbnail, | ||
270 | createTorrentAndSetInfoHash, | ||
271 | getPreviewName, | ||
272 | getPreviewPath, | ||
273 | getThumbnailName, | ||
274 | getThumbnailPath, | ||
275 | getTorrentFileName, | ||
276 | getVideoFilename, | ||
277 | getVideoFilePath, | ||
278 | getOriginalFile, | ||
279 | isOwned, | ||
280 | removeFile, | ||
281 | removePreview, | ||
282 | removeThumbnail, | ||
283 | removeTorrent, | ||
284 | toActivityPubObject, | ||
285 | toFormattedJSON, | ||
286 | toFormattedDetailsJSON, | ||
287 | optimizeOriginalVideofile, | ||
288 | transcodeOriginalVideofile, | ||
289 | getOriginalFileHeight, | ||
290 | getEmbedPath, | ||
291 | getTruncatedDescription, | ||
292 | getDescriptionPath, | ||
293 | getCategoryLabel, | ||
294 | getLicenceLabel, | ||
295 | getLanguageLabel | ||
296 | ] | 103 | ] |
297 | addMethodsToModel(Video, classMethods, instanceMethods) | 104 | }) |
298 | 105 | export class VideoModel extends Model<VideoModel> { | |
299 | return Video | 106 | |
300 | } | 107 | @AllowNull(false) |
301 | 108 | @Default(DataType.UUIDV4) | |
302 | // ------------------------------ METHODS ------------------------------ | 109 | @IsUUID(4) |
303 | 110 | @Column(DataType.UUID) | |
304 | function associate (models) { | 111 | uuid: string |
305 | Video.belongsTo(models.VideoChannel, { | 112 | |
113 | @AllowNull(false) | ||
114 | @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name')) | ||
115 | @Column | ||
116 | name: string | ||
117 | |||
118 | @AllowNull(true) | ||
119 | @Default(null) | ||
120 | @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category')) | ||
121 | @Column | ||
122 | category: number | ||
123 | |||
124 | @AllowNull(true) | ||
125 | @Default(null) | ||
126 | @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence')) | ||
127 | @Column | ||
128 | licence: number | ||
129 | |||
130 | @AllowNull(true) | ||
131 | @Default(null) | ||
132 | @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language')) | ||
133 | @Column | ||
134 | language: number | ||
135 | |||
136 | @AllowNull(false) | ||
137 | @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy')) | ||
138 | @Column | ||
139 | privacy: number | ||
140 | |||
141 | @AllowNull(false) | ||
142 | @Is('VideoNSFW', value => throwIfNotValid(value, isVideoNSFWValid, 'NSFW boolean')) | ||
143 | @Column | ||
144 | nsfw: boolean | ||
145 | |||
146 | @AllowNull(true) | ||
147 | @Default(null) | ||
148 | @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description')) | ||
149 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max)) | ||
150 | description: string | ||
151 | |||
152 | @AllowNull(false) | ||
153 | @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration')) | ||
154 | @Column | ||
155 | duration: number | ||
156 | |||
157 | @AllowNull(false) | ||
158 | @Default(0) | ||
159 | @IsInt | ||
160 | @Min(0) | ||
161 | @Column | ||
162 | views: number | ||
163 | |||
164 | @AllowNull(false) | ||
165 | @Default(0) | ||
166 | @IsInt | ||
167 | @Min(0) | ||
168 | @Column | ||
169 | likes: number | ||
170 | |||
171 | @AllowNull(false) | ||
172 | @Default(0) | ||
173 | @IsInt | ||
174 | @Min(0) | ||
175 | @Column | ||
176 | dislikes: number | ||
177 | |||
178 | @AllowNull(false) | ||
179 | @Column | ||
180 | remote: boolean | ||
181 | |||
182 | @AllowNull(false) | ||
183 | @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | ||
184 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | ||
185 | url: string | ||
186 | |||
187 | @CreatedAt | ||
188 | createdAt: Date | ||
189 | |||
190 | @UpdatedAt | ||
191 | updatedAt: Date | ||
192 | |||
193 | @ForeignKey(() => VideoChannelModel) | ||
194 | @Column | ||
195 | channelId: number | ||
196 | |||
197 | @BelongsTo(() => VideoChannelModel, { | ||
306 | foreignKey: { | 198 | foreignKey: { |
307 | name: 'channelId', | ||
308 | allowNull: false | 199 | allowNull: false |
309 | }, | 200 | }, |
310 | onDelete: 'cascade' | 201 | onDelete: 'cascade' |
311 | }) | 202 | }) |
203 | VideoChannel: VideoChannelModel | ||
312 | 204 | ||
313 | Video.belongsToMany(models.Tag, { | 205 | @BelongsToMany(() => TagModel, { |
314 | foreignKey: 'videoId', | 206 | foreignKey: 'videoId', |
315 | through: models.VideoTag, | 207 | through: () => VideoTagModel, |
316 | onDelete: 'cascade' | 208 | onDelete: 'CASCADE' |
317 | }) | 209 | }) |
210 | Tags: TagModel[] | ||
318 | 211 | ||
319 | Video.hasMany(models.VideoAbuse, { | 212 | @HasMany(() => VideoAbuseModel, { |
320 | foreignKey: { | 213 | foreignKey: { |
321 | name: 'videoId', | 214 | name: 'videoId', |
322 | allowNull: false | 215 | allowNull: false |
323 | }, | 216 | }, |
324 | onDelete: 'cascade' | 217 | onDelete: 'cascade' |
325 | }) | 218 | }) |
219 | VideoAbuses: VideoAbuseModel[] | ||
326 | 220 | ||
327 | Video.hasMany(models.VideoFile, { | 221 | @HasMany(() => VideoFileModel, { |
328 | foreignKey: { | 222 | foreignKey: { |
329 | name: 'videoId', | 223 | name: 'videoId', |
330 | allowNull: false | 224 | allowNull: false |
331 | }, | 225 | }, |
332 | onDelete: 'cascade' | 226 | onDelete: 'cascade' |
333 | }) | 227 | }) |
228 | VideoFiles: VideoFileModel[] | ||
334 | 229 | ||
335 | Video.hasMany(models.VideoShare, { | 230 | @HasMany(() => VideoShareModel, { |
336 | foreignKey: { | 231 | foreignKey: { |
337 | name: 'videoId', | 232 | name: 'videoId', |
338 | allowNull: false | 233 | allowNull: false |
339 | }, | 234 | }, |
340 | onDelete: 'cascade' | 235 | onDelete: 'cascade' |
341 | }) | 236 | }) |
237 | VideoShares: VideoShareModel[] | ||
342 | 238 | ||
343 | Video.hasMany(models.AccountVideoRate, { | 239 | @HasMany(() => AccountVideoRateModel, { |
344 | foreignKey: { | 240 | foreignKey: { |
345 | name: 'videoId', | 241 | name: 'videoId', |
346 | allowNull: false | 242 | allowNull: false |
347 | }, | 243 | }, |
348 | onDelete: 'cascade' | 244 | onDelete: 'cascade' |
349 | }) | 245 | }) |
350 | } | 246 | AccountVideoRates: AccountVideoRateModel[] |
351 | |||
352 | function afterDestroy (video: VideoInstance) { | ||
353 | const tasks = [] | ||
354 | 247 | ||
355 | tasks.push( | 248 | @AfterDestroy |
356 | video.removeThumbnail() | 249 | static removeFilesAndSendDelete (instance: VideoModel) { |
357 | ) | 250 | const tasks = [] |
358 | 251 | ||
359 | if (video.isOwned()) { | ||
360 | tasks.push( | 252 | tasks.push( |
361 | video.removePreview(), | 253 | instance.removeThumbnail() |
362 | sendDeleteVideo(video, undefined) | ||
363 | ) | 254 | ) |
364 | 255 | ||
365 | // Remove physical files and torrents | 256 | if (instance.isOwned()) { |
366 | video.VideoFiles.forEach(file => { | 257 | tasks.push( |
367 | tasks.push(video.removeFile(file)) | 258 | instance.removePreview(), |
368 | tasks.push(video.removeTorrent(file)) | 259 | sendDeleteVideo(instance, undefined) |
369 | }) | 260 | ) |
370 | } | ||
371 | |||
372 | return Promise.all(tasks) | ||
373 | .catch(err => { | ||
374 | logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err) | ||
375 | }) | ||
376 | } | ||
377 | |||
378 | getOriginalFile = function (this: VideoInstance) { | ||
379 | if (Array.isArray(this.VideoFiles) === false) return undefined | ||
380 | 261 | ||
381 | // The original file is the file that have the higher resolution | 262 | // Remove physical files and torrents |
382 | return maxBy(this.VideoFiles, file => file.resolution) | 263 | instance.VideoFiles.forEach(file => { |
383 | } | 264 | tasks.push(instance.removeFile(file)) |
265 | tasks.push(instance.removeTorrent(file)) | ||
266 | }) | ||
267 | } | ||
384 | 268 | ||
385 | getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { | 269 | return Promise.all(tasks) |
386 | return this.uuid + '-' + videoFile.resolution + videoFile.extname | 270 | .catch(err => { |
387 | } | 271 | logger.error('Some errors when removing files of video %s in after destroy hook.', instance.uuid, err) |
272 | }) | ||
273 | } | ||
388 | 274 | ||
389 | getThumbnailName = function (this: VideoInstance) { | 275 | static list () { |
390 | // We always have a copy of the thumbnail | 276 | const query = { |
391 | const extension = '.jpg' | 277 | include: [ VideoFileModel ] |
392 | return this.uuid + extension | 278 | } |
393 | } | ||
394 | 279 | ||
395 | getPreviewName = function (this: VideoInstance) { | 280 | return VideoModel.findAll(query) |
396 | const extension = '.jpg' | 281 | } |
397 | return this.uuid + extension | ||
398 | } | ||
399 | 282 | ||
400 | getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { | 283 | static listAllAndSharedByAccountForOutbox (accountId: number, start: number, count: number) { |
401 | const extension = '.torrent' | 284 | function getRawQuery (select: string) { |
402 | return this.uuid + '-' + videoFile.resolution + extension | 285 | const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + |
403 | } | 286 | 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + |
287 | 'WHERE "VideoChannel"."accountId" = ' + accountId | ||
288 | const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' + | ||
289 | 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' + | ||
290 | 'WHERE "VideoShare"."accountId" = ' + accountId | ||
404 | 291 | ||
405 | isOwned = function (this: VideoInstance) { | 292 | return `(${queryVideo}) UNION (${queryVideoShare})` |
406 | return this.remote === false | 293 | } |
407 | } | ||
408 | 294 | ||
409 | createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { | 295 | const rawQuery = getRawQuery('"Video"."id"') |
410 | const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height | 296 | const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"') |
297 | |||
298 | const query = { | ||
299 | distinct: true, | ||
300 | offset: start, | ||
301 | limit: count, | ||
302 | order: [ getSort('createdAt'), [ 'Tags', 'name', 'ASC' ] ], | ||
303 | where: { | ||
304 | id: { | ||
305 | [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') | ||
306 | } | ||
307 | }, | ||
308 | include: [ | ||
309 | { | ||
310 | model: VideoShareModel, | ||
311 | required: false, | ||
312 | where: { | ||
313 | [Sequelize.Op.and]: [ | ||
314 | { | ||
315 | id: { | ||
316 | [Sequelize.Op.not]: null | ||
317 | } | ||
318 | }, | ||
319 | { | ||
320 | accountId | ||
321 | } | ||
322 | ] | ||
323 | }, | ||
324 | include: [ AccountModel ] | ||
325 | }, | ||
326 | { | ||
327 | model: VideoChannelModel, | ||
328 | required: true, | ||
329 | include: [ | ||
330 | { | ||
331 | model: AccountModel, | ||
332 | required: true | ||
333 | } | ||
334 | ] | ||
335 | }, | ||
336 | { | ||
337 | model: AccountVideoRateModel, | ||
338 | include: [ AccountModel ] | ||
339 | }, | ||
340 | VideoFileModel, | ||
341 | TagModel | ||
342 | ] | ||
343 | } | ||
411 | 344 | ||
412 | return generateImageFromVideoFile( | 345 | return Bluebird.all([ |
413 | this.getVideoFilePath(videoFile), | 346 | // FIXME: typing issue |
414 | CONFIG.STORAGE.PREVIEWS_DIR, | 347 | VideoModel.findAll(query as any), |
415 | this.getPreviewName(), | 348 | VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) |
416 | imageSize | 349 | ]).then(([ rows, totals ]) => { |
417 | ) | 350 | // totals: totalVideos + totalVideoShares |
418 | } | 351 | let totalVideos = 0 |
352 | let totalVideoShares = 0 | ||
353 | if (totals[0]) totalVideos = parseInt(totals[0].total, 10) | ||
354 | if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10) | ||
355 | |||
356 | const total = totalVideos + totalVideoShares | ||
357 | return { | ||
358 | data: rows, | ||
359 | total: total | ||
360 | } | ||
361 | }) | ||
362 | } | ||
419 | 363 | ||
420 | createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { | 364 | static listUserVideosForApi (userId: number, start: number, count: number, sort: string) { |
421 | const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height | 365 | const query = { |
366 | distinct: true, | ||
367 | offset: start, | ||
368 | limit: count, | ||
369 | order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ], | ||
370 | include: [ | ||
371 | { | ||
372 | model: VideoChannelModel, | ||
373 | required: true, | ||
374 | include: [ | ||
375 | { | ||
376 | model: AccountModel, | ||
377 | where: { | ||
378 | userId | ||
379 | }, | ||
380 | required: true | ||
381 | } | ||
382 | ] | ||
383 | }, | ||
384 | TagModel | ||
385 | ] | ||
386 | } | ||
422 | 387 | ||
423 | return generateImageFromVideoFile( | 388 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { |
424 | this.getVideoFilePath(videoFile), | 389 | return { |
425 | CONFIG.STORAGE.THUMBNAILS_DIR, | 390 | data: rows, |
426 | this.getThumbnailName(), | 391 | total: count |
427 | imageSize | 392 | } |
428 | ) | 393 | }) |
429 | } | 394 | } |
430 | 395 | ||
431 | getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { | 396 | static listForApi (start: number, count: number, sort: string) { |
432 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 397 | const query = { |
433 | } | 398 | distinct: true, |
399 | offset: start, | ||
400 | limit: count, | ||
401 | order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ], | ||
402 | include: [ | ||
403 | { | ||
404 | model: VideoChannelModel, | ||
405 | required: true, | ||
406 | include: [ | ||
407 | { | ||
408 | model: AccountModel, | ||
409 | required: true, | ||
410 | include: [ | ||
411 | { | ||
412 | model: ServerModel, | ||
413 | required: false | ||
414 | } | ||
415 | ] | ||
416 | } | ||
417 | ] | ||
418 | }, | ||
419 | TagModel | ||
420 | ], | ||
421 | where: this.createBaseVideosWhere() | ||
422 | } | ||
434 | 423 | ||
435 | createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) { | 424 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { |
436 | const options = { | 425 | return { |
437 | announceList: [ | 426 | data: rows, |
438 | [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] | 427 | total: count |
439 | ], | 428 | } |
440 | urlList: [ | 429 | }) |
441 | CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | ||
442 | ] | ||
443 | } | 430 | } |
444 | 431 | ||
445 | const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) | 432 | static load (id: number) { |
433 | return VideoModel.findById(id) | ||
434 | } | ||
446 | 435 | ||
447 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | 436 | static loadByUUID (uuid: string, t?: Sequelize.Transaction) { |
448 | logger.info('Creating torrent %s.', filePath) | 437 | const query: IFindOptions<VideoModel> = { |
438 | where: { | ||
439 | uuid | ||
440 | }, | ||
441 | include: [ VideoFileModel ] | ||
442 | } | ||
449 | 443 | ||
450 | await writeFilePromise(filePath, torrent) | 444 | if (t !== undefined) query.transaction = t |
451 | 445 | ||
452 | const parsedTorrent = parseTorrent(torrent) | 446 | return VideoModel.findOne(query) |
453 | videoFile.infoHash = parsedTorrent.infoHash | 447 | } |
454 | } | ||
455 | 448 | ||
456 | getEmbedPath = function (this: VideoInstance) { | 449 | static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { |
457 | return '/videos/embed/' + this.uuid | 450 | const query: IFindOptions<VideoModel> = { |
458 | } | 451 | where: { |
452 | url | ||
453 | }, | ||
454 | include: [ | ||
455 | VideoFileModel, | ||
456 | { | ||
457 | model: VideoChannelModel, | ||
458 | include: [ AccountModel ] | ||
459 | } | ||
460 | ] | ||
461 | } | ||
459 | 462 | ||
460 | getThumbnailPath = function (this: VideoInstance) { | 463 | if (t !== undefined) query.transaction = t |
461 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | ||
462 | } | ||
463 | 464 | ||
464 | getPreviewPath = function (this: VideoInstance) { | 465 | return VideoModel.findOne(query) |
465 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | 466 | } |
466 | } | ||
467 | 467 | ||
468 | toFormattedJSON = function (this: VideoInstance) { | 468 | static loadByUUIDOrURL (uuid: string, url: string, t?: Sequelize.Transaction) { |
469 | let serverHost | 469 | const query: IFindOptions<VideoModel> = { |
470 | where: { | ||
471 | [Sequelize.Op.or]: [ | ||
472 | { uuid }, | ||
473 | { url } | ||
474 | ] | ||
475 | }, | ||
476 | include: [ VideoFileModel ] | ||
477 | } | ||
470 | 478 | ||
471 | if (this.VideoChannel.Account.Server) { | 479 | if (t !== undefined) query.transaction = t |
472 | serverHost = this.VideoChannel.Account.Server.host | ||
473 | } else { | ||
474 | // It means it's our video | ||
475 | serverHost = CONFIG.WEBSERVER.HOST | ||
476 | } | ||
477 | 480 | ||
478 | const json = { | 481 | return VideoModel.findOne(query) |
479 | id: this.id, | ||
480 | uuid: this.uuid, | ||
481 | name: this.name, | ||
482 | category: this.category, | ||
483 | categoryLabel: this.getCategoryLabel(), | ||
484 | licence: this.licence, | ||
485 | licenceLabel: this.getLicenceLabel(), | ||
486 | language: this.language, | ||
487 | languageLabel: this.getLanguageLabel(), | ||
488 | nsfw: this.nsfw, | ||
489 | description: this.getTruncatedDescription(), | ||
490 | serverHost, | ||
491 | isLocal: this.isOwned(), | ||
492 | accountName: this.VideoChannel.Account.name, | ||
493 | duration: this.duration, | ||
494 | views: this.views, | ||
495 | likes: this.likes, | ||
496 | dislikes: this.dislikes, | ||
497 | tags: map<TagInstance, string>(this.Tags, 'name'), | ||
498 | thumbnailPath: this.getThumbnailPath(), | ||
499 | previewPath: this.getPreviewPath(), | ||
500 | embedPath: this.getEmbedPath(), | ||
501 | createdAt: this.createdAt, | ||
502 | updatedAt: this.updatedAt | ||
503 | } | 482 | } |
504 | 483 | ||
505 | return json | 484 | static loadAndPopulateAccountAndServerAndTags (id: number) { |
506 | } | 485 | const options = { |
486 | order: [ [ 'Tags', 'name', 'ASC' ] ], | ||
487 | include: [ | ||
488 | { | ||
489 | model: VideoChannelModel, | ||
490 | include: [ | ||
491 | { | ||
492 | model: AccountModel, | ||
493 | include: [ { model: ServerModel, required: false } ] | ||
494 | } | ||
495 | ] | ||
496 | }, | ||
497 | { | ||
498 | model: AccountVideoRateModel, | ||
499 | include: [ AccountModel ] | ||
500 | }, | ||
501 | { | ||
502 | model: VideoShareModel, | ||
503 | include: [ AccountModel ] | ||
504 | }, | ||
505 | TagModel, | ||
506 | VideoFileModel | ||
507 | ] | ||
508 | } | ||
507 | 509 | ||
508 | toFormattedDetailsJSON = function (this: VideoInstance) { | 510 | return VideoModel.findById(id, options) |
509 | const formattedJson = this.toFormattedJSON() | 511 | } |
510 | 512 | ||
511 | // Maybe our server is not up to date and there are new privacy settings since our version | 513 | static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) { |
512 | let privacyLabel = VIDEO_PRIVACIES[this.privacy] | 514 | const options = { |
513 | if (!privacyLabel) privacyLabel = 'Unknown' | 515 | order: [ [ 'Tags', 'name', 'ASC' ] ], |
516 | where: { | ||
517 | uuid | ||
518 | }, | ||
519 | include: [ | ||
520 | { | ||
521 | model: VideoChannelModel, | ||
522 | include: [ | ||
523 | { | ||
524 | model: AccountModel, | ||
525 | include: [ { model: ServerModel, required: false } ] | ||
526 | } | ||
527 | ] | ||
528 | }, | ||
529 | { | ||
530 | model: AccountVideoRateModel, | ||
531 | include: [ AccountModel ] | ||
532 | }, | ||
533 | { | ||
534 | model: VideoShareModel, | ||
535 | include: [ AccountModel ] | ||
536 | }, | ||
537 | TagModel, | ||
538 | VideoFileModel | ||
539 | ] | ||
540 | } | ||
514 | 541 | ||
515 | const detailsJson = { | 542 | return VideoModel.findOne(options) |
516 | privacyLabel, | ||
517 | privacy: this.privacy, | ||
518 | descriptionPath: this.getDescriptionPath(), | ||
519 | channel: this.VideoChannel.toFormattedJSON(), | ||
520 | account: this.VideoChannel.Account.toFormattedJSON(), | ||
521 | files: [] | ||
522 | } | 543 | } |
523 | 544 | ||
524 | // Format and sort video files | 545 | static searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) { |
525 | const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) | 546 | const serverInclude: IIncludeOptions = { |
526 | detailsJson.files = this.VideoFiles | 547 | model: ServerModel, |
527 | .map(videoFile => { | 548 | required: false |
528 | let resolutionLabel = videoFile.resolution + 'p' | 549 | } |
529 | |||
530 | const videoFileJson = { | ||
531 | resolution: videoFile.resolution, | ||
532 | resolutionLabel, | ||
533 | magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs), | ||
534 | size: videoFile.size, | ||
535 | torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp), | ||
536 | fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp) | ||
537 | } | ||
538 | |||
539 | return videoFileJson | ||
540 | }) | ||
541 | .sort((a, b) => { | ||
542 | if (a.resolution < b.resolution) return 1 | ||
543 | if (a.resolution === b.resolution) return 0 | ||
544 | return -1 | ||
545 | }) | ||
546 | |||
547 | return Object.assign(formattedJson, detailsJson) | ||
548 | } | ||
549 | |||
550 | toActivityPubObject = function (this: VideoInstance) { | ||
551 | const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) | ||
552 | if (!this.Tags) this.Tags = [] | ||
553 | 550 | ||
554 | const tag = this.Tags.map(t => ({ | 551 | const accountInclude: IIncludeOptions = { |
555 | type: 'Hashtag' as 'Hashtag', | 552 | model: AccountModel, |
556 | name: t.name | 553 | include: [ serverInclude ] |
557 | })) | 554 | } |
558 | 555 | ||
559 | let language | 556 | const videoChannelInclude: IIncludeOptions = { |
560 | if (this.language) { | 557 | model: VideoChannelModel, |
561 | language = { | 558 | include: [ accountInclude ], |
562 | identifier: this.language + '', | 559 | required: true |
563 | name: this.getLanguageLabel() | ||
564 | } | 560 | } |
565 | } | ||
566 | 561 | ||
567 | let category | 562 | const tagInclude: IIncludeOptions = { |
568 | if (this.category) { | 563 | model: TagModel |
569 | category = { | ||
570 | identifier: this.category + '', | ||
571 | name: this.getCategoryLabel() | ||
572 | } | 564 | } |
573 | } | ||
574 | 565 | ||
575 | let licence | 566 | const query: IFindOptions<VideoModel> = { |
576 | if (this.licence) { | 567 | distinct: true, |
577 | licence = { | 568 | where: this.createBaseVideosWhere(), |
578 | identifier: this.licence + '', | 569 | offset: start, |
579 | name: this.getLicenceLabel() | 570 | limit: count, |
571 | order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ] | ||
580 | } | 572 | } |
581 | } | ||
582 | 573 | ||
583 | let likesObject | 574 | // TODO: search on tags too |
584 | let dislikesObject | 575 | // const escapedValue = Video['sequelize'].escape('%' + value + '%') |
576 | // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( | ||
577 | // `(SELECT "VideoTags"."videoId" | ||
578 | // FROM "Tags" | ||
579 | // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" | ||
580 | // WHERE name ILIKE ${escapedValue} | ||
581 | // )` | ||
582 | // ) | ||
583 | |||
584 | // TODO: search on account too | ||
585 | // accountInclude.where = { | ||
586 | // name: { | ||
587 | // [Sequelize.Op.iLike]: '%' + value + '%' | ||
588 | // } | ||
589 | // } | ||
590 | query.where['name'] = { | ||
591 | [Sequelize.Op.iLike]: '%' + value + '%' | ||
592 | } | ||
585 | 593 | ||
586 | if (Array.isArray(this.AccountVideoRates)) { | 594 | query.include = [ |
587 | const likes: string[] = [] | 595 | videoChannelInclude, tagInclude |
588 | const dislikes: string[] = [] | 596 | ] |
589 | 597 | ||
590 | for (const rate of this.AccountVideoRates) { | 598 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { |
591 | if (rate.type === 'like') { | 599 | return { |
592 | likes.push(rate.Account.url) | 600 | data: rows, |
593 | } else if (rate.type === 'dislike') { | 601 | total: count |
594 | dislikes.push(rate.Account.url) | ||
595 | } | 602 | } |
596 | } | 603 | }) |
597 | |||
598 | likesObject = activityPubCollection(likes) | ||
599 | dislikesObject = activityPubCollection(dislikes) | ||
600 | } | 604 | } |
601 | 605 | ||
602 | let sharesObject | 606 | private static createBaseVideosWhere () { |
603 | if (Array.isArray(this.VideoShares)) { | 607 | return { |
604 | const shares: string[] = [] | 608 | id: { |
605 | 609 | [Sequelize.Op.notIn]: VideoModel.sequelize.literal( | |
606 | for (const videoShare of this.VideoShares) { | 610 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' |
607 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) | 611 | ) |
608 | shares.push(shareUrl) | 612 | }, |
613 | privacy: VideoPrivacy.PUBLIC | ||
609 | } | 614 | } |
610 | |||
611 | sharesObject = activityPubCollection(shares) | ||
612 | } | 615 | } |
613 | 616 | ||
614 | const url = [] | 617 | getOriginalFile () { |
615 | for (const file of this.VideoFiles) { | 618 | if (Array.isArray(this.VideoFiles) === false) return undefined |
616 | url.push({ | ||
617 | type: 'Link', | ||
618 | mimeType: 'video/' + file.extname.replace('.', ''), | ||
619 | url: getVideoFileUrl(this, file, baseUrlHttp), | ||
620 | width: file.resolution, | ||
621 | size: file.size | ||
622 | }) | ||
623 | 619 | ||
624 | url.push({ | 620 | // The original file is the file that have the higher resolution |
625 | type: 'Link', | 621 | return maxBy(this.VideoFiles, file => file.resolution) |
626 | mimeType: 'application/x-bittorrent', | ||
627 | url: getTorrentUrl(this, file, baseUrlHttp), | ||
628 | width: file.resolution | ||
629 | }) | ||
630 | |||
631 | url.push({ | ||
632 | type: 'Link', | ||
633 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet', | ||
634 | url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs), | ||
635 | width: file.resolution | ||
636 | }) | ||
637 | } | 622 | } |
638 | 623 | ||
639 | // Add video url too | 624 | getVideoFilename (videoFile: VideoFileModel) { |
640 | url.push({ | 625 | return this.uuid + '-' + videoFile.resolution + videoFile.extname |
641 | type: 'Link', | 626 | } |
642 | mimeType: 'text/html', | ||
643 | url: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid | ||
644 | }) | ||
645 | 627 | ||
646 | const videoObject: VideoTorrentObject = { | 628 | getThumbnailName () { |
647 | type: 'Video' as 'Video', | 629 | // We always have a copy of the thumbnail |
648 | id: this.url, | 630 | const extension = '.jpg' |
649 | name: this.name, | 631 | return this.uuid + extension |
650 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
651 | duration: 'PT' + this.duration + 'S', | ||
652 | uuid: this.uuid, | ||
653 | tag, | ||
654 | category, | ||
655 | licence, | ||
656 | language, | ||
657 | views: this.views, | ||
658 | nsfw: this.nsfw, | ||
659 | published: this.createdAt.toISOString(), | ||
660 | updated: this.updatedAt.toISOString(), | ||
661 | mediaType: 'text/markdown', | ||
662 | content: this.getTruncatedDescription(), | ||
663 | icon: { | ||
664 | type: 'Image', | ||
665 | url: getThumbnailUrl(this, baseUrlHttp), | ||
666 | mediaType: 'image/jpeg', | ||
667 | width: THUMBNAILS_SIZE.width, | ||
668 | height: THUMBNAILS_SIZE.height | ||
669 | }, | ||
670 | url, | ||
671 | likes: likesObject, | ||
672 | dislikes: dislikesObject, | ||
673 | shares: sharesObject | ||
674 | } | 632 | } |
675 | 633 | ||
676 | return videoObject | 634 | getPreviewName () { |
677 | } | 635 | const extension = '.jpg' |
636 | return this.uuid + extension | ||
637 | } | ||
678 | 638 | ||
679 | getTruncatedDescription = function (this: VideoInstance) { | 639 | getTorrentFileName (videoFile: VideoFileModel) { |
680 | if (!this.description) return null | 640 | const extension = '.torrent' |
641 | return this.uuid + '-' + videoFile.resolution + extension | ||
642 | } | ||
681 | 643 | ||
682 | const options = { | 644 | isOwned () { |
683 | length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max | 645 | return this.remote === false |
684 | } | 646 | } |
685 | 647 | ||
686 | return truncate(this.description, options) | 648 | createPreview (videoFile: VideoFileModel) { |
687 | } | 649 | const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height |
650 | |||
651 | return generateImageFromVideoFile( | ||
652 | this.getVideoFilePath(videoFile), | ||
653 | CONFIG.STORAGE.PREVIEWS_DIR, | ||
654 | this.getPreviewName(), | ||
655 | imageSize | ||
656 | ) | ||
657 | } | ||
688 | 658 | ||
689 | optimizeOriginalVideofile = async function (this: VideoInstance) { | 659 | createThumbnail (videoFile: VideoFileModel) { |
690 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 660 | const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height |
691 | const newExtname = '.mp4' | ||
692 | const inputVideoFile = this.getOriginalFile() | ||
693 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) | ||
694 | const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | ||
695 | 661 | ||
696 | const transcodeOptions = { | 662 | return generateImageFromVideoFile( |
697 | inputPath: videoInputPath, | 663 | this.getVideoFilePath(videoFile), |
698 | outputPath: videoOutputPath | 664 | CONFIG.STORAGE.THUMBNAILS_DIR, |
665 | this.getThumbnailName(), | ||
666 | imageSize | ||
667 | ) | ||
699 | } | 668 | } |
700 | 669 | ||
701 | try { | 670 | getVideoFilePath (videoFile: VideoFileModel) { |
702 | // Could be very long! | 671 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) |
703 | await transcode(transcodeOptions) | 672 | } |
704 | 673 | ||
705 | await unlinkPromise(videoInputPath) | 674 | createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) { |
675 | const options = { | ||
676 | announceList: [ | ||
677 | [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] | ||
678 | ], | ||
679 | urlList: [ | ||
680 | CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | ||
681 | ] | ||
682 | } | ||
706 | 683 | ||
707 | // Important to do this before getVideoFilename() to take in account the new file extension | 684 | const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) |
708 | inputVideoFile.set('extname', newExtname) | ||
709 | 685 | ||
710 | await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) | 686 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) |
711 | const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) | 687 | logger.info('Creating torrent %s.', filePath) |
712 | 688 | ||
713 | inputVideoFile.set('size', stats.size) | 689 | await writeFilePromise(filePath, torrent) |
714 | 690 | ||
715 | await this.createTorrentAndSetInfoHash(inputVideoFile) | 691 | const parsedTorrent = parseTorrent(torrent) |
716 | await inputVideoFile.save() | 692 | videoFile.infoHash = parsedTorrent.infoHash |
693 | } | ||
717 | 694 | ||
718 | } catch (err) { | 695 | getEmbedPath () { |
719 | // Auto destruction... | 696 | return '/videos/embed/' + this.uuid |
720 | this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) | 697 | } |
721 | 698 | ||
722 | throw err | 699 | getThumbnailPath () { |
700 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | ||
723 | } | 701 | } |
724 | } | ||
725 | 702 | ||
726 | transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) { | 703 | getPreviewPath () { |
727 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 704 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) |
728 | const extname = '.mp4' | 705 | } |
729 | 706 | ||
730 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | 707 | toFormattedJSON () { |
731 | const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) | 708 | let serverHost |
732 | 709 | ||
733 | const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({ | 710 | if (this.VideoChannel.Account.Server) { |
734 | resolution, | 711 | serverHost = this.VideoChannel.Account.Server.host |
735 | extname, | 712 | } else { |
736 | size: 0, | 713 | // It means it's our video |
737 | videoId: this.id | 714 | serverHost = CONFIG.WEBSERVER.HOST |
738 | }) | 715 | } |
739 | const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) | ||
740 | 716 | ||
741 | const transcodeOptions = { | 717 | return { |
742 | inputPath: videoInputPath, | 718 | id: this.id, |
743 | outputPath: videoOutputPath, | 719 | uuid: this.uuid, |
744 | resolution | 720 | name: this.name, |
721 | category: this.category, | ||
722 | categoryLabel: this.getCategoryLabel(), | ||
723 | licence: this.licence, | ||
724 | licenceLabel: this.getLicenceLabel(), | ||
725 | language: this.language, | ||
726 | languageLabel: this.getLanguageLabel(), | ||
727 | nsfw: this.nsfw, | ||
728 | description: this.getTruncatedDescription(), | ||
729 | serverHost, | ||
730 | isLocal: this.isOwned(), | ||
731 | accountName: this.VideoChannel.Account.name, | ||
732 | duration: this.duration, | ||
733 | views: this.views, | ||
734 | likes: this.likes, | ||
735 | dislikes: this.dislikes, | ||
736 | tags: map<TagModel, string>(this.Tags, 'name'), | ||
737 | thumbnailPath: this.getThumbnailPath(), | ||
738 | previewPath: this.getPreviewPath(), | ||
739 | embedPath: this.getEmbedPath(), | ||
740 | createdAt: this.createdAt, | ||
741 | updatedAt: this.updatedAt | ||
742 | } | ||
745 | } | 743 | } |
746 | 744 | ||
747 | await transcode(transcodeOptions) | 745 | toFormattedDetailsJSON () { |
746 | const formattedJson = this.toFormattedJSON() | ||
748 | 747 | ||
749 | const stats = await statPromise(videoOutputPath) | 748 | // Maybe our server is not up to date and there are new privacy settings since our version |
749 | let privacyLabel = VIDEO_PRIVACIES[this.privacy] | ||
750 | if (!privacyLabel) privacyLabel = 'Unknown' | ||
750 | 751 | ||
751 | newVideoFile.set('size', stats.size) | 752 | const detailsJson = { |
753 | privacyLabel, | ||
754 | privacy: this.privacy, | ||
755 | descriptionPath: this.getDescriptionPath(), | ||
756 | channel: this.VideoChannel.toFormattedJSON(), | ||
757 | account: this.VideoChannel.Account.toFormattedJSON(), | ||
758 | files: [] | ||
759 | } | ||
752 | 760 | ||
753 | await this.createTorrentAndSetInfoHash(newVideoFile) | 761 | // Format and sort video files |
762 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | ||
763 | detailsJson.files = this.VideoFiles | ||
764 | .map(videoFile => { | ||
765 | let resolutionLabel = videoFile.resolution + 'p' | ||
766 | |||
767 | return { | ||
768 | resolution: videoFile.resolution, | ||
769 | resolutionLabel, | ||
770 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
771 | size: videoFile.size, | ||
772 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), | ||
773 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp) | ||
774 | } | ||
775 | }) | ||
776 | .sort((a, b) => { | ||
777 | if (a.resolution < b.resolution) return 1 | ||
778 | if (a.resolution === b.resolution) return 0 | ||
779 | return -1 | ||
780 | }) | ||
781 | |||
782 | return Object.assign(formattedJson, detailsJson) | ||
783 | } | ||
754 | 784 | ||
755 | await newVideoFile.save() | 785 | toActivityPubObject (): VideoTorrentObject { |
786 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | ||
787 | if (!this.Tags) this.Tags = [] | ||
756 | 788 | ||
757 | this.VideoFiles.push(newVideoFile) | 789 | const tag = this.Tags.map(t => ({ |
758 | } | 790 | type: 'Hashtag' as 'Hashtag', |
791 | name: t.name | ||
792 | })) | ||
759 | 793 | ||
760 | getOriginalFileHeight = function (this: VideoInstance) { | 794 | let language |
761 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | 795 | if (this.language) { |
796 | language = { | ||
797 | identifier: this.language + '', | ||
798 | name: this.getLanguageLabel() | ||
799 | } | ||
800 | } | ||
762 | 801 | ||
763 | return getVideoFileHeight(originalFilePath) | 802 | let category |
764 | } | 803 | if (this.category) { |
804 | category = { | ||
805 | identifier: this.category + '', | ||
806 | name: this.getCategoryLabel() | ||
807 | } | ||
808 | } | ||
765 | 809 | ||
766 | getDescriptionPath = function (this: VideoInstance) { | 810 | let licence |
767 | return `/api/${API_VERSION}/videos/${this.uuid}/description` | 811 | if (this.licence) { |
768 | } | 812 | licence = { |
813 | identifier: this.licence + '', | ||
814 | name: this.getLicenceLabel() | ||
815 | } | ||
816 | } | ||
769 | 817 | ||
770 | getCategoryLabel = function (this: VideoInstance) { | 818 | let likesObject |
771 | let categoryLabel = VIDEO_CATEGORIES[this.category] | 819 | let dislikesObject |
772 | if (!categoryLabel) categoryLabel = 'Misc' | ||
773 | 820 | ||
774 | return categoryLabel | 821 | if (Array.isArray(this.AccountVideoRates)) { |
775 | } | 822 | const likes: string[] = [] |
823 | const dislikes: string[] = [] | ||
776 | 824 | ||
777 | getLicenceLabel = function (this: VideoInstance) { | 825 | for (const rate of this.AccountVideoRates) { |
778 | let licenceLabel = VIDEO_LICENCES[this.licence] | 826 | if (rate.type === 'like') { |
779 | if (!licenceLabel) licenceLabel = 'Unknown' | 827 | likes.push(rate.Account.url) |
828 | } else if (rate.type === 'dislike') { | ||
829 | dislikes.push(rate.Account.url) | ||
830 | } | ||
831 | } | ||
780 | 832 | ||
781 | return licenceLabel | 833 | likesObject = activityPubCollection(likes) |
782 | } | 834 | dislikesObject = activityPubCollection(dislikes) |
835 | } | ||
783 | 836 | ||
784 | getLanguageLabel = function (this: VideoInstance) { | 837 | let sharesObject |
785 | let languageLabel = VIDEO_LANGUAGES[this.language] | 838 | if (Array.isArray(this.VideoShares)) { |
786 | if (!languageLabel) languageLabel = 'Unknown' | 839 | const shares: string[] = [] |
787 | 840 | ||
788 | return languageLabel | 841 | for (const videoShare of this.VideoShares) { |
789 | } | 842 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) |
843 | shares.push(shareUrl) | ||
844 | } | ||
790 | 845 | ||
791 | removeThumbnail = function (this: VideoInstance) { | 846 | sharesObject = activityPubCollection(shares) |
792 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | 847 | } |
793 | return unlinkPromise(thumbnailPath) | ||
794 | } | ||
795 | 848 | ||
796 | removePreview = function (this: VideoInstance) { | 849 | const url = [] |
797 | // Same name than video thumbnail | 850 | for (const file of this.VideoFiles) { |
798 | return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | 851 | url.push({ |
799 | } | 852 | type: 'Link', |
853 | mimeType: 'video/' + file.extname.replace('.', ''), | ||
854 | url: this.getVideoFileUrl(file, baseUrlHttp), | ||
855 | width: file.resolution, | ||
856 | size: file.size | ||
857 | }) | ||
858 | |||
859 | url.push({ | ||
860 | type: 'Link', | ||
861 | mimeType: 'application/x-bittorrent', | ||
862 | url: this.getTorrentUrl(file, baseUrlHttp), | ||
863 | width: file.resolution | ||
864 | }) | ||
865 | |||
866 | url.push({ | ||
867 | type: 'Link', | ||
868 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet', | ||
869 | url: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | ||
870 | width: file.resolution | ||
871 | }) | ||
872 | } | ||
800 | 873 | ||
801 | removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { | 874 | // Add video url too |
802 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 875 | url.push({ |
803 | return unlinkPromise(filePath) | 876 | type: 'Link', |
804 | } | 877 | mimeType: 'text/html', |
878 | url: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid | ||
879 | }) | ||
805 | 880 | ||
806 | removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { | 881 | return { |
807 | const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | 882 | type: 'Video' as 'Video', |
808 | return unlinkPromise(torrentPath) | 883 | id: this.url, |
809 | } | 884 | name: this.name, |
885 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
886 | duration: 'PT' + this.duration + 'S', | ||
887 | uuid: this.uuid, | ||
888 | tag, | ||
889 | category, | ||
890 | licence, | ||
891 | language, | ||
892 | views: this.views, | ||
893 | nsfw: this.nsfw, | ||
894 | published: this.createdAt.toISOString(), | ||
895 | updated: this.updatedAt.toISOString(), | ||
896 | mediaType: 'text/markdown', | ||
897 | content: this.getTruncatedDescription(), | ||
898 | icon: { | ||
899 | type: 'Image', | ||
900 | url: this.getThumbnailUrl(baseUrlHttp), | ||
901 | mediaType: 'image/jpeg', | ||
902 | width: THUMBNAILS_SIZE.width, | ||
903 | height: THUMBNAILS_SIZE.height | ||
904 | }, | ||
905 | url, | ||
906 | likes: likesObject, | ||
907 | dislikes: dislikesObject, | ||
908 | shares: sharesObject | ||
909 | } | ||
910 | } | ||
911 | |||
912 | getTruncatedDescription () { | ||
913 | if (!this.description) return null | ||
810 | 914 | ||
811 | // ------------------------------ STATICS ------------------------------ | 915 | const options = { |
916 | length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max | ||
917 | } | ||
812 | 918 | ||
813 | list = function () { | 919 | return truncate(this.description, options) |
814 | const query = { | ||
815 | include: [ Video['sequelize'].models.VideoFile ] | ||
816 | } | 920 | } |
817 | 921 | ||
818 | return Video.findAll(query) | 922 | optimizeOriginalVideofile = async function () { |
819 | } | 923 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
924 | const newExtname = '.mp4' | ||
925 | const inputVideoFile = this.getOriginalFile() | ||
926 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) | ||
927 | const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | ||
820 | 928 | ||
821 | listAllAndSharedByAccountForOutbox = function (accountId: number, start: number, count: number) { | 929 | const transcodeOptions = { |
822 | function getRawQuery (select: string) { | 930 | inputPath: videoInputPath, |
823 | const queryVideo = 'SELECT ' + select + ' FROM "Videos" AS "Video" ' + | 931 | outputPath: videoOutputPath |
824 | 'INNER JOIN "VideoChannels" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + | 932 | } |
825 | 'WHERE "VideoChannel"."accountId" = ' + accountId | ||
826 | const queryVideoShare = 'SELECT ' + select + ' FROM "VideoShares" AS "VideoShare" ' + | ||
827 | 'INNER JOIN "Videos" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' + | ||
828 | 'WHERE "VideoShare"."accountId" = ' + accountId | ||
829 | 933 | ||
830 | let rawQuery = `(${queryVideo}) UNION (${queryVideoShare})` | 934 | try { |
935 | // Could be very long! | ||
936 | await transcode(transcodeOptions) | ||
831 | 937 | ||
832 | return rawQuery | 938 | await unlinkPromise(videoInputPath) |
833 | } | ||
834 | 939 | ||
835 | const rawQuery = getRawQuery('"Video"."id"') | 940 | // Important to do this before getVideoFilename() to take in account the new file extension |
836 | const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"') | 941 | inputVideoFile.set('extname', newExtname) |
837 | 942 | ||
838 | const query = { | 943 | await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) |
839 | distinct: true, | 944 | const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) |
840 | offset: start, | ||
841 | limit: count, | ||
842 | order: [ getSort('createdAt'), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], | ||
843 | where: { | ||
844 | id: { | ||
845 | [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') | ||
846 | } | ||
847 | }, | ||
848 | include: [ | ||
849 | { | ||
850 | model: Video['sequelize'].models.VideoShare, | ||
851 | required: false, | ||
852 | where: { | ||
853 | [Sequelize.Op.and]: [ | ||
854 | { | ||
855 | id: { | ||
856 | [Sequelize.Op.not]: null | ||
857 | } | ||
858 | }, | ||
859 | { | ||
860 | accountId | ||
861 | } | ||
862 | ] | ||
863 | }, | ||
864 | include: [ Video['sequelize'].models.Account ] | ||
865 | }, | ||
866 | { | ||
867 | model: Video['sequelize'].models.VideoChannel, | ||
868 | required: true, | ||
869 | include: [ | ||
870 | { | ||
871 | model: Video['sequelize'].models.Account, | ||
872 | required: true | ||
873 | } | ||
874 | ] | ||
875 | }, | ||
876 | { | ||
877 | model: Video['sequelize'].models.AccountVideoRate, | ||
878 | include: [ Video['sequelize'].models.Account ] | ||
879 | }, | ||
880 | Video['sequelize'].models.VideoFile, | ||
881 | Video['sequelize'].models.Tag | ||
882 | ] | ||
883 | } | ||
884 | 945 | ||
885 | return Bluebird.all([ | 946 | inputVideoFile.set('size', stats.size) |
886 | Video.findAll(query), | ||
887 | Video['sequelize'].query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) | ||
888 | ]).then(([ rows, totals ]) => { | ||
889 | // totals: totalVideos + totalVideoShares | ||
890 | let totalVideos = 0 | ||
891 | let totalVideoShares = 0 | ||
892 | if (totals[0]) totalVideos = parseInt(totals[0].total, 10) | ||
893 | if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10) | ||
894 | |||
895 | const total = totalVideos + totalVideoShares | ||
896 | return { | ||
897 | data: rows, | ||
898 | total: total | ||
899 | } | ||
900 | }) | ||
901 | } | ||
902 | 947 | ||
903 | listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) { | 948 | await this.createTorrentAndSetInfoHash(inputVideoFile) |
904 | const query = { | 949 | await inputVideoFile.save() |
905 | distinct: true, | ||
906 | offset: start, | ||
907 | limit: count, | ||
908 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], | ||
909 | include: [ | ||
910 | { | ||
911 | model: Video['sequelize'].models.VideoChannel, | ||
912 | required: true, | ||
913 | include: [ | ||
914 | { | ||
915 | model: Video['sequelize'].models.Account, | ||
916 | where: { | ||
917 | userId | ||
918 | }, | ||
919 | required: true | ||
920 | } | ||
921 | ] | ||
922 | }, | ||
923 | Video['sequelize'].models.Tag | ||
924 | ] | ||
925 | } | ||
926 | 950 | ||
927 | return Video.findAndCountAll(query).then(({ rows, count }) => { | 951 | } catch (err) { |
928 | return { | 952 | // Auto destruction... |
929 | data: rows, | 953 | this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) |
930 | total: count | ||
931 | } | ||
932 | }) | ||
933 | } | ||
934 | 954 | ||
935 | listForApi = function (start: number, count: number, sort: string) { | 955 | throw err |
936 | const query = { | 956 | } |
937 | distinct: true, | ||
938 | offset: start, | ||
939 | limit: count, | ||
940 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], | ||
941 | include: [ | ||
942 | { | ||
943 | model: Video['sequelize'].models.VideoChannel, | ||
944 | required: true, | ||
945 | include: [ | ||
946 | { | ||
947 | model: Video['sequelize'].models.Account, | ||
948 | required: true, | ||
949 | include: [ | ||
950 | { | ||
951 | model: Video['sequelize'].models.Server, | ||
952 | required: false | ||
953 | } | ||
954 | ] | ||
955 | } | ||
956 | ] | ||
957 | }, | ||
958 | Video['sequelize'].models.Tag | ||
959 | ], | ||
960 | where: createBaseVideosWhere() | ||
961 | } | 957 | } |
962 | 958 | ||
963 | return Video.findAndCountAll(query).then(({ rows, count }) => { | 959 | transcodeOriginalVideofile = async function (resolution: VideoResolution) { |
964 | return { | 960 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
965 | data: rows, | 961 | const extname = '.mp4' |
966 | total: count | ||
967 | } | ||
968 | }) | ||
969 | } | ||
970 | 962 | ||
971 | load = function (id: number) { | 963 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed |
972 | return Video.findById(id) | 964 | const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) |
973 | } | ||
974 | 965 | ||
975 | loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { | 966 | const newVideoFile = new VideoFileModel({ |
976 | const query: Sequelize.FindOptions<VideoAttributes> = { | 967 | resolution, |
977 | where: { | 968 | extname, |
978 | uuid | 969 | size: 0, |
979 | }, | 970 | videoId: this.id |
980 | include: [ Video['sequelize'].models.VideoFile ] | 971 | }) |
981 | } | 972 | const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) |
982 | 973 | ||
983 | if (t !== undefined) query.transaction = t | 974 | const transcodeOptions = { |
975 | inputPath: videoInputPath, | ||
976 | outputPath: videoOutputPath, | ||
977 | resolution | ||
978 | } | ||
984 | 979 | ||
985 | return Video.findOne(query) | 980 | await transcode(transcodeOptions) |
986 | } | ||
987 | 981 | ||
988 | loadByUrlAndPopulateAccount = function (url: string, t?: Sequelize.Transaction) { | 982 | const stats = await statPromise(videoOutputPath) |
989 | const query: Sequelize.FindOptions<VideoAttributes> = { | ||
990 | where: { | ||
991 | url | ||
992 | }, | ||
993 | include: [ | ||
994 | Video['sequelize'].models.VideoFile, | ||
995 | { | ||
996 | model: Video['sequelize'].models.VideoChannel, | ||
997 | include: [ Video['sequelize'].models.Account ] | ||
998 | } | ||
999 | ] | ||
1000 | } | ||
1001 | 983 | ||
1002 | if (t !== undefined) query.transaction = t | 984 | newVideoFile.set('size', stats.size) |
1003 | 985 | ||
1004 | return Video.findOne(query) | 986 | await this.createTorrentAndSetInfoHash(newVideoFile) |
1005 | } | ||
1006 | 987 | ||
1007 | loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) { | 988 | await newVideoFile.save() |
1008 | const query: Sequelize.FindOptions<VideoAttributes> = { | 989 | |
1009 | where: { | 990 | this.VideoFiles.push(newVideoFile) |
1010 | [Sequelize.Op.or]: [ | ||
1011 | { uuid }, | ||
1012 | { url } | ||
1013 | ] | ||
1014 | }, | ||
1015 | include: [ Video['sequelize'].models.VideoFile ] | ||
1016 | } | 991 | } |
1017 | 992 | ||
1018 | if (t !== undefined) query.transaction = t | 993 | getOriginalFileHeight () { |
994 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | ||
1019 | 995 | ||
1020 | return Video.findOne(query) | 996 | return getVideoFileHeight(originalFilePath) |
1021 | } | 997 | } |
1022 | 998 | ||
1023 | loadAndPopulateAccountAndServerAndTags = function (id: number) { | 999 | getDescriptionPath () { |
1024 | const options = { | 1000 | return `/api/${API_VERSION}/videos/${this.uuid}/description` |
1025 | order: [ [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], | ||
1026 | include: [ | ||
1027 | { | ||
1028 | model: Video['sequelize'].models.VideoChannel, | ||
1029 | include: [ | ||
1030 | { | ||
1031 | model: Video['sequelize'].models.Account, | ||
1032 | include: [ { model: Video['sequelize'].models.Server, required: false } ] | ||
1033 | } | ||
1034 | ] | ||
1035 | }, | ||
1036 | { | ||
1037 | model: Video['sequelize'].models.AccountVideoRate, | ||
1038 | include: [ Video['sequelize'].models.Account ] | ||
1039 | }, | ||
1040 | { | ||
1041 | model: Video['sequelize'].models.VideoShare, | ||
1042 | include: [ Video['sequelize'].models.Account ] | ||
1043 | }, | ||
1044 | Video['sequelize'].models.Tag, | ||
1045 | Video['sequelize'].models.VideoFile | ||
1046 | ] | ||
1047 | } | 1001 | } |
1048 | 1002 | ||
1049 | return Video.findById(id, options) | 1003 | getCategoryLabel () { |
1050 | } | 1004 | let categoryLabel = VIDEO_CATEGORIES[this.category] |
1005 | if (!categoryLabel) categoryLabel = 'Misc' | ||
1051 | 1006 | ||
1052 | loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) { | 1007 | return categoryLabel |
1053 | const options = { | ||
1054 | order: [ [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], | ||
1055 | where: { | ||
1056 | uuid | ||
1057 | }, | ||
1058 | include: [ | ||
1059 | { | ||
1060 | model: Video['sequelize'].models.VideoChannel, | ||
1061 | include: [ | ||
1062 | { | ||
1063 | model: Video['sequelize'].models.Account, | ||
1064 | include: [ { model: Video['sequelize'].models.Server, required: false } ] | ||
1065 | } | ||
1066 | ] | ||
1067 | }, | ||
1068 | { | ||
1069 | model: Video['sequelize'].models.AccountVideoRate, | ||
1070 | include: [ Video['sequelize'].models.Account ] | ||
1071 | }, | ||
1072 | { | ||
1073 | model: Video['sequelize'].models.VideoShare, | ||
1074 | include: [ Video['sequelize'].models.Account ] | ||
1075 | }, | ||
1076 | Video['sequelize'].models.Tag, | ||
1077 | Video['sequelize'].models.VideoFile | ||
1078 | ] | ||
1079 | } | 1008 | } |
1080 | 1009 | ||
1081 | return Video.findOne(options) | 1010 | getLicenceLabel () { |
1082 | } | 1011 | let licenceLabel = VIDEO_LICENCES[this.licence] |
1012 | if (!licenceLabel) licenceLabel = 'Unknown' | ||
1083 | 1013 | ||
1084 | searchAndPopulateAccountAndServerAndTags = function (value: string, start: number, count: number, sort: string) { | 1014 | return licenceLabel |
1085 | const serverInclude: Sequelize.IncludeOptions = { | ||
1086 | model: Video['sequelize'].models.Server, | ||
1087 | required: false | ||
1088 | } | 1015 | } |
1089 | 1016 | ||
1090 | const accountInclude: Sequelize.IncludeOptions = { | 1017 | getLanguageLabel () { |
1091 | model: Video['sequelize'].models.Account, | 1018 | let languageLabel = VIDEO_LANGUAGES[this.language] |
1092 | include: [ serverInclude ] | 1019 | if (!languageLabel) languageLabel = 'Unknown' |
1020 | |||
1021 | return languageLabel | ||
1093 | } | 1022 | } |
1094 | 1023 | ||
1095 | const videoChannelInclude: Sequelize.IncludeOptions = { | 1024 | removeThumbnail () { |
1096 | model: Video['sequelize'].models.VideoChannel, | 1025 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) |
1097 | include: [ accountInclude ], | 1026 | return unlinkPromise(thumbnailPath) |
1098 | required: true | ||
1099 | } | 1027 | } |
1100 | 1028 | ||
1101 | const tagInclude: Sequelize.IncludeOptions = { | 1029 | removePreview () { |
1102 | model: Video['sequelize'].models.Tag | 1030 | // Same name than video thumbnail |
1031 | return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | ||
1103 | } | 1032 | } |
1104 | 1033 | ||
1105 | const query: Sequelize.FindOptions<VideoAttributes> = { | 1034 | removeFile (videoFile: VideoFileModel) { |
1106 | distinct: true, | 1035 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) |
1107 | where: createBaseVideosWhere(), | 1036 | return unlinkPromise(filePath) |
1108 | offset: start, | ||
1109 | limit: count, | ||
1110 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] | ||
1111 | } | 1037 | } |
1112 | 1038 | ||
1113 | // TODO: search on tags too | 1039 | removeTorrent (videoFile: VideoFileModel) { |
1114 | // const escapedValue = Video['sequelize'].escape('%' + value + '%') | 1040 | const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) |
1115 | // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( | 1041 | return unlinkPromise(torrentPath) |
1116 | // `(SELECT "VideoTags"."videoId" | ||
1117 | // FROM "Tags" | ||
1118 | // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" | ||
1119 | // WHERE name ILIKE ${escapedValue} | ||
1120 | // )` | ||
1121 | // ) | ||
1122 | |||
1123 | // TODO: search on account too | ||
1124 | // accountInclude.where = { | ||
1125 | // name: { | ||
1126 | // [Sequelize.Op.iLike]: '%' + value + '%' | ||
1127 | // } | ||
1128 | // } | ||
1129 | query.where['name'] = { | ||
1130 | [Sequelize.Op.iLike]: '%' + value + '%' | ||
1131 | } | 1042 | } |
1132 | 1043 | ||
1133 | query.include = [ | 1044 | private getBaseUrls () { |
1134 | videoChannelInclude, tagInclude | 1045 | let baseUrlHttp |
1135 | ] | 1046 | let baseUrlWs |
1136 | 1047 | ||
1137 | return Video.findAndCountAll(query).then(({ rows, count }) => { | 1048 | if (this.isOwned()) { |
1138 | return { | 1049 | baseUrlHttp = CONFIG.WEBSERVER.URL |
1139 | data: rows, | 1050 | baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT |
1140 | total: count | 1051 | } else { |
1052 | baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host | ||
1053 | baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host | ||
1141 | } | 1054 | } |
1142 | }) | ||
1143 | } | ||
1144 | |||
1145 | // --------------------------------------------------------------------------- | ||
1146 | 1055 | ||
1147 | function createBaseVideosWhere () { | 1056 | return { baseUrlHttp, baseUrlWs } |
1148 | return { | ||
1149 | id: { | ||
1150 | [Sequelize.Op.notIn]: Video['sequelize'].literal( | ||
1151 | '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' | ||
1152 | ) | ||
1153 | }, | ||
1154 | privacy: VideoPrivacy.PUBLIC | ||
1155 | } | 1057 | } |
1156 | } | ||
1157 | 1058 | ||
1158 | function getBaseUrls (video: VideoInstance) { | 1059 | private getThumbnailUrl (baseUrlHttp: string) { |
1159 | let baseUrlHttp | 1060 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() |
1160 | let baseUrlWs | ||
1161 | |||
1162 | if (video.isOwned()) { | ||
1163 | baseUrlHttp = CONFIG.WEBSERVER.URL | ||
1164 | baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | ||
1165 | } else { | ||
1166 | baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Server.host | ||
1167 | baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Server.host | ||
1168 | } | 1061 | } |
1169 | 1062 | ||
1170 | return { baseUrlHttp, baseUrlWs } | 1063 | private getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1171 | } | 1064 | return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) |
1172 | 1065 | } | |
1173 | function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) { | ||
1174 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName() | ||
1175 | } | ||
1176 | 1066 | ||
1177 | function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { | 1067 | private getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1178 | return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile) | 1068 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1179 | } | 1069 | } |
1180 | 1070 | ||
1181 | function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { | 1071 | private generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { |
1182 | return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile) | 1072 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) |
1183 | } | 1073 | const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] |
1074 | const urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] | ||
1075 | |||
1076 | const magnetHash = { | ||
1077 | xs, | ||
1078 | announce, | ||
1079 | urlList, | ||
1080 | infoHash: videoFile.infoHash, | ||
1081 | name: this.name | ||
1082 | } | ||
1184 | 1083 | ||
1185 | function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) { | 1084 | return magnetUtil.encode(magnetHash) |
1186 | const xs = getTorrentUrl(video, videoFile, baseUrlHttp) | ||
1187 | const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | ||
1188 | const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ] | ||
1189 | |||
1190 | const magnetHash = { | ||
1191 | xs, | ||
1192 | announce, | ||
1193 | urlList, | ||
1194 | infoHash: videoFile.infoHash, | ||
1195 | name: video.name | ||
1196 | } | 1085 | } |
1197 | |||
1198 | return magnetUtil.encode(magnetHash) | ||
1199 | } | 1086 | } |