Browse Source

Upgrade sequelize

Chocobozzz 5 years ago
parent
commit
3acc508440
48 changed files with 457 additions and 466 deletions
  1. 1 1
      .gitignore
  2. 3 3
      package.json
  3. 7 13
      server/controllers/api/video-playlist.ts
  4. 5 11
      server/controllers/api/videos/import.ts
  5. 11 20
      server/controllers/api/videos/index.ts
  6. 1 1
      server/controllers/bots.ts
  7. 1 1
      server/controllers/feeds.ts
  8. 6 6
      server/controllers/static.ts
  9. 3 3
      server/initializers/database.ts
  10. 6 8
      server/lib/activitypub/playlist.ts
  11. 1 2
      server/lib/activitypub/video-comments.ts
  12. 12 22
      server/lib/activitypub/videos.ts
  13. 11 7
      server/lib/files-cache/abstract-video-static-file-cache.ts
  14. 7 2
      server/lib/files-cache/videos-caption-cache.ts
  15. 4 2
      server/lib/files-cache/videos-preview-cache.ts
  16. 7 13
      server/lib/job-queue/handlers/video-import.ts
  17. 2 0
      server/lib/oauth-model.ts
  18. 13 13
      server/lib/thumbnail.ts
  19. 2 0
      server/middlewares/oauth.ts
  20. 1 0
      server/middlewares/validators/videos/videos.ts
  21. 5 5
      server/models/account/account-blocklist.ts
  22. 5 5
      server/models/account/account.ts
  23. 22 22
      server/models/account/user-notification.ts
  24. 27 31
      server/models/account/user.ts
  25. 24 23
      server/models/activitypub/actor.ts
  26. 3 3
      server/models/application/application.ts
  27. 2 2
      server/models/oauth/oauth-client.ts
  28. 12 10
      server/models/oauth/oauth-token.ts
  29. 26 30
      server/models/redundancy/video-redundancy.ts
  30. 4 4
      server/models/server/server-blocklist.ts
  31. 11 1
      server/models/utils.ts
  32. 1 1
      server/models/video/tag.ts
  33. 2 2
      server/models/video/thumbnail.ts
  34. 5 8
      server/models/video/video-caption.ts
  35. 7 7
      server/models/video/video-change-ownership.ts
  36. 8 8
      server/models/video/video-channel.ts
  37. 16 17
      server/models/video/video-comment.ts
  38. 12 15
      server/models/video/video-file.ts
  39. 6 4
      server/models/video/video-format-utils.ts
  40. 4 4
      server/models/video/video-import.ts
  41. 21 26
      server/models/video/video-playlist.ts
  42. 5 5
      server/models/video/video-share.ts
  43. 3 3
      server/models/video/video-streaming-playlist.ts
  44. 91 88
      server/models/video/video.ts
  45. 18 0
      server/typings/sequelize.ts
  46. 0 1
      shared/extra-utils/miscs/sql.ts
  47. 1 1
      shared/models/videos/thumbnail.type.ts
  48. 12 12
      yarn.lock

+ 1 - 1
.gitignore

@@ -16,8 +16,8 @@
 /config/production.yaml
 /config/local*
 /ffmpeg/
-/ffmpeg-4/
 /ffmpeg-3/
+/ffmpeg-4/
 /thumbnails/
 /torrents/
 /videos/

+ 3 - 3
package.json

@@ -142,8 +142,8 @@
     "reflect-metadata": "^0.1.12",
     "request": "^2.81.0",
     "scripty": "^1.5.0",
-    "sequelize": "5.6.1",
-    "sequelize-typescript": "^1.0.0-beta.1",
+    "sequelize": "5.7.4",
+    "sequelize-typescript": "1.0.0-beta.2",
     "sharp": "^0.22.0",
     "sitemap": "^2.1.0",
     "socket.io": "^2.2.0",
@@ -212,7 +212,7 @@
     "ts-node": "8.0.3",
     "tslint": "^5.7.0",
     "tslint-config-standard": "^8.0.1",
-    "typescript": "^3.1.6",
+    "typescript": "^3.4.3",
     "xliff": "^4.0.0"
   },
   "scripty": {

+ 7 - 13
server/controllers/api/video-playlist.ts

@@ -41,7 +41,7 @@ import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/vid
 import { JobQueue } from '../../lib/job-queue'
 import { CONFIG } from '../../initializers/config'
 import { sequelizeTypescript } from '../../initializers/database'
-import { createPlaylistThumbnailFromExisting } from '../../lib/thumbnail'
+import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail'
 
 const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
 
@@ -174,16 +174,13 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
 
   const thumbnailField = req.files['thumbnailfile']
   const thumbnailModel = thumbnailField
-    ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylist)
+    ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist)
     : undefined
 
   const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
     const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
 
-    if (thumbnailModel) {
-      thumbnailModel.videoPlaylistId = videoPlaylistCreated.id
-      videoPlaylistCreated.setThumbnail(await thumbnailModel.save({ transaction: t }))
-    }
+    if (thumbnailModel) await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t)
 
     // We need more attributes for the federation
     videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
@@ -210,7 +207,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
 
   const thumbnailField = req.files['thumbnailfile']
   const thumbnailModel = thumbnailField
-    ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylistInstance)
+    ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance)
     : undefined
 
   try {
@@ -239,10 +236,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
 
       const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
 
-      if (thumbnailModel) {
-        thumbnailModel.videoPlaylistId = playlistUpdated.id
-        playlistUpdated.setThumbnail(await thumbnailModel.save({ transaction: t }))
-      }
+      if (thumbnailModel) await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t)
 
       const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
 
@@ -313,8 +307,8 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
   if (playlistElement.position === 1 && videoPlaylist.hasThumbnail() === false) {
     logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
 
-    const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnail().filename)
-    const thumbnailModel = await createPlaylistThumbnailFromExisting(inputPath, videoPlaylist, true)
+    const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename)
+    const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true)
 
     thumbnailModel.videoPlaylistId = videoPlaylist.id
 

+ 5 - 11
server/controllers/api/videos/import.ts

@@ -23,7 +23,7 @@ import { move, readFile } from 'fs-extra'
 import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { createVideoThumbnailFromExisting } from '../../../lib/thumbnail'
+import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 import { ThumbnailModel } from '../../../models/video/thumbnail'
 
@@ -204,7 +204,7 @@ async function processThumbnail (req: express.Request, video: VideoModel) {
   if (thumbnailField) {
     const thumbnailPhysicalFile = thumbnailField[ 0 ]
 
-    return createVideoThumbnailFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.THUMBNAIL)
+    return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE)
   }
 
   return undefined
@@ -215,7 +215,7 @@ async function processPreview (req: express.Request, video: VideoModel) {
   if (previewField) {
     const previewPhysicalFile = previewField[0]
 
-    return createVideoThumbnailFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
+    return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
   }
 
   return undefined
@@ -238,14 +238,8 @@ function insertIntoDB (parameters: {
     const videoCreated = await video.save(sequelizeOptions)
     videoCreated.VideoChannel = videoChannel
 
-    if (thumbnailModel) {
-      thumbnailModel.videoId = videoCreated.id
-      videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
-    }
-    if (previewModel) {
-      previewModel.videoId = videoCreated.id
-      videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
-    }
+    if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
+    if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
 
     await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
 

+ 11 - 20
server/controllers/api/videos/index.ts

@@ -52,7 +52,7 @@ import { Notifier } from '../../../lib/notifier'
 import { sendView } from '../../../lib/activitypub/send/send-view'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { createVideoThumbnailFromExisting, generateVideoThumbnail } from '../../../lib/thumbnail'
+import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 
 const auditLogger = auditLoggerFactory('videos')
@@ -214,14 +214,14 @@ async function addVideo (req: express.Request, res: express.Response) {
   // Process thumbnail or create it from the video
   const thumbnailField = req.files['thumbnailfile']
   const thumbnailModel = thumbnailField
-    ? await createVideoThumbnailFromExisting(thumbnailField[0].path, video, ThumbnailType.THUMBNAIL)
-    : await generateVideoThumbnail(video, videoFile, ThumbnailType.THUMBNAIL)
+    ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE)
+    : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE)
 
   // Process preview or create it from the video
   const previewField = req.files['previewfile']
   const previewModel = previewField
-    ? await createVideoThumbnailFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW)
-    : await generateVideoThumbnail(video, videoFile, ThumbnailType.PREVIEW)
+    ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW)
+    : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW)
 
   // Create the torrent file
   await video.createTorrentAndSetInfoHash(videoFile)
@@ -231,11 +231,8 @@ async function addVideo (req: express.Request, res: express.Response) {
 
     const videoCreated = await video.save(sequelizeOptions)
 
-    thumbnailModel.videoId = videoCreated.id
-    previewModel.videoId = videoCreated.id
-
-    videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
-    videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
+    await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
+    await videoCreated.addAndSaveThumbnail(previewModel, t)
 
     // Do not forget to add video channel information to the created video
     videoCreated.VideoChannel = res.locals.videoChannel
@@ -308,11 +305,11 @@ async function updateVideo (req: express.Request, res: express.Response) {
 
   // Process thumbnail or create it from the video
   const thumbnailModel = req.files && req.files['thumbnailfile']
-    ? await createVideoThumbnailFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.THUMBNAIL)
+    ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE)
     : undefined
 
   const previewModel = req.files && req.files['previewfile']
