diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-06-16 09:45:46 +0200 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-06-16 09:45:46 +0200 |
commit | 74889a71fe687dda74f2a687653122327807af36 (patch) | |
tree | e938e8b6401b74fbec80513a877d9967f2c0dbcd /server/models/video | |
parent | 15a302943d84bc0978b84fe33110c4daa451d311 (diff) | |
download | PeerTube-74889a71fe687dda74f2a687653122327807af36.tar.gz PeerTube-74889a71fe687dda74f2a687653122327807af36.tar.zst PeerTube-74889a71fe687dda74f2a687653122327807af36.zip |
Reorganize model files
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/author-interface.ts | 27 | ||||
-rw-r--r-- | server/models/video/author.ts | 103 | ||||
-rw-r--r-- | server/models/video/index.ts | 6 | ||||
-rw-r--r-- | server/models/video/tag-interface.ts | 20 | ||||
-rw-r--r-- | server/models/video/tag.ts | 81 | ||||
-rw-r--r-- | server/models/video/video-abuse-interface.ts | 28 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 131 | ||||
-rw-r--r-- | server/models/video/video-blacklist-interface.ts | 43 | ||||
-rw-r--r-- | server/models/video/video-blacklist.ts | 103 | ||||
-rw-r--r-- | server/models/video/video-interface.ts | 151 | ||||
-rw-r--r-- | server/models/video/video-tag-interface.ts | 18 | ||||
-rw-r--r-- | server/models/video/video-tag.ts | 27 | ||||
-rw-r--r-- | server/models/video/video.ts | 921 |
13 files changed, 1659 insertions, 0 deletions
diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts new file mode 100644 index 000000000..c1b30848c --- /dev/null +++ b/server/models/video/author-interface.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { PodInstance } from '../pod' | ||
4 | |||
5 | export namespace AuthorMethods { | ||
6 | export type FindOrCreateAuthorCallback = (err: Error, authorInstance?: AuthorInstance) => void | ||
7 | export type FindOrCreateAuthor = (name: string, podId: number, userId: number, transaction: Sequelize.Transaction, callback: FindOrCreateAuthorCallback) => void | ||
8 | } | ||
9 | |||
10 | export interface AuthorClass { | ||
11 | findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor | ||
12 | } | ||
13 | |||
14 | export interface AuthorAttributes { | ||
15 | name: string | ||
16 | } | ||
17 | |||
18 | export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> { | ||
19 | id: number | ||
20 | createdAt: Date | ||
21 | updatedAt: Date | ||
22 | |||
23 | podId: number | ||
24 | Pod: PodInstance | ||
25 | } | ||
26 | |||
27 | export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {} | ||
diff --git a/server/models/video/author.ts b/server/models/video/author.ts new file mode 100644 index 000000000..4a115e328 --- /dev/null +++ b/server/models/video/author.ts | |||
@@ -0,0 +1,103 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { isUserUsernameValid } from '../../helpers' | ||
4 | |||
5 | import { addMethodsToModel } from '../utils' | ||
6 | import { | ||
7 | AuthorClass, | ||
8 | AuthorInstance, | ||
9 | AuthorAttributes, | ||
10 | |||
11 | AuthorMethods | ||
12 | } from './author-interface' | ||
13 | |||
14 | let Author: Sequelize.Model<AuthorInstance, AuthorAttributes> | ||
15 | let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor | ||
16 | |||
17 | export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
18 | Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author', | ||
19 | { | ||
20 | name: { | ||
21 | type: DataTypes.STRING, | ||
22 | allowNull: false, | ||
23 | validate: { | ||
24 | usernameValid: function (value) { | ||
25 | const res = isUserUsernameValid(value) | ||
26 | if (res === false) throw new Error('Username is not valid.') | ||
27 | } | ||
28 | } | ||
29 | } | ||
30 | }, | ||
31 | { | ||
32 | indexes: [ | ||
33 | { | ||
34 | fields: [ 'name' ] | ||
35 | }, | ||
36 | { | ||
37 | fields: [ 'podId' ] | ||
38 | }, | ||
39 | { | ||
40 | fields: [ 'userId' ], | ||
41 | unique: true | ||
42 | }, | ||
43 | { | ||
44 | fields: [ 'name', 'podId' ], | ||
45 | unique: true | ||
46 | } | ||
47 | ] | ||
48 | } | ||
49 | ) | ||
50 | |||
51 | const classMethods = [ associate, findOrCreateAuthor ] | ||
52 | addMethodsToModel(Author, classMethods) | ||
53 | |||
54 | return Author | ||
55 | } | ||
56 | |||
57 | // --------------------------------------------------------------------------- | ||
58 | |||
59 | function associate (models) { | ||
60 | Author.belongsTo(models.Pod, { | ||
61 | foreignKey: { | ||
62 | name: 'podId', | ||
63 | allowNull: true | ||
64 | }, | ||
65 | onDelete: 'cascade' | ||
66 | }) | ||
67 | |||
68 | Author.belongsTo(models.User, { | ||
69 | foreignKey: { | ||
70 | name: 'userId', | ||
71 | allowNull: true | ||
72 | }, | ||
73 | onDelete: 'cascade' | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | findOrCreateAuthor = function ( | ||
78 | name: string, | ||
79 | podId: number, | ||
80 | userId: number, | ||
81 | transaction: Sequelize.Transaction, | ||
82 | callback: AuthorMethods.FindOrCreateAuthorCallback | ||
83 | ) { | ||
84 | const author = { | ||
85 | name, | ||
86 | podId, | ||
87 | userId | ||
88 | } | ||
89 | |||
90 | const query: any = { | ||
91 | where: author, | ||
92 | defaults: author | ||
93 | } | ||
94 | |||
95 | if (transaction !== null) query.transaction = transaction | ||
96 | |||
97 | Author.findOrCreate(query).asCallback(function (err, result) { | ||
98 | if (err) return callback(err) | ||
99 | |||
100 | // [ instance, wasCreated ] | ||
101 | return callback(null, result[0]) | ||
102 | }) | ||
103 | } | ||
diff --git a/server/models/video/index.ts b/server/models/video/index.ts new file mode 100644 index 000000000..84b801c72 --- /dev/null +++ b/server/models/video/index.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | export * from './author-interface' | ||
2 | export * from './tag-interface' | ||
3 | export * from './video-abuse-interface' | ||
4 | export * from './video-blacklist-interface' | ||
5 | export * from './video-tag-interface' | ||
6 | export * from './video-interface' | ||
diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts new file mode 100644 index 000000000..e045e7ca5 --- /dev/null +++ b/server/models/video/tag-interface.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace TagMethods { | ||
4 | export type FindOrCreateTagsCallback = (err: Error, tagInstances: TagInstance[]) => void | ||
5 | export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction, callback: FindOrCreateTagsCallback) => void | ||
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 new file mode 100644 index 000000000..3c657d751 --- /dev/null +++ b/server/models/video/tag.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import { each } from 'async' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | import { addMethodsToModel } from '../utils' | ||
5 | import { | ||
6 | TagClass, | ||
7 | TagInstance, | ||
8 | TagAttributes, | ||
9 | |||
10 | TagMethods | ||
11 | } from './tag-interface' | ||
12 | |||
13 | let Tag: Sequelize.Model<TagInstance, TagAttributes> | ||
14 | let findOrCreateTags: TagMethods.FindOrCreateTags | ||
15 | |||
16 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
17 | Tag = sequelize.define<TagInstance, TagAttributes>('Tag', | ||
18 | { | ||
19 | name: { | ||
20 | type: DataTypes.STRING, | ||
21 | allowNull: false | ||
22 | } | ||
23 | }, | ||
24 | { | ||
25 | timestamps: false, | ||
26 | indexes: [ | ||
27 | { | ||
28 | fields: [ 'name' ], | ||
29 | unique: true | ||
30 | } | ||
31 | ] | ||
32 | } | ||
33 | ) | ||
34 | |||
35 | const classMethods = [ | ||
36 | associate, | ||
37 | |||
38 | findOrCreateTags | ||
39 | ] | ||
40 | addMethodsToModel(Tag, classMethods) | ||
41 | |||
42 | return Tag | ||
43 | } | ||
44 | |||
45 | // --------------------------------------------------------------------------- | ||
46 | |||
47 | function associate (models) { | ||
48 | Tag.belongsToMany(models.Video, { | ||
49 | foreignKey: 'tagId', | ||
50 | through: models.VideoTag, | ||
51 | onDelete: 'cascade' | ||
52 | }) | ||
53 | } | ||
54 | |||
55 | findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction, callback: TagMethods.FindOrCreateTagsCallback) { | ||
56 | const tagInstances = [] | ||
57 | |||
58 | each<string, Error>(tags, function (tag, callbackEach) { | ||
59 | const query: any = { | ||
60 | where: { | ||
61 | name: tag | ||
62 | }, | ||
63 | defaults: { | ||
64 | name: tag | ||
65 | } | ||
66 | } | ||
67 | |||
68 | if (transaction) query.transaction = transaction | ||
69 | |||
70 | Tag.findOrCreate(query).asCallback(function (err, res) { | ||
71 | if (err) return callbackEach(err) | ||
72 | |||
73 | // res = [ tag, isCreated ] | ||
74 | const tag = res[0] | ||
75 | tagInstances.push(tag) | ||
76 | return callbackEach() | ||
77 | }) | ||
78 | }, function (err) { | ||
79 | return callback(err, tagInstances) | ||
80 | }) | ||
81 | } | ||
diff --git a/server/models/video/video-abuse-interface.ts b/server/models/video/video-abuse-interface.ts new file mode 100644 index 000000000..4b7f2a2ec --- /dev/null +++ b/server/models/video/video-abuse-interface.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | // Don't use barrel, import just what we need | ||
4 | import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-abuse.model' | ||
5 | |||
6 | export namespace VideoAbuseMethods { | ||
7 | export type toFormatedJSON = () => FormatedVideoAbuse | ||
8 | |||
9 | export type ListForApiCallback = (err: Error, videoAbuseInstances?: VideoAbuseInstance[], total?: number) => void | ||
10 | export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void | ||
11 | } | ||
12 | |||
13 | export interface VideoAbuseClass { | ||
14 | listForApi: VideoAbuseMethods.ListForApi | ||
15 | } | ||
16 | |||
17 | export interface VideoAbuseAttributes { | ||
18 | reporterUsername: string | ||
19 | reason: string | ||
20 | } | ||
21 | |||
22 | export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance<VideoAbuseAttributes> { | ||
23 | id: number | ||
24 | createdAt: Date | ||
25 | updatedAt: Date | ||
26 | } | ||
27 | |||
28 | 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 new file mode 100644 index 000000000..e0e0bcfe6 --- /dev/null +++ b/server/models/video/video-abuse.ts | |||
@@ -0,0 +1,131 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { CONFIG } from '../../initializers' | ||
4 | import { isVideoAbuseReporterUsernameValid, isVideoAbuseReasonValid } from '../../helpers' | ||
5 | |||
6 | import { addMethodsToModel, getSort } from '../utils' | ||
7 | import { | ||
8 | VideoAbuseClass, | ||
9 | VideoAbuseInstance, | ||
10 | VideoAbuseAttributes, | ||
11 | |||
12 | VideoAbuseMethods | ||
13 | } from './video-abuse-interface' | ||
14 | |||
15 | let VideoAbuse: Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes> | ||
16 | let listForApi: VideoAbuseMethods.ListForApi | ||
17 | |||
18 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
19 | VideoAbuse = sequelize.define<VideoAbuseInstance, VideoAbuseAttributes>('VideoAbuse', | ||
20 | { | ||
21 | reporterUsername: { | ||
22 | type: DataTypes.STRING, | ||
23 | allowNull: false, | ||
24 | validate: { | ||
25 | reporterUsernameValid: function (value) { | ||
26 | const res = isVideoAbuseReporterUsernameValid(value) | ||
27 | if (res === false) throw new Error('Video abuse reporter username is not valid.') | ||
28 | } | ||
29 | } | ||
30 | }, | ||
31 | reason: { | ||
32 | type: DataTypes.STRING, | ||
33 | allowNull: false, | ||
34 | validate: { | ||
35 | reasonValid: function (value) { | ||
36 | const res = isVideoAbuseReasonValid(value) | ||
37 | if (res === false) throw new Error('Video abuse reason is not valid.') | ||
38 | } | ||
39 | } | ||
40 | } | ||
41 | }, | ||
42 | { | ||
43 | indexes: [ | ||
44 | { | ||
45 | fields: [ 'videoId' ] | ||
46 | }, | ||
47 | { | ||
48 | fields: [ 'reporterPodId' ] | ||
49 | } | ||
50 | ] | ||
51 | } | ||
52 | ) | ||
53 | |||
54 | const classMethods = [ | ||
55 | associate, | ||
56 | |||
57 | listForApi | ||
58 | ] | ||
59 | const instanceMethods = [ | ||
60 | toFormatedJSON | ||
61 | ] | ||
62 | addMethodsToModel(VideoAbuse, classMethods, instanceMethods) | ||
63 | |||
64 | return VideoAbuse | ||
65 | } | ||
66 | |||
67 | // ------------------------------ METHODS ------------------------------ | ||
68 | |||
69 | function toFormatedJSON () { | ||
70 | let reporterPodHost | ||
71 | |||
72 | if (this.Pod) { | ||
73 | reporterPodHost = this.Pod.host | ||
74 | } else { | ||
75 | // It means it's our video | ||
76 | reporterPodHost = CONFIG.WEBSERVER.HOST | ||
77 | } | ||
78 | |||
79 | const json = { | ||
80 | id: this.id, | ||
81 | reporterPodHost, | ||
82 | reason: this.reason, | ||
83 | reporterUsername: this.reporterUsername, | ||
84 | videoId: this.videoId, | ||
85 | createdAt: this.createdAt | ||
86 | } | ||
87 | |||
88 | return json | ||
89 | } | ||
90 | |||
91 | // ------------------------------ STATICS ------------------------------ | ||
92 | |||
93 | function associate (models) { | ||
94 | VideoAbuse.belongsTo(models.Pod, { | ||
95 | foreignKey: { | ||
96 | name: 'reporterPodId', | ||
97 | allowNull: true | ||
98 | }, | ||
99 | onDelete: 'cascade' | ||
100 | }) | ||
101 | |||
102 | VideoAbuse.belongsTo(models.Video, { | ||
103 | foreignKey: { | ||
104 | name: 'videoId', | ||
105 | allowNull: false | ||
106 | }, | ||
107 | onDelete: 'cascade' | ||
108 | }) | ||
109 | } | ||
110 | |||
111 | listForApi = function (start, count, sort, callback) { | ||
112 | const query = { | ||
113 | offset: start, | ||
114 | limit: count, | ||
115 | order: [ getSort(sort) ], | ||
116 | include: [ | ||
117 | { | ||
118 | model: VideoAbuse['sequelize'].models.Pod, | ||
119 | required: false | ||
120 | } | ||
121 | ] | ||
122 | } | ||
123 | |||
124 | return VideoAbuse.findAndCountAll(query).asCallback(function (err, result) { | ||
125 | if (err) return callback(err) | ||
126 | |||
127 | return callback(null, result.rows, result.count) | ||
128 | }) | ||
129 | } | ||
130 | |||
131 | |||
diff --git a/server/models/video/video-blacklist-interface.ts b/server/models/video/video-blacklist-interface.ts new file mode 100644 index 000000000..37f579422 --- /dev/null +++ b/server/models/video/video-blacklist-interface.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | // Don't use barrel, import just what we need | ||
4 | import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/models/video-blacklist.model' | ||
5 | |||
6 | export namespace BlacklistedVideoMethods { | ||
7 | export type ToFormatedJSON = () => FormatedBlacklistedVideo | ||
8 | |||
9 | export type CountTotalCallback = (err: Error, total: number) => void | ||
10 | export type CountTotal = (callback: CountTotalCallback) => void | ||
11 | |||
12 | export type ListCallback = (err: Error, backlistedVideoInstances: BlacklistedVideoInstance[]) => void | ||
13 | export type List = (callback: ListCallback) => void | ||
14 | |||
15 | export type ListForApiCallback = (err: Error, blacklistedVIdeoInstances?: BlacklistedVideoInstance[], total?: number) => void | ||
16 | export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void | ||
17 | |||
18 | export type LoadByIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void | ||
19 | export type LoadById = (id: number, callback: LoadByIdCallback) => void | ||
20 | |||
21 | export type LoadByVideoIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void | ||
22 | export type LoadByVideoId = (id: string, callback: LoadByVideoIdCallback) => void | ||
23 | } | ||
24 | |||
25 | export interface BlacklistedVideoClass { | ||
26 | toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON | ||
27 | countTotal: BlacklistedVideoMethods.CountTotal | ||
28 | list: BlacklistedVideoMethods.List | ||
29 | listForApi: BlacklistedVideoMethods.ListForApi | ||
30 | loadById: BlacklistedVideoMethods.LoadById | ||
31 | loadByVideoId: BlacklistedVideoMethods.LoadByVideoId | ||
32 | } | ||
33 | |||
34 | export interface BlacklistedVideoAttributes { | ||
35 | } | ||
36 | |||
37 | export interface BlacklistedVideoInstance extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance<BlacklistedVideoAttributes> { | ||
38 | id: number | ||
39 | createdAt: Date | ||
40 | updatedAt: Date | ||
41 | } | ||
42 | |||
43 | export interface BlacklistedVideoModel extends BlacklistedVideoClass, Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> {} | ||
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts new file mode 100644 index 000000000..f4479986c --- /dev/null +++ b/server/models/video/video-blacklist.ts | |||
@@ -0,0 +1,103 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { addMethodsToModel, getSort } from '../utils' | ||
4 | import { | ||
5 | BlacklistedVideoClass, | ||
6 | BlacklistedVideoInstance, | ||
7 | BlacklistedVideoAttributes, | ||
8 | |||
9 | BlacklistedVideoMethods | ||
10 | } from './video-blacklist-interface' | ||
11 | |||
12 | let BlacklistedVideo: Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> | ||
13 | let toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON | ||
14 | let countTotal: BlacklistedVideoMethods.CountTotal | ||
15 | let list: BlacklistedVideoMethods.List | ||
16 | let listForApi: BlacklistedVideoMethods.ListForApi | ||
17 | let loadById: BlacklistedVideoMethods.LoadById | ||
18 | let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId | ||
19 | |||
20 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
21 | BlacklistedVideo = sequelize.define<BlacklistedVideoInstance, BlacklistedVideoAttributes>('BlacklistedVideo', | ||
22 | {}, | ||
23 | { | ||
24 | indexes: [ | ||
25 | { | ||
26 | fields: [ 'videoId' ], | ||
27 | unique: true | ||
28 | } | ||
29 | ] | ||
30 | } | ||
31 | ) | ||
32 | |||
33 | const classMethods = [ | ||
34 | associate, | ||
35 | |||
36 | countTotal, | ||
37 | list, | ||
38 | listForApi, | ||
39 | loadById, | ||
40 | loadByVideoId | ||
41 | ] | ||
42 | const instanceMethods = [ | ||
43 | toFormatedJSON | ||
44 | ] | ||
45 | addMethodsToModel(BlacklistedVideo, classMethods, instanceMethods) | ||
46 | |||
47 | return BlacklistedVideo | ||
48 | } | ||
49 | |||
50 | // ------------------------------ METHODS ------------------------------ | ||
51 | |||
52 | toFormatedJSON = function () { | ||
53 | return { | ||
54 | id: this.id, | ||
55 | videoId: this.videoId, | ||
56 | createdAt: this.createdAt | ||
57 | } | ||
58 | } | ||
59 | |||
60 | // ------------------------------ STATICS ------------------------------ | ||
61 | |||
62 | function associate (models) { | ||
63 | BlacklistedVideo.belongsTo(models.Video, { | ||
64 | foreignKey: 'videoId', | ||
65 | onDelete: 'cascade' | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | countTotal = function (callback: BlacklistedVideoMethods.CountTotalCallback) { | ||
70 | return BlacklistedVideo.count().asCallback(callback) | ||
71 | } | ||
72 | |||
73 | list = function (callback: BlacklistedVideoMethods.ListCallback) { | ||
74 | return BlacklistedVideo.findAll().asCallback(callback) | ||
75 | } | ||
76 | |||
77 | listForApi = function (start: number, count: number, sort: string, callback: BlacklistedVideoMethods.ListForApiCallback) { | ||
78 | const query = { | ||
79 | offset: start, | ||
80 | limit: count, | ||
81 | order: [ getSort(sort) ] | ||
82 | } | ||
83 | |||
84 | return BlacklistedVideo.findAndCountAll(query).asCallback(function (err, result) { | ||
85 | if (err) return callback(err) | ||
86 | |||
87 | return callback(null, result.rows, result.count) | ||
88 | }) | ||
89 | } | ||
90 | |||
91 | loadById = function (id: number, callback: BlacklistedVideoMethods.LoadByIdCallback) { | ||
92 | return BlacklistedVideo.findById(id).asCallback(callback) | ||
93 | } | ||
94 | |||
95 | loadByVideoId = function (id: string, callback: BlacklistedVideoMethods.LoadByIdCallback) { | ||
96 | const query = { | ||
97 | where: { | ||
98 | videoId: id | ||
99 | } | ||
100 | } | ||
101 | |||
102 | return BlacklistedVideo.find(query).asCallback(callback) | ||
103 | } | ||
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts new file mode 100644 index 000000000..71b9b0a69 --- /dev/null +++ b/server/models/video/video-interface.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { AuthorInstance } from './author-interface' | ||
4 | import { VideoTagInstance } from './video-tag-interface' | ||
5 | |||
6 | // Don't use barrel, import just what we need | ||
7 | import { Video as FormatedVideo } from '../../../shared/models/video.model' | ||
8 | |||
9 | export type FormatedAddRemoteVideo = { | ||
10 | name: string | ||
11 | category: number | ||
12 | licence: number | ||
13 | language: number | ||
14 | nsfw: boolean | ||
15 | description: string | ||
16 | infoHash: string | ||
17 | remoteId: string | ||
18 | author: string | ||
19 | duration: number | ||
20 | thumbnailData: string | ||
21 | tags: string[] | ||
22 | createdAt: Date | ||
23 | updatedAt: Date | ||
24 | extname: string | ||
25 | views: number | ||
26 | likes: number | ||
27 | dislikes: number | ||
28 | } | ||
29 | |||
30 | export type FormatedUpdateRemoteVideo = { | ||
31 | name: string | ||
32 | category: number | ||
33 | licence: number | ||
34 | language: number | ||
35 | nsfw: boolean | ||
36 | description: string | ||
37 | infoHash: string | ||
38 | remoteId: string | ||
39 | author: string | ||
40 | duration: number | ||
41 | tags: string[] | ||
42 | createdAt: Date | ||
43 | updatedAt: Date | ||
44 | extname: string | ||
45 | views: number | ||
46 | likes: number | ||
47 | dislikes: number | ||
48 | } | ||
49 | |||
50 | export namespace VideoMethods { | ||
51 | export type GenerateMagnetUri = () => string | ||
52 | export type GetVideoFilename = () => string | ||
53 | export type GetThumbnailName = () => string | ||
54 | export type GetPreviewName = () => string | ||
55 | export type GetTorrentName = () => string | ||
56 | export type IsOwned = () => boolean | ||
57 | export type ToFormatedJSON = () => FormatedVideo | ||
58 | |||
59 | export type ToAddRemoteJSONCallback = (err: Error, videoFormated?: FormatedAddRemoteVideo) => void | ||
60 | export type ToAddRemoteJSON = (callback: ToAddRemoteJSONCallback) => void | ||
61 | |||
62 | export type ToUpdateRemoteJSON = () => FormatedUpdateRemoteVideo | ||
63 | |||
64 | export type TranscodeVideofileCallback = (err: Error) => void | ||
65 | export type TranscodeVideofile = (callback: TranscodeVideofileCallback) => void | ||
66 | |||
67 | export type GenerateThumbnailFromDataCallback = (err: Error, thumbnailName?: string) => void | ||
68 | export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string, callback: GenerateThumbnailFromDataCallback) => void | ||
69 | |||
70 | export type GetDurationFromFileCallback = (err: Error, duration?: number) => void | ||
71 | export type GetDurationFromFile = (videoPath, callback) => void | ||
72 | |||
73 | export type ListCallback = (err: Error, videoInstances: VideoInstance[]) => void | ||
74 | export type List = (callback: ListCallback) => void | ||
75 | |||
76 | export type ListForApiCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void | ||
77 | export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void | ||
78 | |||
79 | export type LoadByHostAndRemoteIdCallback = (err: Error, videoInstance: VideoInstance) => void | ||
80 | export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string, callback: LoadByHostAndRemoteIdCallback) => void | ||
81 | |||
82 | export type ListOwnedAndPopulateAuthorAndTagsCallback = (err: Error, videoInstances: VideoInstance[]) => void | ||
83 | export type ListOwnedAndPopulateAuthorAndTags = (callback: ListOwnedAndPopulateAuthorAndTagsCallback) => void | ||
84 | |||
85 | export type ListOwnedByAuthorCallback = (err: Error, videoInstances: VideoInstance[]) => void | ||
86 | export type ListOwnedByAuthor = (author: string, callback: ListOwnedByAuthorCallback) => void | ||
87 | |||
88 | export type LoadCallback = (err: Error, videoInstance: VideoInstance) => void | ||
89 | export type Load = (id: string, callback: LoadCallback) => void | ||
90 | |||
91 | export type LoadAndPopulateAuthorCallback = (err: Error, videoInstance: VideoInstance) => void | ||
92 | export type LoadAndPopulateAuthor = (id: string, callback: LoadAndPopulateAuthorCallback) => void | ||
93 | |||
94 | export type LoadAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstance: VideoInstance) => void | ||
95 | export type LoadAndPopulateAuthorAndPodAndTags = (id: string, callback: LoadAndPopulateAuthorAndPodAndTagsCallback) => void | ||
96 | |||
97 | export type SearchAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void | ||
98 | export type SearchAndPopulateAuthorAndPodAndTags = (value: string, field: string, start: number, count: number, sort: string, callback: SearchAndPopulateAuthorAndPodAndTagsCallback) => void | ||
99 | } | ||
100 | |||
101 | export interface VideoClass { | ||
102 | generateMagnetUri: VideoMethods.GenerateMagnetUri | ||
103 | getVideoFilename: VideoMethods.GetVideoFilename | ||
104 | getThumbnailName: VideoMethods.GetThumbnailName | ||
105 | getPreviewName: VideoMethods.GetPreviewName | ||
106 | getTorrentName: VideoMethods.GetTorrentName | ||
107 | isOwned: VideoMethods.IsOwned | ||
108 | toFormatedJSON: VideoMethods.ToFormatedJSON | ||
109 | toAddRemoteJSON: VideoMethods.ToAddRemoteJSON | ||
110 | toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | ||
111 | transcodeVideofile: VideoMethods.TranscodeVideofile | ||
112 | |||
113 | generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | ||
114 | getDurationFromFile: VideoMethods.GetDurationFromFile | ||
115 | list: VideoMethods.List | ||
116 | listForApi: VideoMethods.ListForApi | ||
117 | loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId | ||
118 | listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags | ||
119 | listOwnedByAuthor: VideoMethods.ListOwnedByAuthor | ||
120 | load: VideoMethods.Load | ||
121 | loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor | ||
122 | loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags | ||
123 | searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags | ||
124 | } | ||
125 | |||
126 | export interface VideoAttributes { | ||
127 | name: string | ||
128 | extname: string | ||
129 | remoteId: string | ||
130 | category: number | ||
131 | licence: number | ||
132 | language: number | ||
133 | nsfw: boolean | ||
134 | description: string | ||
135 | infoHash?: string | ||
136 | duration: number | ||
137 | views?: number | ||
138 | likes?: number | ||
139 | dislikes?: number | ||
140 | |||
141 | Author?: AuthorInstance | ||
142 | Tags?: VideoTagInstance[] | ||
143 | } | ||
144 | |||
145 | export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { | ||
146 | id: string | ||
147 | createdAt: Date | ||
148 | updatedAt: Date | ||
149 | } | ||
150 | |||
151 | export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} | ||
diff --git a/server/models/video/video-tag-interface.ts b/server/models/video/video-tag-interface.ts new file mode 100644 index 000000000..f928cecff --- /dev/null +++ b/server/models/video/video-tag-interface.ts | |||
@@ -0,0 +1,18 @@ | |||
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 new file mode 100644 index 000000000..71ca85332 --- /dev/null +++ b/server/models/video/video-tag.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { addMethodsToModel } from '../utils' | ||
4 | import { | ||
5 | VideoTagClass, | ||
6 | VideoTagInstance, | ||
7 | VideoTagAttributes, | ||
8 | |||
9 | VideoTagMethods | ||
10 | } from './video-tag-interface' | ||
11 | |||
12 | let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes> | ||
13 | |||
14 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
15 | VideoTag = sequelize.define<VideoTagInstance, VideoTagAttributes>('VideoTag', {}, { | ||
16 | indexes: [ | ||
17 | { | ||
18 | fields: [ 'videoId' ] | ||
19 | }, | ||
20 | { | ||
21 | fields: [ 'tagId' ] | ||
22 | } | ||
23 | ] | ||
24 | }) | ||
25 | |||
26 | return VideoTag | ||
27 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts new file mode 100644 index 000000000..866b380cc --- /dev/null +++ b/server/models/video/video.ts | |||
@@ -0,0 +1,921 @@ | |||
1 | import * as safeBuffer from 'safe-buffer' | ||
2 | const Buffer = safeBuffer.Buffer | ||
3 | import * as createTorrent from 'create-torrent' | ||
4 | import * as ffmpeg from 'fluent-ffmpeg' | ||
5 | import * as fs from 'fs' | ||
6 | import * as magnetUtil from 'magnet-uri' | ||
7 | import { map, values } from 'lodash' | ||
8 | import { parallel, series } from 'async' | ||
9 | import * as parseTorrent from 'parse-torrent' | ||
10 | import { join } from 'path' | ||
11 | import * as Sequelize from 'sequelize' | ||
12 | |||
13 | import { database as db } from '../../initializers/database' | ||
14 | import { VideoTagInstance } from './video-tag-interface' | ||
15 | import { | ||
16 | logger, | ||
17 | isVideoNameValid, | ||
18 | isVideoCategoryValid, | ||
19 | isVideoLicenceValid, | ||
20 | isVideoLanguageValid, | ||
21 | isVideoNSFWValid, | ||
22 | isVideoDescriptionValid, | ||
23 | isVideoInfoHashValid, | ||
24 | isVideoDurationValid | ||
25 | } from '../../helpers' | ||
26 | import { | ||
27 | CONSTRAINTS_FIELDS, | ||
28 | CONFIG, | ||
29 | REMOTE_SCHEME, | ||
30 | STATIC_PATHS, | ||
31 | VIDEO_CATEGORIES, | ||
32 | VIDEO_LICENCES, | ||
33 | VIDEO_LANGUAGES, | ||
34 | THUMBNAILS_SIZE | ||
35 | } from '../../initializers' | ||
36 | import { JobScheduler, removeVideoToFriends } from '../../lib' | ||
37 | |||
38 | import { addMethodsToModel, getSort } from '../utils' | ||
39 | import { | ||
40 | VideoClass, | ||
41 | VideoInstance, | ||
42 | VideoAttributes, | ||
43 | |||
44 | VideoMethods | ||
45 | } from './video-interface' | ||
46 | |||
47 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> | ||
48 | let generateMagnetUri: VideoMethods.GenerateMagnetUri | ||
49 | let getVideoFilename: VideoMethods.GetVideoFilename | ||
50 | let getThumbnailName: VideoMethods.GetThumbnailName | ||
51 | let getPreviewName: VideoMethods.GetPreviewName | ||
52 | let getTorrentName: VideoMethods.GetTorrentName | ||
53 | let isOwned: VideoMethods.IsOwned | ||
54 | let toFormatedJSON: VideoMethods.ToFormatedJSON | ||
55 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON | ||
56 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | ||
57 | let transcodeVideofile: VideoMethods.TranscodeVideofile | ||
58 | |||
59 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | ||
60 | let getDurationFromFile: VideoMethods.GetDurationFromFile | ||
61 | let list: VideoMethods.List | ||
62 | let listForApi: VideoMethods.ListForApi | ||
63 | let loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId | ||
64 | let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags | ||
65 | let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor | ||
66 | let load: VideoMethods.Load | ||
67 | let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor | ||
68 | let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags | ||
69 | let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags | ||
70 | |||
71 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
72 | Video = sequelize.define<VideoInstance, VideoAttributes>('Video', | ||
73 | { | ||
74 | id: { | ||
75 | type: DataTypes.UUID, | ||
76 | defaultValue: DataTypes.UUIDV4, | ||
77 | primaryKey: true, | ||
78 | validate: { | ||
79 | isUUID: 4 | ||
80 | } | ||
81 | }, | ||
82 | name: { | ||
83 | type: DataTypes.STRING, | ||
84 | allowNull: false, | ||
85 | validate: { | ||
86 | nameValid: function (value) { | ||
87 | const res = isVideoNameValid(value) | ||
88 | if (res === false) throw new Error('Video name is not valid.') | ||
89 | } | ||
90 | } | ||
91 | }, | ||
92 | extname: { | ||
93 | type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), | ||
94 | allowNull: false | ||
95 | }, | ||
96 | remoteId: { | ||
97 | type: DataTypes.UUID, | ||
98 | allowNull: true, | ||
99 | validate: { | ||
100 | isUUID: 4 | ||
101 | } | ||
102 | }, | ||
103 | category: { | ||
104 | type: DataTypes.INTEGER, | ||
105 | allowNull: false, | ||
106 | validate: { | ||
107 | categoryValid: function (value) { | ||
108 | const res = isVideoCategoryValid(value) | ||
109 | if (res === false) throw new Error('Video category is not valid.') | ||
110 | } | ||
111 | } | ||
112 | }, | ||
113 | licence: { | ||
114 | type: DataTypes.INTEGER, | ||
115 | allowNull: false, | ||
116 | defaultValue: null, | ||
117 | validate: { | ||
118 | licenceValid: function (value) { | ||
119 | const res = isVideoLicenceValid(value) | ||
120 | if (res === false) throw new Error('Video licence is not valid.') | ||
121 | } | ||
122 | } | ||
123 | }, | ||
124 | language: { | ||
125 | type: DataTypes.INTEGER, | ||
126 | allowNull: true, | ||
127 | validate: { | ||
128 | languageValid: function (value) { | ||
129 | const res = isVideoLanguageValid(value) | ||
130 | if (res === false) throw new Error('Video language is not valid.') | ||
131 | } | ||
132 | } | ||
133 | }, | ||
134 | nsfw: { | ||
135 | type: DataTypes.BOOLEAN, | ||
136 | allowNull: false, | ||
137 | validate: { | ||
138 | nsfwValid: function (value) { | ||
139 | const res = isVideoNSFWValid(value) | ||
140 | if (res === false) throw new Error('Video nsfw attribute is not valid.') | ||
141 | } | ||
142 | } | ||
143 | }, | ||
144 | description: { | ||
145 | type: DataTypes.STRING, | ||
146 | allowNull: false, | ||
147 | validate: { | ||
148 | descriptionValid: function (value) { | ||
149 | const res = isVideoDescriptionValid(value) | ||
150 | if (res === false) throw new Error('Video description is not valid.') | ||
151 | } | ||
152 | } | ||
153 | }, | ||
154 | infoHash: { | ||
155 | type: DataTypes.STRING, | ||
156 | allowNull: false, | ||
157 | validate: { | ||
158 | infoHashValid: function (value) { | ||
159 | const res = isVideoInfoHashValid(value) | ||
160 | if (res === false) throw new Error('Video info hash is not valid.') | ||
161 | } | ||
162 | } | ||
163 | }, | ||
164 | duration: { | ||
165 | type: DataTypes.INTEGER, | ||
166 | allowNull: false, | ||
167 | validate: { | ||
168 | durationValid: function (value) { | ||
169 | const res = isVideoDurationValid(value) | ||
170 | if (res === false) throw new Error('Video duration is not valid.') | ||
171 | } | ||
172 | } | ||
173 | }, | ||
174 | views: { | ||
175 | type: DataTypes.INTEGER, | ||
176 | allowNull: false, | ||
177 | defaultValue: 0, | ||
178 | validate: { | ||
179 | min: 0, | ||
180 | isInt: true | ||
181 | } | ||
182 | }, | ||
183 | likes: { | ||
184 | type: DataTypes.INTEGER, | ||
185 | allowNull: false, | ||
186 | defaultValue: 0, | ||
187 | validate: { | ||
188 | min: 0, | ||
189 | isInt: true | ||
190 | } | ||
191 | }, | ||
192 | dislikes: { | ||
193 | type: DataTypes.INTEGER, | ||
194 | allowNull: false, | ||
195 | defaultValue: 0, | ||
196 | validate: { | ||
197 | min: 0, | ||
198 | isInt: true | ||
199 | } | ||
200 | } | ||
201 | }, | ||
202 | { | ||
203 | indexes: [ | ||
204 | { | ||
205 | fields: [ 'authorId' ] | ||
206 | }, | ||
207 | { | ||
208 | fields: [ 'remoteId' ] | ||
209 | }, | ||
210 | { | ||
211 | fields: [ 'name' ] | ||
212 | }, | ||
213 | { | ||
214 | fields: [ 'createdAt' ] | ||
215 | }, | ||
216 | { | ||
217 | fields: [ 'duration' ] | ||
218 | }, | ||
219 | { | ||
220 | fields: [ 'infoHash' ] | ||
221 | }, | ||
222 | { | ||
223 | fields: [ 'views' ] | ||
224 | }, | ||
225 | { | ||
226 | fields: [ 'likes' ] | ||
227 | } | ||
228 | ], | ||
229 | hooks: { | ||
230 | beforeValidate, | ||
231 | beforeCreate, | ||
232 | afterDestroy | ||
233 | } | ||
234 | } | ||
235 | ) | ||
236 | |||
237 | const classMethods = [ | ||
238 | associate, | ||
239 | |||
240 | generateThumbnailFromData, | ||
241 | getDurationFromFile, | ||
242 | list, | ||
243 | listForApi, | ||
244 | listOwnedAndPopulateAuthorAndTags, | ||
245 | listOwnedByAuthor, | ||
246 | load, | ||
247 | loadByHostAndRemoteId, | ||
248 | loadAndPopulateAuthor, | ||
249 | loadAndPopulateAuthorAndPodAndTags, | ||
250 | searchAndPopulateAuthorAndPodAndTags | ||
251 | ] | ||
252 | const instanceMethods = [ | ||
253 | generateMagnetUri, | ||
254 | getVideoFilename, | ||
255 | getThumbnailName, | ||
256 | getPreviewName, | ||
257 | getTorrentName, | ||
258 | isOwned, | ||
259 | toFormatedJSON, | ||
260 | toAddRemoteJSON, | ||
261 | toUpdateRemoteJSON, | ||
262 | transcodeVideofile, | ||
263 | removeFromBlacklist | ||
264 | ] | ||
265 | addMethodsToModel(Video, classMethods, instanceMethods) | ||
266 | |||
267 | return Video | ||
268 | } | ||
269 | |||
270 | function beforeValidate (video: VideoInstance) { | ||
271 | // Put a fake infoHash if it does not exists yet | ||
272 | if (video.isOwned() && !video.infoHash) { | ||
273 | // 40 hexa length | ||
274 | video.infoHash = '0123456789abcdef0123456789abcdef01234567' | ||
275 | } | ||
276 | } | ||
277 | |||
278 | function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { | ||
279 | return new Promise(function (resolve, reject) { | ||
280 | const tasks = [] | ||
281 | |||
282 | if (video.isOwned()) { | ||
283 | const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | ||
284 | |||
285 | tasks.push( | ||
286 | function createVideoTorrent (callback) { | ||
287 | createTorrentFromVideo(video, videoPath, callback) | ||
288 | }, | ||
289 | |||
290 | function createVideoThumbnail (callback) { | ||
291 | createThumbnail(video, videoPath, callback) | ||
292 | }, | ||
293 | |||
294 | function createVideoPreview (callback) { | ||
295 | createPreview(video, videoPath, callback) | ||
296 | } | ||
297 | ) | ||
298 | |||
299 | if (CONFIG.TRANSCODING.ENABLED === true) { | ||
300 | tasks.push( | ||
301 | function createVideoTranscoderJob (callback) { | ||
302 | const dataInput = { | ||
303 | id: video.id | ||
304 | } | ||
305 | |||
306 | JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput, callback) | ||
307 | } | ||
308 | ) | ||
309 | } | ||
310 | |||
311 | return parallel(tasks, function (err) { | ||
312 | if (err) return reject(err) | ||
313 | |||
314 | return resolve() | ||
315 | }) | ||
316 | } | ||
317 | |||
318 | return resolve() | ||
319 | }) | ||
320 | } | ||
321 | |||
322 | function afterDestroy (video: VideoInstance) { | ||
323 | return new Promise(function (resolve, reject) { | ||
324 | const tasks = [] | ||
325 | |||
326 | tasks.push( | ||
327 | function (callback) { | ||
328 | removeThumbnail(video, callback) | ||
329 | } | ||
330 | ) | ||
331 | |||
332 | if (video.isOwned()) { | ||
333 | tasks.push( | ||
334 | function removeVideoFile (callback) { | ||
335 | removeFile(video, callback) | ||
336 | }, | ||
337 | |||
338 | function removeVideoTorrent (callback) { | ||
339 | removeTorrent(video, callback) | ||
340 | }, | ||
341 | |||
342 | function removeVideoPreview (callback) { | ||
343 | removePreview(video, callback) | ||
344 | }, | ||
345 | |||
346 | function notifyFriends (callback) { | ||
347 | const params = { | ||
348 | remoteId: video.id | ||
349 | } | ||
350 | |||
351 | removeVideoToFriends(params) | ||
352 | |||
353 | return callback() | ||
354 | } | ||
355 | ) | ||
356 | } | ||
357 | |||
358 | parallel(tasks, function (err) { | ||
359 | if (err) return reject(err) | ||
360 | |||
361 | return resolve() | ||
362 | }) | ||
363 | }) | ||
364 | } | ||
365 | |||
366 | // ------------------------------ METHODS ------------------------------ | ||
367 | |||
368 | function associate (models) { | ||
369 | Video.belongsTo(models.Author, { | ||
370 | foreignKey: { | ||
371 | name: 'authorId', | ||
372 | allowNull: false | ||
373 | }, | ||
374 | onDelete: 'cascade' | ||
375 | }) | ||
376 | |||
377 | Video.belongsToMany(models.Tag, { | ||
378 | foreignKey: 'videoId', | ||
379 | through: models.VideoTag, | ||
380 | onDelete: 'cascade' | ||
381 | }) | ||
382 | |||
383 | Video.hasMany(models.VideoAbuse, { | ||
384 | foreignKey: { | ||
385 | name: 'videoId', | ||
386 | allowNull: false | ||
387 | }, | ||
388 | onDelete: 'cascade' | ||
389 | }) | ||
390 | } | ||
391 | |||
392 | generateMagnetUri = function () { | ||
393 | let baseUrlHttp | ||
394 | let baseUrlWs | ||
395 | |||
396 | if (this.isOwned()) { | ||
397 | baseUrlHttp = CONFIG.WEBSERVER.URL | ||
398 | baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT | ||
399 | } else { | ||
400 | baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host | ||
401 | baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host | ||
402 | } | ||
403 | |||
404 | const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() | ||
405 | const announce = [ baseUrlWs + '/tracker/socket' ] | ||
406 | const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] | ||
407 | |||
408 | const magnetHash = { | ||
409 | xs, | ||
410 | announce, | ||
411 | urlList, | ||
412 | infoHash: this.infoHash, | ||
413 | name: this.name | ||
414 | } | ||
415 | |||
416 | return magnetUtil.encode(magnetHash) | ||
417 | } | ||
418 | |||
419 | getVideoFilename = function () { | ||
420 | if (this.isOwned()) return this.id + this.extname | ||
421 | |||
422 | return this.remoteId + this.extname | ||
423 | } | ||
424 | |||
425 | getThumbnailName = function () { | ||
426 | // We always have a copy of the thumbnail | ||
427 | return this.id + '.jpg' | ||
428 | } | ||
429 | |||
430 | getPreviewName = function () { | ||
431 | const extension = '.jpg' | ||
432 | |||
433 | if (this.isOwned()) return this.id + extension | ||
434 | |||
435 | return this.remoteId + extension | ||
436 | } | ||
437 | |||
438 | getTorrentName = function () { | ||
439 | const extension = '.torrent' | ||
440 | |||
441 | if (this.isOwned()) return this.id + extension | ||
442 | |||
443 | return this.remoteId + extension | ||
444 | } | ||
445 | |||
446 | isOwned = function () { | ||
447 | return this.remoteId === null | ||
448 | } | ||
449 | |||
450 | toFormatedJSON = function (this: VideoInstance) { | ||
451 | let podHost | ||
452 | |||
453 | if (this.Author.Pod) { | ||
454 | podHost = this.Author.Pod.host | ||
455 | } else { | ||
456 | // It means it's our video | ||
457 | podHost = CONFIG.WEBSERVER.HOST | ||
458 | } | ||
459 | |||
460 | // Maybe our pod is not up to date and there are new categories since our version | ||
461 | let categoryLabel = VIDEO_CATEGORIES[this.category] | ||
462 | if (!categoryLabel) categoryLabel = 'Misc' | ||
463 | |||
464 | // Maybe our pod is not up to date and there are new licences since our version | ||
465 | let licenceLabel = VIDEO_LICENCES[this.licence] | ||
466 | if (!licenceLabel) licenceLabel = 'Unknown' | ||
467 | |||
468 | // Language is an optional attribute | ||
469 | let languageLabel = VIDEO_LANGUAGES[this.language] | ||
470 | if (!languageLabel) languageLabel = 'Unknown' | ||
471 | |||
472 | const json = { | ||
473 | id: this.id, | ||
474 | name: this.name, | ||
475 | category: this.category, | ||
476 | categoryLabel, | ||
477 | licence: this.licence, | ||
478 | licenceLabel, | ||
479 | language: this.language, | ||
480 | languageLabel, | ||
481 | nsfw: this.nsfw, | ||
482 | description: this.description, | ||
483 | podHost, | ||
484 | isLocal: this.isOwned(), | ||
485 | magnetUri: this.generateMagnetUri(), | ||
486 | author: this.Author.name, | ||
487 | duration: this.duration, | ||
488 | views: this.views, | ||
489 | likes: this.likes, | ||
490 | dislikes: this.dislikes, | ||
491 | tags: map<VideoTagInstance, string>(this.Tags, 'name'), | ||
492 | thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), | ||
493 | createdAt: this.createdAt, | ||
494 | updatedAt: this.updatedAt | ||
495 | } | ||
496 | |||
497 | return json | ||
498 | } | ||
499 | |||
500 | toAddRemoteJSON = function (callback: VideoMethods.ToAddRemoteJSONCallback) { | ||
501 | // Get thumbnail data to send to the other pod | ||
502 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
503 | fs.readFile(thumbnailPath, (err, thumbnailData) => { | ||
504 | if (err) { | ||
505 | logger.error('Cannot read the thumbnail of the video') | ||
506 | return callback(err) | ||
507 | } | ||
508 | |||
509 | const remoteVideo = { | ||
510 | name: this.name, | ||
511 | category: this.category, | ||
512 | licence: this.licence, | ||
513 | language: this.language, | ||
514 | nsfw: this.nsfw, | ||
515 | description: this.description, | ||
516 | infoHash: this.infoHash, | ||
517 | remoteId: this.id, | ||
518 | author: this.Author.name, | ||
519 | duration: this.duration, | ||
520 | thumbnailData: thumbnailData.toString('binary'), | ||
521 | tags: map<VideoTagInstance, string>(this.Tags, 'name'), | ||
522 | createdAt: this.createdAt, | ||
523 | updatedAt: this.updatedAt, | ||
524 | extname: this.extname, | ||
525 | views: this.views, | ||
526 | likes: this.likes, | ||
527 | dislikes: this.dislikes | ||
528 | } | ||
529 | |||
530 | return callback(null, remoteVideo) | ||
531 | }) | ||
532 | } | ||
533 | |||
534 | toUpdateRemoteJSON = function () { | ||
535 | const json = { | ||
536 | name: this.name, | ||
537 | category: this.category, | ||
538 | licence: this.licence, | ||
539 | language: this.language, | ||
540 | nsfw: this.nsfw, | ||
541 | description: this.description, | ||
542 | infoHash: this.infoHash, | ||
543 | remoteId: this.id, | ||
544 | author: this.Author.name, | ||
545 | duration: this.duration, | ||
546 | tags: map<VideoTagInstance, string>(this.Tags, 'name'), | ||
547 | createdAt: this.createdAt, | ||
548 | updatedAt: this.updatedAt, | ||
549 | extname: this.extname, | ||
550 | views: this.views, | ||
551 | likes: this.likes, | ||
552 | dislikes: this.dislikes | ||
553 | } | ||
554 | |||
555 | return json | ||
556 | } | ||
557 | |||
558 | transcodeVideofile = function (finalCallback: VideoMethods.TranscodeVideofileCallback) { | ||
559 | const video = this | ||
560 | |||
561 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
562 | const newExtname = '.mp4' | ||
563 | const videoInputPath = join(videosDirectory, video.getVideoFilename()) | ||
564 | const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) | ||
565 | |||
566 | ffmpeg(videoInputPath) | ||
567 | .output(videoOutputPath) | ||
568 | .videoCodec('libx264') | ||
569 | .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
570 | .outputOption('-movflags faststart') | ||
571 | .on('error', finalCallback) | ||
572 | .on('end', function () { | ||
573 | series([ | ||
574 | function removeOldFile (callback) { | ||
575 | fs.unlink(videoInputPath, callback) | ||
576 | }, | ||
577 | |||
578 | function moveNewFile (callback) { | ||
579 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
580 | video.set('extname', newExtname) | ||
581 | |||
582 | const newVideoPath = join(videosDirectory, video.getVideoFilename()) | ||
583 | fs.rename(videoOutputPath, newVideoPath, callback) | ||
584 | }, | ||
585 | |||
586 | function torrent (callback) { | ||
587 | const newVideoPath = join(videosDirectory, video.getVideoFilename()) | ||
588 | createTorrentFromVideo(video, newVideoPath, callback) | ||
589 | }, | ||
590 | |||
591 | function videoExtension (callback) { | ||
592 | video.save().asCallback(callback) | ||
593 | } | ||
594 | |||
595 | ], function (err: Error) { | ||
596 | if (err) { | ||
597 | // Autodesctruction... | ||
598 | video.destroy().asCallback(function (err) { | ||
599 | if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err }) | ||
600 | }) | ||
601 | |||
602 | return finalCallback(err) | ||
603 | } | ||
604 | |||
605 | return finalCallback(null) | ||
606 | }) | ||
607 | }) | ||
608 | .run() | ||
609 | } | ||
610 | |||
611 | // ------------------------------ STATICS ------------------------------ | ||
612 | |||
613 | generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string, callback: VideoMethods.GenerateThumbnailFromDataCallback) { | ||
614 | // Creating the thumbnail for a remote video | ||
615 | |||
616 | const thumbnailName = video.getThumbnailName() | ||
617 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
618 | fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { | ||
619 | if (err) return callback(err) | ||
620 | |||
621 | return callback(null, thumbnailName) | ||
622 | }) | ||
623 | } | ||
624 | |||
625 | getDurationFromFile = function (videoPath: string, callback: VideoMethods.GetDurationFromFileCallback) { | ||
626 | ffmpeg.ffprobe(videoPath, function (err, metadata) { | ||
627 | if (err) return callback(err) | ||
628 | |||
629 | return callback(null, Math.floor(metadata.format.duration)) | ||
630 | }) | ||
631 | } | ||
632 | |||
633 | list = function (callback: VideoMethods.ListCallback) { | ||
634 | return Video.findAll().asCallback(callback) | ||
635 | } | ||
636 | |||
637 | listForApi = function (start: number, count: number, sort: string, callback: VideoMethods.ListForApiCallback) { | ||
638 | // Exclude Blakclisted videos from the list | ||
639 | const query = { | ||
640 | distinct: true, | ||
641 | offset: start, | ||
642 | limit: count, | ||
643 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], | ||
644 | include: [ | ||
645 | { | ||
646 | model: Video['sequelize'].models.Author, | ||
647 | include: [ { model: Video['sequelize'].models.Pod, required: false } ] | ||
648 | }, | ||
649 | |||
650 | Video['sequelize'].models.Tag | ||
651 | ], | ||
652 | where: createBaseVideosWhere() | ||
653 | } | ||
654 | |||
655 | return Video.findAndCountAll(query).asCallback(function (err, result) { | ||
656 | if (err) return callback(err) | ||
657 | |||
658 | return callback(null, result.rows, result.count) | ||
659 | }) | ||
660 | } | ||
661 | |||
662 | loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: VideoMethods.LoadByHostAndRemoteIdCallback) { | ||
663 | const query = { | ||
664 | where: { | ||
665 | remoteId: remoteId | ||
666 | }, | ||
667 | include: [ | ||
668 | { | ||
669 | model: Video['sequelize'].models.Author, | ||
670 | include: [ | ||
671 | { | ||
672 | model: Video['sequelize'].models.Pod, | ||
673 | required: true, | ||
674 | where: { | ||
675 | host: fromHost | ||
676 | } | ||
677 | } | ||
678 | ] | ||
679 | } | ||
680 | ] | ||
681 | } | ||
682 | |||
683 | return Video.findOne(query).asCallback(callback) | ||
684 | } | ||
685 | |||
686 | listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAndPopulateAuthorAndTagsCallback) { | ||
687 | // If remoteId is null this is *our* video | ||
688 | const query = { | ||
689 | where: { | ||
690 | remoteId: null | ||
691 | }, | ||
692 | include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] | ||
693 | } | ||
694 | |||
695 | return Video.findAll(query).asCallback(callback) | ||
696 | } | ||
697 | |||
698 | listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedByAuthorCallback) { | ||
699 | const query = { | ||
700 | where: { | ||
701 | remoteId: null | ||
702 | }, | ||
703 | include: [ | ||
704 | { | ||
705 | model: Video['sequelize'].models.Author, | ||
706 | where: { | ||
707 | name: author | ||
708 | } | ||
709 | } | ||
710 | ] | ||
711 | } | ||
712 | |||
713 | return Video.findAll(query).asCallback(callback) | ||
714 | } | ||
715 | |||
716 | load = function (id: string, callback: VideoMethods.LoadCallback) { | ||
717 | return Video.findById(id).asCallback(callback) | ||
718 | } | ||
719 | |||
720 | loadAndPopulateAuthor = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorCallback) { | ||
721 | const options = { | ||
722 | include: [ Video['sequelize'].models.Author ] | ||
723 | } | ||
724 | |||
725 | return Video.findById(id, options).asCallback(callback) | ||
726 | } | ||
727 | |||
728 | loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorAndPodAndTagsCallback) { | ||
729 | const options = { | ||
730 | include: [ | ||
731 | { | ||
732 | model: Video['sequelize'].models.Author, | ||
733 | include: [ { model: Video['sequelize'].models.Pod, required: false } ] | ||
734 | }, | ||
735 | Video['sequelize'].models.Tag | ||
736 | ] | ||
737 | } | ||
738 | |||
739 | return Video.findById(id, options).asCallback(callback) | ||
740 | } | ||
741 | |||
742 | searchAndPopulateAuthorAndPodAndTags = function ( | ||
743 | value: string, | ||
744 | field: string, | ||
745 | start: number, | ||
746 | count: number, | ||
747 | sort: string, | ||
748 | callback: VideoMethods.SearchAndPopulateAuthorAndPodAndTagsCallback | ||
749 | ) { | ||
750 | const podInclude: any = { | ||
751 | model: Video['sequelize'].models.Pod, | ||
752 | required: false | ||
753 | } | ||
754 | |||
755 | const authorInclude: any = { | ||
756 | model: Video['sequelize'].models.Author, | ||
757 | include: [ | ||
758 | podInclude | ||
759 | ] | ||
760 | } | ||
761 | |||
762 | const tagInclude: any = { | ||
763 | model: Video['sequelize'].models.Tag | ||
764 | } | ||
765 | |||
766 | const query: any = { | ||
767 | distinct: true, | ||
768 | where: createBaseVideosWhere(), | ||
769 | offset: start, | ||
770 | limit: count, | ||
771 | order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] | ||
772 | } | ||
773 | |||
774 | // Make an exact search with the magnet | ||
775 | if (field === 'magnetUri') { | ||
776 | const infoHash = magnetUtil.decode(value).infoHash | ||
777 | query.where.infoHash = infoHash | ||
778 | } else if (field === 'tags') { | ||
779 | const escapedValue = Video['sequelize'].escape('%' + value + '%') | ||
780 | query.where.id.$in = Video['sequelize'].literal( | ||
781 | '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' | ||
782 | ) | ||
783 | } else if (field === 'host') { | ||
784 | // FIXME: Include our pod? (not stored in the database) | ||
785 | podInclude.where = { | ||
786 | host: { | ||
787 | $like: '%' + value + '%' | ||
788 | } | ||
789 | } | ||
790 | podInclude.required = true | ||
791 | } else if (field === 'author') { | ||
792 | authorInclude.where = { | ||
793 | name: { | ||
794 | $like: '%' + value + '%' | ||
795 | } | ||
796 | } | ||
797 | |||
798 | // authorInclude.or = true | ||
799 | } else { | ||
800 | query.where[field] = { | ||
801 | $like: '%' + value + '%' | ||
802 | } | ||
803 | } | ||
804 | |||
805 | query.include = [ | ||
806 | authorInclude, tagInclude | ||
807 | ] | ||
808 | |||
809 | if (tagInclude.where) { | ||
810 | // query.include.push([ Video['sequelize'].models.Tag ]) | ||
811 | } | ||
812 | |||
813 | return Video.findAndCountAll(query).asCallback(function (err, result) { | ||
814 | if (err) return callback(err) | ||
815 | |||
816 | return callback(null, result.rows, result.count) | ||
817 | }) | ||
818 | } | ||
819 | |||
820 | // --------------------------------------------------------------------------- | ||
821 | |||
822 | function createBaseVideosWhere () { | ||
823 | return { | ||
824 | id: { | ||
825 | $notIn: Video['sequelize'].literal( | ||
826 | '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' | ||
827 | ) | ||
828 | } | ||
829 | } | ||
830 | } | ||
831 | |||
832 | function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) { | ||
833 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) | ||
834 | fs.unlink(thumbnailPath, callback) | ||
835 | } | ||
836 | |||
837 | function removeFile (video: VideoInstance, callback: (err: Error) => void) { | ||
838 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | ||
839 | fs.unlink(filePath, callback) | ||
840 | } | ||
841 | |||
842 | function removeTorrent (video: VideoInstance, callback: (err: Error) => void) { | ||
843 | const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) | ||
844 | fs.unlink(torrenPath, callback) | ||
845 | } | ||
846 | |||
847 | function removePreview (video: VideoInstance, callback: (err: Error) => void) { | ||
848 | // Same name than video thumnail | ||
849 | fs.unlink(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback) | ||
850 | } | ||
851 | |||
852 | function createTorrentFromVideo (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { | ||
853 | const options = { | ||
854 | announceList: [ | ||
855 | [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] | ||
856 | ], | ||
857 | urlList: [ | ||
858 | CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename() | ||
859 | ] | ||
860 | } | ||
861 | |||
862 | createTorrent(videoPath, options, function (err, torrent) { | ||
863 | if (err) return callback(err) | ||
864 | |||
865 | const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) | ||
866 | fs.writeFile(filePath, torrent, function (err) { | ||
867 | if (err) return callback(err) | ||
868 | |||
869 | const parsedTorrent = parseTorrent(torrent) | ||
870 | video.set('infoHash', parsedTorrent.infoHash) | ||
871 | video.validate().asCallback(callback) | ||
872 | }) | ||
873 | }) | ||
874 | } | ||
875 | |||
876 | function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { | ||
877 | generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback) | ||
878 | } | ||
879 | |||
880 | function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { | ||
881 | generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback) | ||
882 | } | ||
883 | |||
884 | type GenerateImageCallback = (err: Error, imageName: string) => void | ||
885 | function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string, callback?: GenerateImageCallback) { | ||
886 | const options: any = { | ||
887 | filename: imageName, | ||
888 | count: 1, | ||
889 | folder | ||
890 | } | ||
891 | |||
892 | if (size) { | ||
893 | options.size = size | ||
894 | } | ||
895 | |||
896 | ffmpeg(videoPath) | ||
897 | .on('error', callback) | ||
898 | .on('end', function () { | ||
899 | callback(null, imageName) | ||
900 | }) | ||
901 | .thumbnail(options) | ||
902 | } | ||
903 | |||
904 | function removeFromBlacklist (video: VideoInstance, callback: (err: Error) => void) { | ||
905 | // Find the blacklisted video | ||
906 | db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { | ||
907 | // If an error occured, stop here | ||
908 | if (err) { | ||
909 | logger.error('Error when fetching video from blacklist.', { error: err }) | ||
910 | return callback(err) | ||
911 | } | ||
912 | |||
913 | // If we found the video, remove it from the blacklist | ||
914 | if (video) { | ||
915 | video.destroy().asCallback(callback) | ||
916 | } else { | ||
917 | // If haven't found it, simply ignore it and do nothing | ||
918 | return callback(null) | ||
919 | } | ||
920 | }) | ||
921 | } | ||