aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/author-interface.ts27
-rw-r--r--server/models/video/author.ts103
-rw-r--r--server/models/video/index.ts6
-rw-r--r--server/models/video/tag-interface.ts20
-rw-r--r--server/models/video/tag.ts81
-rw-r--r--server/models/video/video-abuse-interface.ts28
-rw-r--r--server/models/video/video-abuse.ts131
-rw-r--r--server/models/video/video-blacklist-interface.ts43
-rw-r--r--server/models/video/video-blacklist.ts103
-rw-r--r--server/models/video/video-interface.ts151
-rw-r--r--server/models/video/video-tag-interface.ts18
-rw-r--r--server/models/video/video-tag.ts27
-rw-r--r--server/models/video/video.ts921
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 @@
1import * as Sequelize from 'sequelize'
2
3import { PodInstance } from '../pod'
4
5export 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
10export interface AuthorClass {
11 findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor
12}
13
14export interface AuthorAttributes {
15 name: string
16}
17
18export 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
27export 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 @@
1import * as Sequelize from 'sequelize'
2
3import { isUserUsernameValid } from '../../helpers'
4
5import { addMethodsToModel } from '../utils'
6import {
7 AuthorClass,
8 AuthorInstance,
9 AuthorAttributes,
10
11 AuthorMethods
12} from './author-interface'
13
14let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
15let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor
16
17export 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
59function 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
77findOrCreateAuthor = 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 @@
1export * from './author-interface'
2export * from './tag-interface'
3export * from './video-abuse-interface'
4export * from './video-blacklist-interface'
5export * from './video-tag-interface'
6export * 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 @@
1import * as Sequelize from 'sequelize'
2
3export 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
8export interface TagClass {
9 findOrCreateTags: TagMethods.FindOrCreateTags
10}
11
12export interface TagAttributes {
13 name: string
14}
15
16export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance<TagAttributes> {
17 id: number
18}
19
20export 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 @@
1import { each } from 'async'
2import * as Sequelize from 'sequelize'
3
4import { addMethodsToModel } from '../utils'
5import {
6 TagClass,
7 TagInstance,
8 TagAttributes,
9
10 TagMethods
11} from './tag-interface'
12
13let Tag: Sequelize.Model<TagInstance, TagAttributes>
14let findOrCreateTags: TagMethods.FindOrCreateTags
15
16export 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
47function associate (models) {
48 Tag.belongsToMany(models.Video, {
49 foreignKey: 'tagId',
50 through: models.VideoTag,
51 onDelete: 'cascade'
52 })
53}
54
55findOrCreateTags = 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 @@
1import * as Sequelize from 'sequelize'
2
3// Don't use barrel, import just what we need
4import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-abuse.model'
5
6export 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
13export interface VideoAbuseClass {
14 listForApi: VideoAbuseMethods.ListForApi
15}
16
17export interface VideoAbuseAttributes {
18 reporterUsername: string
19 reason: string
20}
21
22export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance<VideoAbuseAttributes> {
23 id: number
24 createdAt: Date
25 updatedAt: Date
26}
27
28export 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 @@
1import * as Sequelize from 'sequelize'
2
3import { CONFIG } from '../../initializers'
4import { isVideoAbuseReporterUsernameValid, isVideoAbuseReasonValid } from '../../helpers'
5
6import { addMethodsToModel, getSort } from '../utils'
7import {
8 VideoAbuseClass,
9 VideoAbuseInstance,
10 VideoAbuseAttributes,
11
12 VideoAbuseMethods
13} from './video-abuse-interface'
14
15let VideoAbuse: Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes>
16let listForApi: VideoAbuseMethods.ListForApi
17
18export 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
69function 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
93function 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
111listForApi = 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 @@
1import * as Sequelize from 'sequelize'
2
3// Don't use barrel, import just what we need
4import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/models/video-blacklist.model'
5
6export 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
25export 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
34export interface BlacklistedVideoAttributes {
35}
36
37export interface BlacklistedVideoInstance extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance<BlacklistedVideoAttributes> {
38 id: number
39 createdAt: Date
40 updatedAt: Date
41}
42
43export 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 @@
1import * as Sequelize from 'sequelize'
2
3import { addMethodsToModel, getSort } from '../utils'
4import {
5 BlacklistedVideoClass,
6 BlacklistedVideoInstance,
7 BlacklistedVideoAttributes,
8
9 BlacklistedVideoMethods
10} from './video-blacklist-interface'
11
12let BlacklistedVideo: Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes>
13let toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON
14let countTotal: BlacklistedVideoMethods.CountTotal
15let list: BlacklistedVideoMethods.List
16let listForApi: BlacklistedVideoMethods.ListForApi
17let loadById: BlacklistedVideoMethods.LoadById
18let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId
19
20export 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
52toFormatedJSON = function () {
53 return {
54 id: this.id,
55 videoId: this.videoId,
56 createdAt: this.createdAt
57 }
58}
59
60// ------------------------------ STATICS ------------------------------
61
62function associate (models) {
63 BlacklistedVideo.belongsTo(models.Video, {
64 foreignKey: 'videoId',
65 onDelete: 'cascade'
66 })
67}
68
69countTotal = function (callback: BlacklistedVideoMethods.CountTotalCallback) {
70 return BlacklistedVideo.count().asCallback(callback)
71}
72
73list = function (callback: BlacklistedVideoMethods.ListCallback) {
74 return BlacklistedVideo.findAll().asCallback(callback)
75}
76
77listForApi = 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
91loadById = function (id: number, callback: BlacklistedVideoMethods.LoadByIdCallback) {
92 return BlacklistedVideo.findById(id).asCallback(callback)
93}
94
95loadByVideoId = 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 @@
1import * as Sequelize from 'sequelize'
2
3import { AuthorInstance } from './author-interface'
4import { VideoTagInstance } from './video-tag-interface'
5
6// Don't use barrel, import just what we need
7import { Video as FormatedVideo } from '../../../shared/models/video.model'
8
9export 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
30export 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
50export 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
101export 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
126export 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
145export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
146 id: string
147 createdAt: Date
148 updatedAt: Date
149}
150
151export 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 @@
1import * as Sequelize from 'sequelize'
2
3export namespace VideoTagMethods {
4}
5
6export interface VideoTagClass {
7}
8
9export interface VideoTagAttributes {
10}
11
12export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance<VideoTagAttributes> {
13 id: number
14 createdAt: Date
15 updatedAt: Date
16}
17
18export 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 @@
1import * as Sequelize from 'sequelize'
2
3import { addMethodsToModel } from '../utils'
4import {
5 VideoTagClass,
6 VideoTagInstance,
7 VideoTagAttributes,
8
9 VideoTagMethods
10} from './video-tag-interface'
11
12let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes>
13
14export 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 @@
1import * as safeBuffer from 'safe-buffer'
2const Buffer = safeBuffer.Buffer
3import * as createTorrent from 'create-torrent'
4import * as ffmpeg from 'fluent-ffmpeg'
5import * as fs from 'fs'
6import * as magnetUtil from 'magnet-uri'
7import { map, values } from 'lodash'
8import { parallel, series } from 'async'
9import * as parseTorrent from 'parse-torrent'
10import { join } from 'path'
11import * as Sequelize from 'sequelize'
12
13import { database as db } from '../../initializers/database'
14import { VideoTagInstance } from './video-tag-interface'
15import {
16 logger,
17 isVideoNameValid,
18 isVideoCategoryValid,
19 isVideoLicenceValid,
20 isVideoLanguageValid,
21 isVideoNSFWValid,
22 isVideoDescriptionValid,
23 isVideoInfoHashValid,
24 isVideoDurationValid
25} from '../../helpers'
26import {
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'
36import { JobScheduler, removeVideoToFriends } from '../../lib'
37
38import { addMethodsToModel, getSort } from '../utils'
39import {
40 VideoClass,
41 VideoInstance,
42 VideoAttributes,
43
44 VideoMethods
45} from './video-interface'
46
47let Video: Sequelize.Model<VideoInstance, VideoAttributes>
48let generateMagnetUri: VideoMethods.GenerateMagnetUri
49let getVideoFilename: VideoMethods.GetVideoFilename
50let getThumbnailName: VideoMethods.GetThumbnailName
51let getPreviewName: VideoMethods.GetPreviewName
52let getTorrentName: VideoMethods.GetTorrentName
53let isOwned: VideoMethods.IsOwned
54let toFormatedJSON: VideoMethods.ToFormatedJSON
55let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
56let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
57let transcodeVideofile: VideoMethods.TranscodeVideofile
58
59let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
60let getDurationFromFile: VideoMethods.GetDurationFromFile
61let list: VideoMethods.List
62let listForApi: VideoMethods.ListForApi
63let loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId
64let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
65let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
66let load: VideoMethods.Load
67let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
68let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
69let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
70
71export 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
270function 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
278function 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
322function 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
368function 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
392generateMagnetUri = 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
419getVideoFilename = function () {
420 if (this.isOwned()) return this.id + this.extname
421
422 return this.remoteId + this.extname
423}
424
425getThumbnailName = function () {
426 // We always have a copy of the thumbnail
427 return this.id + '.jpg'
428}
429
430getPreviewName = function () {
431 const extension = '.jpg'
432
433 if (this.isOwned()) return this.id + extension
434
435 return this.remoteId + extension
436}
437
438getTorrentName = function () {
439 const extension = '.torrent'
440
441 if (this.isOwned()) return this.id + extension
442
443 return this.remoteId + extension
444}
445
446isOwned = function () {
447 return this.remoteId === null
448}
449
450toFormatedJSON = 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
500toAddRemoteJSON = 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
534toUpdateRemoteJSON = 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
558transcodeVideofile = 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
613generateThumbnailFromData = 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
625getDurationFromFile = 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
633list = function (callback: VideoMethods.ListCallback) {
634 return Video.findAll().asCallback(callback)
635}
636
637listForApi = 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
662loadByHostAndRemoteId = 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
686listOwnedAndPopulateAuthorAndTags = 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
698listOwnedByAuthor = 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
716load = function (id: string, callback: VideoMethods.LoadCallback) {
717 return Video.findById(id).asCallback(callback)
718}
719
720loadAndPopulateAuthor = 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
728loadAndPopulateAuthorAndPodAndTags = 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
742searchAndPopulateAuthorAndPodAndTags = 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
822function createBaseVideosWhere () {
823 return {
824 id: {
825 $notIn: Video['sequelize'].literal(
826 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
827 )
828 }
829 }
830}
831
832function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) {
833 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
834 fs.unlink(thumbnailPath, callback)
835}
836
837function removeFile (video: VideoInstance, callback: (err: Error) => void) {
838 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
839 fs.unlink(filePath, callback)
840}
841
842function removeTorrent (video: VideoInstance, callback: (err: Error) => void) {
843 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
844 fs.unlink(torrenPath, callback)
845}
846
847function 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
852function 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
876function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) {
877 generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback)
878}
879
880function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) {
881 generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback)
882}
883
884type GenerateImageCallback = (err: Error, imageName: string) => void
885function 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
904function 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}