aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
committerChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
commit73471b1a52f242e86364ffb077ea6cadb3b07ae2 (patch)
tree43dbb7748e281f8d80f15326f489cdea10ec857d /server/models/video
parentc22419dd265c0c7185bf4197a1cb286eb3d8ebc0 (diff)
parentf5305c04aae14467d6f957b713c5a902275cbb89 (diff)
downloadPeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.gz
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.zst
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.zip
Merge branch 'release/v1.2.0'
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-abuse.ts24
-rw-r--r--server/models/video/video-blacklist.ts31
-rw-r--r--server/models/video/video-channel.ts25
-rw-r--r--server/models/video/video-comment.ts45
-rw-r--r--server/models/video/video-file.ts26
-rw-r--r--server/models/video/video-format-utils.ts6
-rw-r--r--server/models/video/video-import.ts4
-rw-r--r--server/models/video/video.ts112
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 @@
1import { 1import { 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'
15import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
16import { VideoAbuse } from '../../../shared/models/videos' 3import { VideoAbuse } from '../../../shared/models/videos'
17import { 4import {
@@ -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'
22import { Emailer } from '../../lib/emailer'
23import { AccountModel } from '../account/account' 9import { AccountModel } from '../account/account'
24import { getSort, throwIfNotValid } from '../utils' 10import { getSort, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 11import { VideoModel } from './video'
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
40export class VideoAbuseModel extends Model<VideoAbuseModel> { 26export 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 @@
1import { 1import { 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'
15import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 2import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 3import { VideoModel } from './video'
17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 4import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
18import { Emailer } from '../../lib/emailer'
19import { VideoBlacklist } from '../../../shared/models/videos' 5import { VideoBlacklist } from '../../../shared/models/videos'
20import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { 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
18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
19import { VideoComment } from '../../../shared/models/videos/video-comment.model' 19import { VideoComment } from '../../../shared/models/videos/video-comment.model'
20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21import { CONSTRAINTS_FIELDS } from '../../initializers' 21import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
22import { sendDeleteVideoComment } from '../../lib/activitypub/send' 22import { sendDeleteVideoComment } from '../../lib/activitypub/send'
23import { AccountModel } from '../account/account' 23import { AccountModel } from '../account/account'
24import { ActorModel } from '../activitypub/actor' 24import { ActorModel } from '../activitypub/actor'
@@ -29,6 +29,9 @@ import { VideoModel } from './video'
29import { VideoChannelModel } from './video-channel' 29import { VideoChannelModel } from './video-channel'
30import { getServerActor } from '../../helpers/utils' 30import { getServerActor } from '../../helpers/utils'
31import { UserModel } from '../account/user' 31import { UserModel } from '../account/user'
32import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
33import { regexpCapture } from '../../helpers/regexp'
34import { uniq } from 'lodash'
32 35
33enum ScopeNames { 36enum 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 @@
1import { values } from 'lodash'
2import { 1import {
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'
16import { 15import {
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'
22import { CONSTRAINTS_FIELDS } from '../../initializers'
23import { throwIfNotValid } from '../utils' 22import { throwIfNotValid } from '../utils'
24import { VideoModel } from './video' 23import { VideoModel } from './video'
25import * as Sequelize from 'sequelize' 24import * 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'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file' 3import { VideoFileModel } from './video-file'
4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' 4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
5import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' 5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
6import { VideoCaptionModel } from './video-caption' 6import { VideoCaptionModel } from './video-caption'
7import { 7import {
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 {
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { 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
99const indexes: Sequelize.DefineIndexesOptions[] = [ 100const 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
141type AvailableForListIDsOptions = { 170type 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 }