-    ? await createVideoThumbnailFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW)
+    ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW)
     : undefined
 
   try {
@@ -346,14 +343,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
 
       const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
 
-      if (thumbnailModel) {
-        thumbnailModel.videoId = videoInstanceUpdated.id
-        videoInstanceUpdated.addThumbnail(await thumbnailModel.save({ transaction: t }))
-      }
-      if (previewModel) {
-        previewModel.videoId = videoInstanceUpdated.id
-        videoInstanceUpdated.addThumbnail(await previewModel.save({ transaction: t }))
-      }
+      if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
+      if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
 
       // Video tags update?
       if (videoInfoToUpdate.tags !== undefined) {

+ 1 - 1
server/controllers/bots.ts

@@ -85,7 +85,7 @@ async function getSitemapLocalVideoUrls () {
         // Sitemap description should be < 2000 characters
         description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
         player_loc: WEBSERVER.URL + '/videos/embed/' + v.uuid,
-        thumbnail_loc: WEBSERVER.URL + v.getThumbnailStaticPath()
+        thumbnail_loc: WEBSERVER.URL + v.getMiniatureStaticPath()
       }
     ]
   }))

+ 1 - 1
server/controllers/feeds.ts

@@ -137,7 +137,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
       torrent: torrents,
       thumbnail: [
         {
-          url: WEBSERVER.URL + video.getThumbnailStaticPath(),
+          url: WEBSERVER.URL + video.getMiniatureStaticPath(),
           height: THUMBNAILS_SIZE.height,
           width: THUMBNAILS_SIZE.width
         }

+ 6 - 6
server/controllers/static.ts

@@ -165,20 +165,20 @@ export {
 // ---------------------------------------------------------------------------
 
 async function getPreview (req: express.Request, res: express.Response) {
-  const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
-  if (!path) return res.sendStatus(404)
+  const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
+  if (!result) return res.sendStatus(404)
 
-  return res.sendFile(path, { maxAge: STATIC_MAX_AGE })
+  return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
 }
 
 async function getVideoCaption (req: express.Request, res: express.Response) {
-  const path = await VideosCaptionCache.Instance.getFilePath({
+  const result = await VideosCaptionCache.Instance.getFilePath({
     videoId: req.params.videoId,
     language: req.params.captionLanguage
   })
-  if (!path) return res.sendStatus(404)
+  if (!result) return res.sendStatus(404)
 
-  return res.sendFile(path, { maxAge: STATIC_MAX_AGE })
+  return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
 }
 
 async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) {

+ 3 - 3
server/initializers/database.ts

@@ -140,15 +140,15 @@ async function checkPostgresExtensions () {
 }
 
 async function checkPostgresExtension (extension: string) {
-  const query = `SELECT true AS enabled FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;`
+  const query = `SELECT 1 FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;`
   const options = {
     type: QueryTypes.SELECT as QueryTypes.SELECT,
     raw: true
   }
 
-  const res = await sequelizeTypescript.query<{ enabled: boolean }>(query, options)
+  const res = await sequelizeTypescript.query<object>(query, options)
 
-  if (!res || res.length === 0 || res[ 0 ][ 'enabled' ] !== true) {
+  if (!res || res.length === 0) {
     // Try to create the extension ourselves
     try {
       await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true })

+ 6 - 8
server/lib/activitypub/playlist.ts

@@ -16,7 +16,8 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
 import { VideoModel } from '../../models/video/video'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { sequelizeTypescript } from '../../initializers/database'
-import { createPlaylistThumbnailFromUrl } from '../thumbnail'
+import { createPlaylistMiniatureFromUrl } from '../thumbnail'
+import { FilteredModelAttributes } from '../../typings/sequelize'
 
 function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
   const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
@@ -86,8 +87,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
     }
   }
 
-  // FIXME: sequelize typings
-  const [ playlist ] = (await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true }) as any)
+  const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true })
 
   let accItems: string[] = []
   await crawlCollectionPage<string>(playlistObject.id, items => {
@@ -100,10 +100,8 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
 
   if (playlistObject.icon) {
     try {
-      const thumbnailModel = await createPlaylistThumbnailFromUrl(playlistObject.icon.url, refreshedPlaylist)
-      thumbnailModel.videoPlaylistId = refreshedPlaylist.id
-
-      refreshedPlaylist.setThumbnail(await thumbnailModel.save())
+      const thumbnailModel = await createPlaylistMiniatureFromUrl(playlistObject.icon.url, refreshedPlaylist)
+      await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined)
     } catch (err) {
       logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err })
     }
@@ -156,7 +154,7 @@ export {
 // ---------------------------------------------------------------------------
 
 async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) {
-  const elementsToCreate: object[] = [] // FIXME: sequelize typings
+  const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
 
   await Bluebird.map(elementUrls, async elementUrl => {
     try {

+ 1 - 2
server/lib/activitypub/video-comments.ts

@@ -73,8 +73,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
   const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
   if (!entry) return { created: false }
 
-  // FIXME: sequelize typings
-  const [ comment, created ] = (await VideoCommentModel.upsert<VideoCommentModel>(entry, { returning: true }) as any)
+  const [ comment, created ] = await VideoCommentModel.upsert<VideoCommentModel>(entry, { returning: true })
   comment.Account = actor.Account
   comment.Video = videoInstance
 

+ 12 - 22
server/lib/activitypub/videos.ts

@@ -49,10 +49,11 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate'
 import { VideoShareModel } from '../../models/video/video-share'
 import { VideoCommentModel } from '../../models/video/video-comment'
 import { sequelizeTypescript } from '../../initializers/database'
-import { createPlaceholderThumbnail, createVideoThumbnailFromUrl } from '../thumbnail'
+import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
 import { ThumbnailModel } from '../../models/video/thumbnail'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 import { join } from 'path'
+import { FilteredModelAttributes } from '../../typings/sequelize'
 
 async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
   // If the video is not private and is published, we federate it
@@ -247,7 +248,7 @@ async function updateVideoFromAP (options: {
     let thumbnailModel: ThumbnailModel
 
     try {
-      thumbnailModel = await createVideoThumbnailFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.THUMBNAIL)
+      thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
     } catch (err) {
       logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
     }
@@ -288,16 +289,12 @@ async function updateVideoFromAP (options: {
 
       await options.video.save(sequelizeOptions)
 
-      if (thumbnailModel) {
-        thumbnailModel.videoId = options.video.id
-        options.video.addThumbnail(await thumbnailModel.save({ transaction: t }))
-      }
+      if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
 
       // FIXME: use icon URL instead
       const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
       const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
-
-      options.video.addThumbnail(await previewModel.save({ transaction: t }))
+      await options.video.addAndSaveThumbnail(previewModel, t)
 
       {
         const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
@@ -311,7 +308,7 @@ async function updateVideoFromAP (options: {
 
         // Update or add other one
         const upsertTasks = videoFileAttributes.map(a => {
-          return (VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t }) as any) // FIXME: sequelize typings
+          return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
             .then(([ file ]) => file)
         })
 
@@ -334,8 +331,7 @@ async function updateVideoFromAP (options: {
 
         // Update or add other one
         const upsertTasks = streamingPlaylistAttributes.map(a => {
-          // FIXME: sequelize typings
-          return (VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) as any)
+          return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
                                .then(([ streamingPlaylist ]) => streamingPlaylist)
         })
 
@@ -464,7 +460,7 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
   const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
   const video = VideoModel.build(videoData)
 
-  const promiseThumbnail = createVideoThumbnailFromUrl(videoObject.icon.url, video, ThumbnailType.THUMBNAIL)
+  const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
 
   let thumbnailModel: ThumbnailModel
   if (waitThumbnail === true) {
@@ -477,18 +473,12 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
     const videoCreated = await video.save(sequelizeOptions)
     videoCreated.VideoChannel = channelActor.VideoChannel
 
-    if (thumbnailModel) {
-      thumbnailModel.videoId = videoCreated.id
-
-      videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
-    }
+    if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
 
     // FIXME: use icon URL instead
     const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
     const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
-    previewModel.videoId = videoCreated.id
-
-    videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
+    if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
 
     // Process files
     const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
@@ -594,7 +584,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
     throw new Error('Cannot find video files for ' + video.url)
   }
 
-  const attributes: object[] = [] // FIXME: add typings
+  const attributes: FilteredModelAttributes<VideoFileModel>[] = []
   for (const fileUrl of fileUrls) {
     // Fetch associated magnet uri
     const magnet = videoObject.url.find(u => {
@@ -629,7 +619,7 @@ function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObj
   const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
   if (playlistUrls.length === 0) return []
 
-  const attributes: object[] = [] // FIXME: add typings
+  const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
   for (const playlistUrlObject of playlistUrls) {
     const segmentsSha256UrlObject = playlistUrlObject.tag
                                                      .find(t => {

+ 11 - 7
server/lib/files-cache/abstract-video-static-file-cache.ts

@@ -4,24 +4,28 @@ import { VideoModel } from '../../models/video/video'
 import { fetchRemoteVideoStaticFile } from '../activitypub'
 import * as memoizee from 'memoizee'
 
+type GetFilePathResult = { isOwned: boolean, path: string } | undefined
+
 export abstract class AbstractVideoStaticFileCache <T> {
 
-  getFilePath: (params: T) => Promise<string>
+  getFilePath: (params: T) => Promise<GetFilePathResult>
 
-  abstract getFilePathImpl (params: T): Promise<string>
+  abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
 
   // Load and save the remote file, then return the local path from filesystem
-  protected abstract loadRemoteFile (key: string): Promise<string>
+  protected abstract loadRemoteFile (key: string): Promise<GetFilePathResult>
 
   init (max: number, maxAge: number) {
     this.getFilePath = memoizee(this.getFilePathImpl, {
       maxAge,
       max,
       promise: true,
-      dispose: (value: string) => {
-        remove(value)
-          .then(() => logger.debug('%s evicted from %s', value, this.constructor.name))
-          .catch(err => logger.error('Cannot remove %s from cache %s.', value, this.constructor.name, { err }))
+      dispose: (result: GetFilePathResult) => {
+        if (result.isOwned !== true) {
+          remove(result.path)
+            .then(() => logger.debug('%s removed from %s', result.path, this.constructor.name))
+            .catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err }))
+        }
       }
     })
   }

+ 7 - 2
server/lib/files-cache/videos-caption-cache.ts

@@ -4,6 +4,7 @@ import { VideoModel } from '../../models/video/video'
 import { VideoCaptionModel } from '../../models/video/video-caption'
 import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
 import { CONFIG } from '../../initializers/config'
+import { logger } from '../../helpers/logger'
 
 type GetPathParam = { videoId: string, language: string }
 
@@ -24,13 +25,15 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
     const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language)
     if (!videoCaption) return undefined
 
-    if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName())
+    if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) }
 
     const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language
     return this.loadRemoteFile(key)
   }
 
   protected async loadRemoteFile (key: string) {
+    logger.debug('Loading remote caption file %s.', key)
+
     const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER)
 
     const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language)
@@ -46,7 +49,9 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
     const remoteStaticPath = videoCaption.getCaptionStaticPath()
     const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
 
-    return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
+    const path = await this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
+
+    return { isOwned: false, path }
   }
 }
 

+ 4 - 2
server/lib/files-cache/videos-preview-cache.ts

@@ -20,7 +20,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
     const video = await VideoModel.loadByUUIDWithFile(videoUUID)
     if (!video) return undefined
 
-    if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename)
+    if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) }
 
     return this.loadRemoteFile(videoUUID)
   }
@@ -35,7 +35,9 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
     const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)
     const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
 
-    return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
+    const path = await this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
+
+    return { isOwned: false, path }
   }
 }
 

