1
0
Fork 0

Upgrade sequelize

This commit is contained in:
Chocobozzz 2019-04-23 09:50:57 +02:00
parent 1735c82572
commit 3acc508440
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
48 changed files with 457 additions and 466 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -165,20 +165,20 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function getPreview (req: express.Request, res: express.Response) { async function getPreview (req: express.Request, res: express.Response) {
const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid) const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
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 getVideoCaption (req: express.Request, res: express.Response) { 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, videoId: req.params.videoId,
language: req.params.captionLanguage 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) { async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) {

View File

@ -140,15 +140,15 @@ async function checkPostgresExtensions () {
} }
async function checkPostgresExtension (extension: string) { 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 = { const options = {
type: QueryTypes.SELECT as QueryTypes.SELECT, type: QueryTypes.SELECT as QueryTypes.SELECT,
raw: true 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 to create the extension ourselves
try { try {
await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true }) await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true })

View File

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

View File

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

View File

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

View File

@ -4,24 +4,28 @@ import { VideoModel } from '../../models/video/video'
import { fetchRemoteVideoStaticFile } from '../activitypub' import { fetchRemoteVideoStaticFile } from '../activitypub'
import * as memoizee from 'memoizee' import * as memoizee from 'memoizee'
type GetFilePathResult = { isOwned: boolean, path: string } | undefined
export abstract class AbstractVideoStaticFileCache <T> { 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 // 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) { init (max: number, maxAge: number) {
this.getFilePath = memoizee(this.getFilePathImpl, { this.getFilePath = memoizee(this.getFilePathImpl, {
maxAge, maxAge,
max, max,
promise: true, promise: true,
dispose: (value: string) => { dispose: (result: GetFilePathResult) => {
remove(value) if (result.isOwned !== true) {
.then(() => logger.debug('%s evicted from %s', value, this.constructor.name)) remove(result.path)
.catch(err => logger.error('Cannot remove %s from cache %s.', value, this.constructor.name, { err })) .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 }))
}
} }
}) })
} }

View File

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

View File

@ -20,7 +20,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
const video = await VideoModel.loadByUUIDWithFile(videoUUID) const video = await VideoModel.loadByUUIDWithFile(videoUUID)
if (!video) return undefined 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) return this.loadRemoteFile(videoUUID)
} }
@ -35,7 +35,9 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename) const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)
const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, 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 }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -118,6 +118,15 @@ function buildWhereIdOrUUID (id: number | string) {
return validator.isInt('' + id) ? { id } : { uuid: id } 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 { export {
@ -131,7 +140,8 @@ export {
buildServerIdsFollowedBy, buildServerIdsFollowedBy,
buildTrigramSearchIndex, buildTrigramSearchIndex,
buildWhereIdOrUUID, buildWhereIdOrUUID,
isOutdated isOutdated,
parseAggregateResult
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -75,7 +75,7 @@ export class TagModel extends Model<TagModel> {
type: QueryTypes.SELECT as QueryTypes.SELECT 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)) .then(data => data.map(d => d.name))
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}

View File

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

View File

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

View File

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