Create a dedicated table to track video thumbnails
This commit is contained in:
parent
94565d52bb
commit
e8bafea35b
|
@ -255,8 +255,6 @@ async function startApplication () {
|
||||||
|
|
||||||
// Make server listening
|
// Make server listening
|
||||||
server.listen(port, hostname, () => {
|
server.listen(port, hostname, () => {
|
||||||
logger.debug('CONFIG', { CONFIG })
|
|
||||||
|
|
||||||
logger.info('Server listening on %s:%d', hostname, port)
|
logger.info('Server listening on %s:%d', hostname, port)
|
||||||
logger.info('Web server: %s', WEBSERVER.URL)
|
logger.info('Web server: %s', WEBSERVER.URL)
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
import { videoPlaylistsSortValidator } from '../../middlewares/validators'
|
import { videoPlaylistsSortValidator } from '../../middlewares/validators'
|
||||||
import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
||||||
import { MIMETYPES, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants'
|
import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
||||||
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
||||||
|
@ -28,7 +28,6 @@ import {
|
||||||
} from '../../middlewares/validators/videos/video-playlists'
|
} from '../../middlewares/validators/videos/video-playlists'
|
||||||
import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model'
|
import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model'
|
||||||
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
||||||
import { processImage } from '../../helpers/image-utils'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
|
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
|
||||||
import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
|
import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
|
||||||
|
@ -37,12 +36,12 @@ import { VideoModel } from '../../models/video/video'
|
||||||
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
|
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
|
||||||
import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
|
import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
|
||||||
import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
|
import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
|
||||||
import { copy, pathExists } from 'fs-extra'
|
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../models/account/account'
|
||||||
import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model'
|
import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model'
|
||||||
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'
|
||||||
|
|
||||||
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,14 +173,18 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnailField = req.files['thumbnailfile']
|
const thumbnailField = req.files['thumbnailfile']
|
||||||
if (thumbnailField) {
|
const thumbnailModel = thumbnailField
|
||||||
const thumbnailPhysicalFile = thumbnailField[ 0 ]
|
? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylist)
|
||||||
await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()), THUMBNAILS_SIZE)
|
: 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) {
|
||||||
|
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)
|
||||||
await sendCreateVideoPlaylist(videoPlaylistCreated, t)
|
await sendCreateVideoPlaylist(videoPlaylistCreated, t)
|
||||||
|
@ -206,14 +209,9 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
|
||||||
const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE
|
const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE
|
||||||
|
|
||||||
const thumbnailField = req.files['thumbnailfile']
|
const thumbnailField = req.files['thumbnailfile']
|
||||||
if (thumbnailField) {
|
const thumbnailModel = thumbnailField
|
||||||
const thumbnailPhysicalFile = thumbnailField[ 0 ]
|
? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylistInstance)
|
||||||
await processImage(
|
: undefined
|
||||||
thumbnailPhysicalFile,
|
|
||||||
join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylistInstance.getThumbnailName()),
|
|
||||||
THUMBNAILS_SIZE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
|
@ -241,6 +239,11 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
|
||||||
|
|
||||||
const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
|
const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
|
||||||
|
|
||||||
|
if (thumbnailModel) {
|
||||||
|
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
|
||||||
|
|
||||||
if (isNewPlaylist) {
|
if (isNewPlaylist) {
|
||||||
|
@ -307,15 +310,15 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If the user did not set a thumbnail, automatically take the video thumbnail
|
// If the user did not set a thumbnail, automatically take the video thumbnail
|
||||||
if (playlistElement.position === 1) {
|
if (playlistElement.position === 1 && videoPlaylist.hasThumbnail() === false) {
|
||||||
const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
|
|
||||||
|
|
||||||
if (await pathExists(playlistThumbnailPath) === false) {
|
|
||||||
logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
|
logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
|
||||||
|
|
||||||
const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
|
const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnail().filename)
|
||||||
await copy(videoThumbnailPath, playlistThumbnailPath)
|
const thumbnailModel = await createPlaylistThumbnailFromExisting(inputPath, videoPlaylist, true)
|
||||||
}
|
|
||||||
|
thumbnailModel.videoPlaylistId = videoPlaylist.id
|
||||||
|
|
||||||
|
await thumbnailModel.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
|
logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as magnetUtil from 'magnet-uri'
|
||||||
import 'multer'
|
import 'multer'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
|
||||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
|
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
|
||||||
import { MIMETYPES, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../../../initializers/constants'
|
import { MIMETYPES } from '../../../initializers/constants'
|
||||||
import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
|
import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
|
||||||
import { createReqFiles } from '../../../helpers/express-utils'
|
import { createReqFiles } from '../../../helpers/express-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
|
@ -13,12 +13,10 @@ import { getVideoActivityPubUrl } from '../../../lib/activitypub'
|
||||||
import { TagModel } from '../../../models/video/tag'
|
import { TagModel } from '../../../models/video/tag'
|
||||||
import { VideoImportModel } from '../../../models/video/video-import'
|
import { VideoImportModel } from '../../../models/video/video-import'
|
||||||
import { JobQueue } from '../../../lib/job-queue/job-queue'
|
import { JobQueue } from '../../../lib/job-queue/job-queue'
|
||||||
import { processImage } from '../../../helpers/image-utils'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { isArray } from '../../../helpers/custom-validators/misc'
|
import { isArray } from '../../../helpers/custom-validators/misc'
|
||||||
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
||||||
import { VideoChannelModel } from '../../../models/video/video-channel'
|
import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||||
import { UserModel } from '../../../models/account/user'
|
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
import * as parseTorrent from 'parse-torrent'
|
import * as parseTorrent from 'parse-torrent'
|
||||||
import { getSecureTorrentName } from '../../../helpers/utils'
|
import { getSecureTorrentName } from '../../../helpers/utils'
|
||||||
|
@ -26,6 +24,9 @@ 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 { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
|
||||||
|
import { ThumbnailModel } from '../../../models/video/thumbnail'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('video-imports')
|
const auditLogger = auditLoggerFactory('video-imports')
|
||||||
const videoImportsRouter = express.Router()
|
const videoImportsRouter = express.Router()
|
||||||
|
@ -89,10 +90,10 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
|
||||||
videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
|
videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }, user)
|
const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
|
||||||
|
|
||||||
await processThumbnail(req, video)
|
const thumbnailModel = await processThumbnail(req, video)
|
||||||
await processPreview(req, video)
|
const previewModel = await processPreview(req, video)
|
||||||
|
|
||||||
const tags = body.tags || undefined
|
const tags = body.tags || undefined
|
||||||
const videoImportAttributes = {
|
const videoImportAttributes = {
|
||||||
|
@ -101,7 +102,14 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
|
||||||
state: VideoImportState.PENDING,
|
state: VideoImportState.PENDING,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
}
|
}
|
||||||
const videoImport = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes)
|
const videoImport = await insertIntoDB({
|
||||||
|
video,
|
||||||
|
thumbnailModel,
|
||||||
|
previewModel,
|
||||||
|
videoChannel: res.locals.videoChannel,
|
||||||
|
tags,
|
||||||
|
videoImportAttributes
|
||||||
|
})
|
||||||
|
|
||||||
// Create job to import the video
|
// Create job to import the video
|
||||||
const payload = {
|
const payload = {
|
||||||
|
@ -132,10 +140,10 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
}).end()
|
}).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo, user)
|
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
|
||||||
|
|
||||||
const downloadThumbnail = !await processThumbnail(req, video)
|
const thumbnailModel = await processThumbnail(req, video)
|
||||||
const downloadPreview = !await processPreview(req, video)
|
const previewModel = await processPreview(req, video)
|
||||||
|
|
||||||
const tags = body.tags || youtubeDLInfo.tags
|
const tags = body.tags || youtubeDLInfo.tags
|
||||||
const videoImportAttributes = {
|
const videoImportAttributes = {
|
||||||
|
@ -143,15 +151,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
state: VideoImportState.PENDING,
|
state: VideoImportState.PENDING,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
}
|
}
|
||||||
const videoImport = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes)
|
const videoImport = await insertIntoDB({
|
||||||
|
video: video,
|
||||||
|
thumbnailModel,
|
||||||
|
previewModel,
|
||||||
|
videoChannel: res.locals.videoChannel,
|
||||||
|
tags,
|
||||||
|
videoImportAttributes
|
||||||
|
})
|
||||||
|
|
||||||
// Create job to import the video
|
// Create job to import the video
|
||||||
const payload = {
|
const payload = {
|
||||||
type: 'youtube-dl' as 'youtube-dl',
|
type: 'youtube-dl' as 'youtube-dl',
|
||||||
videoImportId: videoImport.id,
|
videoImportId: videoImport.id,
|
||||||
thumbnailUrl: youtubeDLInfo.thumbnailUrl,
|
thumbnailUrl: youtubeDLInfo.thumbnailUrl,
|
||||||
downloadThumbnail,
|
downloadThumbnail: !thumbnailModel,
|
||||||
downloadPreview
|
downloadPreview: !previewModel
|
||||||
}
|
}
|
||||||
await JobQueue.Instance.createJob({ type: 'video-import', payload })
|
await JobQueue.Instance.createJob({ type: 'video-import', payload })
|
||||||
|
|
||||||
|
@ -160,7 +175,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
return res.json(videoImport.toFormattedJSON()).end()
|
return res.json(videoImport.toFormattedJSON()).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo, user: UserModel) {
|
function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) {
|
||||||
const videoData = {
|
const videoData = {
|
||||||
name: body.name || importData.name || 'Unknown name',
|
name: body.name || importData.name || 'Unknown name',
|
||||||
remote: false,
|
remote: false,
|
||||||
|
@ -189,32 +204,34 @@ async function processThumbnail (req: express.Request, video: VideoModel) {
|
||||||
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
|
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
|
||||||
if (thumbnailField) {
|
if (thumbnailField) {
|
||||||
const thumbnailPhysicalFile = thumbnailField[ 0 ]
|
const thumbnailPhysicalFile = thumbnailField[ 0 ]
|
||||||
await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE)
|
|
||||||
|
|
||||||
return true
|
return createVideoThumbnailFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.THUMBNAIL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processPreview (req: express.Request, video: VideoModel) {
|
async function processPreview (req: express.Request, video: VideoModel) {
|
||||||
const previewField = req.files ? req.files['previewfile'] : undefined
|
const previewField = req.files ? req.files['previewfile'] : undefined
|
||||||
if (previewField) {
|
if (previewField) {
|
||||||
const previewPhysicalFile = previewField[0]
|
const previewPhysicalFile = previewField[0]
|
||||||
await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE)
|
|
||||||
|
|
||||||
return true
|
return createVideoThumbnailFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertIntoDB (
|
function insertIntoDB (parameters: {
|
||||||
video: VideoModel,
|
video: VideoModel,
|
||||||
|
thumbnailModel: ThumbnailModel,
|
||||||
|
previewModel: ThumbnailModel,
|
||||||
videoChannel: VideoChannelModel,
|
videoChannel: VideoChannelModel,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
videoImportAttributes: FilteredModelAttributes<VideoImportModel>
|
videoImportAttributes: FilteredModelAttributes<VideoImportModel>
|
||||||
): Bluebird<VideoImportModel> {
|
}): Bluebird<VideoImportModel> {
|
||||||
|
let { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes } = parameters
|
||||||
|
|
||||||
return sequelizeTypescript.transaction(async t => {
|
return sequelizeTypescript.transaction(async t => {
|
||||||
const sequelizeOptions = { transaction: t }
|
const sequelizeOptions = { transaction: t }
|
||||||
|
|
||||||
|
@ -222,6 +239,15 @@ function insertIntoDB (
|
||||||
const videoCreated = await video.save(sequelizeOptions)
|
const videoCreated = await video.save(sequelizeOptions)
|
||||||
videoCreated.VideoChannel = videoChannel
|
videoCreated.VideoChannel = videoChannel
|
||||||
|
|
||||||
|
if (thumbnailModel) {
|
||||||
|
thumbnailModel.videoId = videoCreated.id
|
||||||
|
videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
|
||||||
|
}
|
||||||
|
if (previewModel) {
|
||||||
|
previewModel.videoId = videoCreated.id
|
||||||
|
videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
|
||||||
|
}
|
||||||
|
|
||||||
await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
|
await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
|
||||||
|
|
||||||
// Set tags to the video
|
// Set tags to the video
|
||||||
|
|
|
@ -2,20 +2,11 @@ import * as express from 'express'
|
||||||
import { extname, join } from 'path'
|
import { extname, join } from 'path'
|
||||||
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
|
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
|
||||||
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
||||||
import { processImage } from '../../../helpers/image-utils'
|
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
|
||||||
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
||||||
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
||||||
import {
|
import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
|
||||||
MIMETYPES,
|
|
||||||
PREVIEWS_SIZE,
|
|
||||||
THUMBNAILS_SIZE,
|
|
||||||
VIDEO_CATEGORIES,
|
|
||||||
VIDEO_LANGUAGES,
|
|
||||||
VIDEO_LICENCES,
|
|
||||||
VIDEO_PRIVACIES
|
|
||||||
} from '../../../initializers/constants'
|
|
||||||
import {
|
import {
|
||||||
changeVideoChannelShare,
|
changeVideoChannelShare,
|
||||||
federateVideoIfNeeded,
|
federateVideoIfNeeded,
|
||||||
|
@ -61,6 +52,8 @@ 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 { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('videos')
|
const auditLogger = auditLoggerFactory('videos')
|
||||||
const videosRouter = express.Router()
|
const videosRouter = express.Router()
|
||||||
|
@ -220,21 +213,15 @@ 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']
|
||||||
if (thumbnailField) {
|
const thumbnailModel = thumbnailField
|
||||||
const thumbnailPhysicalFile = thumbnailField[0]
|
? await createVideoThumbnailFromExisting(thumbnailField[0].path, video, ThumbnailType.THUMBNAIL)
|
||||||
await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE)
|
: await generateVideoThumbnail(video, videoFile, ThumbnailType.THUMBNAIL)
|
||||||
} else {
|
|
||||||
await video.createThumbnail(videoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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']
|
||||||
if (previewField) {
|
const previewModel = previewField
|
||||||
const previewPhysicalFile = previewField[0]
|
? await createVideoThumbnailFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW)
|
||||||
await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE)
|
: await generateVideoThumbnail(video, videoFile, ThumbnailType.PREVIEW)
|
||||||
} else {
|
|
||||||
await video.createPreview(videoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the torrent file
|
// Create the torrent file
|
||||||
await video.createTorrentAndSetInfoHash(videoFile)
|
await video.createTorrentAndSetInfoHash(videoFile)
|
||||||
|
@ -243,6 +230,13 @@ async function addVideo (req: express.Request, res: express.Response) {
|
||||||
const sequelizeOptions = { transaction: t }
|
const sequelizeOptions = { transaction: t }
|
||||||
|
|
||||||
const videoCreated = await video.save(sequelizeOptions)
|
const videoCreated = await video.save(sequelizeOptions)
|
||||||
|
|
||||||
|
thumbnailModel.videoId = videoCreated.id
|
||||||
|
previewModel.videoId = videoCreated.id
|
||||||
|
|
||||||
|
videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
|
||||||
|
videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
@ -313,16 +307,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
||||||
const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
|
const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
|
||||||
|
|
||||||
// Process thumbnail or create it from the video
|
// Process thumbnail or create it from the video
|
||||||
if (req.files && req.files['thumbnailfile']) {
|
const thumbnailModel = req.files && req.files['thumbnailfile']
|
||||||
const thumbnailPhysicalFile = req.files['thumbnailfile'][0]
|
? await createVideoThumbnailFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.THUMBNAIL)
|
||||||
await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoInstance.getThumbnailName()), THUMBNAILS_SIZE)
|
: undefined
|
||||||
}
|
|
||||||
|
|
||||||
// Process preview or create it from the video
|
const previewModel = req.files && req.files['previewfile']
|
||||||
if (req.files && req.files['previewfile']) {
|
? await createVideoThumbnailFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW)
|
||||||
const previewPhysicalFile = req.files['previewfile'][0]
|
: undefined
|
||||||
await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, videoInstance.getPreviewName()), PREVIEWS_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
|
const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
|
||||||
|
@ -355,6 +346,15 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
|
const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
|
||||||
|
|
||||||
|
if (thumbnailModel) {
|
||||||
|
thumbnailModel.videoId = videoInstanceUpdated.id
|
||||||
|
videoInstanceUpdated.addThumbnail(await thumbnailModel.save({ transaction: t }))
|
||||||
|
}
|
||||||
|
if (previewModel) {
|
||||||
|
previewModel.videoId = videoInstanceUpdated.id
|
||||||
|
videoInstanceUpdated.addThumbnail(await previewModel.save({ transaction: t }))
|
||||||
|
}
|
||||||
|
|
||||||
// Video tags update?
|
// Video tags update?
|
||||||
if (videoInfoToUpdate.tags !== undefined) {
|
if (videoInfoToUpdate.tags !== undefined) {
|
||||||
const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t)
|
const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t)
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
import { changeVideoChannelShare } from '../../../lib/activitypub'
|
import { changeVideoChannelShare } from '../../../lib/activitypub'
|
||||||
import { sendUpdateVideo } from '../../../lib/activitypub/send'
|
import { sendUpdateVideo } from '../../../lib/activitypub/send'
|
||||||
import { UserModel } from '../../../models/account/user'
|
|
||||||
|
|
||||||
const ownershipVideoRouter = express.Router()
|
const ownershipVideoRouter = express.Router()
|
||||||
|
|
||||||
|
|
|
@ -164,7 +164,7 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function getPreview (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function getPreview (req: express.Request, res: express.Response) {
|
||||||
const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
|
const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
|
||||||
if (!path) return res.sendStatus(404)
|
if (!path) return res.sendStatus(404)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { logger } from './logger'
|
||||||
async function processImage (
|
async function processImage (
|
||||||
physicalFile: { path: string },
|
physicalFile: { path: string },
|
||||||
destination: string,
|
destination: string,
|
||||||
newSize: { width: number, height: number }
|
newSize: { width: number, height: number },
|
||||||
|
keepOriginal = false
|
||||||
) {
|
) {
|
||||||
if (physicalFile.path === destination) {
|
if (physicalFile.path === destination) {
|
||||||
throw new Error('Sharp needs an input path different that the output path.')
|
throw new Error('Sharp needs an input path different that the output path.')
|
||||||
|
@ -24,7 +25,7 @@ async function processImage (
|
||||||
.resize(newSize.width, newSize.height)
|
.resize(newSize.width, newSize.height)
|
||||||
.toFile(destination)
|
.toFile(destination)
|
||||||
|
|
||||||
await remove(physicalFile.path)
|
if (keepOriginal !== true) await remove(physicalFile.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { UserNotificationSettingModel } from '../models/account/user-notificatio
|
||||||
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||||
import { VideoPlaylistModel } from '../models/video/video-playlist'
|
import { VideoPlaylistModel } from '../models/video/video-playlist'
|
||||||
import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
|
import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
|
||||||
|
import { ThumbnailModel } from '../models/video/thumbnail'
|
||||||
|
|
||||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -105,7 +106,8 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
UserNotificationSettingModel,
|
UserNotificationSettingModel,
|
||||||
VideoStreamingPlaylistModel,
|
VideoStreamingPlaylistModel,
|
||||||
VideoPlaylistModel,
|
VideoPlaylistModel,
|
||||||
VideoPlaylistElementModel
|
VideoPlaylistElementModel,
|
||||||
|
ThumbnailModel
|
||||||
])
|
])
|
||||||
|
|
||||||
// Check extensions exist in the database
|
// Check extensions exist in the database
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
|
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
|
||||||
import { crawlCollectionPage } from './crawl'
|
import { crawlCollectionPage } from './crawl'
|
||||||
import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY, THUMBNAILS_SIZE } from '../../initializers/constants'
|
import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../models/account/account'
|
||||||
import { isArray } from '../../helpers/custom-validators/misc'
|
import { isArray } from '../../helpers/custom-validators/misc'
|
||||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
||||||
import { doRequest, downloadImage } from '../../helpers/requests'
|
import { doRequest } from '../../helpers/requests'
|
||||||
import { checkUrlsSameHost } from '../../helpers/activitypub'
|
import { checkUrlsSameHost } from '../../helpers/activitypub'
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
|
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
|
||||||
|
@ -16,9 +16,8 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
||||||
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
||||||
import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
|
|
||||||
import { CONFIG } from '../../initializers/config'
|
|
||||||
import { sequelizeTypescript } from '../../initializers/database'
|
import { sequelizeTypescript } from '../../initializers/database'
|
||||||
|
import { createPlaylistThumbnailFromUrl } from '../thumbnail'
|
||||||
|
|
||||||
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
|
||||||
|
@ -97,16 +96,20 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Empty playlists generally do not have a miniature, so skip this
|
const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
|
||||||
if (accItems.length !== 0) {
|
|
||||||
|
if (playlistObject.icon) {
|
||||||
try {
|
try {
|
||||||
await generateThumbnailFromUrl(playlist, playlistObject.icon)
|
const thumbnailModel = await createPlaylistThumbnailFromUrl(playlistObject.icon.url, refreshedPlaylist)
|
||||||
|
thumbnailModel.videoPlaylistId = refreshedPlaylist.id
|
||||||
|
|
||||||
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resetVideoPlaylistElements(accItems, playlist)
|
return resetVideoPlaylistElements(accItems, refreshedPlaylist)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
|
async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
|
||||||
|
@ -191,12 +194,6 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: Vide
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityIconObject) {
|
|
||||||
const thumbnailName = playlist.getThumbnailName()
|
|
||||||
|
|
||||||
return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
|
async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
|
||||||
const options = {
|
const options = {
|
||||||
uri: playlistUrl,
|
uri: playlistUrl,
|
||||||
|
|
|
@ -3,11 +3,10 @@ import * as sequelize from 'sequelize'
|
||||||
import * as magnetUtil from 'magnet-uri'
|
import * as magnetUtil from 'magnet-uri'
|
||||||
import * as request from 'request'
|
import * as request from 'request'
|
||||||
import {
|
import {
|
||||||
ActivityIconObject,
|
|
||||||
ActivityPlaylistSegmentHashesObject,
|
ActivityPlaylistSegmentHashesObject,
|
||||||
ActivityPlaylistUrlObject,
|
ActivityPlaylistUrlObject,
|
||||||
ActivityUrlObject,
|
ActivityUrlObject,
|
||||||
ActivityVideoUrlObject,
|
ActivityVideoUrlObject, VideoCreate,
|
||||||
VideoState
|
VideoState
|
||||||
} from '../../../shared/index'
|
} from '../../../shared/index'
|
||||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||||
|
@ -16,8 +15,15 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
|
||||||
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
|
||||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
|
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { doRequest, downloadImage } from '../../helpers/requests'
|
import { doRequest } from '../../helpers/requests'
|
||||||
import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, REMOTE_SCHEME, THUMBNAILS_SIZE } from '../../initializers/constants'
|
import {
|
||||||
|
ACTIVITY_PUB,
|
||||||
|
MIMETYPES,
|
||||||
|
P2P_MEDIA_LOADER_PEER_VERSION,
|
||||||
|
PREVIEWS_SIZE,
|
||||||
|
REMOTE_SCHEME,
|
||||||
|
STATIC_PATHS
|
||||||
|
} from '../../initializers/constants'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { TagModel } from '../../models/video/tag'
|
import { TagModel } from '../../models/video/tag'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
|
@ -43,8 +49,11 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
|
||||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
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 { CONFIG } from '../../initializers/config'
|
|
||||||
import { sequelizeTypescript } from '../../initializers/database'
|
import { sequelizeTypescript } from '../../initializers/database'
|
||||||
|
import { createPlaceholderThumbnail, createVideoThumbnailFromUrl } from '../thumbnail'
|
||||||
|
import { ThumbnailModel } from '../../models/video/thumbnail'
|
||||||
|
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
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
|
||||||
|
@ -100,18 +109,18 @@ async function fetchRemoteVideoDescription (video: VideoModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) {
|
function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) {
|
||||||
const host = video.VideoChannel.Account.Actor.Server.host
|
const url = buildRemoteBaseUrl(video, path)
|
||||||
|
|
||||||
// We need to provide a callback, if no we could have an uncaught exception
|
// We need to provide a callback, if no we could have an uncaught exception
|
||||||
return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => {
|
return request.get(url, err => {
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
|
function buildRemoteBaseUrl (video: VideoModel, path: string) {
|
||||||
const thumbnailName = video.getThumbnailName()
|
const host = video.VideoChannel.Account.Actor.Server.host
|
||||||
|
|
||||||
return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
|
return REMOTE_SCHEME.HTTP + '://' + host + path
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
|
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
|
||||||
|
@ -236,6 +245,14 @@ async function updateVideoFromAP (options: {
|
||||||
const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
|
const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let thumbnailModel: ThumbnailModel
|
||||||
|
|
||||||
|
try {
|
||||||
|
thumbnailModel = await createVideoThumbnailFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.THUMBNAIL)
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
|
||||||
|
}
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
const sequelizeOptions = { transaction: t }
|
const sequelizeOptions = { transaction: t }
|
||||||
|
|
||||||
|
@ -272,6 +289,17 @@ async function updateVideoFromAP (options: {
|
||||||
|
|
||||||
await options.video.save(sequelizeOptions)
|
await options.video.save(sequelizeOptions)
|
||||||
|
|
||||||
|
if (thumbnailModel) {
|
||||||
|
thumbnailModel.videoId = options.video.id
|
||||||
|
options.video.addThumbnail(await thumbnailModel.save({ transaction: t }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: use icon URL instead
|
||||||
|
const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
|
||||||
|
const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
|
||||||
|
|
||||||
|
options.video.addThumbnail(await previewModel.save({ transaction: t }))
|
||||||
|
|
||||||
{
|
{
|
||||||
const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
|
const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
|
||||||
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
|
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
|
||||||
|
@ -347,12 +375,6 @@ async function updateVideoFromAP (options: {
|
||||||
logger.debug('Cannot update the remote video.', { err })
|
logger.debug('Cannot update the remote video.', { err })
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await generateThumbnailFromUrl(options.video, options.videoObject.icon)
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshVideoIfNeeded (options: {
|
async function refreshVideoIfNeeded (options: {
|
||||||
|
@ -412,7 +434,6 @@ export {
|
||||||
getOrCreateVideoAndAccountAndChannel,
|
getOrCreateVideoAndAccountAndChannel,
|
||||||
fetchRemoteVideoStaticFile,
|
fetchRemoteVideoStaticFile,
|
||||||
fetchRemoteVideoDescription,
|
fetchRemoteVideoDescription,
|
||||||
generateThumbnailFromUrl,
|
|
||||||
getOrCreateVideoChannelFromVideoObject
|
getOrCreateVideoChannelFromVideoObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,13 +461,34 @@ function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistS
|
||||||
async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
|
async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
|
||||||
logger.debug('Adding remote video %s.', videoObject.id)
|
logger.debug('Adding remote video %s.', videoObject.id)
|
||||||
|
|
||||||
const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
|
|
||||||
const sequelizeOptions = { transaction: t }
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
let thumbnailModel: ThumbnailModel
|
||||||
|
if (waitThumbnail === true) {
|
||||||
|
thumbnailModel = await promiseThumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
|
||||||
|
const sequelizeOptions = { transaction: t }
|
||||||
|
|
||||||
const videoCreated = await video.save(sequelizeOptions)
|
const videoCreated = await video.save(sequelizeOptions)
|
||||||
|
videoCreated.VideoChannel = channelActor.VideoChannel
|
||||||
|
|
||||||
|
if (thumbnailModel) {
|
||||||
|
thumbnailModel.videoId = videoCreated.id
|
||||||
|
|
||||||
|
videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: use icon URL instead
|
||||||
|
const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
|
||||||
|
const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
|
||||||
|
previewModel.videoId = videoCreated.id
|
||||||
|
|
||||||
|
videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
|
||||||
|
|
||||||
// Process files
|
// Process files
|
||||||
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
|
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
|
||||||
|
@ -476,14 +518,16 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
|
||||||
|
|
||||||
logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
|
logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
|
||||||
|
|
||||||
videoCreated.VideoChannel = channelActor.VideoChannel
|
|
||||||
return videoCreated
|
return videoCreated
|
||||||
})
|
})
|
||||||
|
|
||||||
const p = generateThumbnailFromUrl(videoCreated, videoObject.icon)
|
if (waitThumbnail === false) {
|
||||||
.catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
|
promiseThumbnail.then(thumbnailModel => {
|
||||||
|
thumbnailModel = videoCreated.id
|
||||||
|
|
||||||
if (waitThumbnail === true) await p
|
return thumbnailModel.save()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return videoCreated
|
return videoCreated
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,29 @@
|
||||||
import * as AsyncLRU from 'async-lru'
|
|
||||||
import { createWriteStream, remove } from 'fs-extra'
|
import { createWriteStream, remove } from 'fs-extra'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
import { fetchRemoteVideoStaticFile } from '../activitypub'
|
import { fetchRemoteVideoStaticFile } from '../activitypub'
|
||||||
|
import * as memoizee from 'memoizee'
|
||||||
|
|
||||||
export abstract class AbstractVideoStaticFileCache <T> {
|
export abstract class AbstractVideoStaticFileCache <T> {
|
||||||
|
|
||||||
protected lru
|
getFilePath: (params: T) => Promise<string>
|
||||||
|
|
||||||
abstract getFilePath (params: T): Promise<string>
|
abstract getFilePathImpl (params: T): Promise<string>
|
||||||
|
|
||||||
// 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<string>
|
||||||
|
|
||||||
init (max: number, maxAge: number) {
|
init (max: number, maxAge: number) {
|
||||||
this.lru = new AsyncLRU({
|
this.getFilePath = memoizee(this.getFilePathImpl, {
|
||||||
max,
|
|
||||||
maxAge,
|
maxAge,
|
||||||
load: (key, cb) => {
|
max,
|
||||||
this.loadRemoteFile(key)
|
promise: true,
|
||||||
.then(res => cb(null, res))
|
dispose: (value: string) => {
|
||||||
.catch(err => cb(err))
|
remove(value)
|
||||||
|
.then(() => logger.debug('%s evicted from %s', value, this.constructor.name))
|
||||||
|
.catch(err => logger.error('Cannot remove %s from cache %s.', value, this.constructor.name, { err }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.lru.on('evict', (obj: { key: string, value: string }) => {
|
|
||||||
remove(obj.value)
|
|
||||||
.then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadFromLRU (key: string) {
|
|
||||||
return new Promise<string>((res, rej) => {
|
|
||||||
this.lru.get(key, (err, value) => {
|
|
||||||
err ? rej(err) : res(value)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) {
|
protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) {
|
||||||
|
|
|
@ -20,14 +20,14 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
|
||||||
return this.instance || (this.instance = new this())
|
return this.instance || (this.instance = new this())
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFilePath (params: GetPathParam) {
|
async getFilePathImpl (params: 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 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.loadFromLRU(key)
|
return this.loadRemoteFile(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadRemoteFile (key: string) {
|
protected async loadRemoteFile (key: string) {
|
||||||
|
@ -42,6 +42,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
|
||||||
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
|
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
|
||||||
if (!video) return undefined
|
if (!video) return undefined
|
||||||
|
|
||||||
|
// FIXME: use URL
|
||||||
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())
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,13 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
|
||||||
return this.instance || (this.instance = new this())
|
return this.instance || (this.instance = new this())
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFilePath (videoUUID: string) {
|
async getFilePathImpl (videoUUID: 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.getPreviewName())
|
if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename)
|
||||||
|
|
||||||
return this.loadFromLRU(videoUUID)
|
return this.loadRemoteFile(videoUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadRemoteFile (key: string) {
|
protected async loadRemoteFile (key: string) {
|
||||||
|
@ -31,8 +31,9 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
|
||||||
|
|
||||||
if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
|
if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
|
||||||
|
|
||||||
const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
|
// FIXME: use URL
|
||||||
const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreviewName())
|
const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)
|
||||||
|
const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
|
||||||
|
|
||||||
return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
|
return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { VideoImportState } from '../../../../shared/models/videos'
|
||||||
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
||||||
import { extname, join } from 'path'
|
import { extname, join } from 'path'
|
||||||
import { VideoFileModel } from '../../../models/video/video-file'
|
import { VideoFileModel } from '../../../models/video/video-file'
|
||||||
import { PREVIEWS_SIZE, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
|
import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
|
||||||
import { downloadImage } from '../../../helpers/requests'
|
|
||||||
import { VideoState } from '../../../../shared'
|
import { VideoState } from '../../../../shared'
|
||||||
import { JobQueue } from '../index'
|
import { JobQueue } from '../index'
|
||||||
import { federateVideoIfNeeded } from '../../activitypub'
|
import { federateVideoIfNeeded } from '../../activitypub'
|
||||||
|
@ -18,6 +17,9 @@ import { move, remove, stat } from 'fs-extra'
|
||||||
import { Notifier } from '../../notifier'
|
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 { createVideoThumbnailFromUrl, generateVideoThumbnail } from '../../thumbnail'
|
||||||
|
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
|
||||||
|
|
||||||
type VideoImportYoutubeDLPayload = {
|
type VideoImportYoutubeDLPayload = {
|
||||||
type: 'youtube-dl'
|
type: 'youtube-dl'
|
||||||
|
@ -146,25 +148,19 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
|
||||||
tempVideoPath = null // This path is not used anymore
|
tempVideoPath = null // This path is not used anymore
|
||||||
|
|
||||||
// Process thumbnail
|
// Process thumbnail
|
||||||
if (options.downloadThumbnail) {
|
let thumbnailModel: ThumbnailModel
|
||||||
if (options.thumbnailUrl) {
|
if (options.downloadThumbnail && options.thumbnailUrl) {
|
||||||
await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE)
|
thumbnailModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.THUMBNAIL)
|
||||||
} else {
|
} else if (options.generateThumbnail || options.downloadThumbnail) {
|
||||||
await videoImport.Video.createThumbnail(videoFile)
|
thumbnailModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.THUMBNAIL)
|
||||||
}
|
|
||||||
} else if (options.generateThumbnail) {
|
|
||||||
await videoImport.Video.createThumbnail(videoFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process preview
|
// Process preview
|
||||||
if (options.downloadPreview) {
|
let previewModel: ThumbnailModel
|
||||||
if (options.thumbnailUrl) {
|
if (options.downloadPreview && options.thumbnailUrl) {
|
||||||
await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE)
|
previewModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
|
||||||
} else {
|
} else if (options.generatePreview || options.downloadPreview) {
|
||||||
await videoImport.Video.createPreview(videoFile)
|
previewModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
|
||||||
}
|
|
||||||
} else if (options.generatePreview) {
|
|
||||||
await videoImport.Video.createPreview(videoFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create torrent
|
// Create torrent
|
||||||
|
@ -184,6 +180,15 @@ 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) {
|
||||||
|
thumbnailModel.videoId = video.id
|
||||||
|
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)
|
||||||
await federateVideoIfNeeded(videoForFederation, true, t)
|
await federateVideoIfNeeded(videoForFederation, true, t)
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { VideoFileModel } from '../models/video/video-file'
|
||||||
|
import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
|
||||||
|
import { CONFIG } from '../initializers/config'
|
||||||
|
import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
|
||||||
|
import { VideoModel } from '../models/video/video'
|
||||||
|
import { ThumbnailModel } from '../models/video/thumbnail'
|
||||||
|
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
|
||||||
|
import { processImage } from '../helpers/image-utils'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { downloadImage } from '../helpers/requests'
|
||||||
|
import { VideoPlaylistModel } from '../models/video/video-playlist'
|
||||||
|
|
||||||
|
type ImageSize = { height: number, width: number }
|
||||||
|
|
||||||
|
function createPlaylistThumbnailFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) {
|
||||||
|
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
|
||||||
|
const type = ThumbnailType.THUMBNAIL
|
||||||
|
|
||||||
|
const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }, keepOriginal)
|
||||||
|
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPlaylistThumbnailFromUrl (url: string, playlist: VideoPlaylistModel, size?: ImageSize) {
|
||||||
|
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
|
||||||
|
const type = ThumbnailType.THUMBNAIL
|
||||||
|
|
||||||
|
const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height })
|
||||||
|
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoThumbnailFromUrl (url: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
|
||||||
|
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
||||||
|
const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height })
|
||||||
|
|
||||||
|
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoThumbnailFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
|
||||||
|
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
||||||
|
const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height })
|
||||||
|
|
||||||
|
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateVideoThumbnail (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
|
||||||
|
const input = video.getVideoFilePath(videoFile)
|
||||||
|
|
||||||
|
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type)
|
||||||
|
const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width })
|
||||||
|
|
||||||
|
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPlaceholderThumbnail (url: string, video: VideoModel, type: ThumbnailType, size: ImageSize) {
|
||||||
|
const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
||||||
|
|
||||||
|
const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
|
||||||
|
|
||||||
|
thumbnail.filename = filename
|
||||||
|
thumbnail.height = height
|
||||||
|
thumbnail.width = width
|
||||||
|
thumbnail.type = type
|
||||||
|
thumbnail.url = url
|
||||||
|
|
||||||
|
return thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
generateVideoThumbnail,
|
||||||
|
createVideoThumbnailFromUrl,
|
||||||
|
createVideoThumbnailFromExisting,
|
||||||
|
createPlaceholderThumbnail,
|
||||||
|
createPlaylistThumbnailFromUrl,
|
||||||
|
createPlaylistThumbnailFromExisting
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) {
|
||||||
|
const filename = playlist.generateThumbnailName()
|
||||||
|
const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
basePath,
|
||||||
|
existingThumbnail: playlist.Thumbnail,
|
||||||
|
outputPath: join(basePath, filename),
|
||||||
|
height: size ? size.height : THUMBNAILS_SIZE.height,
|
||||||
|
width: size ? size.width : THUMBNAILS_SIZE.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?: ImageSize) {
|
||||||
|
const existingThumbnail = Array.isArray(video.Thumbnails)
|
||||||
|
? video.Thumbnails.find(t => t.type === type)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (type === ThumbnailType.THUMBNAIL) {
|
||||||
|
const filename = video.generateThumbnailName()
|
||||||
|
const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
basePath,
|
||||||
|
existingThumbnail,
|
||||||
|
outputPath: join(basePath, filename),
|
||||||
|
height: size ? size.height : THUMBNAILS_SIZE.height,
|
||||||
|
width: size ? size.width : THUMBNAILS_SIZE.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ThumbnailType.PREVIEW) {
|
||||||
|
const filename = video.generatePreviewName()
|
||||||
|
const basePath = CONFIG.STORAGE.PREVIEWS_DIR
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
basePath,
|
||||||
|
existingThumbnail,
|
||||||
|
outputPath: join(basePath, filename),
|
||||||
|
height: size ? size.height : PREVIEWS_SIZE.height,
|
||||||
|
width: size ? size.width : PREVIEWS_SIZE.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createThumbnailFromFunction (parameters: {
|
||||||
|
thumbnailCreator: () => Promise<any>,
|
||||||
|
filename: string,
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
type: ThumbnailType,
|
||||||
|
url?: string,
|
||||||
|
existingThumbnail?: ThumbnailModel
|
||||||
|
}) {
|
||||||
|
const { thumbnailCreator, filename, width, height, type, existingThumbnail, url = null } = parameters
|
||||||
|
|
||||||
|
const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
|
||||||
|
|
||||||
|
thumbnail.filename = filename
|
||||||
|
thumbnail.height = height
|
||||||
|
thumbnail.width = width
|
||||||
|
thumbnail.type = type
|
||||||
|
thumbnail.url = url
|
||||||
|
|
||||||
|
await thumbnailCreator()
|
||||||
|
|
||||||
|
return thumbnail
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { join } from 'path'
|
||||||
|
import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
|
import { STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { remove } from 'fs-extra'
|
||||||
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
import { VideoModel } from './video'
|
||||||
|
import { VideoPlaylistModel } from './video-playlist'
|
||||||
|
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 'thumbnail',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: [ 'videoId' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'videoPlaylistId' ],
|
||||||
|
unique: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ThumbnailModel extends Model<ThumbnailModel> {
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Column
|
||||||
|
filename: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Column
|
||||||
|
height: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Column
|
||||||
|
width: number
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Column
|
||||||
|
type: ThumbnailType
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
url: string
|
||||||
|
|
||||||
|
@ForeignKey(() => VideoModel)
|
||||||
|
@Column
|
||||||
|
videoId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => VideoModel, {
|
||||||
|
foreignKey: {
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
Video: VideoModel
|
||||||
|
|
||||||
|
@ForeignKey(() => VideoPlaylistModel)
|
||||||
|
@Column
|
||||||
|
videoPlaylistId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => VideoPlaylistModel, {
|
||||||
|
foreignKey: {
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
VideoPlaylist: VideoPlaylistModel
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
|
||||||
|
[ThumbnailType.THUMBNAIL]: {
|
||||||
|
label: 'thumbnail',
|
||||||
|
directory: CONFIG.STORAGE.THUMBNAILS_DIR,
|
||||||
|
staticPath: STATIC_PATHS.THUMBNAILS
|
||||||
|
},
|
||||||
|
[ThumbnailType.PREVIEW]: {
|
||||||
|
label: 'preview',
|
||||||
|
directory: CONFIG.STORAGE.PREVIEWS_DIR,
|
||||||
|
staticPath: STATIC_PATHS.PREVIEWS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterDestroy
|
||||||
|
static removeFilesAndSendDelete (instance: ThumbnailModel) {
|
||||||
|
logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename)
|
||||||
|
|
||||||
|
// Don't block the transaction
|
||||||
|
instance.removeThumbnail()
|
||||||
|
.catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateDefaultPreviewName (videoUUID: string) {
|
||||||
|
return videoUUID + '.jpg'
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl () {
|
||||||
|
if (this.url) return this.url
|
||||||
|
|
||||||
|
const staticPath = ThumbnailModel.types[this.type].staticPath
|
||||||
|
return WEBSERVER.URL + staticPath + this.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
removeThumbnail () {
|
||||||
|
const directory = ThumbnailModel.types[this.type].directory
|
||||||
|
const thumbnailPath = join(directory, this.filename)
|
||||||
|
|
||||||
|
return remove(thumbnailPath)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import {
|
||||||
ActivityUrlObject,
|
ActivityUrlObject,
|
||||||
VideoTorrentObject
|
VideoTorrentObject
|
||||||
} from '../../../shared/models/activitypub/objects'
|
} from '../../../shared/models/activitypub/objects'
|
||||||
import { MIMETYPES, THUMBNAILS_SIZE, WEBSERVER } from '../../initializers/constants'
|
import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
|
||||||
import { VideoCaptionModel } from './video-caption'
|
import { VideoCaptionModel } from './video-caption'
|
||||||
import {
|
import {
|
||||||
getVideoCommentsActivityPubUrl,
|
getVideoCommentsActivityPubUrl,
|
||||||
|
@ -326,10 +326,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
|
||||||
subtitleLanguage,
|
subtitleLanguage,
|
||||||
icon: {
|
icon: {
|
||||||
type: 'Image',
|
type: 'Image',
|
||||||
url: video.getThumbnailUrl(baseUrlHttp),
|
url: video.getThumbnail().getUrl(),
|
||||||
mediaType: 'image/jpeg',
|
mediaType: 'image/jpeg',
|
||||||
width: THUMBNAILS_SIZE.width,
|
width: video.getThumbnail().width,
|
||||||
height: THUMBNAILS_SIZE.height
|
height: video.getThumbnail().height
|
||||||
},
|
},
|
||||||
url,
|
url,
|
||||||
likes: getVideoLikesActivityPubUrl(video),
|
likes: getVideoLikesActivityPubUrl(video),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BeforeDestroy,
|
|
||||||
BelongsTo,
|
BelongsTo,
|
||||||
Column,
|
Column,
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
|
@ -8,6 +7,7 @@ import {
|
||||||
Default,
|
Default,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
HasMany,
|
HasMany,
|
||||||
|
HasOne,
|
||||||
Is,
|
Is,
|
||||||
IsUUID,
|
IsUUID,
|
||||||
Model,
|
Model,
|
||||||
|
@ -40,16 +40,16 @@ import { join } from 'path'
|
||||||
import { VideoPlaylistElementModel } from './video-playlist-element'
|
import { VideoPlaylistElementModel } from './video-playlist-element'
|
||||||
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
|
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
|
||||||
import { activityPubCollectionPagination } from '../../helpers/activitypub'
|
import { activityPubCollectionPagination } from '../../helpers/activitypub'
|
||||||
import { remove } from 'fs-extra'
|
|
||||||
import { logger } from '../../helpers/logger'
|
|
||||||
import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
|
import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { ThumbnailModel } from './thumbnail'
|
||||||
|
import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
|
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
|
||||||
WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
|
WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
|
||||||
WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
|
WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
|
WITH_THUMBNAIL = 'WITH_THUMBNAIL',
|
||||||
WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
|
WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +62,14 @@ type AvailableForListOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scopes({
|
@Scopes({
|
||||||
|
[ ScopeNames.WITH_THUMBNAIL ]: {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => ThumbnailModel,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
[ ScopeNames.WITH_VIDEOS_LENGTH ]: {
|
[ ScopeNames.WITH_VIDEOS_LENGTH ]: {
|
||||||
attributes: {
|
attributes: {
|
||||||
include: [
|
include: [
|
||||||
|
@ -256,12 +264,15 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
})
|
})
|
||||||
VideoPlaylistElements: VideoPlaylistElementModel[]
|
VideoPlaylistElements: VideoPlaylistElementModel[]
|
||||||
|
|
||||||
@BeforeDestroy
|
@HasOne(() => ThumbnailModel, {
|
||||||
static async removeFiles (instance: VideoPlaylistModel) {
|
foreignKey: {
|
||||||
logger.info('Removing files of video playlist %s.', instance.url)
|
name: 'videoPlaylistId',
|
||||||
|
allowNull: true
|
||||||
return instance.removeThumbnail()
|
},
|
||||||
}
|
onDelete: 'CASCADE',
|
||||||
|
hooks: true
|
||||||
|
})
|
||||||
|
Thumbnail: ThumbnailModel
|
||||||
|
|
||||||
static listForApi (options: {
|
static listForApi (options: {
|
||||||
followerActorId: number
|
followerActorId: number
|
||||||
|
@ -292,7 +303,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
} as AvailableForListOptions
|
} as AvailableForListOptions
|
||||||
]
|
]
|
||||||
} as any, // FIXME: typings
|
} as any, // FIXME: typings
|
||||||
ScopeNames.WITH_VIDEOS_LENGTH
|
ScopeNames.WITH_VIDEOS_LENGTH,
|
||||||
|
ScopeNames.WITH_THUMBNAIL
|
||||||
]
|
]
|
||||||
|
|
||||||
return VideoPlaylistModel
|
return VideoPlaylistModel
|
||||||
|
@ -365,7 +377,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoPlaylistModel
|
return VideoPlaylistModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
|
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
|
||||||
.findOne(query)
|
.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,7 +390,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoPlaylistModel
|
return VideoPlaylistModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ])
|
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
|
||||||
.findOne(query)
|
.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -389,7 +401,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
|
return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
|
static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
|
||||||
|
@ -411,24 +423,34 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
|
return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailName () {
|
setThumbnail (thumbnail: ThumbnailModel) {
|
||||||
|
this.Thumbnail = thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnail () {
|
||||||
|
return this.Thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
hasThumbnail () {
|
||||||
|
return !!this.Thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
generateThumbnailName () {
|
||||||
const extension = '.jpg'
|
const extension = '.jpg'
|
||||||
|
|
||||||
return 'playlist-' + this.uuid + extension
|
return 'playlist-' + this.uuid + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailUrl () {
|
getThumbnailUrl () {
|
||||||
return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
|
if (!this.hasThumbnail()) return null
|
||||||
|
|
||||||
|
return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailStaticPath () {
|
getThumbnailStaticPath () {
|
||||||
return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
|
if (!this.hasThumbnail()) return null
|
||||||
}
|
|
||||||
|
|
||||||
removeThumbnail () {
|
return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename)
|
||||||
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
|
|
||||||
return remove(thumbnailPath)
|
|
||||||
.catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAsRefreshed () {
|
setAsRefreshed () {
|
||||||
|
@ -482,6 +504,17 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
|
return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let icon: ActivityIconObject
|
||||||
|
if (this.hasThumbnail()) {
|
||||||
|
icon = {
|
||||||
|
type: 'Image' as 'Image',
|
||||||
|
url: this.getThumbnailUrl(),
|
||||||
|
mediaType: 'image/jpeg' as 'image/jpeg',
|
||||||
|
width: THUMBNAILS_SIZE.width,
|
||||||
|
height: THUMBNAILS_SIZE.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return activityPubCollectionPagination(this.url, handler, page)
|
return activityPubCollectionPagination(this.url, handler, page)
|
||||||
.then(o => {
|
.then(o => {
|
||||||
return Object.assign(o, {
|
return Object.assign(o, {
|
||||||
|
@ -492,13 +525,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
published: this.createdAt.toISOString(),
|
published: this.createdAt.toISOString(),
|
||||||
updated: this.updatedAt.toISOString(),
|
updated: this.updatedAt.toISOString(),
|
||||||
attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
|
attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
|
||||||
icon: {
|
icon
|
||||||
type: 'Image' as 'Image',
|
|
||||||
url: this.getThumbnailUrl(),
|
|
||||||
mediaType: 'image/jpeg' as 'image/jpeg',
|
|
||||||
width: THUMBNAILS_SIZE.width,
|
|
||||||
height: THUMBNAILS_SIZE.height
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,8 @@ import { VideoImportModel } from './video-import'
|
||||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||||
import { VideoPlaylistElementModel } from './video-playlist-element'
|
import { VideoPlaylistElementModel } from './video-playlist-element'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
import { ThumbnailModel } from './thumbnail'
|
||||||
|
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
|
||||||
|
|
||||||
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
||||||
const indexes: Sequelize.DefineIndexesOptions[] = [
|
const indexes: Sequelize.DefineIndexesOptions[] = [
|
||||||
|
@ -181,7 +183,8 @@ export enum ScopeNames {
|
||||||
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
|
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
|
||||||
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
|
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
|
||||||
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
|
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
|
||||||
WITH_USER_ID = 'WITH_USER_ID'
|
WITH_USER_ID = 'WITH_USER_ID',
|
||||||
|
WITH_THUMBNAILS = 'WITH_THUMBNAILS'
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForAPIOptions = {
|
type ForAPIOptions = {
|
||||||
|
@ -473,6 +476,14 @@ type AvailableForListIDsOptions = {
|
||||||
|
|
||||||
return query
|
return query
|
||||||
},
|
},
|
||||||
|
[ ScopeNames.WITH_THUMBNAILS ]: {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => ThumbnailModel,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
[ ScopeNames.WITH_USER_ID ]: {
|
[ ScopeNames.WITH_USER_ID ]: {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -771,6 +782,16 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
})
|
})
|
||||||
Tags: TagModel[]
|
Tags: TagModel[]
|
||||||
|
|
||||||
|
@HasMany(() => ThumbnailModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'videoId',
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
hooks: true,
|
||||||
|
onDelete: 'cascade'
|
||||||
|
})
|
||||||
|
Thumbnails: ThumbnailModel[]
|
||||||
|
|
||||||
@HasMany(() => VideoPlaylistElementModel, {
|
@HasMany(() => VideoPlaylistElementModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'videoId',
|
name: 'videoId',
|
||||||
|
@ -920,15 +941,11 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
|
|
||||||
logger.info('Removing files of video %s.', instance.url)
|
logger.info('Removing files of video %s.', instance.url)
|
||||||
|
|
||||||
tasks.push(instance.removeThumbnail())
|
|
||||||
|
|
||||||
if (instance.isOwned()) {
|
if (instance.isOwned()) {
|
||||||
if (!Array.isArray(instance.VideoFiles)) {
|
if (!Array.isArray(instance.VideoFiles)) {
|
||||||
instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
|
instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.push(instance.removePreview())
|
|
||||||
|
|
||||||
// Remove physical files and torrents
|
// Remove physical files and torrents
|
||||||
instance.VideoFiles.forEach(file => {
|
instance.VideoFiles.forEach(file => {
|
||||||
tasks.push(instance.removeFile(file))
|
tasks.push(instance.removeFile(file))
|
||||||
|
@ -955,7 +972,11 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query)
|
return VideoModel.scope([
|
||||||
|
ScopeNames.WITH_FILES,
|
||||||
|
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
||||||
|
ScopeNames.WITH_THUMBNAILS
|
||||||
|
]).findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
|
static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
|
||||||
|
@ -1048,7 +1069,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
|
|
||||||
return Bluebird.all([
|
return Bluebird.all([
|
||||||
// FIXME: typing issue
|
// FIXME: typing issue
|
||||||
VideoModel.findAll(query as any),
|
VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any),
|
||||||
VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
|
VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
|
||||||
]).then(([ rows, totals ]) => {
|
]).then(([ rows, totals ]) => {
|
||||||
// totals: totalVideos + totalVideoShares
|
// totals: totalVideos + totalVideoShares
|
||||||
|
@ -1102,7 +1123,9 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
|
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS)
|
||||||
|
.findAndCountAll(query)
|
||||||
|
.then(({ rows, count }) => {
|
||||||
return {
|
return {
|
||||||
data: rows,
|
data: rows,
|
||||||
total: count
|
total: count
|
||||||
|
@ -1296,7 +1319,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
transaction: t
|
transaction: t
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.findOne(options)
|
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
|
static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
|
||||||
|
@ -1306,7 +1329,11 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
transaction: t
|
transaction: t
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options)
|
return VideoModel.scope([
|
||||||
|
ScopeNames.WITH_BLACKLISTED,
|
||||||
|
ScopeNames.WITH_USER_ID,
|
||||||
|
ScopeNames.WITH_THUMBNAILS
|
||||||
|
]).findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
|
static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
|
||||||
|
@ -1318,12 +1345,15 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
transaction: t
|
transaction: t
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.findOne(options)
|
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
|
static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
|
||||||
return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ])
|
return VideoModel.scope([
|
||||||
.findByPk(id, { transaction: t, logging })
|
ScopeNames.WITH_FILES,
|
||||||
|
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
||||||
|
ScopeNames.WITH_THUMBNAILS
|
||||||
|
]).findByPk(id, { transaction: t, logging })
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByUUIDWithFile (uuid: string) {
|
static loadByUUIDWithFile (uuid: string) {
|
||||||
|
@ -1333,7 +1363,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.findOne(options)
|
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
|
static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
|
||||||
|
@ -1344,7 +1374,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
transaction
|
transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.findOne(query)
|
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
|
static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
|
||||||
|
@ -1358,7 +1388,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return VideoModel.scope([
|
return VideoModel.scope([
|
||||||
ScopeNames.WITH_ACCOUNT_DETAILS,
|
ScopeNames.WITH_ACCOUNT_DETAILS,
|
||||||
ScopeNames.WITH_FILES,
|
ScopeNames.WITH_FILES,
|
||||||
ScopeNames.WITH_STREAMING_PLAYLISTS
|
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
||||||
|
ScopeNames.WITH_THUMBNAILS
|
||||||
]).findOne(query)
|
]).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1377,7 +1408,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
ScopeNames.WITH_ACCOUNT_DETAILS,
|
ScopeNames.WITH_ACCOUNT_DETAILS,
|
||||||
ScopeNames.WITH_SCHEDULED_UPDATE,
|
ScopeNames.WITH_SCHEDULED_UPDATE,
|
||||||
ScopeNames.WITH_FILES,
|
ScopeNames.WITH_FILES,
|
||||||
ScopeNames.WITH_STREAMING_PLAYLISTS
|
ScopeNames.WITH_STREAMING_PLAYLISTS,
|
||||||
|
ScopeNames.WITH_THUMBNAILS
|
||||||
]
|
]
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
@ -1403,6 +1435,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
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,
|
||||||
{ method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
|
{ method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
|
||||||
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
|
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
|
||||||
]
|
]
|
||||||
|
@ -1555,7 +1588,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: typing
|
// FIXME: typing
|
||||||
const apiScope: any[] = []
|
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 ] })
|
||||||
|
@ -1611,18 +1644,37 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return maxBy(this.VideoFiles, file => file.resolution)
|
return maxBy(this.VideoFiles, file => file.resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addThumbnail (thumbnail: ThumbnailModel) {
|
||||||
|
if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
|
||||||
|
|
||||||
|
// Already have this thumbnail, skip
|
||||||
|
if (this.Thumbnails.find(t => t.id === thumbnail.id)) return
|
||||||
|
|
||||||
|
this.Thumbnails.push(thumbnail)
|
||||||
|
}
|
||||||
|
|
||||||
getVideoFilename (videoFile: VideoFileModel) {
|
getVideoFilename (videoFile: VideoFileModel) {
|
||||||
return this.uuid + '-' + videoFile.resolution + videoFile.extname
|
return this.uuid + '-' + videoFile.resolution + videoFile.extname
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailName () {
|
generateThumbnailName () {
|
||||||
const extension = '.jpg'
|
return this.uuid + '.jpg'
|
||||||
return this.uuid + extension
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviewName () {
|
getThumbnail () {
|
||||||
const extension = '.jpg'
|
if (Array.isArray(this.Thumbnails) === false) return undefined
|
||||||
return this.uuid + extension
|
|
||||||
|
return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL)
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePreviewName () {
|
||||||
|
return this.uuid + '.jpg'
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreview () {
|
||||||
|
if (Array.isArray(this.Thumbnails) === false) return undefined
|
||||||
|
|
||||||
|
return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
|
||||||
}
|
}
|
||||||
|
|
||||||
getTorrentFileName (videoFile: VideoFileModel) {
|
getTorrentFileName (videoFile: VideoFileModel) {
|
||||||
|
@ -1634,24 +1686,6 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return this.remote === false
|
return this.remote === false
|
||||||
}
|
}
|
||||||
|
|
||||||
createPreview (videoFile: VideoFileModel) {
|
|
||||||
return generateImageFromVideoFile(
|
|
||||||
this.getVideoFilePath(videoFile),
|
|
||||||
CONFIG.STORAGE.PREVIEWS_DIR,
|
|
||||||
this.getPreviewName(),
|
|
||||||
PREVIEWS_SIZE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
createThumbnail (videoFile: VideoFileModel) {
|
|
||||||
return generateImageFromVideoFile(
|
|
||||||
this.getVideoFilePath(videoFile),
|
|
||||||
CONFIG.STORAGE.THUMBNAILS_DIR,
|
|
||||||
this.getThumbnailName(),
|
|
||||||
THUMBNAILS_SIZE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getTorrentFilePath (videoFile: VideoFileModel) {
|
getTorrentFilePath (videoFile: VideoFileModel) {
|
||||||
return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
|
return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
|
||||||
}
|
}
|
||||||
|
@ -1692,11 +1726,18 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailStaticPath () {
|
getThumbnailStaticPath () {
|
||||||
return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
|
const thumbnail = this.getThumbnail()
|
||||||
|
if (!thumbnail) return null
|
||||||
|
|
||||||
|
return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviewStaticPath () {
|
getPreviewStaticPath () {
|
||||||
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
|
const preview = this.getPreview()
|
||||||
|
if (!preview) return null
|
||||||
|
|
||||||
|
// We use a local cache, so specify our cache endpoint instead of potential remote URL
|
||||||
|
return join(STATIC_PATHS.PREVIEWS, preview.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
|
toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
|
||||||
|
@ -1732,18 +1773,6 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return `/api/${API_VERSION}/videos/${this.uuid}/description`
|
return `/api/${API_VERSION}/videos/${this.uuid}/description`
|
||||||
}
|
}
|
||||||
|
|
||||||
removeThumbnail () {
|
|
||||||
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
|
|
||||||
return remove(thumbnailPath)
|
|
||||||
.catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
|
|
||||||
}
|
|
||||||
|
|
||||||
removePreview () {
|
|
||||||
const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
|
|
||||||
return remove(previewPath)
|
|
||||||
.catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFile (videoFile: VideoFileModel, isRedundancy = false) {
|
removeFile (videoFile: VideoFileModel, isRedundancy = false) {
|
||||||
const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
|
const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
|
||||||
|
|
||||||
|
@ -1816,10 +1845,6 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
|
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
getThumbnailUrl (baseUrlHttp: string) {
|
|
||||||
return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
|
|
||||||
}
|
|
||||||
|
|
||||||
getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
|
getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
|
||||||
return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
|
return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface PlaylistObject {
|
||||||
totalItems: number
|
totalItems: number
|
||||||
attributedTo: string[]
|
attributedTo: string[]
|
||||||
|
|
||||||
icon: ActivityIconObject
|
icon?: ActivityIconObject
|
||||||
|
|
||||||
published: string
|
published: string
|
||||||
updated: string
|
updated: string
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum ThumbnailType {
|
||||||
|
THUMBNAIL = 1,
|
||||||
|
PREVIEW = 2
|
||||||
|
}
|
Loading…
Reference in New Issue