+ 7 - 13
server/lib/job-queue/handlers/video-import.ts

@@ -18,7 +18,7 @@ import { Notifier } from '../../notifier'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { ThumbnailModel } from '../../../models/video/thumbnail'
-import { createVideoThumbnailFromUrl, generateVideoThumbnail } from '../../thumbnail'
+import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 
 type VideoImportYoutubeDLPayload = {
@@ -150,17 +150,17 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
     // Process thumbnail
     let thumbnailModel: ThumbnailModel
     if (options.downloadThumbnail && options.thumbnailUrl) {
-      thumbnailModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.THUMBNAIL)
+      thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.MINIATURE)
     } else if (options.generateThumbnail || options.downloadThumbnail) {
-      thumbnailModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.THUMBNAIL)
+      thumbnailModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.MINIATURE)
     }
 
     // Process preview
     let previewModel: ThumbnailModel
     if (options.downloadPreview && options.thumbnailUrl) {
-      previewModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
+      previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
     } else if (options.generatePreview || options.downloadPreview) {
-      previewModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
+      previewModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
     }
 
     // Create torrent
@@ -180,14 +180,8 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
       video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
       await video.save({ transaction: t })
 
-      if (thumbnailModel) {
-        thumbnailModel.videoId = video.id
-        video.addThumbnail(await thumbnailModel.save({ transaction: t }))
-      }
-      if (previewModel) {
-        previewModel.videoId = video.id
-        video.addThumbnail(await previewModel.save({ transaction: t }))
-      }
+      if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
+      if (previewModel) await video.addAndSaveThumbnail(previewModel, t)
 
       // Now we can federate the video (reload from database, we need more attributes)
       const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)

+ 2 - 0
server/lib/oauth-model.ts

@@ -39,6 +39,8 @@ function clearCacheByToken (token: string) {
 function getAccessToken (bearerToken: string) {
   logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
 
+  if (!bearerToken) return Bluebird.resolve(undefined)
+
   if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken])
 
   return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)

+ 13 - 13
server/lib/thumbnail.ts

@@ -12,37 +12,37 @@ import { VideoPlaylistModel } from '../models/video/video-playlist'
 
 type ImageSize = { height: number, width: number }
 
-function createPlaylistThumbnailFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) {
+function createPlaylistMiniatureFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) {
   const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
-  const type = ThumbnailType.THUMBNAIL
+  const type = ThumbnailType.MINIATURE
 
   const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }, keepOriginal)
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
 }
 
-function createPlaylistThumbnailFromUrl (url: string, playlist: VideoPlaylistModel, size?: ImageSize) {
+function createPlaylistMiniatureFromUrl (url: string, playlist: VideoPlaylistModel, size?: ImageSize) {
   const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
-  const type = ThumbnailType.THUMBNAIL
+  const type = ThumbnailType.MINIATURE
 
   const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height })
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url })
 }
 
-function createVideoThumbnailFromUrl (url: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
+function createVideoMiniatureFromUrl (url: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
   const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
   const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height })
 
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url })
 }
 
-function createVideoThumbnailFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
+function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
   const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
   const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height })
 
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
 }
 
-function generateVideoThumbnail (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
+function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
   const input = video.getVideoFilePath(videoFile)
 
   const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type)
@@ -68,12 +68,12 @@ function createPlaceholderThumbnail (url: string, video: VideoModel, type: Thumb
 // ---------------------------------------------------------------------------
 
 export {
-  generateVideoThumbnail,
-  createVideoThumbnailFromUrl,
-  createVideoThumbnailFromExisting,
+  generateVideoMiniature,
+  createVideoMiniatureFromUrl,
+  createVideoMiniatureFromExisting,
   createPlaceholderThumbnail,
-  createPlaylistThumbnailFromUrl,
-  createPlaylistThumbnailFromExisting
+  createPlaylistMiniatureFromUrl,
+  createPlaylistMiniatureFromExisting
 }
 
 function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) {
@@ -95,7 +95,7 @@ function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?:
     ? video.Thumbnails.find(t => t.type === type)
     : undefined
 
-  if (type === ThumbnailType.THUMBNAIL) {
+  if (type === ThumbnailType.MINIATURE) {
     const filename = video.generateThumbnailName()
     const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
 

+ 2 - 0
server/middlewares/oauth.ts

@@ -35,6 +35,8 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) {
 
   logger.debug('Checking socket access token %s.', accessToken)
 
+  if (!accessToken) return next(new Error('No access token provided'))
+
   getAccessToken(accessToken)
     .then(tokenDB => {
       const now = new Date()

+ 1 - 0
server/middlewares/validators/videos/videos.ts

@@ -68,6 +68,7 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
     if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
 
     const isAble = await user.isAbleToUploadVideo(videoFile)
+
     if (isAble === false) {
       res.status(403)
          .json({ error: 'The user video quota is exceeded with this video.' })

+ 5 - 5
server/models/account/account-blocklist.ts

@@ -8,22 +8,22 @@ enum ScopeNames {
   WITH_ACCOUNTS = 'WITH_ACCOUNTS'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.WITH_ACCOUNTS]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true,
         as: 'ByAccount'
       },
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true,
         as: 'BlockedAccount'
       }
     ]
   }
-})
+}))
 
 @Table({
   tableName: 'accountBlocklist',
@@ -83,7 +83,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
       attributes: [ 'accountId', 'id' ],
       where: {
         accountId: {
-          [Op.any]: accountIds
+          [Op.in]: accountIds // FIXME: sequelize ANY seems broken
         },
         targetAccountId
       },

+ 5 - 5
server/models/account/account.ts

@@ -33,15 +33,15 @@ export enum ScopeNames {
   SUMMARY = 'SUMMARY'
 }
 
-@DefaultScope({
+@DefaultScope(() => ({
   include: [
     {
-      model: () => ActorModel, // Default scope includes avatar and server
+      model: ActorModel, // Default scope includes avatar and server
       required: true
     }
   ]
-})
-@Scopes({
+}))
+@Scopes(() => ({
   [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => {
     return {
       attributes: [ 'id', 'name' ],
@@ -66,7 +66,7 @@ export enum ScopeNames {
       ]
     }
   }
-})
+}))
 @Table({
   tableName: 'account',
   indexes: [

+ 22 - 22
server/models/account/user-notification.ts

@@ -6,7 +6,7 @@ import { isUserNotificationTypeValid } from '../../helpers/custom-validators/use
 import { UserModel } from './user'
 import { VideoModel } from '../video/video'
 import { VideoCommentModel } from '../video/video-comment'
-import { FindOptions, Op } from 'sequelize'
+import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
 import { VideoChannelModel } from '../video/video-channel'
 import { AccountModel } from './account'
 import { VideoAbuseModel } from '../video/video-abuse'
@@ -24,17 +24,17 @@ enum ScopeNames {
 function buildActorWithAvatarInclude () {
   return {
     attributes: [ 'preferredUsername' ],
-    model: () => ActorModel.unscoped(),
+    model: ActorModel.unscoped(),
     required: true,
     include: [
       {
         attributes: [ 'filename' ],
-        model: () => AvatarModel.unscoped(),
+        model: AvatarModel.unscoped(),
         required: false
       },
       {
         attributes: [ 'host' ],
-        model: () => ServerModel.unscoped(),
+        model: ServerModel.unscoped(),
         required: false
       }
     ]
@@ -44,7 +44,7 @@ function buildActorWithAvatarInclude () {
 function buildVideoInclude (required: boolean) {
   return {
     attributes: [ 'id', 'uuid', 'name' ],
-    model: () => VideoModel.unscoped(),
+    model: VideoModel.unscoped(),
     required
   }
 }
@@ -53,7 +53,7 @@ function buildChannelInclude (required: boolean, withActor = false) {
   return {
     required,
     attributes: [ 'id', 'name' ],
-    model: () => VideoChannelModel.unscoped(),
+    model: VideoChannelModel.unscoped(),
     include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
   }
 }
@@ -62,12 +62,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
   return {
     required,
     attributes: [ 'id', 'name' ],
-    model: () => AccountModel.unscoped(),
+    model: AccountModel.unscoped(),
     include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
   }
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.WITH_ALL]: {
     include: [
       Object.assign(buildVideoInclude(false), {
@@ -76,7 +76,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
 
       {
         attributes: [ 'id', 'originCommentId' ],
-        model: () => VideoCommentModel.unscoped(),
+        model: VideoCommentModel.unscoped(),
         required: false,
         include: [
           buildAccountInclude(true, true),
@@ -86,56 +86,56 @@ function buildAccountInclude (required: boolean, withActor = false) {
 
       {
         attributes: [ 'id' ],
-        model: () => VideoAbuseModel.unscoped(),
+        model: VideoAbuseModel.unscoped(),
         required: false,
         include: [ buildVideoInclude(true) ]
       },
 
       {
         attributes: [ 'id' ],
-        model: () => VideoBlacklistModel.unscoped(),
+        model: VideoBlacklistModel.unscoped(),
         required: false,
         include: [ buildVideoInclude(true) ]
       },
 
       {
         attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
-        model: () => VideoImportModel.unscoped(),
+        model: VideoImportModel.unscoped(),
         required: false,
         include: [ buildVideoInclude(false) ]
       },
 
       {
         attributes: [ 'id', 'state' ],
-        model: () => ActorFollowModel.unscoped(),
+        model: ActorFollowModel.unscoped(),
         required: false,
         include: [
           {
             attributes: [ 'preferredUsername' ],
-            model: () => ActorModel.unscoped(),
+            model: ActorModel.unscoped(),
             required: true,
             as: 'ActorFollower',
             include: [
               {
                 attributes: [ 'id', 'name' ],
-                model: () => AccountModel.unscoped(),
+                model: AccountModel.unscoped(),
                 required: true
               },
               {
                 attributes: [ 'filename' ],
-                model: () => AvatarModel.unscoped(),
+                model: AvatarModel.unscoped(),
                 required: false
               },
               {
                 attributes: [ 'host' ],
-                model: () => ServerModel.unscoped(),
+                model: ServerModel.unscoped(),
                 required: false
               }
             ]
           },
           {
             attributes: [ 'preferredUsername' ],
-            model: () => ActorModel.unscoped(),
+            model: ActorModel.unscoped(),
             required: true,
             as: 'ActorFollowing',
             include: [
@@ -147,9 +147,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
       },
 
       buildAccountInclude(false, true)
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 @Table({
   tableName: 'userNotification',
   indexes: [
@@ -212,7 +212,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
         }
       }
     }
-  ] as any // FIXME: sequelize typings
+  ] as (ModelIndexesOptions & { where?: WhereOptions })[]
 })
 export class UserNotificationModel extends Model<UserNotificationModel> {
 
@@ -357,7 +357,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
       where: {
         userId,
         id: {
-          [Op.any]: notificationIds
+          [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
         }
       }
     }

+ 27 - 31
server/models/account/user.ts

@@ -1,4 +1,4 @@
-import * as Sequelize from 'sequelize'
+import { FindOptions, literal, Op, QueryTypes } from 'sequelize'
 import {
   AfterDestroy,
   AfterUpdate,
@@ -56,33 +56,33 @@ enum ScopeNames {
   WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
 }
 
-@DefaultScope({
+@DefaultScope(() => ({
   include: [
     {
-      model: () => AccountModel,
+      model: AccountModel,
       required: true
     },
     {
-      model: () => UserNotificationSettingModel,
+      model: UserNotificationSettingModel,
       required: true
     }
   ]
-})
-@Scopes({
+}))
+@Scopes(() => ({
   [ScopeNames.WITH_VIDEO_CHANNEL]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true,
-        include: [ () => VideoChannelModel ]
+        include: [ VideoChannelModel ]
       },
       {
-        model: () => UserNotificationSettingModel,
+        model: UserNotificationSettingModel,
         required: true
       }
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 @Table({
   tableName: 'user',
   indexes: [
@@ -233,26 +233,26 @@ export class UserModel extends Model<UserModel> {
     let where = undefined
     if (search) {
       where = {
-        [Sequelize.Op.or]: [
+        [Op.or]: [
           {
             email: {
-              [Sequelize.Op.iLike]: '%' + search + '%'
+              [Op.iLike]: '%' + search + '%'
             }
           },
           {
             username: {
-              [ Sequelize.Op.iLike ]: '%' + search + '%'
+              [ Op.iLike ]: '%' + search + '%'
             }
           }
         ]
       }
     }
 
-    const query = {
+    const query: FindOptions = {
       attributes: {
         include: [
           [
-            Sequelize.literal(
+            literal(
               '(' +
                 'SELECT COALESCE(SUM("size"), 0) ' +
                 'FROM (' +
@@ -265,7 +265,7 @@ export class UserModel extends Model<UserModel> {
               ')'
             ),
             'videoQuotaUsed'
-          ] as any // FIXME: typings
+          ]
         ]
       },
       offset: start,
@@ -291,7 +291,7 @@ export class UserModel extends Model<UserModel> {
     const query = {
       where: {
         role: {
-          [Sequelize.Op.in]: roles
+          [Op.in]: roles
         }
       }
     }
@@ -387,7 +387,7 @@ export class UserModel extends Model<UserModel> {
 
     const query = {
       where: {
-        [ Sequelize.Op.or ]: [ { username }, { email } ]
+        [ Op.or ]: [ { username }, { email } ]
       }
     }
 
@@ -510,7 +510,7 @@ export class UserModel extends Model<UserModel> {
     const query = {
       where: {
         username: {
-          [ Sequelize.Op.like ]: `%${search}%`
+          [ Op.like ]: `%${search}%`
         }
       },
       limit: 10
@@ -591,15 +591,11 @@ export class UserModel extends Model<UserModel> {
 
     const uploadedTotal = videoFile.size + totalBytes
     const uploadedDaily = videoFile.size + totalBytesDaily
-    if (this.videoQuotaDaily === -1) {
-      return uploadedTotal < this.videoQuota
-    }
-    if (this.videoQuota === -1) {
-      return uploadedDaily < this.videoQuotaDaily
-    }
 
-    return (uploadedTotal < this.videoQuota) &&
-        (uploadedDaily < this.videoQuotaDaily)
+    if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
+    if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
+
+    return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
   }
 
   private static generateUserQuotaBaseSQL (where?: string) {
@@ -619,14 +615,14 @@ export class UserModel extends Model<UserModel> {
   private static getTotalRawQuery (query: string, userId: number) {
     const options = {
       bind: { userId },
-      type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT
+      type: QueryTypes.SELECT as QueryTypes.SELECT
     }
 
-    return UserModel.sequelize.query<{ total: number }>(query, options)
+    return UserModel.sequelize.query<{ total: string }>(query, options)
                     .then(([ { total } ]) => {
                       if (total === null) return 0
 
-                      return parseInt(total + '', 10)
+                      return parseInt(total, 10)
                     })
   }
 }

+ 24 - 23
server/models/activitypub/actor.ts

@@ -56,46 +56,46 @@ export const unusedActorAttributesForAPI = [
   'updatedAt'
 ]
 
-@DefaultScope({
+@DefaultScope(() => ({
   include: [
     {
-      model: () => ServerModel,
+      model: ServerModel,
       required: false
     },
     {
-      model: () => AvatarModel,
+      model: AvatarModel,
       required: false
     }
   ]
-})
-@Scopes({
+}))
+@Scopes(() => ({
   [ScopeNames.FULL]: {
     include: [
       {
-        model: () => AccountModel.unscoped(),
+        model: AccountModel.unscoped(),
         required: false
       },
       {
-        model: () => VideoChannelModel.unscoped(),
+        model: VideoChannelModel.unscoped(),
         required: false,
         include: [
           {
-            model: () => AccountModel,
+            model: AccountModel,
             required: true
           }
         ]
       },
       {
-        model: () => ServerModel,
+        model: ServerModel,
         required: false
       },
       {
-        model: () => AvatarModel,
+        model: AvatarModel,
         required: false
       }
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 @Table({
   tableName: 'actor',
   indexes: [
@@ -131,7 +131,7 @@ export const unusedActorAttributesForAPI = [
 export class ActorModel extends Model<ActorModel> {
 
   @AllowNull(false)
-  @Column({ type: DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)) }) // FIXME: sequelize typings
+  @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
   type: ActivityPubActorType
 
   @AllowNull(false)
@@ -280,14 +280,16 @@ export class ActorModel extends Model<ActorModel> {
               attributes: [ 'id' ],
               model: VideoChannelModel.unscoped(),
               required: true,
-              include: {
-                attributes: [ 'id' ],
-                model: VideoModel.unscoped(),
-                required: true,
-                where: {
-                  id: videoId
+              include: [
+                {
+                  attributes: [ 'id' ],
+                  model: VideoModel.unscoped(),
+                  required: true,
+                  where: {
+                    id: videoId
+                  }
                 }
-              }
+              ]
             }
           ]
         }
@@ -295,7 +297,7 @@ export class ActorModel extends Model<ActorModel> {
       transaction
     }
 
-    return ActorModel.unscoped().findOne(query as any) // FIXME: typings
+    return ActorModel.unscoped().findOne(query)
   }
 
   static isActorUrlExist (url: string) {
@@ -389,8 +391,7 @@ export class ActorModel extends Model<ActorModel> {
   }
 
   static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
-    // FIXME: typings
-    return (ActorModel as any).increment(column, {
+    return ActorModel.increment(column, {
       by,
       where: {
         id

+ 3 - 3
server/models/application/application.ts

@@ -1,14 +1,14 @@
 import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
 import { AccountModel } from '../account/account'
 
-@DefaultScope({
+@DefaultScope(() => ({
   include: [
     {
-      model: () => AccountModel,
+      model: AccountModel,
       required: true
     }
   ]
-})
+}))
 @Table({
   tableName: 'application'
 })

+ 2 - 2
server/models/oauth/oauth-client.ts

@@ -24,10 +24,10 @@ export class OAuthClientModel extends Model<OAuthClientModel> {
   @Column
   clientSecret: string
 
-  @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: sequelize typings
+  @Column(DataType.ARRAY(DataType.STRING))
   grants: string[]
 
-  @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: sequelize typings
+  @Column(DataType.ARRAY(DataType.STRING))
   redirectUris: string[]
 
   @CreatedAt

+ 12 - 10
server/models/oauth/oauth-token.ts

@@ -34,30 +34,30 @@ enum ScopeNames {
   WITH_USER = 'WITH_USER'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.WITH_USER]: {
     include: [
       {
-        model: () => UserModel.unscoped(),
+        model: UserModel.unscoped(),
         required: true,
         include: [
           {
             attributes: [ 'id' ],
-            model: () => AccountModel.unscoped(),
+            model: AccountModel.unscoped(),
             required: true,
             include: [
               {
                 attributes: [ 'id', 'url' ],
-                model: () => ActorModel.unscoped(),
+                model: ActorModel.unscoped(),
                 required: true
               }
             ]
           }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 @Table({
   tableName: 'oAuthToken',
   indexes: [
@@ -167,11 +167,13 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
       }
     }
 
-    return OAuthTokenModel.scope(ScopeNames.WITH_USER).findOne(query).then(token => {
-      if (token) token['user'] = token.User
+    return OAuthTokenModel.scope(ScopeNames.WITH_USER)
+                          .findOne(query)
+                          .then(token => {
+                            if (token) token[ 'user' ] = token.User
 
-      return token
-    })
+                            return token
+                          })
   }
 
   static getByRefreshTokenAndPopulateUser (refreshToken: string) {

+ 26 - 30
server/models/redundancy/video-redundancy.ts

@@ -13,7 +13,7 @@ import {
   UpdatedAt
 } from 'sequelize-typescript'
 import { ActorModel } from '../activitypub/actor'
-import { getVideoSort, throwIfNotValid } from '../utils'
+import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
 import { VideoFileModel } from '../video/video-file'
@@ -27,7 +27,7 @@ import { ServerModel } from '../server/server'
 import { sample } from 'lodash'
 import { isTestInstance } from '../../helpers/core-utils'
 import * as Bluebird from 'bluebird'
-import * as Sequelize from 'sequelize'
+import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
 import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
 import { CONFIG } from '../../initializers/config'
 
@@ -35,32 +35,32 @@ export enum ScopeNames {
   WITH_VIDEO = 'WITH_VIDEO'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ ScopeNames.WITH_VIDEO ]: {
     include: [
       {
-        model: () => VideoFileModel,
+        model: VideoFileModel,
         required: false,
         include: [
           {
-            model: () => VideoModel,
+            model: VideoModel,
             required: true
           }
         ]
       },
       {
-        model: () => VideoStreamingPlaylistModel,
+        model: VideoStreamingPlaylistModel,
         required: false,
         include: [
           {
-            model: () => VideoModel,
+            model: VideoModel,
             required: true
           }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 
 @Table({
   tableName: 'videoRedundancy',
@@ -192,7 +192,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
   }
 
-  static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
+  static loadByUrl (url: string, transaction?: Transaction) {
     const query = {
       where: {
         url
@@ -292,7 +292,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
       where: {
         privacy: VideoPrivacy.PUBLIC,
         views: {
-          [ Sequelize.Op.gte ]: minViews
+          [ Op.gte ]: minViews
         }
       },
       include: [
@@ -315,7 +315,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
         actorId: actor.id,
         strategy,
         createdAt: {
-          [ Sequelize.Op.lt ]: expiredDate
+          [ Op.lt ]: expiredDate
         }
       }
     }
@@ -326,7 +326,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
   static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
     const actor = await getServerActor()
 
-    const options = {
+    const query: FindOptions = {
       include: [
         {
           attributes: [],
@@ -340,12 +340,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
       ]
     }
 
-    return VideoFileModel.sum('size', options as any) // FIXME: typings
-      .then(v => {
-        if (!v || isNaN(v)) return 0
-
-        return v
-      })
+    return VideoFileModel.aggregate('size', 'SUM', query)
+      .then(result => parseAggregateResult(result))
   }
 
   static async listLocalExpired () {
@@ -355,7 +351,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
       where: {
         actorId: actor.id,
         expiresOn: {
-          [ Sequelize.Op.lt ]: new Date()
+          [ Op.lt ]: new Date()
         }
       }
     }
@@ -369,10 +365,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     const query = {
       where: {
         actorId: {
-          [Sequelize.Op.ne]: actor.id
+          [Op.ne]: actor.id
         },
         expiresOn: {
-          [ Sequelize.Op.lt ]: new Date()
+          [ Op.lt ]: new Date()
         }
       }
     }
@@ -428,12 +424,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
   static async getStats (strategy: VideoRedundancyStrategy) {
     const actor = await getServerActor()
 
-    const query = {
+    const query: FindOptions = {
       raw: true,
       attributes: [
-        [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
-        [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
-        [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
+        [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ],
+        [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ],
+        [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ]
       ],
       where: {
         strategy,
@@ -448,9 +444,9 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
       ]
     }
 
-    return VideoRedundancyModel.findOne(query as any) // FIXME: typings
+    return VideoRedundancyModel.findOne(query)
       .then((r: any) => ({
-        totalUsed: parseInt(r.totalUsed.toString(), 10),
+        totalUsed: parseAggregateResult(r.totalUsed),
         totalVideos: r.totalVideos,
         totalVideoFiles: r.totalVideoFiles
       }))
@@ -503,7 +499,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
   private static async buildVideoFileForDuplication () {
     const actor = await getServerActor()
 
-    const notIn = Sequelize.literal(
+    const notIn = literal(
       '(' +
         `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
       ')'
@@ -515,7 +511,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
       required: true,
       where: {
         id: {
-          [ Sequelize.Op.notIn ]: notIn
+          [ Op.notIn ]: notIn
         }
       }
     }

+ 4 - 4
server/models/server/server-blocklist.ts

@@ -9,11 +9,11 @@ enum ScopeNames {
   WITH_SERVER = 'WITH_SERVER'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.WITH_ACCOUNT]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true
       }
     ]
@@ -21,12 +21,12 @@ enum ScopeNames {
   [ScopeNames.WITH_SERVER]: {
     include: [
       {
-        model: () => ServerModel,
+        model: ServerModel,
         required: true
       }
     ]
   }
-})
+}))
 
 @Table({
   tableName: 'serverBlocklist',

+ 11 - 1
server/models/utils.ts

@@ -118,6 +118,15 @@ function buildWhereIdOrUUID (id: number | string) {
   return validator.isInt('' + id) ? { id } : { uuid: id }
 }
 
+function parseAggregateResult (result: any) {
+  if (!result) return 0
+
+  const total = parseInt(result + '', 10)
+  if (isNaN(total)) return 0
+
+  return total
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -131,7 +140,8 @@ export {
   buildServerIdsFollowedBy,
   buildTrigramSearchIndex,
   buildWhereIdOrUUID,
-  isOutdated
+  isOutdated,
+  parseAggregateResult
 }
 
 // ---------------------------------------------------------------------------

+ 1 - 1
server/models/video/tag.ts

@@ -75,7 +75,7 @@ export class TagModel extends Model<TagModel> {
       type: QueryTypes.SELECT as QueryTypes.SELECT
     }
 
-    return TagModel.sequelize.query<{ name }>(query, options)
+    return TagModel.sequelize.query<{ name: string }>(query, options)
                     .then(data => data.map(d => d.name))
   }
 }

+ 2 - 2
server/models/video/thumbnail.ts

@@ -75,8 +75,8 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
   updatedAt: Date
 
   private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
-    [ThumbnailType.THUMBNAIL]: {
-      label: 'thumbnail',
+    [ThumbnailType.MINIATURE]: {
+      label: 'miniature',
       directory: CONFIG.STORAGE.THUMBNAILS_DIR,
       staticPath: STATIC_PATHS.THUMBNAILS
     },

+ 5 - 8
server/models/video/video-caption.ts

@@ -12,7 +12,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { throwIfNotValid } from '../utils'
+import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
 import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
@@ -26,17 +26,17 @@ export enum ScopeNames {
   WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
     include: [
       {
         attributes: [ 'uuid', 'remote' ],
-        model: () => VideoModel.unscoped(),
+        model: VideoModel.unscoped(),
         required: true
       }
     ]
   }
-})
+}))
 
 @Table({
   tableName: 'videoCaption',
@@ -97,12 +97,9 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
     const videoInclude = {
       model: VideoModel.unscoped(),
       attributes: [ 'id', 'remote', 'uuid' ],
-      where: { }
+      where: buildWhereIdOrUUID(videoId)
     }
 
-    if (typeof videoId === 'string') videoInclude.where['uuid'] = videoId
-    else videoInclude.where['id'] = videoId
-
     const query = {
       where: {
         language

+ 7 - 7
server/models/video/video-change-ownership.ts

@@ -23,29 +23,29 @@ enum ScopeNames {
     }
   ]
 })
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.FULL]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         as: 'Initiator',
         required: true
       },
       {
-        model: () => AccountModel,
+        model: AccountModel,
         as: 'NextOwner',
         required: true
       },
       {
-        model: () => VideoModel,
+        model: VideoModel,
         required: true,
         include: [
-          { model: () => VideoFileModel }
+          { model: VideoFileModel }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> {
   @CreatedAt
   createdAt: Date

+ 8 - 8
server/models/video/video-channel.ts

@@ -58,15 +58,15 @@ type AvailableForListOptions = {
   actorId: number
 }
 
-@DefaultScope({
+@DefaultScope(() => ({
   include: [
     {
-      model: () => ActorModel,
+      model: ActorModel,
       required: true
     }
   ]
-})
-@Scopes({
+}))
+@Scopes(() => ({
   [ScopeNames.SUMMARY]: (withAccount = false) => {
     const base: FindOptions = {
       attributes: [ 'name', 'description', 'id', 'actorId' ],
@@ -142,22 +142,22 @@ type AvailableForListOptions = {
   [ScopeNames.WITH_ACCOUNT]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true
       }
     ]
   },
   [ScopeNames.WITH_VIDEOS]: {
     include: [
-      () => VideoModel
+      VideoModel
     ]
   },
   [ScopeNames.WITH_ACTOR]: {
     include: [
-      () => ActorModel
+      ActorModel
     ]
   }
-})
+}))
 @Table({
   tableName: 'videoChannel',
   indexes

+ 16 - 17
server/models/video/video-comment.ts

@@ -30,7 +30,7 @@ import { UserModel } from '../account/user'
 import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
 import { regexpCapture } from '../../helpers/regexp'
 import { uniq } from 'lodash'
-import { FindOptions, Op, Order, Sequelize, Transaction } from 'sequelize'
+import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
 
 enum ScopeNames {
   WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -39,7 +39,7 @@ enum ScopeNames {
   ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
     return {
       attributes: {
@@ -63,34 +63,34 @@ enum ScopeNames {
           ]
         ]
       }
-    }
+    } as FindOptions
   },
   [ScopeNames.WITH_ACCOUNT]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         include: [
           {
-            model: () => ActorModel,
+            model: ActorModel,
             include: [
               {
-                model: () => ServerModel,
+                model: ServerModel,
                 required: false
               },
               {
-                model: () => AvatarModel,
+                model: AvatarModel,
                 required: false
               }
             ]
           }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   },
   [ScopeNames.WITH_IN_REPLY_TO]: {
     include: [
       {
-        model: () => VideoCommentModel,
+        model: VideoCommentModel,
         as: 'InReplyToVideoComment'
       }
     ]
@@ -98,19 +98,19 @@ enum ScopeNames {
   [ScopeNames.WITH_VIDEO]: {
     include: [
       {
-        model: () => VideoModel,
+        model: VideoModel,
         required: true,
         include: [
           {
-            model: () => VideoChannelModel.unscoped(),
+            model: VideoChannelModel.unscoped(),
             required: true,
             include: [
               {
-                model: () => AccountModel,
+                model: AccountModel,
                 required: true,
                 include: [
                   {
-                    model: () => ActorModel,
+                    model: ActorModel,
                     required: true
                   }
                 ]
@@ -119,9 +119,9 @@ enum ScopeNames {
           }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   }
-})
+}))
 @Table({
   tableName: 'videoComment',
   indexes: [
@@ -313,8 +313,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
       }
     }
 
-    // FIXME: typings
-    const scopes: any[] = [
+    const scopes: (string | ScopeOptions)[] = [
       ScopeNames.WITH_ACCOUNT,
       {
         method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]

+ 12 - 15
server/models/video/video-file.ts

@@ -19,11 +19,11 @@ import {
   isVideoFileSizeValid,
   isVideoFPSResolutionValid
 } from '../../helpers/custom-validators/videos'
-import { throwIfNotValid } from '../utils'
+import { parseAggregateResult, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
-import * as Sequelize from 'sequelize'
 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
+import { FindOptions, QueryTypes, Transaction } from 'sequelize'
 
 @Table({
   tableName: 'videoFile',
@@ -97,15 +97,13 @@ export class VideoFileModel extends Model<VideoFileModel> {
   static doesInfohashExist (infoHash: string) {
     const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
     const options = {
-      type: Sequelize.QueryTypes.SELECT,
+      type: QueryTypes.SELECT,
       bind: { infoHash },
       raw: true
     }
 
     return VideoModel.sequelize.query(query, options)
-              .then(results => {
-                return results.length === 1
-              })
+              .then(results => results.length === 1)
   }
 
   static loadWithVideo (id: number) {
@@ -121,7 +119,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
     return VideoFileModel.findByPk(id, options)
   }
 
-  static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Sequelize.Transaction) {
+  static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
     const query = {
       include: [
         {
@@ -144,8 +142,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
     return VideoFileModel.findAll(query)
   }
 
-  static async getStats () {
-    let totalLocalVideoFilesSize = await VideoFileModel.sum('size', {
+  static getStats () {
+    const query: FindOptions = {
       include: [
         {
           attributes: [],
@@ -155,13 +153,12 @@ export class VideoFileModel extends Model<VideoFileModel> {
           }
         }
       ]
-    } as any)
-    // Sequelize could return null...
-    if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0
-
-    return {
-      totalLocalVideoFilesSize
     }
+
+    return VideoFileModel.aggregate('size', 'SUM', query)
+      .then(result => ({
+        totalLocalVideoFilesSize: parseAggregateResult(result)
+      }))
   }
 
   hasSameUniqueKeysThan (other: VideoFileModel) {

+ 6 - 4
server/models/video/video-format-utils.ts

@@ -59,7 +59,7 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
     views: video.views,
     likes: video.likes,
     dislikes: video.dislikes,
-    thumbnailPath: video.getThumbnailStaticPath(),
+    thumbnailPath: video.getMiniatureStaticPath(),
     previewPath: video.getPreviewStaticPath(),
     embedPath: video.getEmbedStaticPath(),
     createdAt: video.createdAt,
@@ -301,6 +301,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
     })
   }
 
+  const miniature = video.getMiniature()
+
   return {
     type: 'Video' as 'Video',
     id: video.url,
@@ -326,10 +328,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
     subtitleLanguage,
     icon: {
       type: 'Image',
-      url: video.getThumbnail().getUrl(),
+      url: miniature.getUrl(),
       mediaType: 'image/jpeg',
-      width: video.getThumbnail().width,
-      height: video.getThumbnail().height
+      width: miniature.width,
+      height: miniature.height
     },
     url,
     likes: getVideoLikesActivityPubUrl(video),

+ 4 - 4
server/models/video/video-import.ts

@@ -21,18 +21,18 @@ import { VideoImport, VideoImportState } from '../../../shared'
 import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
 import { UserModel } from '../account/user'
 
-@DefaultScope({
+@DefaultScope(() => ({
   include: [
     {
-      model: () => UserModel.unscoped(),
+      model: UserModel.unscoped(),
       required: true
     },
     {
-      model: () => VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]),
+      model: VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]),
       required: false
     }
   ]
-})
+}))
 
 @Table({
   tableName: 'videoImport',

+ 21 - 26
server/models/video/video-playlist.ts

@@ -42,7 +42,7 @@ import { activityPubCollectionPagination } from '../../helpers/activitypub'
 import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
 import { ThumbnailModel } from './thumbnail'
 import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
-import { fn, literal, Op, Transaction } from 'sequelize'
+import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
 
 enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -61,11 +61,11 @@ type AvailableForListOptions = {
   privateAndUnlisted?: boolean
 }
 
-@Scopes({
+@Scopes(() => ({
   [ ScopeNames.WITH_THUMBNAIL ]: {
     include: [
       {
-        model: () => ThumbnailModel,
+        model: ThumbnailModel,
         required: false
       }
     ]
@@ -73,21 +73,17 @@ type AvailableForListOptions = {
   [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
     attributes: {
       include: [
-        [
-          fn('COUNT', 'toto'),
-          'coucou'
-        ],
         [
           literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'),
           'videosLength'
         ]
       ]
     }
-  },
+  } as FindOptions,
   [ ScopeNames.WITH_ACCOUNT ]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true
       }
     ]
@@ -95,11 +91,11 @@ type AvailableForListOptions = {
   [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
     include: [
       {
-        model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
+        model: AccountModel.scope(AccountScopeNames.SUMMARY),
         required: true
       },
       {
-        model: () => VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
+        model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
         required: false
       }
     ]
@@ -107,11 +103,11 @@ type AvailableForListOptions = {
   [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
     include: [
       {
-        model: () => AccountModel,
+        model: AccountModel,
         required: true
       },
       {
-        model: () => VideoChannelModel,
+        model: VideoChannelModel,
         required: false
       }
     ]
@@ -132,7 +128,7 @@ type AvailableForListOptions = {
       ]
     }
 
-    const whereAnd: any[] = []
+    const whereAnd: WhereOptions[] = []
 
     if (options.privateAndUnlisted !== true) {
       whereAnd.push({
@@ -178,9 +174,9 @@ type AvailableForListOptions = {
           required: false
         }
       ]
-    }
+    } as FindOptions
   }
-})
+}))
 
 @Table({
   tableName: 'videoPlaylist',
@@ -269,6 +265,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
   VideoPlaylistElements: VideoPlaylistElementModel[]
 
   @HasOne(() => ThumbnailModel, {
+
     foreignKey: {
       name: 'videoPlaylistId',
       allowNull: true
@@ -294,7 +291,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       order: getSort(options.sort)
     }
 
-    const scopes = [
+    const scopes: (string | ScopeOptions)[] = [
       {
         method: [
           ScopeNames.AVAILABLE_FOR_LIST,
@@ -306,7 +303,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
             privateAndUnlisted: options.privateAndUnlisted
           } as AvailableForListOptions
         ]
-      } as any, // FIXME: typings
+      },
       ScopeNames.WITH_VIDEOS_LENGTH,
       ScopeNames.WITH_THUMBNAIL
     ]
@@ -348,7 +345,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
           model: VideoPlaylistElementModel.unscoped(),
           where: {
             videoId: {
-              [Op.any]: videoIds
+              [Op.in]: videoIds // FIXME: sequelize ANY seems broken
             }
           },
           required: true
@@ -427,12 +424,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
   }
 
-  setThumbnail (thumbnail: ThumbnailModel) {
-    this.Thumbnail = thumbnail
-  }
+  async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) {
+    thumbnail.videoPlaylistId = this.id
 
-  getThumbnail () {
-    return this.Thumbnail
+    this.Thumbnail = await thumbnail.save({ transaction: t })
   }
 
   hasThumbnail () {
@@ -448,13 +443,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
   getThumbnailUrl () {
     if (!this.hasThumbnail()) return null
 
-    return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename
+    return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
   }
 
   getThumbnailStaticPath () {
     if (!this.hasThumbnail()) return null
 
-    return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename)
+    return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
   }
 
   setAsRefreshed () {

+ 5 - 5
server/models/video/video-share.ts

@@ -14,15 +14,15 @@ enum ScopeNames {
   WITH_ACTOR = 'WITH_ACTOR'
 }
 
-@Scopes({
+@Scopes(() => ({
   [ScopeNames.FULL]: {
     include: [
       {
-        model: () => ActorModel,
+        model: ActorModel,
         required: true
       },
       {
-        model: () => VideoModel,
+        model: VideoModel,
         required: true
       }
     ]
@@ -30,12 +30,12 @@ enum ScopeNames {
   [ScopeNames.WITH_ACTOR]: {
     include: [
       {
-        model: () => ActorModel,
+        model: ActorModel,
         required: true
       }
     ]
   }
-})
+}))
 @Table({
   tableName: 'videoShare',
   indexes: [

+ 3 - 3
server/models/video/video-streaming-playlist.ts

@@ -26,7 +26,7 @@ import { QueryTypes, Op } from 'sequelize'
       fields: [ 'p2pMediaLoaderInfohashes' ],
       using: 'gin'
     }
-  ] as any // FIXME: sequelize typings
+  ]
 })
 export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> {
   @CreatedAt
@@ -46,7 +46,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
 
   @AllowNull(false)
   @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
-  @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: typings
+  @Column(DataType.ARRAY(DataType.STRING))
   p2pMediaLoaderInfohashes: string[]
 
   @AllowNull(false)
@@ -87,7 +87,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
       raw: true
     }
 
-    return VideoModel.sequelize.query<any>(query, options)
+    return VideoModel.sequelize.query<object>(query, options)
               .then(results => results.length === 1)
   }
 

+ 91 - 88
server/models/video/video.ts

@@ -227,12 +227,12 @@ type AvailableForListIDsOptions = {
   historyOfUser?: UserModel
 }
 
-@Scopes({
+@Scopes(() => ({
   [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
     const query: FindOptions = {
       where: {
         id: {
-          [ Op.in ]: options.ids // FIXME: sequelize any seems broken
+          [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
         }
       },
       include: [
@@ -486,7 +486,7 @@ type AvailableForListIDsOptions = {
   [ ScopeNames.WITH_THUMBNAILS ]: {
     include: [
       {
-        model: () => ThumbnailModel,
+        model: ThumbnailModel,
         required: false
       }
     ]
@@ -495,48 +495,48 @@ type AvailableForListIDsOptions = {
     include: [
       {
         attributes: [ 'accountId' ],
-        model: () => VideoChannelModel.unscoped(),
+        model: VideoChannelModel.unscoped(),
         required: true,
         include: [
           {
             attributes: [ 'userId' ],
-            model: () => AccountModel.unscoped(),
+            model: AccountModel.unscoped(),
             required: true
           }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   },
   [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
     include: [
       {
-        model: () => VideoChannelModel.unscoped(),
+        model: VideoChannelModel.unscoped(),
         required: true,
         include: [
           {
             attributes: {
               exclude: [ 'privateKey', 'publicKey' ]
             },
-            model: () => ActorModel.unscoped(),
+            model: ActorModel.unscoped(),
             required: true,
             include: [
               {
                 attributes: [ 'host' ],
-                model: () => ServerModel.unscoped(),
+                model: ServerModel.unscoped(),
                 required: false
               },
               {
-                model: () => AvatarModel.unscoped(),
+                model: AvatarModel.unscoped(),
                 required: false
               }
             ]
           },
           {
-            model: () => AccountModel.unscoped(),
+            model: AccountModel.unscoped(),
             required: true,
             include: [
               {
-                model: () => ActorModel.unscoped(),
+                model: ActorModel.unscoped(),
                 attributes: {
                   exclude: [ 'privateKey', 'publicKey' ]
                 },
@@ -544,11 +544,11 @@ type AvailableForListIDsOptions = {
                 include: [
                   {
                     attributes: [ 'host' ],
-                    model: () => ServerModel.unscoped(),
+                    model: ServerModel.unscoped(),
                     required: false
                   },
                   {
-                    model: () => AvatarModel.unscoped(),
+                    model: AvatarModel.unscoped(),
                     required: false
                   }
                 ]
@@ -557,16 +557,16 @@ type AvailableForListIDsOptions = {
           }
         ]
       }
-    ] as any // FIXME: sequelize typings
+    ]
   },
   [ ScopeNames.WITH_TAGS ]: {
-    include: [ () => TagModel ]
+    include: [ TagModel ]
   },
   [ ScopeNames.WITH_BLACKLISTED ]: {
     include: [
       {
         attributes: [ 'id', 'reason' ],
-        model: () => VideoBlacklistModel,
+        model: VideoBlacklistModel,
         required: false
       }
     ]
@@ -588,8 +588,7 @@ type AvailableForListIDsOptions = {
       include: [
         {
           model: VideoFileModel.unscoped(),
-          // FIXME: typings
-          [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
+          separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
           required: false,
           include: subInclude
         }
@@ -613,8 +612,7 @@ type AvailableForListIDsOptions = {
       include: [
         {
           model: VideoStreamingPlaylistModel.unscoped(),
-          // FIXME: typings
-          [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
+          separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
           required: false,
           include: subInclude
         }
@@ -624,7 +622,7 @@ type AvailableForListIDsOptions = {
   [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
     include: [
       {
-        model: () => ScheduleVideoUpdateModel.unscoped(),
+        model: ScheduleVideoUpdateModel.unscoped(),
         required: false
       }
     ]
@@ -643,7 +641,7 @@ type AvailableForListIDsOptions = {
       ]
     }
   }
-})
+}))
 @Table({
   tableName: 'video',
   indexes
@@ -1075,15 +1073,14 @@ export class VideoModel extends Model<VideoModel> {
     }
 
     return Bluebird.all([
-      // FIXME: typing issue
-      VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any),
-      VideoModel.sequelize.query<{ total: number }>(rawCountQuery, { type: QueryTypes.SELECT })
+      VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
+      VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
     ]).then(([ rows, totals ]) => {
       // totals: totalVideos + totalVideoShares
       let totalVideos = 0
       let totalVideoShares = 0
-      if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total + '', 10)
-      if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total + '', 10)
+      if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10)
+      if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10)
 
       const total = totalVideos + totalVideoShares
       return {
@@ -1094,50 +1091,58 @@ export class VideoModel extends Model<VideoModel> {
   }
 
   static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
-    const query: FindOptions = {
-      offset: start,
-      limit: count,
-      order: getVideoSort(sort),
-      include: [
-        {
-          model: VideoChannelModel,
-          required: true,
-          include: [
-            {
-              model: AccountModel,
-              where: {
-                id: accountId
-              },
-              required: true
-            }
-          ]
-        },
-        {
-          model: ScheduleVideoUpdateModel,
-          required: false
-        },
-        {
-          model: VideoBlacklistModel,
-          required: false
-        }
-      ]
+    function buildBaseQuery (): FindOptions {
+      return {
+        offset: start,
+        limit: count,
+        order: getVideoSort(sort),
+        include: [
+          {
+            model: VideoChannelModel,
+            required: true,
+            include: [
+              {
+                model: AccountModel,
+                where: {
+                  id: accountId
+                },
+                required: true
+              }
+            ]
+          }
+        ]
+      }
     }
 
+    const countQuery = buildBaseQuery()
+    const findQuery = buildBaseQuery()
+
+    findQuery.include.push({
+      model: ScheduleVideoUpdateModel,
+      required: false
+    })
+
+    findQuery.include.push({
+      model: VideoBlacklistModel,
+      required: false
+    })
+
     if (withFiles === true) {
-      query.include.push({
+      findQuery.include.push({
         model: VideoFileModel.unscoped(),
         required: true
       })
     }
 
-    return VideoModel.scope(ScopeNames.WITH_THUMBNAILS)
-                     .findAndCountAll(query)
-                     .then(({ rows, count }) => {
-                       return {
-                         data: rows,
-                         total: count
-                       }
-                     })
+    return Promise.all([
+      VideoModel.count(countQuery),
+      VideoModel.findAll(findQuery)
+    ]).then(([ count, rows ]) => {
+      return {
+        data: rows,
+        total: count
+      }
+    })
   }
 
   static async listForApi (options: {
@@ -1404,12 +1409,12 @@ export class VideoModel extends Model<VideoModel> {
     const where = buildWhereIdOrUUID(id)
 
     const options = {
-      order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
+      order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
       where,
       transaction: t
     }
 
-    const scopes = [
+    const scopes: (string | ScopeOptions)[] = [
       ScopeNames.WITH_TAGS,
       ScopeNames.WITH_BLACKLISTED,
       ScopeNames.WITH_ACCOUNT_DETAILS,
@@ -1420,7 +1425,7 @@ export class VideoModel extends Model<VideoModel> {
     ]
 
     if (userId) {
-      scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
+      scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
     }
 
     return VideoModel
@@ -1437,18 +1442,18 @@ export class VideoModel extends Model<VideoModel> {
       transaction: t
     }
 
-    const scopes = [
+    const scopes: (string | ScopeOptions)[] = [
       ScopeNames.WITH_TAGS,
       ScopeNames.WITH_BLACKLISTED,
       ScopeNames.WITH_ACCOUNT_DETAILS,
       ScopeNames.WITH_SCHEDULED_UPDATE,
       ScopeNames.WITH_THUMBNAILS,
-      { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
-      { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
+      { method: [ ScopeNames.WITH_FILES, true ] },
+      { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
     ]
 
     if (userId) {
-      scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
+      scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
     }
 
     return VideoModel
@@ -1520,9 +1525,9 @@ export class VideoModel extends Model<VideoModel> {
       attributes: [ field ],
       limit: count,
       group: field,
-      having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), {
-        [ Op.gte ]: threshold
-      }) as any, // FIXME: typings
+      having: Sequelize.where(
+        Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold }
+      ),
       order: [ (this.sequelize as any).random() ]
     }
 
@@ -1594,16 +1599,10 @@ export class VideoModel extends Model<VideoModel> {
       ]
     }
 
-    // FIXME: typing
-    const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ]
+    const apiScope: (string | ScopeOptions)[] = [ ScopeNames.WITH_THUMBNAILS ]
 
     if (options.user) {
       apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
-
-      // Even if the relation is n:m, we know that a user only have 0..1 video history
-      // So we won't have multiple rows for the same video
-      // A subquery adds some bugs in our query so disable it
-      secondQuery.subQuery = false
     }
 
     apiScope.push({
@@ -1651,13 +1650,17 @@ export class VideoModel extends Model<VideoModel> {
     return maxBy(this.VideoFiles, file => file.resolution)
   }
 
-  addThumbnail (thumbnail: ThumbnailModel) {
+  async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
+    thumbnail.videoId = this.id
+
+    const savedThumbnail = await thumbnail.save({ transaction })
+
     if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
 
     // Already have this thumbnail, skip
-    if (this.Thumbnails.find(t => t.id === thumbnail.id)) return
+    if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
 
-    this.Thumbnails.push(thumbnail)
+    this.Thumbnails.push(savedThumbnail)
   }
 
   getVideoFilename (videoFile: VideoFileModel) {
@@ -1668,10 +1671,10 @@ export class VideoModel extends Model<VideoModel> {
     return this.uuid + '.jpg'
   }
 
-  getThumbnail () {
+  getMiniature () {
     if (Array.isArray(this.Thumbnails) === false) return undefined
 
-    return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL)
+    return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
   }
 
   generatePreviewName () {
@@ -1732,8 +1735,8 @@ export class VideoModel extends Model<VideoModel> {
     return '/videos/embed/' + this.uuid
   }
 
-  getThumbnailStaticPath () {
-    const thumbnail = this.getThumbnail()
+  getMiniatureStaticPath () {
+    const thumbnail = this.getMiniature()
     if (!thumbnail) return null
 
     return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)

+ 18 - 0
server/typings/sequelize.ts

@@ -0,0 +1,18 @@
+import { Model } from 'sequelize-typescript'
+
+// Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript
+
+export type Diff<T extends string | symbol | number, U extends string | symbol | number> =
+  ({ [P in T]: P } & { [P in U]: never } & { [ x: string ]: never })[T]
+
+export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }
+
+export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }
+
+export type FilteredModelAttributes<T extends Model<T>> = RecursivePartial<Omit<T, keyof Model<any>>> & {
+  id?: number | any
+  createdAt?: Date | any
+  updatedAt?: Date | any
+  deletedAt?: Date | any
+  version?: number | any
+}

+ 0 - 1
shared/extra-utils/miscs/sql.ts

@@ -15,7 +15,6 @@ function getSequelize (serverNumber: number) {
     dialect: 'postgres',
     host,
     port,
-    operatorsAliases: false,
     logging: false
   })
 

+ 1 - 1
shared/models/videos/thumbnail.type.ts

@@ -1,4 +1,4 @@
 export enum ThumbnailType {
-  THUMBNAIL = 1,
+  MINIATURE = 1,
   PREVIEW = 2
 }

+ 12 - 12
yarn.lock

@@ -7461,17 +7461,17 @@ sequelize-pool@^1.0.2:
   dependencies:
     bluebird "^3.5.3"
 
-sequelize-typescript@^1.0.0-beta.1:
-  version "1.0.0-beta.1"
-  resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-1.0.0-beta.1.tgz#402279fec52669cbd78ecbf50e189638483a7360"
-  integrity sha512-xD28kqa1rIKujlmgA4hWQgtwFfRM6tLv1/mnZOrOFEZxvSWazUbTzqGB7OZydZDNj3iJnyrV1l6i6HOfvrpvEw==
+sequelize-typescript@1.0.0-beta.2:
+  version "1.0.0-beta.2"
+  resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-1.0.0-beta.2.tgz#fd9ae47ecf8b159e32e19c1298426cc9773cebd8"
+  integrity sha512-Iu67kF/RunoeBQBsU5llViJkxAHBVmeS9DBP+eC63hkEwxeDGZgxOkodyW5v5k3h2DJ0MBO+clRURXoDb+/OHg==
   dependencies:
     glob "7.1.2"
 
-sequelize@5.6.1:
-  version "5.6.1"
-  resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.6.1.tgz#fc22306109fb2504a6573edfb3c469ec86fae873"
-  integrity sha512-QsXUDar6ow0HrF9BtnHRaNumu6qRYb97dfwvez/Z5guH3i6w6k8+bp6gP3VCiDC+2qX+jQIyrYohKg9evy8GFg==
+sequelize@5.7.4:
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-5.7.4.tgz#1631faadff65f3a345b9757fca60429c65ba8e57"
+  integrity sha512-CaVYpAgZQEsGDuZ+Oq6uIZy4pxQxscotuh5UGIaFRa0VkTIgV0IiF7vAhSv+1Wn+NvhKCvgJJ85M34BP3AdGNg==
   dependencies:
     bluebird "^3.5.0"
     cls-bluebird "^2.1.0"
@@ -8678,10 +8678,10 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@^3.1.6:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6"
-  integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==
+typescript@^3.4.3:
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.3.tgz#0eb320e4ace9b10eadf5bc6103286b0f8b7c224f"
+  integrity sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==
 
 uid-number@0.0.6:
   version "0.0.6"