diff options
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/video-abuse.ts | 24 | ||||
-rw-r--r-- | server/models/video/video-blacklist.ts | 31 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 25 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 45 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 26 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-import.ts | 4 | ||||
-rw-r--r-- | server/models/video/video.ts | 112 |
8 files changed, 194 insertions, 79 deletions
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index dbb88ca45..cc47644f2 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,17 +1,4 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 2 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
16 | import { VideoAbuse } from '../../../shared/models/videos' | 3 | import { VideoAbuse } from '../../../shared/models/videos' |
17 | import { | 4 | import { |
@@ -19,7 +6,6 @@ import { | |||
19 | isVideoAbuseReasonValid, | 6 | isVideoAbuseReasonValid, |
20 | isVideoAbuseStateValid | 7 | isVideoAbuseStateValid |
21 | } from '../../helpers/custom-validators/video-abuses' | 8 | } from '../../helpers/custom-validators/video-abuses' |
22 | import { Emailer } from '../../lib/emailer' | ||
23 | import { AccountModel } from '../account/account' | 9 | import { AccountModel } from '../account/account' |
24 | import { getSort, throwIfNotValid } from '../utils' | 10 | import { getSort, throwIfNotValid } from '../utils' |
25 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' | |||
40 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | 26 | export class VideoAbuseModel extends Model<VideoAbuseModel> { |
41 | 27 | ||
42 | @AllowNull(false) | 28 | @AllowNull(false) |
29 | @Default(null) | ||
43 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) | 30 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) |
44 | @Column | 31 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) |
45 | reason: string | 32 | reason: string |
46 | 33 | ||
47 | @AllowNull(false) | 34 | @AllowNull(false) |
@@ -86,11 +73,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
86 | }) | 73 | }) |
87 | Video: VideoModel | 74 | Video: VideoModel |
88 | 75 | ||
89 | @AfterCreate | ||
90 | static sendEmailNotification (instance: VideoAbuseModel) { | ||
91 | return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) | ||
92 | } | ||
93 | |||
94 | static loadByIdAndVideoId (id: number, videoId: number) { | 76 | static loadByIdAndVideoId (id: number, videoId: number) { |
95 | const query = { | 77 | const query = { |
96 | where: { | 78 | where: { |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 67f7cd487..3b567e488 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,21 +1,7 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AfterDestroy, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' | 2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' |
16 | import { VideoModel } from './video' | 3 | import { VideoModel } from './video' |
17 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' | 4 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' |
18 | import { Emailer } from '../../lib/emailer' | ||
19 | import { VideoBlacklist } from '../../../shared/models/videos' | 5 | import { VideoBlacklist } from '../../../shared/models/videos' |
20 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
21 | 7 | ||
@@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
35 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) | 21 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) |
36 | reason: string | 22 | reason: string |
37 | 23 | ||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | unfederated: boolean | ||
27 | |||
38 | @CreatedAt | 28 | @CreatedAt |
39 | createdAt: Date | 29 | createdAt: Date |
40 | 30 | ||
@@ -53,16 +43,6 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
53 | }) | 43 | }) |
54 | Video: VideoModel | 44 | Video: VideoModel |
55 | 45 | ||
56 | @AfterCreate | ||
57 | static sendBlacklistEmailNotification (instance: VideoBlacklistModel) { | ||
58 | return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason) | ||
59 | } | ||
60 | |||
61 | @AfterDestroy | ||
62 | static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) { | ||
63 | return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId) | ||
64 | } | ||
65 | |||
66 | static listForApi (start: number, count: number, sort: SortType) { | 46 | static listForApi (start: number, count: number, sort: SortType) { |
67 | const query = { | 47 | const query = { |
68 | offset: start, | 48 | offset: start, |
@@ -103,6 +83,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
103 | createdAt: this.createdAt, | 83 | createdAt: this.createdAt, |
104 | updatedAt: this.updatedAt, | 84 | updatedAt: this.updatedAt, |
105 | reason: this.reason, | 85 | reason: this.reason, |
86 | unfederated: this.unfederated, | ||
106 | 87 | ||
107 | video: { | 88 | video: { |
108 | id: video.id, | 89 | id: video.id, |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f4586917e..5598d80f6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
233 | }) | 233 | }) |
234 | } | 234 | } |
235 | 235 | ||
236 | static listLocalsForSitemap (sort: string) { | ||
237 | const query = { | ||
238 | attributes: [ ], | ||
239 | offset: 0, | ||
240 | order: getSort(sort), | ||
241 | include: [ | ||
242 | { | ||
243 | attributes: [ 'preferredUsername', 'serverId' ], | ||
244 | model: ActorModel.unscoped(), | ||
245 | where: { | ||
246 | serverId: null | ||
247 | } | ||
248 | } | ||
249 | ] | ||
250 | } | ||
251 | |||
252 | return VideoChannelModel | ||
253 | .unscoped() | ||
254 | .findAll(query) | ||
255 | } | ||
256 | |||
236 | static searchForApi (options: { | 257 | static searchForApi (options: { |
237 | actorId: number | 258 | actorId: number |
238 | search: string | 259 | search: string |
@@ -449,4 +470,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
449 | getDisplayName () { | 470 | getDisplayName () { |
450 | return this.name | 471 | return this.name |
451 | } | 472 | } |
473 | |||
474 | isOutdated () { | ||
475 | return this.Actor.isOutdated() | ||
476 | } | ||
452 | } | 477 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index dd6d08139..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co | |||
18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
21 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 21 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' | 22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' |
23 | import { AccountModel } from '../account/account' | 23 | import { AccountModel } from '../account/account' |
24 | import { ActorModel } from '../activitypub/actor' | 24 | import { ActorModel } from '../activitypub/actor' |
@@ -29,6 +29,9 @@ import { VideoModel } from './video' | |||
29 | import { VideoChannelModel } from './video-channel' | 29 | import { VideoChannelModel } from './video-channel' |
30 | import { getServerActor } from '../../helpers/utils' | 30 | import { getServerActor } from '../../helpers/utils' |
31 | import { UserModel } from '../account/user' | 31 | import { UserModel } from '../account/user' |
32 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | ||
33 | import { regexpCapture } from '../../helpers/regexp' | ||
34 | import { uniq } from 'lodash' | ||
32 | 35 | ||
33 | enum ScopeNames { | 36 | enum ScopeNames { |
34 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 37 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -370,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
370 | id: { | 373 | id: { |
371 | [ Sequelize.Op.in ]: Sequelize.literal('(' + | 374 | [ Sequelize.Op.in ]: Sequelize.literal('(' + |
372 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | 375 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + |
373 | 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + | 376 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + |
374 | 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + | 377 | 'UNION ' + |
375 | 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + | 378 | 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' + |
379 | 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' + | ||
380 | ') ' + | ||
376 | 'SELECT id FROM children' + | 381 | 'SELECT id FROM children' + |
377 | ')'), | 382 | ')'), |
378 | [ Sequelize.Op.ne ]: comment.id | 383 | [ Sequelize.Op.ne ]: comment.id |
@@ -448,6 +453,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
448 | } | 453 | } |
449 | } | 454 | } |
450 | 455 | ||
456 | getCommentStaticPath () { | ||
457 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
458 | } | ||
459 | |||
451 | getThreadId (): number { | 460 | getThreadId (): number { |
452 | return this.originCommentId || this.id | 461 | return this.originCommentId || this.id |
453 | } | 462 | } |
@@ -456,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
456 | return this.Account.isOwned() | 465 | return this.Account.isOwned() |
457 | } | 466 | } |
458 | 467 | ||
468 | extractMentions () { | ||
469 | if (!this.text) return [] | ||
470 | |||
471 | const localMention = `@(${actorNameAlphabet}+)` | ||
472 | const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` | ||
473 | |||
474 | const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') | ||
475 | const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') | ||
476 | const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g') | ||
477 | const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g') | ||
478 | |||
479 | return uniq( | ||
480 | [].concat( | ||
481 | regexpCapture(this.text, remoteMentionsRegex) | ||
482 | .map(([ , username ]) => username), | ||
483 | |||
484 | regexpCapture(this.text, localMentionsRegex) | ||
485 | .map(([ , username ]) => username), | ||
486 | |||
487 | regexpCapture(this.text, firstMentionRegex) | ||
488 | .map(([ , username1, username2 ]) => username1 || username2), | ||
489 | |||
490 | regexpCapture(this.text, endMentionRegex) | ||
491 | .map(([ , username1, username2 ]) => username1 || username2) | ||
492 | ) | ||
493 | ) | ||
494 | } | ||
495 | |||
459 | toFormattedJSON () { | 496 | toFormattedJSON () { |
460 | return { | 497 | return { |
461 | id: this.id, | 498 | id: this.id, |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index adebdf0c7..1f1b76c1e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import { values } from 'lodash' | ||
2 | import { | 1 | import { |
3 | AllowNull, | 2 | AllowNull, |
4 | BelongsTo, | 3 | BelongsTo, |
@@ -14,12 +13,12 @@ import { | |||
14 | UpdatedAt | 13 | UpdatedAt |
15 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
16 | import { | 15 | import { |
16 | isVideoFileExtnameValid, | ||
17 | isVideoFileInfoHashValid, | 17 | isVideoFileInfoHashValid, |
18 | isVideoFileResolutionValid, | 18 | isVideoFileResolutionValid, |
19 | isVideoFileSizeValid, | 19 | isVideoFileSizeValid, |
20 | isVideoFPSResolutionValid | 20 | isVideoFPSResolutionValid |
21 | } from '../../helpers/custom-validators/videos' | 21 | } from '../../helpers/custom-validators/videos' |
22 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
23 | import { throwIfNotValid } from '../utils' | 22 | import { throwIfNotValid } from '../utils' |
24 | import { VideoModel } from './video' | 23 | import { VideoModel } from './video' |
25 | import * as Sequelize from 'sequelize' | 24 | import * as Sequelize from 'sequelize' |
@@ -58,7 +57,8 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
58 | size: number | 57 | size: number |
59 | 58 | ||
60 | @AllowNull(false) | 59 | @AllowNull(false) |
61 | @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) | 60 | @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname')) |
61 | @Column | ||
62 | extname: string | 62 | extname: string |
63 | 63 | ||
64 | @AllowNull(false) | 64 | @AllowNull(false) |
@@ -120,6 +120,26 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
120 | return VideoFileModel.findById(id, options) | 120 | return VideoFileModel.findById(id, options) |
121 | } | 121 | } |
122 | 122 | ||
123 | static async getStats () { | ||
124 | let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [], | ||
128 | model: VideoModel.unscoped(), | ||
129 | where: { | ||
130 | remote: false | ||
131 | } | ||
132 | } | ||
133 | ] | ||
134 | } as any) | ||
135 | // Sequelize could return null... | ||
136 | if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0 | ||
137 | |||
138 | return { | ||
139 | totalLocalVideoFilesSize | ||
140 | } | ||
141 | } | ||
142 | |||
123 | hasSameUniqueKeysThan (other: VideoFileModel) { | 143 | hasSameUniqueKeysThan (other: VideoFileModel) { |
124 | return this.fps === other.fps && | 144 | return this.fps === other.fps && |
125 | this.resolution === other.resolution && | 145 | this.resolution === other.resolution && |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index e3f8d525b..de0747f22 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -2,7 +2,7 @@ import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | |||
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { VideoFileModel } from './video-file' | 3 | import { VideoFileModel } from './video-file' |
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
5 | import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' | 5 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' |
6 | import { VideoCaptionModel } from './video-caption' | 6 | import { VideoCaptionModel } from './video-caption' |
7 | import { | 7 | import { |
8 | getVideoCommentsActivityPubUrl, | 8 | getVideoCommentsActivityPubUrl, |
@@ -207,8 +207,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
207 | for (const file of video.VideoFiles) { | 207 | for (const file of video.VideoFiles) { |
208 | url.push({ | 208 | url.push({ |
209 | type: 'Link', | 209 | type: 'Link', |
210 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 210 | mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
211 | mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 211 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, |
212 | href: video.getVideoFileUrl(file, baseUrlHttp), | 212 | href: video.getVideoFileUrl(file, baseUrlHttp), |
213 | height: file.resolution, | 213 | height: file.resolution, |
214 | size: file.size, | 214 | size: file.size, |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8d442b3f8..c723e57c0 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -144,6 +144,10 @@ export class VideoImportModel extends Model<VideoImportModel> { | |||
144 | }) | 144 | }) |
145 | } | 145 | } |
146 | 146 | ||
147 | getTargetIdentifier () { | ||
148 | return this.targetUrl || this.magnetUri || this.torrentName | ||
149 | } | ||
150 | |||
147 | toFormattedJSON (): VideoImport { | 151 | toFormattedJSON (): VideoImport { |
148 | const videoFormatOptions = { | 152 | const videoFormatOptions = { |
149 | completeDescription: true, | 153 | completeDescription: true, |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0f18d9f0c..80a6c7832 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -94,6 +94,7 @@ import { | |||
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | 96 | import { UserModel } from '../account/user' |
97 | import { VideoImportModel } from './video-import' | ||
97 | 98 | ||
98 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 99 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
99 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 100 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -102,17 +103,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [ | |||
102 | { fields: [ 'createdAt' ] }, | 103 | { fields: [ 'createdAt' ] }, |
103 | { fields: [ 'publishedAt' ] }, | 104 | { fields: [ 'publishedAt' ] }, |
104 | { fields: [ 'duration' ] }, | 105 | { fields: [ 'duration' ] }, |
105 | { fields: [ 'category' ] }, | ||
106 | { fields: [ 'licence' ] }, | ||
107 | { fields: [ 'nsfw' ] }, | ||
108 | { fields: [ 'language' ] }, | ||
109 | { fields: [ 'waitTranscoding' ] }, | ||
110 | { fields: [ 'state' ] }, | ||
111 | { fields: [ 'remote' ] }, | ||
112 | { fields: [ 'views' ] }, | 106 | { fields: [ 'views' ] }, |
113 | { fields: [ 'likes' ] }, | ||
114 | { fields: [ 'channelId' ] }, | 107 | { fields: [ 'channelId' ] }, |
115 | { | 108 | { |
109 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
110 | where: { | ||
111 | category: { | ||
112 | [Sequelize.Op.ne]: null | ||
113 | } | ||
114 | } | ||
115 | }, | ||
116 | { | ||
117 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
118 | where: { | ||
119 | licence: { | ||
120 | [Sequelize.Op.ne]: null | ||
121 | } | ||
122 | } | ||
123 | }, | ||
124 | { | ||
125 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
126 | where: { | ||
127 | language: { | ||
128 | [Sequelize.Op.ne]: null | ||
129 | } | ||
130 | } | ||
131 | }, | ||
132 | { | ||
133 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
134 | where: { | ||
135 | nsfw: true | ||
136 | } | ||
137 | }, | ||
138 | { | ||
139 | fields: [ 'remote' ], // Only index local videos | ||
140 | where: { | ||
141 | remote: false | ||
142 | } | ||
143 | }, | ||
144 | { | ||
116 | fields: [ 'uuid' ], | 145 | fields: [ 'uuid' ], |
117 | unique: true | 146 | unique: true |
118 | }, | 147 | }, |
@@ -140,7 +169,7 @@ type ForAPIOptions = { | |||
140 | 169 | ||
141 | type AvailableForListIDsOptions = { | 170 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | 171 | serverAccountId: number |
143 | actorId: number | 172 | followerActorId: number |
144 | includeLocalVideos: boolean | 173 | includeLocalVideos: boolean |
145 | filter?: VideoFilter | 174 | filter?: VideoFilter |
146 | categoryOneOf?: number[] | 175 | categoryOneOf?: number[] |
@@ -153,7 +182,8 @@ type AvailableForListIDsOptions = { | |||
153 | accountId?: number | 182 | accountId?: number |
154 | videoChannelId?: number | 183 | videoChannelId?: number |
155 | trendingDays?: number | 184 | trendingDays?: number |
156 | user?: UserModel | 185 | user?: UserModel, |
186 | historyOfUser?: UserModel | ||
157 | } | 187 | } |
158 | 188 | ||
159 | @Scopes({ | 189 | @Scopes({ |
@@ -315,7 +345,7 @@ type AvailableForListIDsOptions = { | |||
315 | query.include.push(videoChannelInclude) | 345 | query.include.push(videoChannelInclude) |
316 | } | 346 | } |
317 | 347 | ||
318 | if (options.actorId) { | 348 | if (options.followerActorId) { |
319 | let localVideosReq = '' | 349 | let localVideosReq = '' |
320 | if (options.includeLocalVideos === true) { | 350 | if (options.includeLocalVideos === true) { |
321 | localVideosReq = ' UNION ALL ' + | 351 | localVideosReq = ' UNION ALL ' + |
@@ -327,7 +357,7 @@ type AvailableForListIDsOptions = { | |||
327 | } | 357 | } |
328 | 358 | ||
329 | // Force actorId to be a number to avoid SQL injections | 359 | // Force actorId to be a number to avoid SQL injections |
330 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 360 | const actorIdNumber = parseInt(options.followerActorId.toString(), 10) |
331 | query.where[ 'id' ][ Sequelize.Op.and ].push({ | 361 | query.where[ 'id' ][ Sequelize.Op.and ].push({ |
332 | [ Sequelize.Op.in ]: Sequelize.literal( | 362 | [ Sequelize.Op.in ]: Sequelize.literal( |
333 | '(' + | 363 | '(' + |
@@ -416,6 +446,21 @@ type AvailableForListIDsOptions = { | |||
416 | query.subQuery = false | 446 | query.subQuery = false |
417 | } | 447 | } |
418 | 448 | ||
449 | if (options.historyOfUser) { | ||
450 | query.include.push({ | ||
451 | model: UserVideoHistoryModel, | ||
452 | required: true, | ||
453 | where: { | ||
454 | userId: options.historyOfUser.id | ||
455 | } | ||
456 | }) | ||
457 | |||
458 | // Even if the relation is n:m, we know that a user only have 0..1 video history | ||
459 | // So we won't have multiple rows for the same video | ||
460 | // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel | ||
461 | query.subQuery = false | ||
462 | } | ||
463 | |||
419 | return query | 464 | return query |
420 | }, | 465 | }, |
421 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 466 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
@@ -741,6 +786,15 @@ export class VideoModel extends Model<VideoModel> { | |||
741 | }) | 786 | }) |
742 | VideoBlacklist: VideoBlacklistModel | 787 | VideoBlacklist: VideoBlacklistModel |
743 | 788 | ||
789 | @HasOne(() => VideoImportModel, { | ||
790 | foreignKey: { | ||
791 | name: 'videoId', | ||
792 | allowNull: true | ||
793 | }, | ||
794 | onDelete: 'set null' | ||
795 | }) | ||
796 | VideoImport: VideoImportModel | ||
797 | |||
744 | @HasMany(() => VideoCaptionModel, { | 798 | @HasMany(() => VideoCaptionModel, { |
745 | foreignKey: { | 799 | foreignKey: { |
746 | name: 'videoId', | 800 | name: 'videoId', |
@@ -985,9 +1039,10 @@ export class VideoModel extends Model<VideoModel> { | |||
985 | filter?: VideoFilter, | 1039 | filter?: VideoFilter, |
986 | accountId?: number, | 1040 | accountId?: number, |
987 | videoChannelId?: number, | 1041 | videoChannelId?: number, |
988 | actorId?: number | 1042 | followerActorId?: number |
989 | trendingDays?: number, | 1043 | trendingDays?: number, |
990 | user?: UserModel | 1044 | user?: UserModel, |
1045 | historyOfUser?: UserModel | ||
991 | }, countVideos = true) { | 1046 | }, countVideos = true) { |
992 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1047 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
993 | throw new Error('Try to filter all-local but no user has not the see all videos right') | 1048 | throw new Error('Try to filter all-local but no user has not the see all videos right') |
@@ -1008,11 +1063,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1008 | 1063 | ||
1009 | const serverActor = await getServerActor() | 1064 | const serverActor = await getServerActor() |
1010 | 1065 | ||
1011 | // actorId === null has a meaning, so just check undefined | 1066 | // followerActorId === null has a meaning, so just check undefined |
1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id | 1067 | const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id |
1013 | 1068 | ||
1014 | const queryOptions = { | 1069 | const queryOptions = { |
1015 | actorId, | 1070 | followerActorId, |
1016 | serverAccountId: serverActor.Account.id, | 1071 | serverAccountId: serverActor.Account.id, |
1017 | nsfw: options.nsfw, | 1072 | nsfw: options.nsfw, |
1018 | categoryOneOf: options.categoryOneOf, | 1073 | categoryOneOf: options.categoryOneOf, |
@@ -1026,6 +1081,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1026 | videoChannelId: options.videoChannelId, | 1081 | videoChannelId: options.videoChannelId, |
1027 | includeLocalVideos: options.includeLocalVideos, | 1082 | includeLocalVideos: options.includeLocalVideos, |
1028 | user: options.user, | 1083 | user: options.user, |
1084 | historyOfUser: options.historyOfUser, | ||
1029 | trendingDays | 1085 | trendingDays |
1030 | } | 1086 | } |
1031 | 1087 | ||
@@ -1118,7 +1174,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1118 | 1174 | ||
1119 | const serverActor = await getServerActor() | 1175 | const serverActor = await getServerActor() |
1120 | const queryOptions = { | 1176 | const queryOptions = { |
1121 | actorId: serverActor.id, | 1177 | followerActorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | 1178 | serverAccountId: serverActor.Account.id, |
1123 | includeLocalVideos: options.includeLocalVideos, | 1179 | includeLocalVideos: options.includeLocalVideos, |
1124 | nsfw: options.nsfw, | 1180 | nsfw: options.nsfw, |
@@ -1273,11 +1329,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1273 | // threshold corresponds to how many video the field should have to be returned | 1329 | // threshold corresponds to how many video the field should have to be returned |
1274 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1330 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1275 | const serverActor = await getServerActor() | 1331 | const serverActor = await getServerActor() |
1276 | const actorId = serverActor.id | 1332 | const followerActorId = serverActor.id |
1277 | 1333 | ||
1278 | const scopeOptions: AvailableForListIDsOptions = { | 1334 | const scopeOptions: AvailableForListIDsOptions = { |
1279 | serverAccountId: serverActor.Account.id, | 1335 | serverAccountId: serverActor.Account.id, |
1280 | actorId, | 1336 | followerActorId, |
1281 | includeLocalVideos: true | 1337 | includeLocalVideos: true |
1282 | } | 1338 | } |
1283 | 1339 | ||
@@ -1341,7 +1397,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1341 | } | 1397 | } |
1342 | 1398 | ||
1343 | const [ count, rowsId ] = await Promise.all([ | 1399 | const [ count, rowsId ] = await Promise.all([ |
1344 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), | 1400 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined), |
1345 | VideoModel.scope(idsScope).findAll(query) | 1401 | VideoModel.scope(idsScope).findAll(query) |
1346 | ]) | 1402 | ]) |
1347 | const ids = rowsId.map(r => r.id) | 1403 | const ids = rowsId.map(r => r.id) |
@@ -1481,6 +1537,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1481 | videoFile.infoHash = parsedTorrent.infoHash | 1537 | videoFile.infoHash = parsedTorrent.infoHash |
1482 | } | 1538 | } |
1483 | 1539 | ||
1540 | getWatchStaticPath () { | ||
1541 | return '/videos/watch/' + this.uuid | ||
1542 | } | ||
1543 | |||
1484 | getEmbedStaticPath () { | 1544 | getEmbedStaticPath () { |
1485 | return '/videos/embed/' + this.uuid | 1545 | return '/videos/embed/' + this.uuid |
1486 | } | 1546 | } |
@@ -1538,8 +1598,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1538 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | 1598 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) |
1539 | } | 1599 | } |
1540 | 1600 | ||
1541 | removeFile (videoFile: VideoFileModel) { | 1601 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1542 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1602 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1603 | |||
1604 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | ||
1543 | return remove(filePath) | 1605 | return remove(filePath) |
1544 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1606 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1545 | } | 1607 | } |
@@ -1617,6 +1679,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1617 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 1679 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1618 | } | 1680 | } |
1619 | 1681 | ||
1682 | getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | ||
1683 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | ||
1684 | } | ||
1685 | |||
1620 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1686 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1621 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1687 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1622 | } | 1688 | } |