aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/thumbnail.ts15
-rw-r--r--server/models/video/video-abuse.ts6
-rw-r--r--server/models/video/video-caption.ts27
-rw-r--r--server/models/video/video-channel.ts33
-rw-r--r--server/models/video/video-comment.ts20
-rw-r--r--server/models/video/video-format-utils.ts34
-rw-r--r--server/models/video/video-playlist-element.ts8
-rw-r--r--server/models/video/video-playlist.ts36
-rw-r--r--server/models/video/video.ts372
9 files changed, 302 insertions, 249 deletions
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 3b011b1d2..e396784d2 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -19,6 +19,8 @@ import { CONFIG } from '../../initializers/config'
19import { VideoModel } from './video' 19import { VideoModel } from './video'
20import { VideoPlaylistModel } from './video-playlist' 20import { VideoPlaylistModel } from './video-playlist'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { MVideoAccountLight } from '@server/typings/models'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
22 24
23@Table({ 25@Table({
24 tableName: 'thumbnail', 26 tableName: 'thumbnail',
@@ -90,7 +92,7 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
90 @UpdatedAt 92 @UpdatedAt
91 updatedAt: Date 93 updatedAt: Date
92 94
93 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { 95 private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
94 [ThumbnailType.MINIATURE]: { 96 [ThumbnailType.MINIATURE]: {
95 label: 'miniature', 97 label: 'miniature',
96 directory: CONFIG.STORAGE.THUMBNAILS_DIR, 98 directory: CONFIG.STORAGE.THUMBNAILS_DIR,
@@ -126,11 +128,14 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
126 return videoUUID + '.jpg' 128 return videoUUID + '.jpg'
127 } 129 }
128 130
129 getFileUrl (isLocal: boolean) { 131 getFileUrl (video: MVideoAccountLight) {
130 if (isLocal === false) return this.fileUrl 132 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
131 133
132 const staticPath = ThumbnailModel.types[this.type].staticPath 134 if (video.isOwned()) return WEBSERVER.URL + staticPath
133 return WEBSERVER.URL + staticPath + this.filename 135 if (this.fileUrl) return this.fileUrl
136
137 // Fallback if we don't have a file URL
138 return buildRemoteVideoBaseUrl(video, staticPath)
134 } 139 }
135 140
136 getPath () { 141 getPath () {
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 3636db18d..da8c1577c 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -87,9 +87,9 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
87 } 87 }
88 88
89 static listForApi (parameters: { 89 static listForApi (parameters: {
90 start: number, 90 start: number
91 count: number, 91 count: number
92 sort: string, 92 sort: string
93 serverAccountId: number 93 serverAccountId: number
94 user?: MUserAccountId 94 user?: MUserAccountId
95 }) { 95 }) {
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index eeb2a4afd..59d3e1050 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -5,6 +5,7 @@ import {
5 BelongsTo, 5 BelongsTo,
6 Column, 6 Column,
7 CreatedAt, 7 CreatedAt,
8 DataType,
8 ForeignKey, 9 ForeignKey,
9 Is, 10 Is,
10 Model, 11 Model,
@@ -16,13 +17,14 @@ import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 17import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 18import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 19import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
19import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants' 20import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
20import { join } from 'path' 21import { join } from 'path'
21import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
22import { remove } from 'fs-extra' 23import { remove } from 'fs-extra'
23import { CONFIG } from '../../initializers/config' 24import { CONFIG } from '../../initializers/config'
24import * as Bluebird from 'bluebird' 25import * as Bluebird from 'bluebird'
25import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models' 26import { MVideoAccountLight, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
27import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
26 28
27export enum ScopeNames { 29export enum ScopeNames {
28 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -64,6 +66,10 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
64 @Column 66 @Column
65 language: string 67 language: string
66 68
69 @AllowNull(true)
70 @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
71 fileUrl: string
72
67 @ForeignKey(() => VideoModel) 73 @ForeignKey(() => VideoModel)
68 @Column 74 @Column
69 videoId: number 75 videoId: number
@@ -114,13 +120,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
114 return VideoCaptionModel.findOne(query) 120 return VideoCaptionModel.findOne(query)
115 } 121 }
116 122
117 static insertOrReplaceLanguage (videoId: number, language: string, transaction: Transaction) { 123 static insertOrReplaceLanguage (videoId: number, language: string, fileUrl: string, transaction: Transaction) {
118 const values = { 124 const values = {
119 videoId, 125 videoId,
120 language 126 language,
127 fileUrl
121 } 128 }
122 129
123 return (VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) as any) // FIXME: typings 130 return VideoCaptionModel.upsert(values, { transaction, returning: true })
124 .then(([ caption ]) => caption) 131 .then(([ caption ]) => caption)
125 } 132 }
126 133
@@ -175,4 +182,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
175 removeCaptionFile (this: MVideoCaptionFormattable) { 182 removeCaptionFile (this: MVideoCaptionFormattable) {
176 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName()) 183 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
177 } 184 }
185
186 getFileUrl (video: MVideoAccountLight) {
187 if (!this.Video) this.Video = video as VideoModel
188
189 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
190 if (this.fileUrl) return this.fileUrl
191
192 // Fallback if we don't have a file URL
193 return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
194 }
178} 195}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index e10adcb3a..835216671 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -30,7 +30,7 @@ import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttr
30import { VideoModel } from './video' 30import { VideoModel } from './video'
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize' 33import { FindOptions, Op } from 'sequelize'
34import { AvatarModel } from '../avatar/avatar' 34import { AvatarModel } from '../avatar/avatar'
35import { VideoPlaylistModel } from './video-playlist' 35import { VideoPlaylistModel } from './video-playlist'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
@@ -43,18 +43,6 @@ import {
43 MChannelSummaryFormattable 43 MChannelSummaryFormattable
44} from '../../typings/models/video' 44} from '../../typings/models/video'
45 45
46// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
47const indexes: ModelIndexesOptions[] = [
48 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
49
50 {
51 fields: [ 'accountId' ]
52 },
53 {
54 fields: [ 'actorId' ]
55 }
56]
57
58export enum ScopeNames { 46export enum ScopeNames {
59 FOR_API = 'FOR_API', 47 FOR_API = 'FOR_API',
60 WITH_ACCOUNT = 'WITH_ACCOUNT', 48 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -133,7 +121,7 @@ export type SummaryOptions = {
133 }, 121 },
134 { 122 {
135 serverId: { 123 serverId: {
136 [ Op.in ]: Sequelize.literal(inQueryInstanceFollow) 124 [Op.in]: Sequelize.literal(inQueryInstanceFollow)
137 } 125 }
138 } 126 }
139 ] 127 ]
@@ -176,7 +164,16 @@ export type SummaryOptions = {
176})) 164}))
177@Table({ 165@Table({
178 tableName: 'videoChannel', 166 tableName: 'videoChannel',
179 indexes 167 indexes: [
168 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
169
170 {
171 fields: [ 'accountId' ]
172 },
173 {
174 fields: [ 'actorId' ]
175 }
176 ]
180}) 177})
181export class VideoChannelModel extends Model<VideoChannelModel> { 178export class VideoChannelModel extends Model<VideoChannelModel> {
182 179
@@ -351,9 +348,9 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
351 } 348 }
352 349
353 static listByAccount (options: { 350 static listByAccount (options: {
354 accountId: number, 351 accountId: number
355 start: number, 352 start: number
356 count: number, 353 count: number
357 sort: string 354 sort: string
358 }) { 355 }) {
359 const query = { 356 const query = {
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index fb4d16b4d..b33c33d5e 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -257,10 +257,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
257 } 257 }
258 258
259 static async listThreadsForApi (parameters: { 259 static async listThreadsForApi (parameters: {
260 videoId: number, 260 videoId: number
261 start: number, 261 start: number
262 count: number, 262 count: number
263 sort: string, 263 sort: string
264 user?: MUserAccountId 264 user?: MUserAccountId
265 }) { 265 }) {
266 const { videoId, start, count, sort, user } = parameters 266 const { videoId, start, count, sort, user } = parameters
@@ -300,8 +300,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
300 } 300 }
301 301
302 static async listThreadCommentsForApi (parameters: { 302 static async listThreadCommentsForApi (parameters: {
303 videoId: number, 303 videoId: number
304 threadId: number, 304 threadId: number
305 user?: MUserAccountId 305 user?: MUserAccountId
306 }) { 306 }) {
307 const { videoId, threadId, user } = parameters 307 const { videoId, threadId, user } = parameters
@@ -314,7 +314,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
314 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, 314 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order,
315 where: { 315 where: {
316 videoId, 316 videoId,
317 [ Op.or ]: [ 317 [Op.or]: [
318 { id: threadId }, 318 { id: threadId },
319 { originCommentId: threadId } 319 { originCommentId: threadId }
320 ], 320 ],
@@ -346,7 +346,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
346 order: [ [ 'createdAt', order ] ] as Order, 346 order: [ [ 'createdAt', order ] ] as Order,
347 where: { 347 where: {
348 id: { 348 id: {
349 [ Op.in ]: Sequelize.literal('(' + 349 [Op.in]: Sequelize.literal('(' +
350 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + 350 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
351 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + 351 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
352 'UNION ' + 352 'UNION ' +
@@ -355,7 +355,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
355 ') ' + 355 ') ' +
356 'SELECT id FROM children' + 356 'SELECT id FROM children' +
357 ')'), 357 ')'),
358 [ Op.ne ]: comment.id 358 [Op.ne]: comment.id
359 } 359 }
360 }, 360 },
361 transaction: t 361 transaction: t
@@ -461,7 +461,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
461 } 461 }
462 462
463 isDeleted () { 463 isDeleted () {
464 return null !== this.deletedAt 464 return this.deletedAt !== null
465 } 465 }
466 466
467 extractMentions () { 467 extractMentions () {
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 67395e5c0..1fa66fd63 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -27,12 +27,13 @@ import { generateMagnetUri } from '@server/helpers/webtorrent'
27export type VideoFormattingJSONOptions = { 27export type VideoFormattingJSONOptions = {
28 completeDescription?: boolean 28 completeDescription?: boolean
29 additionalAttributes: { 29 additionalAttributes: {
30 state?: boolean, 30 state?: boolean
31 waitTranscoding?: boolean, 31 waitTranscoding?: boolean
32 scheduledUpdate?: boolean, 32 scheduledUpdate?: boolean
33 blacklistInfo?: boolean 33 blacklistInfo?: boolean
34 } 34 }
35} 35}
36
36function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video { 37function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
37 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined 38 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
38 39
@@ -181,12 +182,10 @@ function videoFilesModelToFormattedJSON (
181): VideoFile[] { 182): VideoFile[] {
182 return videoFiles 183 return videoFiles
183 .map(videoFile => { 184 .map(videoFile => {
184 let resolutionLabel = videoFile.resolution + 'p'
185
186 return { 185 return {
187 resolution: { 186 resolution: {
188 id: videoFile.resolution, 187 id: videoFile.resolution,
189 label: resolutionLabel 188 label: videoFile.resolution + 'p'
190 }, 189 },
191 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), 190 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs),
192 size: videoFile.size, 191 size: videoFile.size,
@@ -214,7 +213,7 @@ function addVideoFilesInAPAcc (
214 for (const file of files) { 213 for (const file of files) {
215 acc.push({ 214 acc.push({
216 type: 'Link', 215 type: 'Link',
217 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, 216 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any,
218 href: model.getVideoFileUrl(file, baseUrlHttp), 217 href: model.getVideoFileUrl(file, baseUrlHttp),
219 height: file.resolution, 218 height: file.resolution,
220 size: file.size, 219 size: file.size,
@@ -282,10 +281,8 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
282 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) 281 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
283 282
284 for (const playlist of (video.VideoStreamingPlaylists || [])) { 283 for (const playlist of (video.VideoStreamingPlaylists || [])) {
285 let tag: ActivityTagObject[] 284 const tag = playlist.p2pMediaLoaderInfohashes
286 285 .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[]
287 tag = playlist.p2pMediaLoaderInfohashes
288 .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
289 tag.push({ 286 tag.push({
290 type: 'Link', 287 type: 'Link',
291 name: 'sha256', 288 name: 'sha256',
@@ -308,11 +305,12 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
308 for (const caption of video.VideoCaptions) { 305 for (const caption of video.VideoCaptions) {
309 subtitleLanguage.push({ 306 subtitleLanguage.push({
310 identifier: caption.language, 307 identifier: caption.language,
311 name: VideoCaptionModel.getLanguageLabel(caption.language) 308 name: VideoCaptionModel.getLanguageLabel(caption.language),
309 url: caption.getFileUrl(video)
312 }) 310 })
313 } 311 }
314 312
315 const miniature = video.getMiniature() 313 const icons = [ video.getMiniature(), video.getPreview() ]
316 314
317 return { 315 return {
318 type: 'Video' as 'Video', 316 type: 'Video' as 'Video',
@@ -337,13 +335,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
337 content: video.getTruncatedDescription(), 335 content: video.getTruncatedDescription(),
338 support: video.support, 336 support: video.support,
339 subtitleLanguage, 337 subtitleLanguage,
340 icon: { 338 icon: icons.map(i => ({
341 type: 'Image', 339 type: 'Image',
342 url: miniature.getFileUrl(video.isOwned()), 340 url: i.getFileUrl(video),
343 mediaType: 'image/jpeg', 341 mediaType: 'image/jpeg',
344 width: miniature.width, 342 width: i.width,
345 height: miniature.height 343 height: i.height
346 }, 344 })),
347 url, 345 url,
348 likes: getVideoLikesActivityPubUrl(video), 346 likes: getVideoLikesActivityPubUrl(video),
349 dislikes: getVideoDislikesActivityPubUrl(video), 347 dislikes: getVideoDislikesActivityPubUrl(video),
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index f2d71357f..4ba16f5fd 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -120,10 +120,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
120 } 120 }
121 121
122 static listForApi (options: { 122 static listForApi (options: {
123 start: number, 123 start: number
124 count: number, 124 count: number
125 videoPlaylistId: number, 125 videoPlaylistId: number
126 serverAccount: AccountModel, 126 serverAccount: AccountModel
127 user?: MUserAccountId 127 user?: MUserAccountId
128 }) { 128 }) {
129 const accountIds = [ options.serverAccount.id ] 129 const accountIds = [ options.serverAccount.id ]
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index bcdda36e5..4ca17ebec 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -68,12 +68,12 @@ type AvailableForListOptions = {
68 type?: VideoPlaylistType 68 type?: VideoPlaylistType
69 accountId?: number 69 accountId?: number
70 videoChannelId?: number 70 videoChannelId?: number
71 listMyPlaylists?: boolean, 71 listMyPlaylists?: boolean
72 search?: string 72 search?: string
73} 73}
74 74
75@Scopes(() => ({ 75@Scopes(() => ({
76 [ ScopeNames.WITH_THUMBNAIL ]: { 76 [ScopeNames.WITH_THUMBNAIL]: {
77 include: [ 77 include: [
78 { 78 {
79 model: ThumbnailModel, 79 model: ThumbnailModel,
@@ -81,7 +81,7 @@ type AvailableForListOptions = {
81 } 81 }
82 ] 82 ]
83 }, 83 },
84 [ ScopeNames.WITH_VIDEOS_LENGTH ]: { 84 [ScopeNames.WITH_VIDEOS_LENGTH]: {
85 attributes: { 85 attributes: {
86 include: [ 86 include: [
87 [ 87 [
@@ -91,7 +91,7 @@ type AvailableForListOptions = {
91 ] 91 ]
92 } 92 }
93 } as FindOptions, 93 } as FindOptions,
94 [ ScopeNames.WITH_ACCOUNT ]: { 94 [ScopeNames.WITH_ACCOUNT]: {
95 include: [ 95 include: [
96 { 96 {
97 model: AccountModel, 97 model: AccountModel,
@@ -99,7 +99,7 @@ type AvailableForListOptions = {
99 } 99 }
100 ] 100 ]
101 }, 101 },
102 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { 102 [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: {
103 include: [ 103 include: [
104 { 104 {
105 model: AccountModel.scope(AccountScopeNames.SUMMARY), 105 model: AccountModel.scope(AccountScopeNames.SUMMARY),
@@ -111,7 +111,7 @@ type AvailableForListOptions = {
111 } 111 }
112 ] 112 ]
113 }, 113 },
114 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: { 114 [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
115 include: [ 115 include: [
116 { 116 {
117 model: AccountModel, 117 model: AccountModel,
@@ -123,7 +123,7 @@ type AvailableForListOptions = {
123 } 123 }
124 ] 124 ]
125 }, 125 },
126 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { 126 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
127 127
128 let whereActor: WhereOptions = {} 128 let whereActor: WhereOptions = {}
129 129
@@ -138,13 +138,13 @@ type AvailableForListOptions = {
138 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) 138 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
139 139
140 whereActor = { 140 whereActor = {
141 [ Op.or ]: [ 141 [Op.or]: [
142 { 142 {
143 serverId: null 143 serverId: null
144 }, 144 },
145 { 145 {
146 serverId: { 146 serverId: {
147 [ Op.in ]: literal(inQueryInstanceFollow) 147 [Op.in]: literal(inQueryInstanceFollow)
148 } 148 }
149 } 149 }
150 ] 150 ]
@@ -172,7 +172,7 @@ type AvailableForListOptions = {
172 if (options.search) { 172 if (options.search) {
173 whereAnd.push({ 173 whereAnd.push({
174 name: { 174 name: {
175 [ Op.iLike ]: '%' + options.search + '%' 175 [Op.iLike]: '%' + options.search + '%'
176 } 176 }
177 }) 177 })
178 } 178 }
@@ -299,13 +299,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
299 299
300 static listForApi (options: { 300 static listForApi (options: {
301 followerActorId: number 301 followerActorId: number
302 start: number, 302 start: number
303 count: number, 303 count: number
304 sort: string, 304 sort: string
305 type?: VideoPlaylistType, 305 type?: VideoPlaylistType
306 accountId?: number, 306 accountId?: number
307 videoChannelId?: number, 307 videoChannelId?: number
308 listMyPlaylists?: boolean, 308 listMyPlaylists?: boolean
309 search?: string 309 search?: string
310 }) { 310 }) {
311 const query = { 311 const query = {
@@ -369,7 +369,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
369 model: VideoPlaylistElementModel.unscoped(), 369 model: VideoPlaylistElementModel.unscoped(),
370 where: { 370 where: {
371 videoId: { 371 videoId: {
372 [Op.in]: videoIds // FIXME: sequelize ANY seems broken 372 [Op.in]: videoIds
373 } 373 }
374 }, 374 },
375 required: true 375 required: true
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index eacffe186..bd4ca63ea 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,18 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import { join } from 'path' 3import { join } from 'path'
4import { 4import { CountOptions, FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
5 CountOptions,
6 FindOptions,
7 IncludeOptions,
8 ModelIndexesOptions,
9 Op,
10 QueryTypes,
11 ScopeOptions,
12 Sequelize,
13 Transaction,
14 WhereOptions
15} from 'sequelize'
16import { 5import {
17 AllowNull, 6 AllowNull,
18 BeforeDestroy, 7 BeforeDestroy,
@@ -131,87 +120,19 @@ import {
131 MVideoFormattableDetails, 120 MVideoFormattableDetails,
132 MVideoForUser, 121 MVideoForUser,
133 MVideoFullLight, 122 MVideoFullLight,
134 MVideoIdThumbnail, 123 MVideoIdThumbnail, MVideoImmutable,
135 MVideoThumbnail, 124 MVideoThumbnail,
136 MVideoThumbnailBlacklist, 125 MVideoThumbnailBlacklist,
137 MVideoWithAllFiles, 126 MVideoWithAllFiles,
138 MVideoWithFile, 127 MVideoWithFile,
139 MVideoWithRights, 128 MVideoWithRights
140 MStreamingPlaylistFiles
141} from '../../typings/models' 129} from '../../typings/models'
142import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' 130import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
143import { MThumbnail } from '../../typings/models/video/thumbnail' 131import { MThumbnail } from '../../typings/models/video/thumbnail'
144import { VideoFile } from '@shared/models/videos/video-file.model' 132import { VideoFile } from '@shared/models/videos/video-file.model'
145import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 133import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
146import validator from 'validator' 134import validator from 'validator'
147 135import { ModelCache } from '@server/models/model-cache'
148// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
149const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
150 buildTrigramSearchIndex('video_name_trigram', 'name'),
151
152 { fields: [ 'createdAt' ] },
153 {
154 fields: [
155 { name: 'publishedAt', order: 'DESC' },
156 { name: 'id', order: 'ASC' }
157 ]
158 },
159 { fields: [ 'duration' ] },
160 { fields: [ 'views' ] },
161 { fields: [ 'channelId' ] },
162 {
163 fields: [ 'originallyPublishedAt' ],
164 where: {
165 originallyPublishedAt: {
166 [Op.ne]: null
167 }
168 }
169 },
170 {
171 fields: [ 'category' ], // We don't care videos with an unknown category
172 where: {
173 category: {
174 [Op.ne]: null
175 }
176 }
177 },
178 {
179 fields: [ 'licence' ], // We don't care videos with an unknown licence
180 where: {
181 licence: {
182 [Op.ne]: null
183 }
184 }
185 },
186 {
187 fields: [ 'language' ], // We don't care videos with an unknown language
188 where: {
189 language: {
190 [Op.ne]: null
191 }
192 }
193 },
194 {
195 fields: [ 'nsfw' ], // Most of the videos are not NSFW
196 where: {
197 nsfw: true
198 }
199 },
200 {
201 fields: [ 'remote' ], // Only index local videos
202 where: {
203 remote: false
204 }
205 },
206 {
207 fields: [ 'uuid' ],
208 unique: true
209 },
210 {
211 fields: [ 'url' ],
212 unique: true
213 }
214]
215 136
216export enum ScopeNames { 137export enum ScopeNames {
217 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 138 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -224,6 +145,7 @@ export enum ScopeNames {
224 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 145 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
225 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 146 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
226 WITH_USER_ID = 'WITH_USER_ID', 147 WITH_USER_ID = 'WITH_USER_ID',
148 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
227 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 149 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
228} 150}
229 151
@@ -267,7 +189,10 @@ export type AvailableForListIDsOptions = {
267} 189}
268 190
269@Scopes(() => ({ 191@Scopes(() => ({
270 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 192 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
193 attributes: [ 'id', 'url', 'uuid', 'remote' ]
194 },
195 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
271 const query: FindOptions = { 196 const query: FindOptions = {
272 include: [ 197 include: [
273 { 198 {
@@ -292,7 +217,7 @@ export type AvailableForListIDsOptions = {
292 if (options.ids) { 217 if (options.ids) {
293 query.where = { 218 query.where = {
294 id: { 219 id: {
295 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken 220 [Op.in]: options.ids
296 } 221 }
297 } 222 }
298 } 223 }
@@ -316,7 +241,7 @@ export type AvailableForListIDsOptions = {
316 241
317 return query 242 return query
318 }, 243 },
319 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 244 [ScopeNames.AVAILABLE_FOR_LIST_IDS]: (options: AvailableForListIDsOptions) => {
320 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : [] 245 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : []
321 246
322 const query: FindOptions = { 247 const query: FindOptions = {
@@ -327,11 +252,11 @@ export type AvailableForListIDsOptions = {
327 const attributesType = options.attributesType || 'id' 252 const attributesType = options.attributesType || 'id'
328 253
329 if (attributesType === 'id') query.attributes = [ 'id' ] 254 if (attributesType === 'id') query.attributes = [ 'id' ]
330 else if (attributesType === 'none') query.attributes = [ ] 255 else if (attributesType === 'none') query.attributes = []
331 256
332 whereAnd.push({ 257 whereAnd.push({
333 id: { 258 id: {
334 [ Op.notIn ]: Sequelize.literal( 259 [Op.notIn]: Sequelize.literal(
335 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' 260 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
336 ) 261 )
337 } 262 }
@@ -340,7 +265,7 @@ export type AvailableForListIDsOptions = {
340 if (options.serverAccountId) { 265 if (options.serverAccountId) {
341 whereAnd.push({ 266 whereAnd.push({
342 channelId: { 267 channelId: {
343 [ Op.notIn ]: Sequelize.literal( 268 [Op.notIn]: Sequelize.literal(
344 '(' + 269 '(' +
345 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + 270 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
346 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + 271 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
@@ -353,15 +278,14 @@ export type AvailableForListIDsOptions = {
353 278
354 // Only list public/published videos 279 // Only list public/published videos
355 if (!options.filter || options.filter !== 'all-local') { 280 if (!options.filter || options.filter !== 'all-local') {
356
357 const publishWhere = { 281 const publishWhere = {
358 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 282 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
359 [ Op.or ]: [ 283 [Op.or]: [
360 { 284 {
361 state: VideoState.PUBLISHED 285 state: VideoState.PUBLISHED
362 }, 286 },
363 { 287 {
364 [ Op.and ]: { 288 [Op.and]: {
365 state: VideoState.TO_TRANSCODE, 289 state: VideoState.TO_TRANSCODE,
366 waitTranscoding: false 290 waitTranscoding: false
367 } 291 }
@@ -448,7 +372,7 @@ export type AvailableForListIDsOptions = {
448 [Op.or]: [ 372 [Op.or]: [
449 { 373 {
450 id: { 374 id: {
451 [ Op.in ]: Sequelize.literal( 375 [Op.in]: Sequelize.literal(
452 '(' + 376 '(' +
453 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 377 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
454 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 378 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
@@ -459,7 +383,7 @@ export type AvailableForListIDsOptions = {
459 }, 383 },
460 { 384 {
461 id: { 385 id: {
462 [ Op.in ]: Sequelize.literal( 386 [Op.in]: Sequelize.literal(
463 '(' + 387 '(' +
464 'SELECT "video"."id" AS "id" FROM "video" ' + 388 'SELECT "video"."id" AS "id" FROM "video" ' +
465 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 389 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
@@ -479,7 +403,7 @@ export type AvailableForListIDsOptions = {
479 if (options.withFiles === true) { 403 if (options.withFiles === true) {
480 whereAnd.push({ 404 whereAnd.push({
481 id: { 405 id: {
482 [ Op.in ]: Sequelize.literal( 406 [Op.in]: Sequelize.literal(
483 '(SELECT "videoId" FROM "videoFile")' 407 '(SELECT "videoId" FROM "videoFile")'
484 ) 408 )
485 } 409 }
@@ -493,7 +417,7 @@ export type AvailableForListIDsOptions = {
493 417
494 whereAnd.push({ 418 whereAnd.push({
495 id: { 419 id: {
496 [ Op.in ]: Sequelize.literal( 420 [Op.in]: Sequelize.literal(
497 '(' + 421 '(' +
498 'SELECT "videoId" FROM "videoTag" ' + 422 'SELECT "videoId" FROM "videoTag" ' +
499 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 423 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -509,7 +433,7 @@ export type AvailableForListIDsOptions = {
509 433
510 whereAnd.push({ 434 whereAnd.push({
511 id: { 435 id: {
512 [ Op.in ]: Sequelize.literal( 436 [Op.in]: Sequelize.literal(
513 '(' + 437 '(' +
514 'SELECT "videoId" FROM "videoTag" ' + 438 'SELECT "videoId" FROM "videoTag" ' +
515 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 439 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -529,7 +453,7 @@ export type AvailableForListIDsOptions = {
529 if (options.categoryOneOf) { 453 if (options.categoryOneOf) {
530 whereAnd.push({ 454 whereAnd.push({
531 category: { 455 category: {
532 [ Op.or ]: options.categoryOneOf 456 [Op.or]: options.categoryOneOf
533 } 457 }
534 }) 458 })
535 } 459 }
@@ -537,7 +461,7 @@ export type AvailableForListIDsOptions = {
537 if (options.licenceOneOf) { 461 if (options.licenceOneOf) {
538 whereAnd.push({ 462 whereAnd.push({
539 licence: { 463 licence: {
540 [ Op.or ]: options.licenceOneOf 464 [Op.or]: options.licenceOneOf
541 } 465 }
542 }) 466 })
543 } 467 }
@@ -552,12 +476,12 @@ export type AvailableForListIDsOptions = {
552 [Op.or]: [ 476 [Op.or]: [
553 { 477 {
554 language: { 478 language: {
555 [ Op.or ]: videoLanguages 479 [Op.or]: videoLanguages
556 } 480 }
557 }, 481 },
558 { 482 {
559 id: { 483 id: {
560 [ Op.in ]: Sequelize.literal( 484 [Op.in]: Sequelize.literal(
561 '(' + 485 '(' +
562 'SELECT "videoId" FROM "videoCaption" ' + 486 'SELECT "videoId" FROM "videoCaption" ' +
563 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' + 487 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' +
@@ -591,12 +515,12 @@ export type AvailableForListIDsOptions = {
591 } 515 }
592 516
593 query.where = { 517 query.where = {
594 [ Op.and ]: whereAnd 518 [Op.and]: whereAnd
595 } 519 }
596 520
597 return query 521 return query
598 }, 522 },
599 [ ScopeNames.WITH_THUMBNAILS ]: { 523 [ScopeNames.WITH_THUMBNAILS]: {
600 include: [ 524 include: [
601 { 525 {
602 model: ThumbnailModel, 526 model: ThumbnailModel,
@@ -604,7 +528,7 @@ export type AvailableForListIDsOptions = {
604 } 528 }
605 ] 529 ]
606 }, 530 },
607 [ ScopeNames.WITH_USER_ID ]: { 531 [ScopeNames.WITH_USER_ID]: {
608 include: [ 532 include: [
609 { 533 {
610 attributes: [ 'accountId' ], 534 attributes: [ 'accountId' ],
@@ -620,7 +544,7 @@ export type AvailableForListIDsOptions = {
620 } 544 }
621 ] 545 ]
622 }, 546 },
623 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 547 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
624 include: [ 548 include: [
625 { 549 {
626 model: VideoChannelModel.unscoped(), 550 model: VideoChannelModel.unscoped(),
@@ -672,10 +596,10 @@ export type AvailableForListIDsOptions = {
672 } 596 }
673 ] 597 ]
674 }, 598 },
675 [ ScopeNames.WITH_TAGS ]: { 599 [ScopeNames.WITH_TAGS]: {
676 include: [ TagModel ] 600 include: [ TagModel ]
677 }, 601 },
678 [ ScopeNames.WITH_BLACKLISTED ]: { 602 [ScopeNames.WITH_BLACKLISTED]: {
679 include: [ 603 include: [
680 { 604 {
681 attributes: [ 'id', 'reason', 'unfederated' ], 605 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -684,7 +608,7 @@ export type AvailableForListIDsOptions = {
684 } 608 }
685 ] 609 ]
686 }, 610 },
687 [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => { 611 [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
688 let subInclude: any[] = [] 612 let subInclude: any[] = []
689 613
690 if (withRedundancies === true) { 614 if (withRedundancies === true) {
@@ -708,7 +632,7 @@ export type AvailableForListIDsOptions = {
708 ] 632 ]
709 } 633 }
710 }, 634 },
711 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { 635 [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
712 const subInclude: IncludeOptions[] = [ 636 const subInclude: IncludeOptions[] = [
713 { 637 {
714 model: VideoFileModel.unscoped(), 638 model: VideoFileModel.unscoped(),
@@ -735,7 +659,7 @@ export type AvailableForListIDsOptions = {
735 ] 659 ]
736 } 660 }
737 }, 661 },
738 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 662 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
739 include: [ 663 include: [
740 { 664 {
741 model: ScheduleVideoUpdateModel.unscoped(), 665 model: ScheduleVideoUpdateModel.unscoped(),
@@ -743,7 +667,7 @@ export type AvailableForListIDsOptions = {
743 } 667 }
744 ] 668 ]
745 }, 669 },
746 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { 670 [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
747 return { 671 return {
748 include: [ 672 include: [
749 { 673 {
@@ -760,7 +684,72 @@ export type AvailableForListIDsOptions = {
760})) 684}))
761@Table({ 685@Table({
762 tableName: 'video', 686 tableName: 'video',
763 indexes 687 indexes: [
688 buildTrigramSearchIndex('video_name_trigram', 'name'),
689
690 { fields: [ 'createdAt' ] },
691 {
692 fields: [
693 { name: 'publishedAt', order: 'DESC' },
694 { name: 'id', order: 'ASC' }
695 ]
696 },
697 { fields: [ 'duration' ] },
698 { fields: [ 'views' ] },
699 { fields: [ 'channelId' ] },
700 {
701 fields: [ 'originallyPublishedAt' ],
702 where: {
703 originallyPublishedAt: {
704 [Op.ne]: null
705 }
706 }
707 },
708 {
709 fields: [ 'category' ], // We don't care videos with an unknown category
710 where: {
711 category: {
712 [Op.ne]: null
713 }
714 }
715 },
716 {
717 fields: [ 'licence' ], // We don't care videos with an unknown licence
718 where: {
719 licence: {
720 [Op.ne]: null
721 }
722 }
723 },
724 {
725 fields: [ 'language' ], // We don't care videos with an unknown language
726 where: {
727 language: {
728 [Op.ne]: null
729 }
730 }
731 },
732 {
733 fields: [ 'nsfw' ], // Most of the videos are not NSFW
734 where: {
735 nsfw: true
736 }
737 },
738 {
739 fields: [ 'remote' ], // Only index local videos
740 where: {
741 remote: false
742 }
743 },
744 {
745 fields: [ 'uuid' ],
746 unique: true
747 },
748 {
749 fields: [ 'url' ],
750 unique: true
751 }
752 ]
764}) 753})
765export class VideoModel extends Model<VideoModel> { 754export class VideoModel extends Model<VideoModel> {
766 755
@@ -1031,7 +1020,7 @@ export class VideoModel extends Model<VideoModel> {
1031 }, 1020 },
1032 onDelete: 'cascade', 1021 onDelete: 'cascade',
1033 hooks: true, 1022 hooks: true,
1034 [ 'separate' as any ]: true 1023 ['separate' as any]: true
1035 }) 1024 })
1036 VideoCaptions: VideoCaptionModel[] 1025 VideoCaptions: VideoCaptionModel[]
1037 1026
@@ -1090,6 +1079,11 @@ export class VideoModel extends Model<VideoModel> {
1090 return undefined 1079 return undefined
1091 } 1080 }
1092 1081
1082 @BeforeDestroy
1083 static invalidateCache (instance: VideoModel) {
1084 ModelCache.Instance.invalidateCache('video', instance.id)
1085 }
1086
1093 static listLocal (): Bluebird<MVideoWithAllFiles[]> { 1087 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
1094 const query = { 1088 const query = {
1095 where: { 1089 where: {
@@ -1127,16 +1121,16 @@ export class VideoModel extends Model<VideoModel> {
1127 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings 1121 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
1128 where: { 1122 where: {
1129 id: { 1123 id: {
1130 [ Op.in ]: Sequelize.literal('(' + rawQuery + ')') 1124 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
1131 }, 1125 },
1132 [ Op.or ]: [ 1126 [Op.or]: [
1133 { privacy: VideoPrivacy.PUBLIC }, 1127 { privacy: VideoPrivacy.PUBLIC },
1134 { privacy: VideoPrivacy.UNLISTED } 1128 { privacy: VideoPrivacy.UNLISTED }
1135 ] 1129 ]
1136 }, 1130 },
1137 include: [ 1131 include: [
1138 { 1132 {
1139 attributes: [ 'language' ], 1133 attributes: [ 'language', 'fileUrl' ],
1140 model: VideoCaptionModel.unscoped(), 1134 model: VideoCaptionModel.unscoped(),
1141 required: false 1135 required: false
1142 }, 1136 },
@@ -1146,10 +1140,10 @@ export class VideoModel extends Model<VideoModel> {
1146 required: false, 1140 required: false,
1147 // We only want videos shared by this actor 1141 // We only want videos shared by this actor
1148 where: { 1142 where: {
1149 [ Op.and ]: [ 1143 [Op.and]: [
1150 { 1144 {
1151 id: { 1145 id: {
1152 [ Op.not ]: null 1146 [Op.not]: null
1153 } 1147 }
1154 }, 1148 },
1155 { 1149 {
@@ -1199,8 +1193,8 @@ export class VideoModel extends Model<VideoModel> {
1199 // totals: totalVideos + totalVideoShares 1193 // totals: totalVideos + totalVideoShares
1200 let totalVideos = 0 1194 let totalVideos = 0
1201 let totalVideoShares = 0 1195 let totalVideoShares = 0
1202 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10) 1196 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
1203 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10) 1197 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
1204 1198
1205 const total = totalVideos + totalVideoShares 1199 const total = totalVideos + totalVideoShares
1206 return { 1200 return {
@@ -1243,7 +1237,7 @@ export class VideoModel extends Model<VideoModel> {
1243 baseQuery = Object.assign(baseQuery, { 1237 baseQuery = Object.assign(baseQuery, {
1244 where: { 1238 where: {
1245 name: { 1239 name: {
1246 [ Op.iLike ]: '%' + search + '%' 1240 [Op.iLike]: '%' + search + '%'
1247 } 1241 }
1248 } 1242 }
1249 }) 1243 })
@@ -1273,25 +1267,25 @@ export class VideoModel extends Model<VideoModel> {
1273 } 1267 }
1274 1268
1275 static async listForApi (options: { 1269 static async listForApi (options: {
1276 start: number, 1270 start: number
1277 count: number, 1271 count: number
1278 sort: string, 1272 sort: string
1279 nsfw: boolean, 1273 nsfw: boolean
1280 includeLocalVideos: boolean, 1274 includeLocalVideos: boolean
1281 withFiles: boolean, 1275 withFiles: boolean
1282 categoryOneOf?: number[], 1276 categoryOneOf?: number[]
1283 licenceOneOf?: number[], 1277 licenceOneOf?: number[]
1284 languageOneOf?: string[], 1278 languageOneOf?: string[]
1285 tagsOneOf?: string[], 1279 tagsOneOf?: string[]
1286 tagsAllOf?: string[], 1280 tagsAllOf?: string[]
1287 filter?: VideoFilter, 1281 filter?: VideoFilter
1288 accountId?: number, 1282 accountId?: number
1289 videoChannelId?: number, 1283 videoChannelId?: number
1290 followerActorId?: number 1284 followerActorId?: number
1291 videoPlaylistId?: number, 1285 videoPlaylistId?: number
1292 trendingDays?: number, 1286 trendingDays?: number
1293 user?: MUserAccountId, 1287 user?: MUserAccountId
1294 historyOfUser?: MUserId, 1288 historyOfUser?: MUserId
1295 countVideos?: boolean 1289 countVideos?: boolean
1296 }) { 1290 }) {
1297 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1291 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
@@ -1357,7 +1351,7 @@ export class VideoModel extends Model<VideoModel> {
1357 tagsAllOf?: string[] 1351 tagsAllOf?: string[]
1358 durationMin?: number // seconds 1352 durationMin?: number // seconds
1359 durationMax?: number // seconds 1353 durationMax?: number // seconds
1360 user?: MUserAccountId, 1354 user?: MUserAccountId
1361 filter?: VideoFilter 1355 filter?: VideoFilter
1362 }) { 1356 }) {
1363 const whereAnd = [] 1357 const whereAnd = []
@@ -1365,8 +1359,8 @@ export class VideoModel extends Model<VideoModel> {
1365 if (options.startDate || options.endDate) { 1359 if (options.startDate || options.endDate) {
1366 const publishedAtRange = {} 1360 const publishedAtRange = {}
1367 1361
1368 if (options.startDate) publishedAtRange[ Op.gte ] = options.startDate 1362 if (options.startDate) publishedAtRange[Op.gte] = options.startDate
1369 if (options.endDate) publishedAtRange[ Op.lte ] = options.endDate 1363 if (options.endDate) publishedAtRange[Op.lte] = options.endDate
1370 1364
1371 whereAnd.push({ publishedAt: publishedAtRange }) 1365 whereAnd.push({ publishedAt: publishedAtRange })
1372 } 1366 }
@@ -1374,8 +1368,8 @@ export class VideoModel extends Model<VideoModel> {
1374 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) { 1368 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) {
1375 const originallyPublishedAtRange = {} 1369 const originallyPublishedAtRange = {}
1376 1370
1377 if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Op.gte ] = options.originallyPublishedStartDate 1371 if (options.originallyPublishedStartDate) originallyPublishedAtRange[Op.gte] = options.originallyPublishedStartDate
1378 if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Op.lte ] = options.originallyPublishedEndDate 1372 if (options.originallyPublishedEndDate) originallyPublishedAtRange[Op.lte] = options.originallyPublishedEndDate
1379 1373
1380 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange }) 1374 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange })
1381 } 1375 }
@@ -1383,8 +1377,8 @@ export class VideoModel extends Model<VideoModel> {
1383 if (options.durationMin || options.durationMax) { 1377 if (options.durationMin || options.durationMax) {
1384 const durationRange = {} 1378 const durationRange = {}
1385 1379
1386 if (options.durationMin) durationRange[ Op.gte ] = options.durationMin 1380 if (options.durationMin) durationRange[Op.gte] = options.durationMin
1387 if (options.durationMax) durationRange[ Op.lte ] = options.durationMax 1381 if (options.durationMax) durationRange[Op.lte] = options.durationMax
1388 1382
1389 whereAnd.push({ duration: durationRange }) 1383 whereAnd.push({ duration: durationRange })
1390 } 1384 }
@@ -1395,7 +1389,7 @@ export class VideoModel extends Model<VideoModel> {
1395 if (options.search) { 1389 if (options.search) {
1396 const trigramSearch = { 1390 const trigramSearch = {
1397 id: { 1391 id: {
1398 [ Op.in ]: Sequelize.literal( 1392 [Op.in]: Sequelize.literal(
1399 '(' + 1393 '(' +
1400 'SELECT "video"."id" FROM "video" ' + 1394 'SELECT "video"."id" FROM "video" ' +
1401 'WHERE ' + 1395 'WHERE ' +
@@ -1484,6 +1478,24 @@ export class VideoModel extends Model<VideoModel> {
1484 ]).findOne(options) 1478 ]).findOne(options)
1485 } 1479 }
1486 1480
1481 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1482 const fun = () => {
1483 const query = {
1484 where: buildWhereIdOrUUID(id),
1485 transaction: t
1486 }
1487
1488 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1489 }
1490
1491 return ModelCache.Instance.doCache({
1492 cacheType: 'load-video-immutable-id',
1493 key: '' + id,
1494 deleteKey: 'video',
1495 fun
1496 })
1497 }
1498
1487 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { 1499 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
1488 const where = buildWhereIdOrUUID(id) 1500 const where = buildWhereIdOrUUID(id)
1489 const options = { 1501 const options = {
@@ -1547,6 +1559,26 @@ export class VideoModel extends Model<VideoModel> {
1547 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) 1559 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1548 } 1560 }
1549 1561
1562 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1563 const fun = () => {
1564 const query: FindOptions = {
1565 where: {
1566 url
1567 },
1568 transaction
1569 }
1570
1571 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1572 }
1573
1574 return ModelCache.Instance.doCache({
1575 cacheType: 'load-video-immutable-url',
1576 key: url,
1577 deleteKey: 'video',
1578 fun
1579 })
1580 }
1581
1550 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { 1582 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1551 const query: FindOptions = { 1583 const query: FindOptions = {
1552 where: { 1584 where: {
@@ -1593,8 +1625,8 @@ export class VideoModel extends Model<VideoModel> {
1593 } 1625 }
1594 1626
1595 static loadForGetAPI (parameters: { 1627 static loadForGetAPI (parameters: {
1596 id: number | string, 1628 id: number | string
1597 t?: Transaction, 1629 t?: Transaction
1598 userId?: number 1630 userId?: number
1599 }): Bluebird<MVideoDetails> { 1631 }): Bluebird<MVideoDetails> {
1600 const { id, t, userId } = parameters 1632 const { id, t, userId } = parameters
@@ -1660,9 +1692,9 @@ export class VideoModel extends Model<VideoModel> {
1660 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { 1692 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1661 // Instances only share videos 1693 // Instances only share videos
1662 const query = 'SELECT 1 FROM "videoShare" ' + 1694 const query = 'SELECT 1 FROM "videoShare" ' +
1663 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 1695 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1664 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + 1696 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1665 'LIMIT 1' 1697 'LIMIT 1'
1666 1698
1667 const options = { 1699 const options = {
1668 type: QueryTypes.SELECT as QueryTypes.SELECT, 1700 type: QueryTypes.SELECT as QueryTypes.SELECT,
@@ -1694,7 +1726,7 @@ export class VideoModel extends Model<VideoModel> {
1694 } 1726 }
1695 1727
1696 return VideoModel.findAll(query) 1728 return VideoModel.findAll(query)
1697 .then(videos => videos.map(v => v.id)) 1729 .then(videos => videos.map(v => v.id))
1698 } 1730 }
1699 1731
1700 // threshold corresponds to how many video the field should have to be returned 1732 // threshold corresponds to how many video the field should have to be returned
@@ -1714,14 +1746,14 @@ export class VideoModel extends Model<VideoModel> {
1714 limit: count, 1746 limit: count,
1715 group: field, 1747 group: field,
1716 having: Sequelize.where( 1748 having: Sequelize.where(
1717 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold } 1749 Sequelize.fn('COUNT', Sequelize.col(field)), { [Op.gte]: threshold }
1718 ), 1750 ),
1719 order: [ (this.sequelize as any).random() ] 1751 order: [ (this.sequelize as any).random() ]
1720 } 1752 }
1721 1753
1722 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) 1754 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] })
1723 .findAll(query) 1755 .findAll(query)
1724 .then(rows => rows.map(r => r[ field ])) 1756 .then(rows => rows.map(r => r[field]))
1725 } 1757 }
1726 1758
1727 static buildTrendingQuery (trendingDays: number) { 1759 static buildTrendingQuery (trendingDays: number) {
@@ -1732,7 +1764,7 @@ export class VideoModel extends Model<VideoModel> {
1732 required: false, 1764 required: false,
1733 where: { 1765 where: {
1734 startDate: { 1766 startDate: {
1735 [ Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) 1767 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
1736 } 1768 }
1737 } 1769 }
1738 } 1770 }
@@ -1815,23 +1847,23 @@ export class VideoModel extends Model<VideoModel> {
1815 } 1847 }
1816 1848
1817 static getCategoryLabel (id: number) { 1849 static getCategoryLabel (id: number) {
1818 return VIDEO_CATEGORIES[ id ] || 'Misc' 1850 return VIDEO_CATEGORIES[id] || 'Misc'
1819 } 1851 }
1820 1852
1821 static getLicenceLabel (id: number) { 1853 static getLicenceLabel (id: number) {
1822 return VIDEO_LICENCES[ id ] || 'Unknown' 1854 return VIDEO_LICENCES[id] || 'Unknown'
1823 } 1855 }
1824 1856
1825 static getLanguageLabel (id: string) { 1857 static getLanguageLabel (id: string) {
1826 return VIDEO_LANGUAGES[ id ] || 'Unknown' 1858 return VIDEO_LANGUAGES[id] || 'Unknown'
1827 } 1859 }
1828 1860
1829 static getPrivacyLabel (id: number) { 1861 static getPrivacyLabel (id: number) {
1830 return VIDEO_PRIVACIES[ id ] || 'Unknown' 1862 return VIDEO_PRIVACIES[id] || 'Unknown'
1831 } 1863 }
1832 1864
1833 static getStateLabel (id: number) { 1865 static getStateLabel (id: number) {
1834 return VIDEO_STATES[ id ] || 'Unknown' 1866 return VIDEO_STATES[id] || 'Unknown'
1835 } 1867 }
1836 1868
1837 isBlacklisted () { 1869 isBlacklisted () {
@@ -1843,7 +1875,7 @@ export class VideoModel extends Model<VideoModel> {
1843 this.VideoChannel.Account.isBlocked() 1875 this.VideoChannel.Account.isBlocked()
1844 } 1876 }
1845 1877
1846 getQualityFileBy <T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { 1878 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
1847 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { 1879 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
1848 const file = fun(this.VideoFiles, file => file.resolution) 1880 const file = fun(this.VideoFiles, file => file.resolution)
1849 1881
@@ -1861,15 +1893,15 @@ export class VideoModel extends Model<VideoModel> {
1861 return undefined 1893 return undefined
1862 } 1894 }
1863 1895
1864 getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1896 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1865 return this.getQualityFileBy(maxBy) 1897 return this.getQualityFileBy(maxBy)
1866 } 1898 }
1867 1899
1868 getMinQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1900 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1869 return this.getQualityFileBy(minBy) 1901 return this.getQualityFileBy(minBy)
1870 } 1902 }
1871 1903
1872 getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { 1904 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
1873 if (Array.isArray(this.VideoFiles) === false) return undefined 1905 if (Array.isArray(this.VideoFiles) === false) return undefined
1874 1906
1875 const file = this.VideoFiles.find(f => f.resolution === resolution) 1907 const file = this.VideoFiles.find(f => f.resolution === resolution)
@@ -1905,6 +1937,10 @@ export class VideoModel extends Model<VideoModel> {
1905 return this.uuid + '.jpg' 1937 return this.uuid + '.jpg'
1906 } 1938 }
1907 1939
1940 hasPreview () {
1941 return !!this.getPreview()
1942 }
1943
1908 getPreview () { 1944 getPreview () {
1909 if (Array.isArray(this.Thumbnails) === false) return undefined 1945 if (Array.isArray(this.Thumbnails) === false) return undefined
1910 1946
@@ -1992,8 +2028,8 @@ export class VideoModel extends Model<VideoModel> {
1992 } 2028 }
1993 2029
1994 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists 2030 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
1995 .filter(s => s.type !== VideoStreamingPlaylistType.HLS) 2031 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1996 .concat(toAdd) 2032 .concat(toAdd)
1997 } 2033 }
1998 2034
1999 removeFile (videoFile: MVideoFile, isRedundancy = false) { 2035 removeFile (videoFile: MVideoFile, isRedundancy = false) {
@@ -2014,7 +2050,7 @@ export class VideoModel extends Model<VideoModel> {
2014 await remove(directoryPath) 2050 await remove(directoryPath)
2015 2051
2016 if (isRedundancy !== true) { 2052 if (isRedundancy !== true) {
2017 let streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo 2053 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
2018 streamingPlaylistWithFiles.Video = this 2054 streamingPlaylistWithFiles.Video = this
2019 2055
2020 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) { 2056 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {