aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/video-playlist.ts20
-rw-r--r--server/controllers/api/videos/import.ts16
-rw-r--r--server/controllers/api/videos/index.ts31
-rw-r--r--server/controllers/bots.ts2
-rw-r--r--server/controllers/feeds.ts2
-rw-r--r--server/controllers/static.ts12
-rw-r--r--server/initializers/database.ts6
-rw-r--r--server/lib/activitypub/playlist.ts14
-rw-r--r--server/lib/activitypub/video-comments.ts3
-rw-r--r--server/lib/activitypub/videos.ts34
-rw-r--r--server/lib/files-cache/abstract-video-static-file-cache.ts18
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts9
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts6
-rw-r--r--server/lib/job-queue/handlers/video-import.ts20
-rw-r--r--server/lib/oauth-model.ts2
-rw-r--r--server/lib/thumbnail.ts26
-rw-r--r--server/middlewares/oauth.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts1
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account.ts10
-rw-r--r--server/models/account/user-notification.ts44
-rw-r--r--server/models/account/user.ts58
-rw-r--r--server/models/activitypub/actor.ts47
-rw-r--r--server/models/application/application.ts6
-rw-r--r--server/models/oauth/oauth-client.ts4
-rw-r--r--server/models/oauth/oauth-token.ts22
-rw-r--r--server/models/redundancy/video-redundancy.ts56
-rw-r--r--server/models/server/server-blocklist.ts8
-rw-r--r--server/models/utils.ts12
-rw-r--r--server/models/video/tag.ts2
-rw-r--r--server/models/video/thumbnail.ts4
-rw-r--r--server/models/video/video-caption.ts13
-rw-r--r--server/models/video/video-change-ownership.ts14
-rw-r--r--server/models/video/video-channel.ts16
-rw-r--r--server/models/video/video-comment.ts33
-rw-r--r--server/models/video/video-file.ts27
-rw-r--r--server/models/video/video-format-utils.ts10
-rw-r--r--server/models/video/video-import.ts8
-rw-r--r--server/models/video/video-playlist.ts47
-rw-r--r--server/models/video/video-share.ts10
-rw-r--r--server/models/video/video-streaming-playlist.ts6
-rw-r--r--server/models/video/video.ts179
-rw-r--r--server/typings/sequelize.ts18
43 files changed, 440 insertions, 448 deletions
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index 99325aa9d..6a1d23529 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -41,7 +41,7 @@ import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/vid
41import { JobQueue } from '../../lib/job-queue' 41import { JobQueue } from '../../lib/job-queue'
42import { CONFIG } from '../../initializers/config' 42import { CONFIG } from '../../initializers/config'
43import { sequelizeTypescript } from '../../initializers/database' 43import { sequelizeTypescript } from '../../initializers/database'
44import { createPlaylistThumbnailFromExisting } from '../../lib/thumbnail' 44import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail'
45 45
46const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) 46const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
47 47
@@ -174,16 +174,13 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
174 174
175 const thumbnailField = req.files['thumbnailfile'] 175 const thumbnailField = req.files['thumbnailfile']
176 const thumbnailModel = thumbnailField 176 const thumbnailModel = thumbnailField
177 ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylist) 177 ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist)
178 : undefined 178 : undefined
179 179
180 const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { 180 const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
181 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) 181 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
182 182
183 if (thumbnailModel) { 183 if (thumbnailModel) await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t)
184 thumbnailModel.videoPlaylistId = videoPlaylistCreated.id
185 videoPlaylistCreated.setThumbnail(await thumbnailModel.save({ transaction: t }))
186 }
187 184
188 // We need more attributes for the federation 185 // We need more attributes for the federation
189 videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) 186 videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
@@ -210,7 +207,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
210 207
211 const thumbnailField = req.files['thumbnailfile'] 208 const thumbnailField = req.files['thumbnailfile']
212 const thumbnailModel = thumbnailField 209 const thumbnailModel = thumbnailField
213 ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylistInstance) 210 ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance)
214 : undefined 211 : undefined
215 212
216 try { 213 try {
@@ -239,10 +236,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
239 236
240 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) 237 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
241 238
242 if (thumbnailModel) { 239 if (thumbnailModel) await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t)
243 thumbnailModel.videoPlaylistId = playlistUpdated.id
244 playlistUpdated.setThumbnail(await thumbnailModel.save({ transaction: t }))
245 }
246 240
247 const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE 241 const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
248 242
@@ -313,8 +307,8 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
313 if (playlistElement.position === 1 && videoPlaylist.hasThumbnail() === false) { 307 if (playlistElement.position === 1 && videoPlaylist.hasThumbnail() === false) {
314 logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) 308 logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
315 309
316 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnail().filename) 310 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename)
317 const thumbnailModel = await createPlaylistThumbnailFromExisting(inputPath, videoPlaylist, true) 311 const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true)
318 312
319 thumbnailModel.videoPlaylistId = videoPlaylist.id 313 thumbnailModel.videoPlaylistId = videoPlaylist.id
320 314
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index a4ec41d44..bfb690906 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -23,7 +23,7 @@ import { move, readFile } from 'fs-extra'
23import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 23import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
24import { CONFIG } from '../../../initializers/config' 24import { CONFIG } from '../../../initializers/config'
25import { sequelizeTypescript } from '../../../initializers/database' 25import { sequelizeTypescript } from '../../../initializers/database'
26import { createVideoThumbnailFromExisting } from '../../../lib/thumbnail' 26import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
28import { ThumbnailModel } from '../../../models/video/thumbnail' 28import { ThumbnailModel } from '../../../models/video/thumbnail'
29 29
@@ -204,7 +204,7 @@ async function processThumbnail (req: express.Request, video: VideoModel) {
204 if (thumbnailField) { 204 if (thumbnailField) {
205 const thumbnailPhysicalFile = thumbnailField[ 0 ] 205 const thumbnailPhysicalFile = thumbnailField[ 0 ]
206 206
207 return createVideoThumbnailFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.THUMBNAIL) 207 return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE)
208 } 208 }
209 209
210 return undefined 210 return undefined
@@ -215,7 +215,7 @@ async function processPreview (req: express.Request, video: VideoModel) {
215 if (previewField) { 215 if (previewField) {
216 const previewPhysicalFile = previewField[0] 216 const previewPhysicalFile = previewField[0]
217 217
218 return createVideoThumbnailFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW) 218 return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
219 } 219 }
220 220
221 return undefined 221 return undefined
@@ -238,14 +238,8 @@ function insertIntoDB (parameters: {
238 const videoCreated = await video.save(sequelizeOptions) 238 const videoCreated = await video.save(sequelizeOptions)
239 videoCreated.VideoChannel = videoChannel 239 videoCreated.VideoChannel = videoChannel
240 240
241 if (thumbnailModel) { 241 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
242 thumbnailModel.videoId = videoCreated.id 242 if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
243 videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
244 }
245 if (previewModel) {
246 previewModel.videoId = videoCreated.id
247 videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
248 }
249 243
250 await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t) 244 await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
251 245
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index ad2fe958c..5bbce11b4 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -52,7 +52,7 @@ import { Notifier } from '../../../lib/notifier'
52import { sendView } from '../../../lib/activitypub/send/send-view' 52import { sendView } from '../../../lib/activitypub/send/send-view'
53import { CONFIG } from '../../../initializers/config' 53import { CONFIG } from '../../../initializers/config'
54import { sequelizeTypescript } from '../../../initializers/database' 54import { sequelizeTypescript } from '../../../initializers/database'
55import { createVideoThumbnailFromExisting, generateVideoThumbnail } from '../../../lib/thumbnail' 55import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
56import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 56import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
57 57
58const auditLogger = auditLoggerFactory('videos') 58const auditLogger = auditLoggerFactory('videos')
@@ -214,14 +214,14 @@ async function addVideo (req: express.Request, res: express.Response) {
214 // Process thumbnail or create it from the video 214 // Process thumbnail or create it from the video
215 const thumbnailField = req.files['thumbnailfile'] 215 const thumbnailField = req.files['thumbnailfile']
216 const thumbnailModel = thumbnailField 216 const thumbnailModel = thumbnailField
217 ? await createVideoThumbnailFromExisting(thumbnailField[0].path, video, ThumbnailType.THUMBNAIL) 217 ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE)
218 : await generateVideoThumbnail(video, videoFile, ThumbnailType.THUMBNAIL) 218 : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE)
219 219
220 // Process preview or create it from the video 220 // Process preview or create it from the video
221 const previewField = req.files['previewfile'] 221 const previewField = req.files['previewfile']
222 const previewModel = previewField 222 const previewModel = previewField
223 ? await createVideoThumbnailFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW) 223 ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW)
224 : await generateVideoThumbnail(video, videoFile, ThumbnailType.PREVIEW) 224 : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW)
225 225
226 // Create the torrent file 226 // Create the torrent file
227 await video.createTorrentAndSetInfoHash(videoFile) 227 await video.createTorrentAndSetInfoHash(videoFile)
@@ -231,11 +231,8 @@ async function addVideo (req: express.Request, res: express.Response) {
231 231
232 const videoCreated = await video.save(sequelizeOptions) 232 const videoCreated = await video.save(sequelizeOptions)
233 233
234 thumbnailModel.videoId = videoCreated.id 234 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
235 previewModel.videoId = videoCreated.id 235 await videoCreated.addAndSaveThumbnail(previewModel, t)
236
237 videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
238 videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
239 236
240 // Do not forget to add video channel information to the created video 237 // Do not forget to add video channel information to the created video
241 videoCreated.VideoChannel = res.locals.videoChannel 238 videoCreated.VideoChannel = res.locals.videoChannel
@@ -308,11 +305,11 @@ async function updateVideo (req: express.Request, res: express.Response) {
308 305
309 // Process thumbnail or create it from the video 306 // Process thumbnail or create it from the video
310 const thumbnailModel = req.files && req.files['thumbnailfile'] 307 const thumbnailModel = req.files && req.files['thumbnailfile']
311 ? await createVideoThumbnailFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.THUMBNAIL) 308 ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE)
312 : undefined 309 : undefined
313 310
314 const previewModel = req.files && req.files['previewfile'] 311 const previewModel = req.files && req.files['previewfile']
315 ? await createVideoThumbnailFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW) 312 ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW)
316 : undefined 313 : undefined
317 314
318 try { 315 try {
@@ -346,14 +343,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
346 343
347 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) 344 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
348 345
349 if (thumbnailModel) { 346 if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
350 thumbnailModel.videoId = videoInstanceUpdated.id 347 if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
351 videoInstanceUpdated.addThumbnail(await thumbnailModel.save({ transaction: t }))
352 }
353 if (previewModel) {
354 previewModel.videoId = videoInstanceUpdated.id
355 videoInstanceUpdated.addThumbnail(await previewModel.save({ transaction: t }))
356 }
357 348
358 // Video tags update? 349 // Video tags update?
359 if (videoInfoToUpdate.tags !== undefined) { 350 if (videoInfoToUpdate.tags !== undefined) {
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 7e8e6eff6..e25d9c21b 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -85,7 +85,7 @@ async function getSitemapLocalVideoUrls () {
85 // Sitemap description should be < 2000 characters 85 // Sitemap description should be < 2000 characters
86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }), 86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
87 player_loc: WEBSERVER.URL + '/videos/embed/' + v.uuid, 87 player_loc: WEBSERVER.URL + '/videos/embed/' + v.uuid,
88 thumbnail_loc: WEBSERVER.URL + v.getThumbnailStaticPath() 88 thumbnail_loc: WEBSERVER.URL + v.getMiniatureStaticPath()
89 } 89 }
90 ] 90 ]
91 })) 91 }))
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 5064097cd..d3f581615 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -137,7 +137,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
137 torrent: torrents, 137 torrent: torrents,
138 thumbnail: [ 138 thumbnail: [
139 { 139 {
140 url: WEBSERVER.URL + video.getThumbnailStaticPath(), 140 url: WEBSERVER.URL + video.getMiniatureStaticPath(),
141 height: THUMBNAILS_SIZE.height, 141 height: THUMBNAILS_SIZE.height,
142 width: THUMBNAILS_SIZE.width 142 width: THUMBNAILS_SIZE.width
143 } 143 }
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index d75b95f52..05019fcc2 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -165,20 +165,20 @@ export {
165// --------------------------------------------------------------------------- 165// ---------------------------------------------------------------------------
166 166
167async function getPreview (req: express.Request, res: express.Response) { 167async function getPreview (req: express.Request, res: express.Response) {
168 const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid) 168 const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
169 if (!path) return res.sendStatus(404) 169 if (!result) return res.sendStatus(404)
170 170
171 return res.sendFile(path, { maxAge: STATIC_MAX_AGE }) 171 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
172} 172}
173 173
174async function getVideoCaption (req: express.Request, res: express.Response) { 174async function getVideoCaption (req: express.Request, res: express.Response) {
175 const path = await VideosCaptionCache.Instance.getFilePath({ 175 const result = await VideosCaptionCache.Instance.getFilePath({
176 videoId: req.params.videoId, 176 videoId: req.params.videoId,
177 language: req.params.captionLanguage 177 language: req.params.captionLanguage
178 }) 178 })
179 if (!path) return res.sendStatus(404) 179 if (!result) return res.sendStatus(404)
180 180
181 return res.sendFile(path, { maxAge: STATIC_MAX_AGE }) 181 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
182} 182}
183 183
184async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) { 184async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index d1744d21f..d9a265e7a 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -140,15 +140,15 @@ async function checkPostgresExtensions () {
140} 140}
141 141
142async function checkPostgresExtension (extension: string) { 142async function checkPostgresExtension (extension: string) {
143 const query = `SELECT true AS enabled FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;` 143 const query = `SELECT 1 FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;`
144 const options = { 144 const options = {
145 type: QueryTypes.SELECT as QueryTypes.SELECT, 145 type: QueryTypes.SELECT as QueryTypes.SELECT,
146 raw: true 146 raw: true
147 } 147 }
148 148
149 const res = await sequelizeTypescript.query<{ enabled: boolean }>(query, options) 149 const res = await sequelizeTypescript.query<object>(query, options)
150 150
151 if (!res || res.length === 0 || res[ 0 ][ 'enabled' ] !== true) { 151 if (!res || res.length === 0) {
152 // Try to create the extension ourselves 152 // Try to create the extension ourselves
153 try { 153 try {
154 await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true }) 154 await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true })
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index 721c19603..36a91faec 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -16,7 +16,8 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
16import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
17import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 17import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
18import { sequelizeTypescript } from '../../initializers/database' 18import { sequelizeTypescript } from '../../initializers/database'
19import { createPlaylistThumbnailFromUrl } from '../thumbnail' 19import { createPlaylistMiniatureFromUrl } from '../thumbnail'
20import { FilteredModelAttributes } from '../../typings/sequelize'
20 21
21function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { 22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
22 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED 23 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
@@ -86,8 +87,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
86 } 87 }
87 } 88 }
88 89
89 // FIXME: sequelize typings 90 const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true })
90 const [ playlist ] = (await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true }) as any)
91 91
92 let accItems: string[] = [] 92 let accItems: string[] = []
93 await crawlCollectionPage<string>(playlistObject.id, items => { 93 await crawlCollectionPage<string>(playlistObject.id, items => {
@@ -100,10 +100,8 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
100 100
101 if (playlistObject.icon) { 101 if (playlistObject.icon) {
102 try { 102 try {
103 const thumbnailModel = await createPlaylistThumbnailFromUrl(playlistObject.icon.url, refreshedPlaylist) 103 const thumbnailModel = await createPlaylistMiniatureFromUrl(playlistObject.icon.url, refreshedPlaylist)
104 thumbnailModel.videoPlaylistId = refreshedPlaylist.id 104 await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined)
105
106 refreshedPlaylist.setThumbnail(await thumbnailModel.save())
107 } catch (err) { 105 } catch (err) {
108 logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) 106 logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err })
109 } 107 }
@@ -156,7 +154,7 @@ export {
156// --------------------------------------------------------------------------- 154// ---------------------------------------------------------------------------
157 155
158async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) { 156async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) {
159 const elementsToCreate: object[] = [] // FIXME: sequelize typings 157 const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
160 158
161 await Bluebird.map(elementUrls, async elementUrl => { 159 await Bluebird.map(elementUrls, async elementUrl => {
162 try { 160 try {
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index cb67bf9a4..18f44d50e 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -73,8 +73,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
73 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) 73 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
74 if (!entry) return { created: false } 74 if (!entry) return { created: false }
75 75
76 // FIXME: sequelize typings 76 const [ comment, created ] = await VideoCommentModel.upsert<VideoCommentModel>(entry, { returning: true })
77 const [ comment, created ] = (await VideoCommentModel.upsert<VideoCommentModel>(entry, { returning: true }) as any)
78 comment.Account = actor.Account 77 comment.Account = actor.Account
79 comment.Video = videoInstance 78 comment.Video = videoInstance
80 79
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 5a56942a9..63bb07ec1 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -49,10 +49,11 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate'
49import { VideoShareModel } from '../../models/video/video-share' 49import { VideoShareModel } from '../../models/video/video-share'
50import { VideoCommentModel } from '../../models/video/video-comment' 50import { VideoCommentModel } from '../../models/video/video-comment'
51import { sequelizeTypescript } from '../../initializers/database' 51import { sequelizeTypescript } from '../../initializers/database'
52import { createPlaceholderThumbnail, createVideoThumbnailFromUrl } from '../thumbnail' 52import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
53import { ThumbnailModel } from '../../models/video/thumbnail' 53import { ThumbnailModel } from '../../models/video/thumbnail'
54import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 54import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
55import { join } from 'path' 55import { join } from 'path'
56import { FilteredModelAttributes } from '../../typings/sequelize'
56 57
57async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 58async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
58 // If the video is not private and is published, we federate it 59 // If the video is not private and is published, we federate it
@@ -247,7 +248,7 @@ async function updateVideoFromAP (options: {
247 let thumbnailModel: ThumbnailModel 248 let thumbnailModel: ThumbnailModel
248 249
249 try { 250 try {
250 thumbnailModel = await createVideoThumbnailFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.THUMBNAIL) 251 thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
251 } catch (err) { 252 } catch (err) {
252 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) 253 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
253 } 254 }
@@ -288,16 +289,12 @@ async function updateVideoFromAP (options: {
288 289
289 await options.video.save(sequelizeOptions) 290 await options.video.save(sequelizeOptions)
290 291
291 if (thumbnailModel) { 292 if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
292 thumbnailModel.videoId = options.video.id
293 options.video.addThumbnail(await thumbnailModel.save({ transaction: t }))
294 }
295 293
296 // FIXME: use icon URL instead 294 // FIXME: use icon URL instead
297 const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename)) 295 const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
298 const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 296 const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
299 297 await options.video.addAndSaveThumbnail(previewModel, t)
300 options.video.addThumbnail(await previewModel.save({ transaction: t }))
301 298
302 { 299 {
303 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) 300 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
@@ -311,7 +308,7 @@ async function updateVideoFromAP (options: {
311 308
312 // Update or add other one 309 // Update or add other one
313 const upsertTasks = videoFileAttributes.map(a => { 310 const upsertTasks = videoFileAttributes.map(a => {
314 return (VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t }) as any) // FIXME: sequelize typings 311 return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
315 .then(([ file ]) => file) 312 .then(([ file ]) => file)
316 }) 313 })
317 314
@@ -334,8 +331,7 @@ async function updateVideoFromAP (options: {
334 331
335 // Update or add other one 332 // Update or add other one
336 const upsertTasks = streamingPlaylistAttributes.map(a => { 333 const upsertTasks = streamingPlaylistAttributes.map(a => {
337 // FIXME: sequelize typings 334 return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
338 return (VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) as any)
339 .then(([ streamingPlaylist ]) => streamingPlaylist) 335 .then(([ streamingPlaylist ]) => streamingPlaylist)
340 }) 336 })
341 337
@@ -464,7 +460,7 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
464 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) 460 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
465 const video = VideoModel.build(videoData) 461 const video = VideoModel.build(videoData)
466 462
467 const promiseThumbnail = createVideoThumbnailFromUrl(videoObject.icon.url, video, ThumbnailType.THUMBNAIL) 463 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
468 464
469 let thumbnailModel: ThumbnailModel 465 let thumbnailModel: ThumbnailModel
470 if (waitThumbnail === true) { 466 if (waitThumbnail === true) {
@@ -477,18 +473,12 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
477 const videoCreated = await video.save(sequelizeOptions) 473 const videoCreated = await video.save(sequelizeOptions)
478 videoCreated.VideoChannel = channelActor.VideoChannel 474 videoCreated.VideoChannel = channelActor.VideoChannel
479 475
480 if (thumbnailModel) { 476 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
481 thumbnailModel.videoId = videoCreated.id
482
483 videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
484 }
485 477
486 // FIXME: use icon URL instead 478 // FIXME: use icon URL instead
487 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 479 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
488 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 480 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
489 previewModel.videoId = videoCreated.id 481 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
490
491 videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
492 482
493 // Process files 483 // Process files
494 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) 484 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
@@ -594,7 +584,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
594 throw new Error('Cannot find video files for ' + video.url) 584 throw new Error('Cannot find video files for ' + video.url)
595 } 585 }
596 586
597 const attributes: object[] = [] // FIXME: add typings 587 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
598 for (const fileUrl of fileUrls) { 588 for (const fileUrl of fileUrls) {
599 // Fetch associated magnet uri 589 // Fetch associated magnet uri
600 const magnet = videoObject.url.find(u => { 590 const magnet = videoObject.url.find(u => {
@@ -629,7 +619,7 @@ function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObj
629 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] 619 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
630 if (playlistUrls.length === 0) return [] 620 if (playlistUrls.length === 0) return []
631 621
632 const attributes: object[] = [] // FIXME: add typings 622 const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
633 for (const playlistUrlObject of playlistUrls) { 623 for (const playlistUrlObject of playlistUrls) {
634 const segmentsSha256UrlObject = playlistUrlObject.tag 624 const segmentsSha256UrlObject = playlistUrlObject.tag
635 .find(t => { 625 .find(t => {
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/abstract-video-static-file-cache.ts
index 61837e0f8..84ed74c98 100644
--- a/server/lib/files-cache/abstract-video-static-file-cache.ts
+++ b/server/lib/files-cache/abstract-video-static-file-cache.ts
@@ -4,24 +4,28 @@ import { VideoModel } from '../../models/video/video'
4import { fetchRemoteVideoStaticFile } from '../activitypub' 4import { fetchRemoteVideoStaticFile } from '../activitypub'
5import * as memoizee from 'memoizee' 5import * as memoizee from 'memoizee'
6 6
7type GetFilePathResult = { isOwned: boolean, path: string } | undefined
8
7export abstract class AbstractVideoStaticFileCache <T> { 9export abstract class AbstractVideoStaticFileCache <T> {
8 10
9 getFilePath: (params: T) => Promise<string> 11 getFilePath: (params: T) => Promise<GetFilePathResult>
10 12
11 abstract getFilePathImpl (params: T): Promise<string> 13 abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
12 14
13 // Load and save the remote file, then return the local path from filesystem 15 // Load and save the remote file, then return the local path from filesystem
14 protected abstract loadRemoteFile (key: string): Promise<string> 16 protected abstract loadRemoteFile (key: string): Promise<GetFilePathResult>
15 17
16 init (max: number, maxAge: number) { 18 init (max: number, maxAge: number) {
17 this.getFilePath = memoizee(this.getFilePathImpl, { 19 this.getFilePath = memoizee(this.getFilePathImpl, {
18 maxAge, 20 maxAge,
19 max, 21 max,
20 promise: true, 22 promise: true,
21 dispose: (value: string) => { 23 dispose: (result: GetFilePathResult) => {
22 remove(value) 24 if (result.isOwned !== true) {
23 .then(() => logger.debug('%s evicted from %s', value, this.constructor.name)) 25 remove(result.path)
24 .catch(err => logger.error('Cannot remove %s from cache %s.', value, this.constructor.name, { err })) 26 .then(() => logger.debug('%s removed from %s', result.path, this.constructor.name))
27 .catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err }))
28 }
25 } 29 }
26 }) 30 })
27 } 31 }
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index d4a0a3345..305e39c35 100644
--- a/server/lib/files-cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -4,6 +4,7 @@ import { VideoModel } from '../../models/video/video'
4import { VideoCaptionModel } from '../../models/video/video-caption' 4import { VideoCaptionModel } from '../../models/video/video-caption'
5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
7import { logger } from '../../helpers/logger'
7 8
8type GetPathParam = { videoId: string, language: string } 9type GetPathParam = { videoId: string, language: string }
9 10
@@ -24,13 +25,15 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
24 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) 25 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language)
25 if (!videoCaption) return undefined 26 if (!videoCaption) return undefined
26 27
27 if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) 28 if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) }
28 29
29 const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language 30 const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language
30 return this.loadRemoteFile(key) 31 return this.loadRemoteFile(key)
31 } 32 }
32 33
33 protected async loadRemoteFile (key: string) { 34 protected async loadRemoteFile (key: string) {
35 logger.debug('Loading remote caption file %s.', key)
36
34 const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER) 37 const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER)
35 38
36 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language) 39 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language)
@@ -46,7 +49,9 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
46 const remoteStaticPath = videoCaption.getCaptionStaticPath() 49 const remoteStaticPath = videoCaption.getCaptionStaticPath()
47 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) 50 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
48 51
49 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) 52 const path = await this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
53
54 return { isOwned: false, path }
50 } 55 }
51} 56}
52 57
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index fc0d92c78..c117ae426 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -20,7 +20,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
20 const video = await VideoModel.loadByUUIDWithFile(videoUUID) 20 const video = await VideoModel.loadByUUIDWithFile(videoUUID)
21 if (!video) return undefined 21 if (!video) return undefined
22 22
23 if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) 23 if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) }
24 24
25 return this.loadRemoteFile(videoUUID) 25 return this.loadRemoteFile(videoUUID)
26 } 26 }
@@ -35,7 +35,9 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
35 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename) 35 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)
36 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename) 36 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
37 37
38 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) 38 const path = await this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
39
40 return { isOwned: false, path }
39 } 41 }
40} 42}
41 43
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 3fa0dd65d..1650916a6 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -18,7 +18,7 @@ import { Notifier } from '../../notifier'
18import { CONFIG } from '../../../initializers/config' 18import { CONFIG } from '../../../initializers/config'
19import { sequelizeTypescript } from '../../../initializers/database' 19import { sequelizeTypescript } from '../../../initializers/database'
20import { ThumbnailModel } from '../../../models/video/thumbnail' 20import { ThumbnailModel } from '../../../models/video/thumbnail'
21import { createVideoThumbnailFromUrl, generateVideoThumbnail } from '../../thumbnail' 21import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail'
22import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 22import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
23 23
24type VideoImportYoutubeDLPayload = { 24type VideoImportYoutubeDLPayload = {
@@ -150,17 +150,17 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
150 // Process thumbnail 150 // Process thumbnail
151 let thumbnailModel: ThumbnailModel 151 let thumbnailModel: ThumbnailModel
152 if (options.downloadThumbnail && options.thumbnailUrl) { 152 if (options.downloadThumbnail && options.thumbnailUrl) {
153 thumbnailModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.THUMBNAIL) 153 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.MINIATURE)
154 } else if (options.generateThumbnail || options.downloadThumbnail) { 154 } else if (options.generateThumbnail || options.downloadThumbnail) {
155 thumbnailModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.THUMBNAIL) 155 thumbnailModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.MINIATURE)
156 } 156 }
157 157
158 // Process preview 158 // Process preview
159 let previewModel: ThumbnailModel 159 let previewModel: ThumbnailModel
160 if (options.downloadPreview && options.thumbnailUrl) { 160 if (options.downloadPreview && options.thumbnailUrl) {
161 previewModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW) 161 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
162 } else if (options.generatePreview || options.downloadPreview) { 162 } else if (options.generatePreview || options.downloadPreview) {
163 previewModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.PREVIEW) 163 previewModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
164 } 164 }
165 165
166 // Create torrent 166 // Create torrent
@@ -180,14 +180,8 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
180 video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED 180 video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
181 await video.save({ transaction: t }) 181 await video.save({ transaction: t })
182 182
183 if (thumbnailModel) { 183 if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
184 thumbnailModel.videoId = video.id 184 if (previewModel) await video.addAndSaveThumbnail(previewModel, t)
185 video.addThumbnail(await thumbnailModel.save({ transaction: t }))
186 }
187 if (previewModel) {
188 previewModel.videoId = video.id
189 video.addThumbnail(await previewModel.save({ transaction: t }))
190 }
191 185
192 // Now we can federate the video (reload from database, we need more attributes) 186 // Now we can federate the video (reload from database, we need more attributes)
193 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 187 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index eb0e63bc8..45ac3e7c4 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -39,6 +39,8 @@ function clearCacheByToken (token: string) {
39function getAccessToken (bearerToken: string) { 39function getAccessToken (bearerToken: string) {
40 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') 40 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
41 41
42 if (!bearerToken) return Bluebird.resolve(undefined)
43
42 if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken]) 44 if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken])
43 45
44 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 46 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index 344c28566..8ad82ee80 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -12,37 +12,37 @@ import { VideoPlaylistModel } from '../models/video/video-playlist'
12 12
13type ImageSize = { height: number, width: number } 13type ImageSize = { height: number, width: number }
14 14
15function createPlaylistThumbnailFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) { 15function createPlaylistMiniatureFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) {
16 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) 16 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
17 const type = ThumbnailType.THUMBNAIL 17 const type = ThumbnailType.MINIATURE
18 18
19 const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }, keepOriginal) 19 const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }, keepOriginal)
20 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) 20 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
21} 21}
22 22
23function createPlaylistThumbnailFromUrl (url: string, playlist: VideoPlaylistModel, size?: ImageSize) { 23function createPlaylistMiniatureFromUrl (url: string, playlist: VideoPlaylistModel, size?: ImageSize) {
24 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) 24 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
25 const type = ThumbnailType.THUMBNAIL 25 const type = ThumbnailType.MINIATURE
26 26
27 const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height }) 27 const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height })
28 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url }) 28 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url })
29} 29}
30 30
31function createVideoThumbnailFromUrl (url: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { 31function createVideoMiniatureFromUrl (url: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
32 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 32 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
33 const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height }) 33 const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height })
34 34
35 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url }) 35 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url })
36} 36}
37 37
38function createVideoThumbnailFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { 38function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
39 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 39 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
40 const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }) 40 const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height })
41 41
42 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) 42 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
43} 43}
44 44
45function generateVideoThumbnail (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { 45function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
46 const input = video.getVideoFilePath(videoFile) 46 const input = video.getVideoFilePath(videoFile)
47 47
48 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type) 48 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type)
@@ -68,12 +68,12 @@ function createPlaceholderThumbnail (url: string, video: VideoModel, type: Thumb
68// --------------------------------------------------------------------------- 68// ---------------------------------------------------------------------------
69 69
70export { 70export {
71 generateVideoThumbnail, 71 generateVideoMiniature,
72 createVideoThumbnailFromUrl, 72 createVideoMiniatureFromUrl,
73 createVideoThumbnailFromExisting, 73 createVideoMiniatureFromExisting,
74 createPlaceholderThumbnail, 74 createPlaceholderThumbnail,
75 createPlaylistThumbnailFromUrl, 75 createPlaylistMiniatureFromUrl,
76 createPlaylistThumbnailFromExisting 76 createPlaylistMiniatureFromExisting
77} 77}
78 78
79function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) { 79function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) {
@@ -95,7 +95,7 @@ function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?:
95 ? video.Thumbnails.find(t => t.type === type) 95 ? video.Thumbnails.find(t => t.type === type)
96 : undefined 96 : undefined
97 97
98 if (type === ThumbnailType.THUMBNAIL) { 98 if (type === ThumbnailType.MINIATURE) {
99 const filename = video.generateThumbnailName() 99 const filename = video.generateThumbnailName()
100 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR 100 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
101 101
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index de736e593..2b4e300e4 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -35,6 +35,8 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) {
35 35
36 logger.debug('Checking socket access token %s.', accessToken) 36 logger.debug('Checking socket access token %s.', accessToken)
37 37
38 if (!accessToken) return next(new Error('No access token provided'))
39
38 getAccessToken(accessToken) 40 getAccessToken(accessToken)
39 .then(tokenDB => { 41 .then(tokenDB => {
40 const now = new Date() 42 const now = new Date()
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index e9b036a02..2b01f108d 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -68,6 +68,7 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
69 69
70 const isAble = await user.isAbleToUploadVideo(videoFile) 70 const isAble = await user.isAbleToUploadVideo(videoFile)
71
71 if (isAble === false) { 72 if (isAble === false) {
72 res.status(403) 73 res.status(403)
73 .json({ error: 'The user video quota is exceeded with this video.' }) 74 .json({ error: 'The user video quota is exceeded with this video.' })
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index efd6ed59e..d5746ad76 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -8,22 +8,22 @@ enum ScopeNames {
8 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 8 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
9} 9}
10 10
11@Scopes({ 11@Scopes(() => ({
12 [ScopeNames.WITH_ACCOUNTS]: { 12 [ScopeNames.WITH_ACCOUNTS]: {
13 include: [ 13 include: [
14 { 14 {
15 model: () => AccountModel, 15 model: AccountModel,
16 required: true, 16 required: true,
17 as: 'ByAccount' 17 as: 'ByAccount'
18 }, 18 },
19 { 19 {
20 model: () => AccountModel, 20 model: AccountModel,
21 required: true, 21 required: true,
22 as: 'BlockedAccount' 22 as: 'BlockedAccount'
23 } 23 }
24 ] 24 ]
25 } 25 }
26}) 26}))
27 27
28@Table({ 28@Table({
29 tableName: 'accountBlocklist', 29 tableName: 'accountBlocklist',
@@ -83,7 +83,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
83 attributes: [ 'accountId', 'id' ], 83 attributes: [ 'accountId', 'id' ],
84 where: { 84 where: {
85 accountId: { 85 accountId: {
86 [Op.any]: accountIds 86 [Op.in]: accountIds // FIXME: sequelize ANY seems broken
87 }, 87 },
88 targetAccountId 88 targetAccountId
89 }, 89 },
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index bf2ed0a61..c53312990 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -33,15 +33,15 @@ export enum ScopeNames {
33 SUMMARY = 'SUMMARY' 33 SUMMARY = 'SUMMARY'
34} 34}
35 35
36@DefaultScope({ 36@DefaultScope(() => ({
37 include: [ 37 include: [
38 { 38 {
39 model: () => ActorModel, // Default scope includes avatar and server 39 model: ActorModel, // Default scope includes avatar and server
40 required: true 40 required: true
41 } 41 }
42 ] 42 ]
43}) 43}))
44@Scopes({ 44@Scopes(() => ({
45 [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => { 45 [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => {
46 return { 46 return {
47 attributes: [ 'id', 'name' ], 47 attributes: [ 'id', 'name' ],
@@ -66,7 +66,7 @@ export enum ScopeNames {
66 ] 66 ]
67 } 67 }
68 } 68 }
69}) 69}))
70@Table({ 70@Table({
71 tableName: 'account', 71 tableName: 'account',
72 indexes: [ 72 indexes: [
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 08388f268..a4f97037b 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -6,7 +6,7 @@ import { isUserNotificationTypeValid } from '../../helpers/custom-validators/use
6import { UserModel } from './user' 6import { UserModel } from './user'
7import { VideoModel } from '../video/video' 7import { VideoModel } from '../video/video'
8import { VideoCommentModel } from '../video/video-comment' 8import { VideoCommentModel } from '../video/video-comment'
9import { FindOptions, Op } from 'sequelize' 9import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10import { VideoChannelModel } from '../video/video-channel' 10import { VideoChannelModel } from '../video/video-channel'
11import { AccountModel } from './account' 11import { AccountModel } from './account'
12import { VideoAbuseModel } from '../video/video-abuse' 12import { VideoAbuseModel } from '../video/video-abuse'
@@ -24,17 +24,17 @@ enum ScopeNames {
24function buildActorWithAvatarInclude () { 24function buildActorWithAvatarInclude () {
25 return { 25 return {
26 attributes: [ 'preferredUsername' ], 26 attributes: [ 'preferredUsername' ],
27 model: () => ActorModel.unscoped(), 27 model: ActorModel.unscoped(),
28 required: true, 28 required: true,
29 include: [ 29 include: [
30 { 30 {
31 attributes: [ 'filename' ], 31 attributes: [ 'filename' ],
32 model: () => AvatarModel.unscoped(), 32 model: AvatarModel.unscoped(),
33 required: false 33 required: false
34 }, 34 },
35 { 35 {
36 attributes: [ 'host' ], 36 attributes: [ 'host' ],
37 model: () => ServerModel.unscoped(), 37 model: ServerModel.unscoped(),
38 required: false 38 required: false
39 } 39 }
40 ] 40 ]
@@ -44,7 +44,7 @@ function buildActorWithAvatarInclude () {
44function buildVideoInclude (required: boolean) { 44function buildVideoInclude (required: boolean) {
45 return { 45 return {
46 attributes: [ 'id', 'uuid', 'name' ], 46 attributes: [ 'id', 'uuid', 'name' ],
47 model: () => VideoModel.unscoped(), 47 model: VideoModel.unscoped(),
48 required 48 required
49 } 49 }
50} 50}
@@ -53,7 +53,7 @@ function buildChannelInclude (required: boolean, withActor = false) {
53 return { 53 return {
54 required, 54 required,
55 attributes: [ 'id', 'name' ], 55 attributes: [ 'id', 'name' ],
56 model: () => VideoChannelModel.unscoped(), 56 model: VideoChannelModel.unscoped(),
57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] 57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
58 } 58 }
59} 59}
@@ -62,12 +62,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
62 return { 62 return {
63 required, 63 required,
64 attributes: [ 'id', 'name' ], 64 attributes: [ 'id', 'name' ],
65 model: () => AccountModel.unscoped(), 65 model: AccountModel.unscoped(),
66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] 66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
67 } 67 }
68} 68}
69 69
70@Scopes({ 70@Scopes(() => ({
71 [ScopeNames.WITH_ALL]: { 71 [ScopeNames.WITH_ALL]: {
72 include: [ 72 include: [
73 Object.assign(buildVideoInclude(false), { 73 Object.assign(buildVideoInclude(false), {
@@ -76,7 +76,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
76 76
77 { 77 {
78 attributes: [ 'id', 'originCommentId' ], 78 attributes: [ 'id', 'originCommentId' ],
79 model: () => VideoCommentModel.unscoped(), 79 model: VideoCommentModel.unscoped(),
80 required: false, 80 required: false,
81 include: [ 81 include: [
82 buildAccountInclude(true, true), 82 buildAccountInclude(true, true),
@@ -86,56 +86,56 @@ function buildAccountInclude (required: boolean, withActor = false) {
86 86
87 { 87 {
88 attributes: [ 'id' ], 88 attributes: [ 'id' ],
89 model: () => VideoAbuseModel.unscoped(), 89 model: VideoAbuseModel.unscoped(),
90 required: false, 90 required: false,
91 include: [ buildVideoInclude(true) ] 91 include: [ buildVideoInclude(true) ]
92 }, 92 },
93 93
94 { 94 {
95 attributes: [ 'id' ], 95 attributes: [ 'id' ],
96 model: () => VideoBlacklistModel.unscoped(), 96 model: VideoBlacklistModel.unscoped(),
97 required: false, 97 required: false,
98 include: [ buildVideoInclude(true) ] 98 include: [ buildVideoInclude(true) ]
99 }, 99 },
100 100
101 { 101 {
102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], 102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
103 model: () => VideoImportModel.unscoped(), 103 model: VideoImportModel.unscoped(),
104 required: false, 104 required: false,
105 include: [ buildVideoInclude(false) ] 105 include: [ buildVideoInclude(false) ]
106 }, 106 },
107 107
108 { 108 {
109 attributes: [ 'id', 'state' ], 109 attributes: [ 'id', 'state' ],
110 model: () => ActorFollowModel.unscoped(), 110 model: ActorFollowModel.unscoped(),
111 required: false, 111 required: false,
112 include: [ 112 include: [
113 { 113 {
114 attributes: [ 'preferredUsername' ], 114 attributes: [ 'preferredUsername' ],
115 model: () => ActorModel.unscoped(), 115 model: ActorModel.unscoped(),
116 required: true, 116 required: true,
117 as: 'ActorFollower', 117 as: 'ActorFollower',
118 include: [ 118 include: [
119 { 119 {
120 attributes: [ 'id', 'name' ], 120 attributes: [ 'id', 'name' ],
121 model: () => AccountModel.unscoped(), 121 model: AccountModel.unscoped(),
122 required: true 122 required: true
123 }, 123 },
124 { 124 {
125 attributes: [ 'filename' ], 125 attributes: [ 'filename' ],
126 model: () => AvatarModel.unscoped(), 126 model: AvatarModel.unscoped(),
127 required: false 127 required: false
128 }, 128 },
129 { 129 {
130 attributes: [ 'host' ], 130 attributes: [ 'host' ],
131 model: () => ServerModel.unscoped(), 131 model: ServerModel.unscoped(),
132 required: false 132 required: false
133 } 133 }
134 ] 134 ]
135 }, 135 },
136 { 136 {
137 attributes: [ 'preferredUsername' ], 137 attributes: [ 'preferredUsername' ],
138 model: () => ActorModel.unscoped(), 138 model: ActorModel.unscoped(),
139 required: true, 139 required: true,
140 as: 'ActorFollowing', 140 as: 'ActorFollowing',
141 include: [ 141 include: [
@@ -147,9 +147,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
147 }, 147 },
148 148
149 buildAccountInclude(false, true) 149 buildAccountInclude(false, true)
150 ] as any // FIXME: sequelize typings 150 ]
151 } 151 }
152}) 152}))
153@Table({ 153@Table({
154 tableName: 'userNotification', 154 tableName: 'userNotification',
155 indexes: [ 155 indexes: [
@@ -212,7 +212,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
212 } 212 }
213 } 213 }
214 } 214 }
215 ] as any // FIXME: sequelize typings 215 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
216}) 216})
217export class UserNotificationModel extends Model<UserNotificationModel> { 217export class UserNotificationModel extends Model<UserNotificationModel> {
218 218
@@ -357,7 +357,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
357 where: { 357 where: {
358 userId, 358 userId,
359 id: { 359 id: {
360 [Op.any]: notificationIds 360 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
361 } 361 }
362 } 362 }
363 } 363 }
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 8bd0397dd..4a9acd703 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,4 +1,4 @@
1import * as Sequelize from 'sequelize' 1import { FindOptions, literal, Op, QueryTypes } from 'sequelize'
2import { 2import {
3 AfterDestroy, 3 AfterDestroy,
4 AfterUpdate, 4 AfterUpdate,
@@ -56,33 +56,33 @@ enum ScopeNames {
56 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' 56 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
57} 57}
58 58
59@DefaultScope({ 59@DefaultScope(() => ({
60 include: [ 60 include: [
61 { 61 {
62 model: () => AccountModel, 62 model: AccountModel,
63 required: true 63 required: true
64 }, 64 },
65 { 65 {
66 model: () => UserNotificationSettingModel, 66 model: UserNotificationSettingModel,
67 required: true 67 required: true
68 } 68 }
69 ] 69 ]
70}) 70}))
71@Scopes({ 71@Scopes(() => ({
72 [ScopeNames.WITH_VIDEO_CHANNEL]: { 72 [ScopeNames.WITH_VIDEO_CHANNEL]: {
73 include: [ 73 include: [
74 { 74 {
75 model: () => AccountModel, 75 model: AccountModel,
76 required: true, 76 required: true,
77 include: [ () => VideoChannelModel ] 77 include: [ VideoChannelModel ]
78 }, 78 },
79 { 79 {
80 model: () => UserNotificationSettingModel, 80 model: UserNotificationSettingModel,
81 required: true 81 required: true
82 } 82 }
83 ] as any // FIXME: sequelize typings 83 ]
84 } 84 }
85}) 85}))
86@Table({ 86@Table({
87 tableName: 'user', 87 tableName: 'user',
88 indexes: [ 88 indexes: [
@@ -233,26 +233,26 @@ export class UserModel extends Model<UserModel> {
233 let where = undefined 233 let where = undefined
234 if (search) { 234 if (search) {
235 where = { 235 where = {
236 [Sequelize.Op.or]: [ 236 [Op.or]: [
237 { 237 {
238 email: { 238 email: {
239 [Sequelize.Op.iLike]: '%' + search + '%' 239 [Op.iLike]: '%' + search + '%'
240 } 240 }
241 }, 241 },
242 { 242 {
243 username: { 243 username: {
244 [ Sequelize.Op.iLike ]: '%' + search + '%' 244 [ Op.iLike ]: '%' + search + '%'
245 } 245 }
246 } 246 }
247 ] 247 ]
248 } 248 }
249 } 249 }
250 250
251 const query = { 251 const query: FindOptions = {
252 attributes: { 252 attributes: {
253 include: [ 253 include: [
254 [ 254 [
255 Sequelize.literal( 255 literal(
256 '(' + 256 '(' +
257 'SELECT COALESCE(SUM("size"), 0) ' + 257 'SELECT COALESCE(SUM("size"), 0) ' +
258 'FROM (' + 258 'FROM (' +
@@ -265,7 +265,7 @@ export class UserModel extends Model<UserModel> {
265 ')' 265 ')'
266 ), 266 ),
267 'videoQuotaUsed' 267 'videoQuotaUsed'
268 ] as any // FIXME: typings 268 ]
269 ] 269 ]
270 }, 270 },
271 offset: start, 271 offset: start,
@@ -291,7 +291,7 @@ export class UserModel extends Model<UserModel> {
291 const query = { 291 const query = {
292 where: { 292 where: {
293 role: { 293 role: {
294 [Sequelize.Op.in]: roles 294 [Op.in]: roles
295 } 295 }
296 } 296 }
297 } 297 }
@@ -387,7 +387,7 @@ export class UserModel extends Model<UserModel> {
387 387
388 const query = { 388 const query = {
389 where: { 389 where: {
390 [ Sequelize.Op.or ]: [ { username }, { email } ] 390 [ Op.or ]: [ { username }, { email } ]
391 } 391 }
392 } 392 }
393 393
@@ -510,7 +510,7 @@ export class UserModel extends Model<UserModel> {
510 const query = { 510 const query = {
511 where: { 511 where: {
512 username: { 512 username: {
513 [ Sequelize.Op.like ]: `%${search}%` 513 [ Op.like ]: `%${search}%`
514 } 514 }
515 }, 515 },
516 limit: 10 516 limit: 10
@@ -591,15 +591,11 @@ export class UserModel extends Model<UserModel> {
591 591
592 const uploadedTotal = videoFile.size + totalBytes 592 const uploadedTotal = videoFile.size + totalBytes
593 const uploadedDaily = videoFile.size + totalBytesDaily 593 const uploadedDaily = videoFile.size + totalBytesDaily
594 if (this.videoQuotaDaily === -1) {
595 return uploadedTotal < this.videoQuota
596 }
597 if (this.videoQuota === -1) {
598 return uploadedDaily < this.videoQuotaDaily
599 }
600 594
601 return (uploadedTotal < this.videoQuota) && 595 if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
602 (uploadedDaily < this.videoQuotaDaily) 596 if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
597
598 return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
603 } 599 }
604 600
605 private static generateUserQuotaBaseSQL (where?: string) { 601 private static generateUserQuotaBaseSQL (where?: string) {
@@ -619,14 +615,14 @@ export class UserModel extends Model<UserModel> {
619 private static getTotalRawQuery (query: string, userId: number) { 615 private static getTotalRawQuery (query: string, userId: number) {
620 const options = { 616 const options = {
621 bind: { userId }, 617 bind: { userId },
622 type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT 618 type: QueryTypes.SELECT as QueryTypes.SELECT
623 } 619 }
624 620
625 return UserModel.sequelize.query<{ total: number }>(query, options) 621 return UserModel.sequelize.query<{ total: string }>(query, options)
626 .then(([ { total } ]) => { 622 .then(([ { total } ]) => {
627 if (total === null) return 0 623 if (total === null) return 0
628 624
629 return parseInt(total + '', 10) 625 return parseInt(total, 10)
630 }) 626 })
631 } 627 }
632} 628}
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 1ebee8df5..4a466441c 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -56,46 +56,46 @@ export const unusedActorAttributesForAPI = [
56 'updatedAt' 56 'updatedAt'
57] 57]
58 58
59@DefaultScope({ 59@DefaultScope(() => ({
60 include: [ 60 include: [
61 { 61 {
62 model: () => ServerModel, 62 model: ServerModel,
63 required: false 63 required: false
64 }, 64 },
65 { 65 {
66 model: () => AvatarModel, 66 model: AvatarModel,
67 required: false 67 required: false
68 } 68 }
69 ] 69 ]
70}) 70}))
71@Scopes({ 71@Scopes(() => ({
72 [ScopeNames.FULL]: { 72 [ScopeNames.FULL]: {
73 include: [ 73 include: [
74 { 74 {
75 model: () => AccountModel.unscoped(), 75 model: AccountModel.unscoped(),
76 required: false 76 required: false
77 }, 77 },
78 { 78 {
79 model: () => VideoChannelModel.unscoped(), 79 model: VideoChannelModel.unscoped(),
80 required: false, 80 required: false,
81 include: [ 81 include: [
82 { 82 {
83 model: () => AccountModel, 83 model: AccountModel,
84 required: true 84 required: true
85 } 85 }
86 ] 86 ]
87 }, 87 },
88 { 88 {
89 model: () => ServerModel, 89 model: ServerModel,
90 required: false 90 required: false
91 }, 91 },
92 { 92 {
93 model: () => AvatarModel, 93 model: AvatarModel,
94 required: false 94 required: false
95 } 95 }
96 ] as any // FIXME: sequelize typings 96 ]
97 } 97 }
98}) 98}))
99@Table({ 99@Table({
100 tableName: 'actor', 100 tableName: 'actor',
101 indexes: [ 101 indexes: [
@@ -131,7 +131,7 @@ export const unusedActorAttributesForAPI = [
131export class ActorModel extends Model<ActorModel> { 131export class ActorModel extends Model<ActorModel> {
132 132
133 @AllowNull(false) 133 @AllowNull(false)
134 @Column({ type: DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)) }) // FIXME: sequelize typings 134 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
135 type: ActivityPubActorType 135 type: ActivityPubActorType
136 136
137 @AllowNull(false) 137 @AllowNull(false)
@@ -280,14 +280,16 @@ export class ActorModel extends Model<ActorModel> {
280 attributes: [ 'id' ], 280 attributes: [ 'id' ],
281 model: VideoChannelModel.unscoped(), 281 model: VideoChannelModel.unscoped(),
282 required: true, 282 required: true,
283 include: { 283 include: [
284 attributes: [ 'id' ], 284 {
285 model: VideoModel.unscoped(), 285 attributes: [ 'id' ],
286 required: true, 286 model: VideoModel.unscoped(),
287 where: { 287 required: true,
288 id: videoId 288 where: {
289 id: videoId
290 }
289 } 291 }
290 } 292 ]
291 } 293 }
292 ] 294 ]
293 } 295 }
@@ -295,7 +297,7 @@ export class ActorModel extends Model<ActorModel> {
295 transaction 297 transaction
296 } 298 }
297 299
298 return ActorModel.unscoped().findOne(query as any) // FIXME: typings 300 return ActorModel.unscoped().findOne(query)
299 } 301 }
300 302
301 static isActorUrlExist (url: string) { 303 static isActorUrlExist (url: string) {
@@ -389,8 +391,7 @@ export class ActorModel extends Model<ActorModel> {
389 } 391 }
390 392
391 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) { 393 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
392 // FIXME: typings 394 return ActorModel.increment(column, {
393 return (ActorModel as any).increment(column, {
394 by, 395 by,
395 where: { 396 where: {
396 id 397 id
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 854a5fb36..a02208b4e 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,14 +1,14 @@
1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' 1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
2import { AccountModel } from '../account/account' 2import { AccountModel } from '../account/account'
3 3
4@DefaultScope({ 4@DefaultScope(() => ({
5 include: [ 5 include: [
6 { 6 {
7 model: () => AccountModel, 7 model: AccountModel,
8 required: true 8 required: true
9 } 9 }
10 ] 10 ]
11}) 11}))
12@Table({ 12@Table({
13 tableName: 'application' 13 tableName: 'application'
14}) 14})
diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts
index b4a841edd..42c59bb79 100644
--- a/server/models/oauth/oauth-client.ts
+++ b/server/models/oauth/oauth-client.ts
@@ -24,10 +24,10 @@ export class OAuthClientModel extends Model<OAuthClientModel> {
24 @Column 24 @Column
25 clientSecret: string 25 clientSecret: string
26 26
27 @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: sequelize typings 27 @Column(DataType.ARRAY(DataType.STRING))
28 grants: string[] 28 grants: string[]
29 29
30 @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: sequelize typings 30 @Column(DataType.ARRAY(DataType.STRING))
31 redirectUris: string[] 31 redirectUris: string[]
32 32
33 @CreatedAt 33 @CreatedAt
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 3f41ee63b..903d551df 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -34,30 +34,30 @@ enum ScopeNames {
34 WITH_USER = 'WITH_USER' 34 WITH_USER = 'WITH_USER'
35} 35}
36 36
37@Scopes({ 37@Scopes(() => ({
38 [ScopeNames.WITH_USER]: { 38 [ScopeNames.WITH_USER]: {
39 include: [ 39 include: [
40 { 40 {
41 model: () => UserModel.unscoped(), 41 model: UserModel.unscoped(),
42 required: true, 42 required: true,
43 include: [ 43 include: [
44 { 44 {
45 attributes: [ 'id' ], 45 attributes: [ 'id' ],
46 model: () => AccountModel.unscoped(), 46 model: AccountModel.unscoped(),
47 required: true, 47 required: true,
48 include: [ 48 include: [
49 { 49 {
50 attributes: [ 'id', 'url' ], 50 attributes: [ 'id', 'url' ],
51 model: () => ActorModel.unscoped(), 51 model: ActorModel.unscoped(),
52 required: true 52 required: true
53 } 53 }
54 ] 54 ]
55 } 55 }
56 ] 56 ]
57 } 57 }
58 ] as any // FIXME: sequelize typings 58 ]
59 } 59 }
60}) 60}))
61@Table({ 61@Table({
62 tableName: 'oAuthToken', 62 tableName: 'oAuthToken',
63 indexes: [ 63 indexes: [
@@ -167,11 +167,13 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
167 } 167 }
168 } 168 }
169 169
170 return OAuthTokenModel.scope(ScopeNames.WITH_USER).findOne(query).then(token => { 170 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
171 if (token) token['user'] = token.User 171 .findOne(query)
172 .then(token => {
173 if (token) token[ 'user' ] = token.User
172 174
173 return token 175 return token
174 }) 176 })
175 } 177 }
176 178
177 static getByRefreshTokenAndPopulateUser (refreshToken: string) { 179 static getByRefreshTokenAndPopulateUser (refreshToken: string) {
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index cbeaa662b..eb2222256 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -13,7 +13,7 @@ import {
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' 18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
@@ -27,7 +27,7 @@ import { ServerModel } from '../server/server'
27import { sample } from 'lodash' 27import { sample } from 'lodash'
28import { isTestInstance } from '../../helpers/core-utils' 28import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 29import * as Bluebird from 'bluebird'
30import * as Sequelize from 'sequelize' 30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' 31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config' 32import { CONFIG } from '../../initializers/config'
33 33
@@ -35,32 +35,32 @@ export enum ScopeNames {
35 WITH_VIDEO = 'WITH_VIDEO' 35 WITH_VIDEO = 'WITH_VIDEO'
36} 36}
37 37
38@Scopes({ 38@Scopes(() => ({
39 [ ScopeNames.WITH_VIDEO ]: { 39 [ ScopeNames.WITH_VIDEO ]: {
40 include: [ 40 include: [
41 { 41 {
42 model: () => VideoFileModel, 42 model: VideoFileModel,
43 required: false, 43 required: false,
44 include: [ 44 include: [
45 { 45 {
46 model: () => VideoModel, 46 model: VideoModel,
47 required: true 47 required: true
48 } 48 }
49 ] 49 ]
50 }, 50 },
51 { 51 {
52 model: () => VideoStreamingPlaylistModel, 52 model: VideoStreamingPlaylistModel,
53 required: false, 53 required: false,
54 include: [ 54 include: [
55 { 55 {
56 model: () => VideoModel, 56 model: VideoModel,
57 required: true 57 required: true
58 } 58 }
59 ] 59 ]
60 } 60 }
61 ] as any // FIXME: sequelize typings 61 ]
62 } 62 }
63}) 63}))
64 64
65@Table({ 65@Table({
66 tableName: 'videoRedundancy', 66 tableName: 'videoRedundancy',
@@ -192,7 +192,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
193 } 193 }
194 194
195 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 195 static loadByUrl (url: string, transaction?: Transaction) {
196 const query = { 196 const query = {
197 where: { 197 where: {
198 url 198 url
@@ -292,7 +292,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
292 where: { 292 where: {
293 privacy: VideoPrivacy.PUBLIC, 293 privacy: VideoPrivacy.PUBLIC,
294 views: { 294 views: {
295 [ Sequelize.Op.gte ]: minViews 295 [ Op.gte ]: minViews
296 } 296 }
297 }, 297 },
298 include: [ 298 include: [
@@ -315,7 +315,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
315 actorId: actor.id, 315 actorId: actor.id,
316 strategy, 316 strategy,
317 createdAt: { 317 createdAt: {
318 [ Sequelize.Op.lt ]: expiredDate 318 [ Op.lt ]: expiredDate
319 } 319 }
320 } 320 }
321 } 321 }
@@ -326,7 +326,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { 326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
327 const actor = await getServerActor() 327 const actor = await getServerActor()
328 328
329 const options = { 329 const query: FindOptions = {
330 include: [ 330 include: [
331 { 331 {
332 attributes: [], 332 attributes: [],
@@ -340,12 +340,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
340 ] 340 ]
341 } 341 }
342 342
343 return VideoFileModel.sum('size', options as any) // FIXME: typings 343 return VideoFileModel.aggregate('size', 'SUM', query)
344 .then(v => { 344 .then(result => parseAggregateResult(result))
345 if (!v || isNaN(v)) return 0
346
347 return v
348 })
349 } 345 }
350 346
351 static async listLocalExpired () { 347 static async listLocalExpired () {
@@ -355,7 +351,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
355 where: { 351 where: {
356 actorId: actor.id, 352 actorId: actor.id,
357 expiresOn: { 353 expiresOn: {
358 [ Sequelize.Op.lt ]: new Date() 354 [ Op.lt ]: new Date()
359 } 355 }
360 } 356 }
361 } 357 }
@@ -369,10 +365,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
369 const query = { 365 const query = {
370 where: { 366 where: {
371 actorId: { 367 actorId: {
372 [Sequelize.Op.ne]: actor.id 368 [Op.ne]: actor.id
373 }, 369 },
374 expiresOn: { 370 expiresOn: {
375 [ Sequelize.Op.lt ]: new Date() 371 [ Op.lt ]: new Date()
376 } 372 }
377 } 373 }
378 } 374 }
@@ -428,12 +424,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
428 static async getStats (strategy: VideoRedundancyStrategy) { 424 static async getStats (strategy: VideoRedundancyStrategy) {
429 const actor = await getServerActor() 425 const actor = await getServerActor()
430 426
431 const query = { 427 const query: FindOptions = {
432 raw: true, 428 raw: true,
433 attributes: [ 429 attributes: [
434 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ], 430 [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ],
435 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ], 431 [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ],
436 [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ] 432 [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ]
437 ], 433 ],
438 where: { 434 where: {
439 strategy, 435 strategy,
@@ -448,9 +444,9 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
448 ] 444 ]
449 } 445 }
450 446
451 return VideoRedundancyModel.findOne(query as any) // FIXME: typings 447 return VideoRedundancyModel.findOne(query)
452 .then((r: any) => ({ 448 .then((r: any) => ({
453 totalUsed: parseInt(r.totalUsed.toString(), 10), 449 totalUsed: parseAggregateResult(r.totalUsed),
454 totalVideos: r.totalVideos, 450 totalVideos: r.totalVideos,
455 totalVideoFiles: r.totalVideoFiles 451 totalVideoFiles: r.totalVideoFiles
456 })) 452 }))
@@ -503,7 +499,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
503 private static async buildVideoFileForDuplication () { 499 private static async buildVideoFileForDuplication () {
504 const actor = await getServerActor() 500 const actor = await getServerActor()
505 501
506 const notIn = Sequelize.literal( 502 const notIn = literal(
507 '(' + 503 '(' +
508 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + 504 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
509 ')' 505 ')'
@@ -515,7 +511,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
515 required: true, 511 required: true,
516 where: { 512 where: {
517 id: { 513 id: {
518 [ Sequelize.Op.notIn ]: notIn 514 [ Op.notIn ]: notIn
519 } 515 }
520 } 516 }
521 } 517 }
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 450f27152..92c01f642 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -9,11 +9,11 @@ enum ScopeNames {
9 WITH_SERVER = 'WITH_SERVER' 9 WITH_SERVER = 'WITH_SERVER'
10} 10}
11 11
12@Scopes({ 12@Scopes(() => ({
13 [ScopeNames.WITH_ACCOUNT]: { 13 [ScopeNames.WITH_ACCOUNT]: {
14 include: [ 14 include: [
15 { 15 {
16 model: () => AccountModel, 16 model: AccountModel,
17 required: true 17 required: true
18 } 18 }
19 ] 19 ]
@@ -21,12 +21,12 @@ enum ScopeNames {
21 [ScopeNames.WITH_SERVER]: { 21 [ScopeNames.WITH_SERVER]: {
22 include: [ 22 include: [
23 { 23 {
24 model: () => ServerModel, 24 model: ServerModel,
25 required: true 25 required: true
26 } 26 }
27 ] 27 ]
28 } 28 }
29}) 29}))
30 30
31@Table({ 31@Table({
32 tableName: 'serverBlocklist', 32 tableName: 'serverBlocklist',
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 98170a00e..2b172f608 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -118,6 +118,15 @@ function buildWhereIdOrUUID (id: number | string) {
118 return validator.isInt('' + id) ? { id } : { uuid: id } 118 return validator.isInt('' + id) ? { id } : { uuid: id }
119} 119}
120 120
121function parseAggregateResult (result: any) {
122 if (!result) return 0
123
124 const total = parseInt(result + '', 10)
125 if (isNaN(total)) return 0
126
127 return total
128}
129
121// --------------------------------------------------------------------------- 130// ---------------------------------------------------------------------------
122 131
123export { 132export {
@@ -131,7 +140,8 @@ export {
131 buildServerIdsFollowedBy, 140 buildServerIdsFollowedBy,
132 buildTrigramSearchIndex, 141 buildTrigramSearchIndex,
133 buildWhereIdOrUUID, 142 buildWhereIdOrUUID,
134 isOutdated 143 isOutdated,
144 parseAggregateResult
135} 145}
136 146
137// --------------------------------------------------------------------------- 147// ---------------------------------------------------------------------------
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts
index 048b47613..0fc3cfd4c 100644
--- a/server/models/video/tag.ts
+++ b/server/models/video/tag.ts
@@ -75,7 +75,7 @@ export class TagModel extends Model<TagModel> {
75 type: QueryTypes.SELECT as QueryTypes.SELECT 75 type: QueryTypes.SELECT as QueryTypes.SELECT
76 } 76 }
77 77
78 return TagModel.sequelize.query<{ name }>(query, options) 78 return TagModel.sequelize.query<{ name: string }>(query, options)
79 .then(data => data.map(d => d.name)) 79 .then(data => data.map(d => d.name))
80 } 80 }
81} 81}
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index baa5533ac..ec945893f 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -75,8 +75,8 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
75 updatedAt: Date 75 updatedAt: Date
76 76
77 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { 77 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
78 [ThumbnailType.THUMBNAIL]: { 78 [ThumbnailType.MINIATURE]: {
79 label: 'thumbnail', 79 label: 'miniature',
80 directory: CONFIG.STORAGE.THUMBNAILS_DIR, 80 directory: CONFIG.STORAGE.THUMBNAILS_DIR,
81 staticPath: STATIC_PATHS.THUMBNAILS 81 staticPath: STATIC_PATHS.THUMBNAILS
82 }, 82 },
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 45c60e26b..76243bf48 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -12,7 +12,7 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { throwIfNotValid } from '../utils' 15import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 16import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
@@ -26,17 +26,17 @@ export enum ScopeNames {
26 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 26 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
27} 27}
28 28
29@Scopes({ 29@Scopes(() => ({
30 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: { 30 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
31 include: [ 31 include: [
32 { 32 {
33 attributes: [ 'uuid', 'remote' ], 33 attributes: [ 'uuid', 'remote' ],
34 model: () => VideoModel.unscoped(), 34 model: VideoModel.unscoped(),
35 required: true 35 required: true
36 } 36 }
37 ] 37 ]
38 } 38 }
39}) 39}))
40 40
41@Table({ 41@Table({
42 tableName: 'videoCaption', 42 tableName: 'videoCaption',
@@ -97,12 +97,9 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
97 const videoInclude = { 97 const videoInclude = {
98 model: VideoModel.unscoped(), 98 model: VideoModel.unscoped(),
99 attributes: [ 'id', 'remote', 'uuid' ], 99 attributes: [ 'id', 'remote', 'uuid' ],
100 where: { } 100 where: buildWhereIdOrUUID(videoId)
101 } 101 }
102 102
103 if (typeof videoId === 'string') videoInclude.where['uuid'] = videoId
104 else videoInclude.where['id'] = videoId
105
106 const query = { 103 const query = {
107 where: { 104 where: {
108 language 105 language
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts
index a4f4d53f1..171d4574d 100644
--- a/server/models/video/video-change-ownership.ts
+++ b/server/models/video/video-change-ownership.ts
@@ -23,29 +23,29 @@ enum ScopeNames {
23 } 23 }
24 ] 24 ]
25}) 25})
26@Scopes({ 26@Scopes(() => ({
27 [ScopeNames.FULL]: { 27 [ScopeNames.FULL]: {
28 include: [ 28 include: [
29 { 29 {
30 model: () => AccountModel, 30 model: AccountModel,
31 as: 'Initiator', 31 as: 'Initiator',
32 required: true 32 required: true
33 }, 33 },
34 { 34 {
35 model: () => AccountModel, 35 model: AccountModel,
36 as: 'NextOwner', 36 as: 'NextOwner',
37 required: true 37 required: true
38 }, 38 },
39 { 39 {
40 model: () => VideoModel, 40 model: VideoModel,
41 required: true, 41 required: true,
42 include: [ 42 include: [
43 { model: () => VideoFileModel } 43 { model: VideoFileModel }
44 ] 44 ]
45 } 45 }
46 ] as any // FIXME: sequelize typings 46 ]
47 } 47 }
48}) 48}))
49export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> { 49export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> {
50 @CreatedAt 50 @CreatedAt
51 createdAt: Date 51 createdAt: Date
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 901006dea..fb70e6625 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -58,15 +58,15 @@ type AvailableForListOptions = {
58 actorId: number 58 actorId: number
59} 59}
60 60
61@DefaultScope({ 61@DefaultScope(() => ({
62 include: [ 62 include: [
63 { 63 {
64 model: () => ActorModel, 64 model: ActorModel,
65 required: true 65 required: true
66 } 66 }
67 ] 67 ]
68}) 68}))
69@Scopes({ 69@Scopes(() => ({
70 [ScopeNames.SUMMARY]: (withAccount = false) => { 70 [ScopeNames.SUMMARY]: (withAccount = false) => {
71 const base: FindOptions = { 71 const base: FindOptions = {
72 attributes: [ 'name', 'description', 'id', 'actorId' ], 72 attributes: [ 'name', 'description', 'id', 'actorId' ],
@@ -142,22 +142,22 @@ type AvailableForListOptions = {
142 [ScopeNames.WITH_ACCOUNT]: { 142 [ScopeNames.WITH_ACCOUNT]: {
143 include: [ 143 include: [
144 { 144 {
145 model: () => AccountModel, 145 model: AccountModel,
146 required: true 146 required: true
147 } 147 }
148 ] 148 ]
149 }, 149 },
150 [ScopeNames.WITH_VIDEOS]: { 150 [ScopeNames.WITH_VIDEOS]: {
151 include: [ 151 include: [
152 () => VideoModel 152 VideoModel
153 ] 153 ]
154 }, 154 },
155 [ScopeNames.WITH_ACTOR]: { 155 [ScopeNames.WITH_ACTOR]: {
156 include: [ 156 include: [
157 () => ActorModel 157 ActorModel
158 ] 158 ]
159 } 159 }
160}) 160}))
161@Table({ 161@Table({
162 tableName: 'videoChannel', 162 tableName: 'videoChannel',
163 indexes 163 indexes
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 5f7cd3671..fee11ec5f 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -30,7 +30,7 @@ import { UserModel } from '../account/user'
30import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 30import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
31import { regexpCapture } from '../../helpers/regexp' 31import { regexpCapture } from '../../helpers/regexp'
32import { uniq } from 'lodash' 32import { uniq } from 'lodash'
33import { FindOptions, Op, Order, Sequelize, Transaction } from 'sequelize' 33import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
34 34
35enum ScopeNames { 35enum ScopeNames {
36 WITH_ACCOUNT = 'WITH_ACCOUNT', 36 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -39,7 +39,7 @@ enum ScopeNames {
39 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' 39 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
40} 40}
41 41
42@Scopes({ 42@Scopes(() => ({
43 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { 43 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
44 return { 44 return {
45 attributes: { 45 attributes: {
@@ -63,34 +63,34 @@ enum ScopeNames {
63 ] 63 ]
64 ] 64 ]
65 } 65 }
66 } 66 } as FindOptions
67 }, 67 },
68 [ScopeNames.WITH_ACCOUNT]: { 68 [ScopeNames.WITH_ACCOUNT]: {
69 include: [ 69 include: [
70 { 70 {
71 model: () => AccountModel, 71 model: AccountModel,
72 include: [ 72 include: [
73 { 73 {
74 model: () => ActorModel, 74 model: ActorModel,
75 include: [ 75 include: [
76 { 76 {
77 model: () => ServerModel, 77 model: ServerModel,
78 required: false 78 required: false
79 }, 79 },
80 { 80 {
81 model: () => AvatarModel, 81 model: AvatarModel,
82 required: false 82 required: false
83 } 83 }
84 ] 84 ]
85 } 85 }
86 ] 86 ]
87 } 87 }
88 ] as any // FIXME: sequelize typings 88 ]
89 }, 89 },
90 [ScopeNames.WITH_IN_REPLY_TO]: { 90 [ScopeNames.WITH_IN_REPLY_TO]: {
91 include: [ 91 include: [
92 { 92 {
93 model: () => VideoCommentModel, 93 model: VideoCommentModel,
94 as: 'InReplyToVideoComment' 94 as: 'InReplyToVideoComment'
95 } 95 }
96 ] 96 ]
@@ -98,19 +98,19 @@ enum ScopeNames {
98 [ScopeNames.WITH_VIDEO]: { 98 [ScopeNames.WITH_VIDEO]: {
99 include: [ 99 include: [
100 { 100 {
101 model: () => VideoModel, 101 model: VideoModel,
102 required: true, 102 required: true,
103 include: [ 103 include: [
104 { 104 {
105 model: () => VideoChannelModel.unscoped(), 105 model: VideoChannelModel.unscoped(),
106 required: true, 106 required: true,
107 include: [ 107 include: [
108 { 108 {
109 model: () => AccountModel, 109 model: AccountModel,
110 required: true, 110 required: true,
111 include: [ 111 include: [
112 { 112 {
113 model: () => ActorModel, 113 model: ActorModel,
114 required: true 114 required: true
115 } 115 }
116 ] 116 ]
@@ -119,9 +119,9 @@ enum ScopeNames {
119 } 119 }
120 ] 120 ]
121 } 121 }
122 ] as any // FIXME: sequelize typings 122 ]
123 } 123 }
124}) 124}))
125@Table({ 125@Table({
126 tableName: 'videoComment', 126 tableName: 'videoComment',
127 indexes: [ 127 indexes: [
@@ -313,8 +313,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
313 } 313 }
314 } 314 }
315 315
316 // FIXME: typings 316 const scopes: (string | ScopeOptions)[] = [
317 const scopes: any[] = [
318 ScopeNames.WITH_ACCOUNT, 317 ScopeNames.WITH_ACCOUNT,
319 { 318 {
320 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 319 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index c14d96bc5..2203a7aba 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -19,11 +19,11 @@ import {
19 isVideoFileSizeValid, 19 isVideoFileSizeValid,
20 isVideoFPSResolutionValid 20 isVideoFPSResolutionValid
21} from '../../helpers/custom-validators/videos' 21} from '../../helpers/custom-validators/videos'
22import { throwIfNotValid } from '../utils' 22import { parseAggregateResult, throwIfNotValid } from '../utils'
23import { VideoModel } from './video' 23import { VideoModel } from './video'
24import * as Sequelize from 'sequelize'
25import { VideoRedundancyModel } from '../redundancy/video-redundancy' 24import { VideoRedundancyModel } from '../redundancy/video-redundancy'
26import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 25import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26import { FindOptions, QueryTypes, Transaction } from 'sequelize'
27 27
28@Table({ 28@Table({
29 tableName: 'videoFile', 29 tableName: 'videoFile',
@@ -97,15 +97,13 @@ export class VideoFileModel extends Model<VideoFileModel> {
97 static doesInfohashExist (infoHash: string) { 97 static doesInfohashExist (infoHash: string) {
98 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' 98 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
99 const options = { 99 const options = {
100 type: Sequelize.QueryTypes.SELECT, 100 type: QueryTypes.SELECT,
101 bind: { infoHash }, 101 bind: { infoHash },
102 raw: true 102 raw: true
103 } 103 }
104 104
105 return VideoModel.sequelize.query(query, options) 105 return VideoModel.sequelize.query(query, options)
106 .then(results => { 106 .then(results => results.length === 1)
107 return results.length === 1
108 })
109 } 107 }
110 108
111 static loadWithVideo (id: number) { 109 static loadWithVideo (id: number) {
@@ -121,7 +119,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
121 return VideoFileModel.findByPk(id, options) 119 return VideoFileModel.findByPk(id, options)
122 } 120 }
123 121
124 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Sequelize.Transaction) { 122 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
125 const query = { 123 const query = {
126 include: [ 124 include: [
127 { 125 {
@@ -144,8 +142,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
144 return VideoFileModel.findAll(query) 142 return VideoFileModel.findAll(query)
145 } 143 }
146 144
147 static async getStats () { 145 static getStats () {
148 let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { 146 const query: FindOptions = {
149 include: [ 147 include: [
150 { 148 {
151 attributes: [], 149 attributes: [],
@@ -155,13 +153,12 @@ export class VideoFileModel extends Model<VideoFileModel> {
155 } 153 }
156 } 154 }
157 ] 155 ]
158 } as any)
159 // Sequelize could return null...
160 if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0
161
162 return {
163 totalLocalVideoFilesSize
164 } 156 }
157
158 return VideoFileModel.aggregate('size', 'SUM', query)
159 .then(result => ({
160 totalLocalVideoFilesSize: parseAggregateResult(result)
161 }))
165 } 162 }
166 163
167 hasSameUniqueKeysThan (other: VideoFileModel) { 164 hasSameUniqueKeysThan (other: VideoFileModel) {
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 89992a5a8..877fcbc57 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -59,7 +59,7 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
59 views: video.views, 59 views: video.views,
60 likes: video.likes, 60 likes: video.likes,
61 dislikes: video.dislikes, 61 dislikes: video.dislikes,
62 thumbnailPath: video.getThumbnailStaticPath(), 62 thumbnailPath: video.getMiniatureStaticPath(),
63 previewPath: video.getPreviewStaticPath(), 63 previewPath: video.getPreviewStaticPath(),
64 embedPath: video.getEmbedStaticPath(), 64 embedPath: video.getEmbedStaticPath(),
65 createdAt: video.createdAt, 65 createdAt: video.createdAt,
@@ -301,6 +301,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
301 }) 301 })
302 } 302 }
303 303
304 const miniature = video.getMiniature()
305
304 return { 306 return {
305 type: 'Video' as 'Video', 307 type: 'Video' as 'Video',
306 id: video.url, 308 id: video.url,
@@ -326,10 +328,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
326 subtitleLanguage, 328 subtitleLanguage,
327 icon: { 329 icon: {
328 type: 'Image', 330 type: 'Image',
329 url: video.getThumbnail().getUrl(), 331 url: miniature.getUrl(),
330 mediaType: 'image/jpeg', 332 mediaType: 'image/jpeg',
331 width: video.getThumbnail().width, 333 width: miniature.width,
332 height: video.getThumbnail().height 334 height: miniature.height
333 }, 335 },
334 url, 336 url,
335 likes: getVideoLikesActivityPubUrl(video), 337 likes: getVideoLikesActivityPubUrl(video),
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index 588a13a4f..480a671c8 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -21,18 +21,18 @@ import { VideoImport, VideoImportState } from '../../../shared'
21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' 21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
22import { UserModel } from '../account/user' 22import { UserModel } from '../account/user'
23 23
24@DefaultScope({ 24@DefaultScope(() => ({
25 include: [ 25 include: [
26 { 26 {
27 model: () => UserModel.unscoped(), 27 model: UserModel.unscoped(),
28 required: true 28 required: true
29 }, 29 },
30 { 30 {
31 model: () => VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]), 31 model: VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]),
32 required: false 32 required: false
33 } 33 }
34 ] 34 ]
35}) 35}))
36 36
37@Table({ 37@Table({
38 tableName: 'videoImport', 38 tableName: 'videoImport',
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 3e436acfc..63b4a0715 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -42,7 +42,7 @@ import { activityPubCollectionPagination } from '../../helpers/activitypub'
42import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' 42import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
43import { ThumbnailModel } from './thumbnail' 43import { ThumbnailModel } from './thumbnail'
44import { ActivityIconObject } from '../../../shared/models/activitypub/objects' 44import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
45import { fn, literal, Op, Transaction } from 'sequelize' 45import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
46 46
47enum ScopeNames { 47enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -61,11 +61,11 @@ type AvailableForListOptions = {
61 privateAndUnlisted?: boolean 61 privateAndUnlisted?: boolean
62} 62}
63 63
64@Scopes({ 64@Scopes(() => ({
65 [ ScopeNames.WITH_THUMBNAIL ]: { 65 [ ScopeNames.WITH_THUMBNAIL ]: {
66 include: [ 66 include: [
67 { 67 {
68 model: () => ThumbnailModel, 68 model: ThumbnailModel,
69 required: false 69 required: false
70 } 70 }
71 ] 71 ]
@@ -74,20 +74,16 @@ type AvailableForListOptions = {
74 attributes: { 74 attributes: {
75 include: [ 75 include: [
76 [ 76 [
77 fn('COUNT', 'toto'),
78 'coucou'
79 ],
80 [
81 literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), 77 literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'),
82 'videosLength' 78 'videosLength'
83 ] 79 ]
84 ] 80 ]
85 } 81 }
86 }, 82 } as FindOptions,
87 [ ScopeNames.WITH_ACCOUNT ]: { 83 [ ScopeNames.WITH_ACCOUNT ]: {
88 include: [ 84 include: [
89 { 85 {
90 model: () => AccountModel, 86 model: AccountModel,
91 required: true 87 required: true
92 } 88 }
93 ] 89 ]
@@ -95,11 +91,11 @@ type AvailableForListOptions = {
95 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { 91 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
96 include: [ 92 include: [
97 { 93 {
98 model: () => AccountModel.scope(AccountScopeNames.SUMMARY), 94 model: AccountModel.scope(AccountScopeNames.SUMMARY),
99 required: true 95 required: true
100 }, 96 },
101 { 97 {
102 model: () => VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), 98 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
103 required: false 99 required: false
104 } 100 }
105 ] 101 ]
@@ -107,11 +103,11 @@ type AvailableForListOptions = {
107 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: { 103 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
108 include: [ 104 include: [
109 { 105 {
110 model: () => AccountModel, 106 model: AccountModel,
111 required: true 107 required: true
112 }, 108 },
113 { 109 {
114 model: () => VideoChannelModel, 110 model: VideoChannelModel,
115 required: false 111 required: false
116 } 112 }
117 ] 113 ]
@@ -132,7 +128,7 @@ type AvailableForListOptions = {
132 ] 128 ]
133 } 129 }
134 130
135 const whereAnd: any[] = [] 131 const whereAnd: WhereOptions[] = []
136 132
137 if (options.privateAndUnlisted !== true) { 133 if (options.privateAndUnlisted !== true) {
138 whereAnd.push({ 134 whereAnd.push({
@@ -178,9 +174,9 @@ type AvailableForListOptions = {
178 required: false 174 required: false
179 } 175 }
180 ] 176 ]
181 } 177 } as FindOptions
182 } 178 }
183}) 179}))
184 180
185@Table({ 181@Table({
186 tableName: 'videoPlaylist', 182 tableName: 'videoPlaylist',
@@ -269,6 +265,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
269 VideoPlaylistElements: VideoPlaylistElementModel[] 265 VideoPlaylistElements: VideoPlaylistElementModel[]
270 266
271 @HasOne(() => ThumbnailModel, { 267 @HasOne(() => ThumbnailModel, {
268
272 foreignKey: { 269 foreignKey: {
273 name: 'videoPlaylistId', 270 name: 'videoPlaylistId',
274 allowNull: true 271 allowNull: true
@@ -294,7 +291,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
294 order: getSort(options.sort) 291 order: getSort(options.sort)
295 } 292 }
296 293
297 const scopes = [ 294 const scopes: (string | ScopeOptions)[] = [
298 { 295 {
299 method: [ 296 method: [
300 ScopeNames.AVAILABLE_FOR_LIST, 297 ScopeNames.AVAILABLE_FOR_LIST,
@@ -306,7 +303,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
306 privateAndUnlisted: options.privateAndUnlisted 303 privateAndUnlisted: options.privateAndUnlisted
307 } as AvailableForListOptions 304 } as AvailableForListOptions
308 ] 305 ]
309 } as any, // FIXME: typings 306 },
310 ScopeNames.WITH_VIDEOS_LENGTH, 307 ScopeNames.WITH_VIDEOS_LENGTH,
311 ScopeNames.WITH_THUMBNAIL 308 ScopeNames.WITH_THUMBNAIL
312 ] 309 ]
@@ -348,7 +345,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
348 model: VideoPlaylistElementModel.unscoped(), 345 model: VideoPlaylistElementModel.unscoped(),
349 where: { 346 where: {
350 videoId: { 347 videoId: {
351 [Op.any]: videoIds 348 [Op.in]: videoIds // FIXME: sequelize ANY seems broken
352 } 349 }
353 }, 350 },
354 required: true 351 required: true
@@ -427,12 +424,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
427 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) 424 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
428 } 425 }
429 426
430 setThumbnail (thumbnail: ThumbnailModel) { 427 async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) {
431 this.Thumbnail = thumbnail 428 thumbnail.videoPlaylistId = this.id
432 }
433 429
434 getThumbnail () { 430 this.Thumbnail = await thumbnail.save({ transaction: t })
435 return this.Thumbnail
436 } 431 }
437 432
438 hasThumbnail () { 433 hasThumbnail () {
@@ -448,13 +443,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
448 getThumbnailUrl () { 443 getThumbnailUrl () {
449 if (!this.hasThumbnail()) return null 444 if (!this.hasThumbnail()) return null
450 445
451 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename 446 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
452 } 447 }
453 448
454 getThumbnailStaticPath () { 449 getThumbnailStaticPath () {
455 if (!this.hasThumbnail()) return null 450 if (!this.hasThumbnail()) return null
456 451
457 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename) 452 return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
458 } 453 }
459 454
460 setAsRefreshed () { 455 setAsRefreshed () {
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index c83f6c5b0..fda2d7cea 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -14,15 +14,15 @@ enum ScopeNames {
14 WITH_ACTOR = 'WITH_ACTOR' 14 WITH_ACTOR = 'WITH_ACTOR'
15} 15}
16 16
17@Scopes({ 17@Scopes(() => ({
18 [ScopeNames.FULL]: { 18 [ScopeNames.FULL]: {
19 include: [ 19 include: [
20 { 20 {
21 model: () => ActorModel, 21 model: ActorModel,
22 required: true 22 required: true
23 }, 23 },
24 { 24 {
25 model: () => VideoModel, 25 model: VideoModel,
26 required: true 26 required: true
27 } 27 }
28 ] 28 ]
@@ -30,12 +30,12 @@ enum ScopeNames {
30 [ScopeNames.WITH_ACTOR]: { 30 [ScopeNames.WITH_ACTOR]: {
31 include: [ 31 include: [
32 { 32 {
33 model: () => ActorModel, 33 model: ActorModel,
34 required: true 34 required: true
35 } 35 }
36 ] 36 ]
37 } 37 }
38}) 38}))
39@Table({ 39@Table({
40 tableName: 'videoShare', 40 tableName: 'videoShare',
41 indexes: [ 41 indexes: [
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index b30267e09..31dc82c54 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -26,7 +26,7 @@ import { QueryTypes, Op } from 'sequelize'
26 fields: [ 'p2pMediaLoaderInfohashes' ], 26 fields: [ 'p2pMediaLoaderInfohashes' ],
27 using: 'gin' 27 using: 'gin'
28 } 28 }
29 ] as any // FIXME: sequelize typings 29 ]
30}) 30})
31export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> { 31export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> {
32 @CreatedAt 32 @CreatedAt
@@ -46,7 +46,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
46 46
47 @AllowNull(false) 47 @AllowNull(false)
48 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes')) 48 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
49 @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: typings 49 @Column(DataType.ARRAY(DataType.STRING))
50 p2pMediaLoaderInfohashes: string[] 50 p2pMediaLoaderInfohashes: string[]
51 51
52 @AllowNull(false) 52 @AllowNull(false)
@@ -87,7 +87,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
87 raw: true 87 raw: true
88 } 88 }
89 89
90 return VideoModel.sequelize.query<any>(query, options) 90 return VideoModel.sequelize.query<object>(query, options)
91 .then(results => results.length === 1) 91 .then(results => results.length === 1)
92 } 92 }
93 93
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 329cebd28..18f18795e 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -227,12 +227,12 @@ type AvailableForListIDsOptions = {
227 historyOfUser?: UserModel 227 historyOfUser?: UserModel
228} 228}
229 229
230@Scopes({ 230@Scopes(() => ({
231 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 231 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
232 const query: FindOptions = { 232 const query: FindOptions = {
233 where: { 233 where: {
234 id: { 234 id: {
235 [ Op.in ]: options.ids // FIXME: sequelize any seems broken 235 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
236 } 236 }
237 }, 237 },
238 include: [ 238 include: [
@@ -486,7 +486,7 @@ type AvailableForListIDsOptions = {
486 [ ScopeNames.WITH_THUMBNAILS ]: { 486 [ ScopeNames.WITH_THUMBNAILS ]: {
487 include: [ 487 include: [
488 { 488 {
489 model: () => ThumbnailModel, 489 model: ThumbnailModel,
490 required: false 490 required: false
491 } 491 }
492 ] 492 ]
@@ -495,48 +495,48 @@ type AvailableForListIDsOptions = {
495 include: [ 495 include: [
496 { 496 {
497 attributes: [ 'accountId' ], 497 attributes: [ 'accountId' ],
498 model: () => VideoChannelModel.unscoped(), 498 model: VideoChannelModel.unscoped(),
499 required: true, 499 required: true,
500 include: [ 500 include: [
501 { 501 {
502 attributes: [ 'userId' ], 502 attributes: [ 'userId' ],
503 model: () => AccountModel.unscoped(), 503 model: AccountModel.unscoped(),
504 required: true 504 required: true
505 } 505 }
506 ] 506 ]
507 } 507 }
508 ] as any // FIXME: sequelize typings 508 ]
509 }, 509 },
510 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 510 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
511 include: [ 511 include: [
512 { 512 {
513 model: () => VideoChannelModel.unscoped(), 513 model: VideoChannelModel.unscoped(),
514 required: true, 514 required: true,
515 include: [ 515 include: [
516 { 516 {
517 attributes: { 517 attributes: {
518 exclude: [ 'privateKey', 'publicKey' ] 518 exclude: [ 'privateKey', 'publicKey' ]
519 }, 519 },
520 model: () => ActorModel.unscoped(), 520 model: ActorModel.unscoped(),
521 required: true, 521 required: true,
522 include: [ 522 include: [
523 { 523 {
524 attributes: [ 'host' ], 524 attributes: [ 'host' ],
525 model: () => ServerModel.unscoped(), 525 model: ServerModel.unscoped(),
526 required: false 526 required: false
527 }, 527 },
528 { 528 {
529 model: () => AvatarModel.unscoped(), 529 model: AvatarModel.unscoped(),
530 required: false 530 required: false
531 } 531 }
532 ] 532 ]
533 }, 533 },
534 { 534 {
535 model: () => AccountModel.unscoped(), 535 model: AccountModel.unscoped(),
536 required: true, 536 required: true,
537 include: [ 537 include: [
538 { 538 {
539 model: () => ActorModel.unscoped(), 539 model: ActorModel.unscoped(),
540 attributes: { 540 attributes: {
541 exclude: [ 'privateKey', 'publicKey' ] 541 exclude: [ 'privateKey', 'publicKey' ]
542 }, 542 },
@@ -544,11 +544,11 @@ type AvailableForListIDsOptions = {
544 include: [ 544 include: [
545 { 545 {
546 attributes: [ 'host' ], 546 attributes: [ 'host' ],
547 model: () => ServerModel.unscoped(), 547 model: ServerModel.unscoped(),
548 required: false 548 required: false
549 }, 549 },
550 { 550 {
551 model: () => AvatarModel.unscoped(), 551 model: AvatarModel.unscoped(),
552 required: false 552 required: false
553 } 553 }
554 ] 554 ]
@@ -557,16 +557,16 @@ type AvailableForListIDsOptions = {
557 } 557 }
558 ] 558 ]
559 } 559 }
560 ] as any // FIXME: sequelize typings 560 ]
561 }, 561 },
562 [ ScopeNames.WITH_TAGS ]: { 562 [ ScopeNames.WITH_TAGS ]: {
563 include: [ () => TagModel ] 563 include: [ TagModel ]
564 }, 564 },
565 [ ScopeNames.WITH_BLACKLISTED ]: { 565 [ ScopeNames.WITH_BLACKLISTED ]: {
566 include: [ 566 include: [
567 { 567 {
568 attributes: [ 'id', 'reason' ], 568 attributes: [ 'id', 'reason' ],
569 model: () => VideoBlacklistModel, 569 model: VideoBlacklistModel,
570 required: false 570 required: false
571 } 571 }
572 ] 572 ]
@@ -588,8 +588,7 @@ type AvailableForListIDsOptions = {
588 include: [ 588 include: [
589 { 589 {
590 model: VideoFileModel.unscoped(), 590 model: VideoFileModel.unscoped(),
591 // FIXME: typings 591 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
592 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
593 required: false, 592 required: false,
594 include: subInclude 593 include: subInclude
595 } 594 }
@@ -613,8 +612,7 @@ type AvailableForListIDsOptions = {
613 include: [ 612 include: [
614 { 613 {
615 model: VideoStreamingPlaylistModel.unscoped(), 614 model: VideoStreamingPlaylistModel.unscoped(),
616 // FIXME: typings 615 separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
617 [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
618 required: false, 616 required: false,
619 include: subInclude 617 include: subInclude
620 } 618 }
@@ -624,7 +622,7 @@ type AvailableForListIDsOptions = {
624 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 622 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
625 include: [ 623 include: [
626 { 624 {
627 model: () => ScheduleVideoUpdateModel.unscoped(), 625 model: ScheduleVideoUpdateModel.unscoped(),
628 required: false 626 required: false
629 } 627 }
630 ] 628 ]
@@ -643,7 +641,7 @@ type AvailableForListIDsOptions = {
643 ] 641 ]
644 } 642 }
645 } 643 }
646}) 644}))
647@Table({ 645@Table({
648 tableName: 'video', 646 tableName: 'video',
649 indexes 647 indexes
@@ -1075,15 +1073,14 @@ export class VideoModel extends Model<VideoModel> {
1075 } 1073 }
1076 1074
1077 return Bluebird.all([ 1075 return Bluebird.all([
1078 // FIXME: typing issue 1076 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
1079 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any), 1077 VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
1080 VideoModel.sequelize.query<{ total: number }>(rawCountQuery, { type: QueryTypes.SELECT })
1081 ]).then(([ rows, totals ]) => { 1078 ]).then(([ rows, totals ]) => {
1082 // totals: totalVideos + totalVideoShares 1079 // totals: totalVideos + totalVideoShares
1083 let totalVideos = 0 1080 let totalVideos = 0
1084 let totalVideoShares = 0 1081 let totalVideoShares = 0
1085 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total + '', 10) 1082 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10)
1086 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total + '', 10) 1083 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10)
1087 1084
1088 const total = totalVideos + totalVideoShares 1085 const total = totalVideos + totalVideoShares
1089 return { 1086 return {
@@ -1094,50 +1091,58 @@ export class VideoModel extends Model<VideoModel> {
1094 } 1091 }
1095 1092
1096 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { 1093 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
1097 const query: FindOptions = { 1094 function buildBaseQuery (): FindOptions {
1098 offset: start, 1095 return {
1099 limit: count, 1096 offset: start,
1100 order: getVideoSort(sort), 1097 limit: count,
1101 include: [ 1098 order: getVideoSort(sort),
1102 { 1099 include: [
1103 model: VideoChannelModel, 1100 {
1104 required: true, 1101 model: VideoChannelModel,
1105 include: [ 1102 required: true,
1106 { 1103 include: [
1107 model: AccountModel, 1104 {
1108 where: { 1105 model: AccountModel,
1109 id: accountId 1106 where: {
1110 }, 1107 id: accountId
1111 required: true 1108 },
1112 } 1109 required: true
1113 ] 1110 }
1114 }, 1111 ]
1115 { 1112 }
1116 model: ScheduleVideoUpdateModel, 1113 ]
1117 required: false 1114 }
1118 },
1119 {
1120 model: VideoBlacklistModel,
1121 required: false
1122 }
1123 ]
1124 } 1115 }
1125 1116
1117 const countQuery = buildBaseQuery()
1118 const findQuery = buildBaseQuery()
1119
1120 findQuery.include.push({
1121 model: ScheduleVideoUpdateModel,
1122 required: false
1123 })
1124
1125 findQuery.include.push({
1126 model: VideoBlacklistModel,
1127 required: false
1128 })
1129
1126 if (withFiles === true) { 1130 if (withFiles === true) {
1127 query.include.push({ 1131 findQuery.include.push({
1128 model: VideoFileModel.unscoped(), 1132 model: VideoFileModel.unscoped(),
1129 required: true 1133 required: true
1130 }) 1134 })
1131 } 1135 }
1132 1136
1133 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS) 1137 return Promise.all([
1134 .findAndCountAll(query) 1138 VideoModel.count(countQuery),
1135 .then(({ rows, count }) => { 1139 VideoModel.findAll(findQuery)
1136 return { 1140 ]).then(([ count, rows ]) => {
1137 data: rows, 1141 return {
1138 total: count 1142 data: rows,
1139 } 1143 total: count
1140 }) 1144 }
1145 })
1141 } 1146 }
1142 1147
1143 static async listForApi (options: { 1148 static async listForApi (options: {
@@ -1404,12 +1409,12 @@ export class VideoModel extends Model<VideoModel> {
1404 const where = buildWhereIdOrUUID(id) 1409 const where = buildWhereIdOrUUID(id)
1405 1410
1406 const options = { 1411 const options = {
1407 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings 1412 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
1408 where, 1413 where,
1409 transaction: t 1414 transaction: t
1410 } 1415 }
1411 1416
1412 const scopes = [ 1417 const scopes: (string | ScopeOptions)[] = [
1413 ScopeNames.WITH_TAGS, 1418 ScopeNames.WITH_TAGS,
1414 ScopeNames.WITH_BLACKLISTED, 1419 ScopeNames.WITH_BLACKLISTED,
1415 ScopeNames.WITH_ACCOUNT_DETAILS, 1420 ScopeNames.WITH_ACCOUNT_DETAILS,
@@ -1420,7 +1425,7 @@ export class VideoModel extends Model<VideoModel> {
1420 ] 1425 ]
1421 1426
1422 if (userId) { 1427 if (userId) {
1423 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings 1428 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1424 } 1429 }
1425 1430
1426 return VideoModel 1431 return VideoModel
@@ -1437,18 +1442,18 @@ export class VideoModel extends Model<VideoModel> {
1437 transaction: t 1442 transaction: t
1438 } 1443 }
1439 1444
1440 const scopes = [ 1445 const scopes: (string | ScopeOptions)[] = [
1441 ScopeNames.WITH_TAGS, 1446 ScopeNames.WITH_TAGS,
1442 ScopeNames.WITH_BLACKLISTED, 1447 ScopeNames.WITH_BLACKLISTED,
1443 ScopeNames.WITH_ACCOUNT_DETAILS, 1448 ScopeNames.WITH_ACCOUNT_DETAILS,
1444 ScopeNames.WITH_SCHEDULED_UPDATE, 1449 ScopeNames.WITH_SCHEDULED_UPDATE,
1445 ScopeNames.WITH_THUMBNAILS, 1450 ScopeNames.WITH_THUMBNAILS,
1446 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings 1451 { method: [ ScopeNames.WITH_FILES, true ] },
1447 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings 1452 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1448 ] 1453 ]
1449 1454
1450 if (userId) { 1455 if (userId) {
1451 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings 1456 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1452 } 1457 }
1453 1458
1454 return VideoModel 1459 return VideoModel
@@ -1520,9 +1525,9 @@ export class VideoModel extends Model<VideoModel> {
1520 attributes: [ field ], 1525 attributes: [ field ],
1521 limit: count, 1526 limit: count,
1522 group: field, 1527 group: field,
1523 having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { 1528 having: Sequelize.where(
1524 [ Op.gte ]: threshold 1529 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold }
1525 }) as any, // FIXME: typings 1530 ),
1526 order: [ (this.sequelize as any).random() ] 1531 order: [ (this.sequelize as any).random() ]
1527 } 1532 }
1528 1533
@@ -1594,16 +1599,10 @@ export class VideoModel extends Model<VideoModel> {
1594 ] 1599 ]
1595 } 1600 }
1596 1601
1597 // FIXME: typing 1602 const apiScope: (string | ScopeOptions)[] = [ ScopeNames.WITH_THUMBNAILS ]
1598 const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ]
1599 1603
1600 if (options.user) { 1604 if (options.user) {
1601 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) 1605 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1602
1603 // Even if the relation is n:m, we know that a user only have 0..1 video history
1604 // So we won't have multiple rows for the same video
1605 // A subquery adds some bugs in our query so disable it
1606 secondQuery.subQuery = false
1607 } 1606 }
1608 1607
1609 apiScope.push({ 1608 apiScope.push({
@@ -1651,13 +1650,17 @@ export class VideoModel extends Model<VideoModel> {
1651 return maxBy(this.VideoFiles, file => file.resolution) 1650 return maxBy(this.VideoFiles, file => file.resolution)
1652 } 1651 }
1653 1652
1654 addThumbnail (thumbnail: ThumbnailModel) { 1653 async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
1654 thumbnail.videoId = this.id
1655
1656 const savedThumbnail = await thumbnail.save({ transaction })
1657
1655 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = [] 1658 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1656 1659
1657 // Already have this thumbnail, skip 1660 // Already have this thumbnail, skip
1658 if (this.Thumbnails.find(t => t.id === thumbnail.id)) return 1661 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
1659 1662
1660 this.Thumbnails.push(thumbnail) 1663 this.Thumbnails.push(savedThumbnail)
1661 } 1664 }
1662 1665
1663 getVideoFilename (videoFile: VideoFileModel) { 1666 getVideoFilename (videoFile: VideoFileModel) {
@@ -1668,10 +1671,10 @@ export class VideoModel extends Model<VideoModel> {
1668 return this.uuid + '.jpg' 1671 return this.uuid + '.jpg'
1669 } 1672 }
1670 1673
1671 getThumbnail () { 1674 getMiniature () {
1672 if (Array.isArray(this.Thumbnails) === false) return undefined 1675 if (Array.isArray(this.Thumbnails) === false) return undefined
1673 1676
1674 return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL) 1677 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
1675 } 1678 }
1676 1679
1677 generatePreviewName () { 1680 generatePreviewName () {
@@ -1732,8 +1735,8 @@ export class VideoModel extends Model<VideoModel> {
1732 return '/videos/embed/' + this.uuid 1735 return '/videos/embed/' + this.uuid
1733 } 1736 }
1734 1737
1735 getThumbnailStaticPath () { 1738 getMiniatureStaticPath () {
1736 const thumbnail = this.getThumbnail() 1739 const thumbnail = this.getMiniature()
1737 if (!thumbnail) return null 1740 if (!thumbnail) return null
1738 1741
1739 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename) 1742 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
diff --git a/server/typings/sequelize.ts b/server/typings/sequelize.ts
new file mode 100644
index 000000000..9cd83612d
--- /dev/null
+++ b/server/typings/sequelize.ts
@@ -0,0 +1,18 @@
1import { Model } from 'sequelize-typescript'
2
3// Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript
4
5export type Diff<T extends string | symbol | number, U extends string | symbol | number> =
6 ({ [P in T]: P } & { [P in U]: never } & { [ x: string ]: never })[T]
7
8export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }
9
10export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }
11
12export type FilteredModelAttributes<T extends Model<T>> = RecursivePartial<Omit<T, keyof Model<any>>> & {
13 id?: number | any
14 createdAt?: Date | any
15 updatedAt?: Date | any
16 deletedAt?: Date | any
17 version?: number | any
18}