// Make server listening
server.listen(port, hostname, () => {
- logger.debug('CONFIG', { CONFIG })
-
logger.info('Server listening on %s:%d', hostname, port)
logger.info('Web server: %s', WEBSERVER.URL)
})
} from '../../middlewares'
import { videoPlaylistsSortValidator } from '../../middlewares/validators'
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 { resetSequelizeInstance } from '../../helpers/database-utils'
import { VideoPlaylistModel } from '../../models/video/video-playlist'
} from '../../middlewares/validators/videos/video-playlists'
import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model'
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { processImage } from '../../helpers/image-utils'
import { join } from 'path'
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
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 { copy, pathExists } from 'fs-extra'
import { AccountModel } from '../../models/account/account'
import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model'
import { JobQueue } from '../../lib/job-queue'
import { CONFIG } from '../../initializers/config'
import { sequelizeTypescript } from '../../initializers/database'
+import { createPlaylistThumbnailFromExisting } from '../../lib/thumbnail'
const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
}
const thumbnailField = req.files['thumbnailfile']
- if (thumbnailField) {
- const thumbnailPhysicalFile = thumbnailField[ 0 ]
- await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()), THUMBNAILS_SIZE)
- }
+ const thumbnailModel = thumbnailField
+ ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylist)
+ : undefined
const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
+ if (thumbnailModel) {
+ thumbnailModel.videoPlaylistId = videoPlaylistCreated.id
+ videoPlaylistCreated.setThumbnail(await thumbnailModel.save({ transaction: t }))
+ }
+
// We need more attributes for the federation
videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
await sendCreateVideoPlaylist(videoPlaylistCreated, t)
const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE
const thumbnailField = req.files['thumbnailfile']
- if (thumbnailField) {
- const thumbnailPhysicalFile = thumbnailField[ 0 ]
- await processImage(
- thumbnailPhysicalFile,
- join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylistInstance.getThumbnailName()),
- THUMBNAILS_SIZE
- )
- }
+ const thumbnailModel = thumbnailField
+ ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylistInstance)
+ : undefined
try {
await sequelizeTypescript.transaction(async t => {
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
if (isNewPlaylist) {
})
// If the user did not set a thumbnail, automatically take the video thumbnail
- if (playlistElement.position === 1) {
- const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
+ if (playlistElement.position === 1 && videoPlaylist.hasThumbnail() === false) {
+ logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
- if (await pathExists(playlistThumbnailPath) === false) {
- logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
+ const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnail().filename)
+ const thumbnailModel = await createPlaylistThumbnailFromExisting(inputPath, videoPlaylist, true)
- const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
- await copy(videoThumbnailPath, playlistThumbnailPath)
- }
+ thumbnailModel.videoPlaylistId = videoPlaylist.id
+
+ await thumbnailModel.save()
}
logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
import 'multer'
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
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 { createReqFiles } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger'
import { TagModel } from '../../../models/video/tag'
import { VideoImportModel } from '../../../models/video/video-import'
import { JobQueue } from '../../../lib/job-queue/job-queue'
-import { processImage } from '../../../helpers/image-utils'
import { join } from 'path'
import { isArray } from '../../../helpers/custom-validators/misc'
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
import { VideoChannelModel } from '../../../models/video/video-channel'
-import { UserModel } from '../../../models/account/user'
import * as Bluebird from 'bluebird'
import * as parseTorrent from 'parse-torrent'
import { getSecureTorrentName } from '../../../helpers/utils'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import { CONFIG } from '../../../initializers/config'
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 videoImportsRouter = express.Router()
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)
- await processPreview(req, video)
+ const thumbnailModel = await processThumbnail(req, video)
+ const previewModel = await processPreview(req, video)
const tags = body.tags || undefined
const videoImportAttributes = {
state: VideoImportState.PENDING,
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
const payload = {
}).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 downloadPreview = !await processPreview(req, video)
+ const thumbnailModel = await processThumbnail(req, video)
+ const previewModel = await processPreview(req, video)
const tags = body.tags || youtubeDLInfo.tags
const videoImportAttributes = {
state: VideoImportState.PENDING,
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
const payload = {
type: 'youtube-dl' as 'youtube-dl',
videoImportId: videoImport.id,
thumbnailUrl: youtubeDLInfo.thumbnailUrl,
- downloadThumbnail,
- downloadPreview
+ downloadThumbnail: !thumbnailModel,
+ downloadPreview: !previewModel
}
await JobQueue.Instance.createJob({ type: 'video-import', payload })
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 = {
name: body.name || importData.name || 'Unknown name',
remote: false,
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
if (thumbnailField) {
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) {
const previewField = req.files ? req.files['previewfile'] : undefined
if (previewField) {
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,
+ thumbnailModel: ThumbnailModel,
+ previewModel: ThumbnailModel,
videoChannel: VideoChannelModel,
tags: string[],
videoImportAttributes: FilteredModelAttributes<VideoImportModel>
-): Bluebird<VideoImportModel> {
+}): Bluebird<VideoImportModel> {
+ let { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes } = parameters
+
return sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
const videoCreated = await video.save(sequelizeOptions)
videoCreated.VideoChannel = videoChannel
+ if (thumbnailModel) {
+ thumbnailModel.videoId = videoCreated.id
+ videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t }))
+ }
+ if (previewModel) {
+ previewModel.videoId = videoCreated.id
+ videoCreated.addThumbnail(await previewModel.save({ transaction: t }))
+ }
+
await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
// Set tags to the video
import { extname, join } from 'path'
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
-import { processImage } from '../../../helpers/image-utils'
import { logger } from '../../../helpers/logger'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
-import {
- MIMETYPES,
- PREVIEWS_SIZE,
- THUMBNAILS_SIZE,
- VIDEO_CATEGORIES,
- VIDEO_LANGUAGES,
- VIDEO_LICENCES,
- VIDEO_PRIVACIES
-} from '../../../initializers/constants'
+import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
import {
changeVideoChannelShare,
federateVideoIfNeeded,
import { sendView } from '../../../lib/activitypub/send/send-view'
import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database'
+import { createVideoThumbnailFromExisting, generateVideoThumbnail } from '../../../lib/thumbnail'
+import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
// Process thumbnail or create it from the video
const thumbnailField = req.files['thumbnailfile']
- if (thumbnailField) {
- const thumbnailPhysicalFile = thumbnailField[0]
- await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE)
- } else {
- await video.createThumbnail(videoFile)
- }
+ const thumbnailModel = thumbnailField
+ ? await createVideoThumbnailFromExisting(thumbnailField[0].path, video, ThumbnailType.THUMBNAIL)
+ : await generateVideoThumbnail(video, videoFile, ThumbnailType.THUMBNAIL)
// Process preview or create it from the video
const previewField = req.files['previewfile']
- if (previewField) {
- const previewPhysicalFile = previewField[0]
- await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE)
- } else {
- await video.createPreview(videoFile)
- }
+ const previewModel = previewField
+ ? await createVideoThumbnailFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW)
+ : await generateVideoThumbnail(video, videoFile, ThumbnailType.PREVIEW)
// Create the torrent file
await video.createTorrentAndSetInfoHash(videoFile)
const sequelizeOptions = { transaction: t }
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
videoCreated.VideoChannel = res.locals.videoChannel
const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
// Process thumbnail or create it from the video
- if (req.files && req.files['thumbnailfile']) {
- const thumbnailPhysicalFile = req.files['thumbnailfile'][0]
- await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoInstance.getThumbnailName()), THUMBNAILS_SIZE)
- }
+ const thumbnailModel = req.files && req.files['thumbnailfile']
+ ? await createVideoThumbnailFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.THUMBNAIL)
+ : undefined
- // Process preview or create it from the video
- if (req.files && req.files['previewfile']) {
- const previewPhysicalFile = req.files['previewfile'][0]
- await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, videoInstance.getPreviewName()), PREVIEWS_SIZE)
- }
+ const previewModel = req.files && req.files['previewfile']
+ ? await createVideoThumbnailFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW)
+ : undefined
try {
const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
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?
if (videoInfoToUpdate.tags !== undefined) {
const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t)
import { getFormattedObjects } from '../../../helpers/utils'
import { changeVideoChannelShare } from '../../../lib/activitypub'
import { sendUpdateVideo } from '../../../lib/activitypub/send'
-import { UserModel } from '../../../models/account/user'
const ownershipVideoRouter = express.Router()
// ---------------------------------------------------------------------------
-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)
if (!path) return res.sendStatus(404)
async function processImage (
physicalFile: { path: string },
destination: string,
- newSize: { width: number, height: number }
+ newSize: { width: number, height: number },
+ keepOriginal = false
) {
if (physicalFile.path === destination) {
throw new Error('Sharp needs an input path different that the output path.')
.resize(newSize.width, newSize.height)
.toFile(destination)
- await remove(physicalFile.path)
+ if (keepOriginal !== true) await remove(physicalFile.path)
}
// ---------------------------------------------------------------------------
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
import { VideoPlaylistModel } from '../models/video/video-playlist'
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
UserNotificationSettingModel,
VideoStreamingPlaylistModel,
VideoPlaylistModel,
- VideoPlaylistElementModel
+ VideoPlaylistElementModel,
+ ThumbnailModel
])
// Check extensions exist in the database
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
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 { isArray } from '../../helpers/custom-validators/misc'
import { getOrCreateActorAndServerAndModel } from './actor'
import { logger } from '../../helpers/logger'
import { VideoPlaylistModel } from '../../models/video/video-playlist'
-import { doRequest, downloadImage } from '../../helpers/requests'
+import { doRequest } from '../../helpers/requests'
import { checkUrlsSameHost } from '../../helpers/activitypub'
import * as Bluebird from 'bluebird'
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
import { VideoModel } from '../../models/video/video'
import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/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 { createPlaylistThumbnailFromUrl } from '../thumbnail'
function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
return Promise.resolve()
})
- // Empty playlists generally do not have a miniature, so skip this
- if (accItems.length !== 0) {
+ const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
+
+ if (playlistObject.icon) {
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) {
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> {
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 }> {
const options = {
uri: playlistUrl,
import * as magnetUtil from 'magnet-uri'
import * as request from 'request'
import {
- ActivityIconObject,
ActivityPlaylistSegmentHashesObject,
ActivityPlaylistUrlObject,
ActivityUrlObject,
- ActivityVideoUrlObject,
+ ActivityVideoUrlObject, VideoCreate,
VideoState
} from '../../../shared/index'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
-import { doRequest, downloadImage } from '../../helpers/requests'
-import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, REMOTE_SCHEME, THUMBNAILS_SIZE } from '../../initializers/constants'
+import { doRequest } from '../../helpers/requests'
+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 { TagModel } from '../../models/video/tag'
import { VideoModel } from '../../models/video/video'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { VideoShareModel } from '../../models/video/video-share'
import { VideoCommentModel } from '../../models/video/video-comment'
-import { CONFIG } from '../../initializers/config'
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) {
// If the video is not private and is published, we federate it
}
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
- return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => {
+ return request.get(url, err => {
if (err) reject(err)
})
}
-function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
- const thumbnailName = video.getThumbnailName()
+function buildRemoteBaseUrl (video: VideoModel, path: string) {
+ 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) {
const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
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 => {
const sequelizeOptions = { transaction: t }
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 newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
logger.debug('Cannot update the remote video.', { 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: {
getOrCreateVideoAndAccountAndChannel,
fetchRemoteVideoStaticFile,
fetchRemoteVideoDescription,
- generateThumbnailFromUrl,
getOrCreateVideoChannelFromVideoObject
}
async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
logger.debug('Adding remote video %s.', videoObject.id)
+ const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
+ 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 videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
- const video = VideoModel.build(videoData)
-
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
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
- videoCreated.VideoChannel = channelActor.VideoChannel
return videoCreated
})
- const p = generateThumbnailFromUrl(videoCreated, videoObject.icon)
- .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
+ if (waitThumbnail === false) {
+ promiseThumbnail.then(thumbnailModel => {
+ thumbnailModel = videoCreated.id
- if (waitThumbnail === true) await p
+ return thumbnailModel.save()
+ })
+ }
return videoCreated
}
-import * as AsyncLRU from 'async-lru'
import { createWriteStream, remove } from 'fs-extra'
import { logger } from '../../helpers/logger'
import { VideoModel } from '../../models/video/video'
import { fetchRemoteVideoStaticFile } from '../activitypub'
+import * as memoizee from 'memoizee'
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
protected abstract loadRemoteFile (key: string): Promise<string>
init (max: number, maxAge: number) {
- this.lru = new AsyncLRU({
- max,
+ this.getFilePath = memoizee(this.getFilePathImpl, {
maxAge,
- load: (key, cb) => {
- this.loadRemoteFile(key)
- .then(res => cb(null, res))
- .catch(err => cb(err))
+ max,
+ promise: true,
+ dispose: (value: string) => {
+ remove(value)
+ .then(() => logger.debug('%s evicted from %s', value, this.constructor.name))
+ .catch(err => logger.error('Cannot remove %s from cache %s.', value, this.constructor.name, { err }))
}
})
-
- 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) {
return this.instance || (this.instance = new this())
}
- async getFilePath (params: GetPathParam) {
+ async getFilePathImpl (params: GetPathParam) {
const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language)
if (!videoCaption) return undefined
if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName())
const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language
- return this.loadFromLRU(key)
+ return this.loadRemoteFile(key)
}
protected async loadRemoteFile (key: string) {
const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
if (!video) return undefined
+ // FIXME: use URL
const remoteStaticPath = videoCaption.getCaptionStaticPath()
const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
return this.instance || (this.instance = new this())
}
- async getFilePath (videoUUID: string) {
+ async getFilePathImpl (videoUUID: string) {
const video = await VideoModel.loadByUUIDWithFile(videoUUID)
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) {
if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
- const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
- const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreviewName())
+ // FIXME: use URL
+ 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)
}
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { extname, join } from 'path'
import { VideoFileModel } from '../../../models/video/video-file'
-import { PREVIEWS_SIZE, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
-import { downloadImage } from '../../../helpers/requests'
+import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
import { VideoState } from '../../../../shared'
import { JobQueue } from '../index'
import { federateVideoIfNeeded } from '../../activitypub'
import { Notifier } from '../../notifier'
import { CONFIG } from '../../../initializers/config'
import { sequelizeTypescript } from '../../../initializers/database'
+import { ThumbnailModel } from '../../../models/video/thumbnail'
+import { createVideoThumbnailFromUrl, generateVideoThumbnail } from '../../thumbnail'
+import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
type VideoImportYoutubeDLPayload = {
type: 'youtube-dl'
tempVideoPath = null // This path is not used anymore
// Process thumbnail
- if (options.downloadThumbnail) {
- if (options.thumbnailUrl) {
- await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE)
- } else {
- await videoImport.Video.createThumbnail(videoFile)
- }
- } else if (options.generateThumbnail) {
- await videoImport.Video.createThumbnail(videoFile)
+ let thumbnailModel: ThumbnailModel
+ if (options.downloadThumbnail && options.thumbnailUrl) {
+ thumbnailModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.THUMBNAIL)
+ } else if (options.generateThumbnail || options.downloadThumbnail) {
+ thumbnailModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.THUMBNAIL)
}
// Process preview
- if (options.downloadPreview) {
- if (options.thumbnailUrl) {
- await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE)
- } else {
- await videoImport.Video.createPreview(videoFile)
- }
- } else if (options.generatePreview) {
- await videoImport.Video.createPreview(videoFile)
+ let previewModel: ThumbnailModel
+ if (options.downloadPreview && options.thumbnailUrl) {
+ previewModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
+ } else if (options.generatePreview || options.downloadPreview) {
+ previewModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
}
// Create torrent
video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
await video.save({ transaction: t })
+ if (thumbnailModel) {
+ thumbnailModel.videoId = video.id
+ video.addThumbnail(await thumbnailModel.save({ transaction: t }))
+ }
+ if (previewModel) {
+ previewModel.videoId = video.id
+ video.addThumbnail(await previewModel.save({ transaction: t }))
+ }
+
// Now we can federate the video (reload from database, we need more attributes)
const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
await federateVideoIfNeeded(videoForFederation, true, t)
--- /dev/null
+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
+}
--- /dev/null
+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)
+ }
+}
ActivityUrlObject,
VideoTorrentObject
} 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 {
getVideoCommentsActivityPubUrl,
subtitleLanguage,
icon: {
type: 'Image',
- url: video.getThumbnailUrl(baseUrlHttp),
+ url: video.getThumbnail().getUrl(),
mediaType: 'image/jpeg',
- width: THUMBNAILS_SIZE.width,
- height: THUMBNAILS_SIZE.height
+ width: video.getThumbnail().width,
+ height: video.getThumbnail().height
},
url,
likes: getVideoLikesActivityPubUrl(video),
import {
AllowNull,
- BeforeDestroy,
BelongsTo,
Column,
CreatedAt,
Default,
ForeignKey,
HasMany,
+ HasOne,
Is,
IsUUID,
Model,
import { VideoPlaylistElementModel } from './video-playlist-element'
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
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 { CONFIG } from '../../initializers/config'
+import { ThumbnailModel } from './thumbnail'
+import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
WITH_ACCOUNT = 'WITH_ACCOUNT',
+ WITH_THUMBNAIL = 'WITH_THUMBNAIL',
WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
}
}
@Scopes({
+ [ ScopeNames.WITH_THUMBNAIL ]: {
+ include: [
+ {
+ model: () => ThumbnailModel,
+ required: false
+ }
+ ]
+ },
[ ScopeNames.WITH_VIDEOS_LENGTH ]: {
attributes: {
include: [
})
VideoPlaylistElements: VideoPlaylistElementModel[]
- @BeforeDestroy
- static async removeFiles (instance: VideoPlaylistModel) {
- logger.info('Removing files of video playlist %s.', instance.url)
-
- return instance.removeThumbnail()
- }
+ @HasOne(() => ThumbnailModel, {
+ foreignKey: {
+ name: 'videoPlaylistId',
+ allowNull: true
+ },
+ onDelete: 'CASCADE',
+ hooks: true
+ })
+ Thumbnail: ThumbnailModel
static listForApi (options: {
followerActorId: number
} as AvailableForListOptions
]
} as any, // FIXME: typings
- ScopeNames.WITH_VIDEOS_LENGTH
+ ScopeNames.WITH_VIDEOS_LENGTH,
+ ScopeNames.WITH_THUMBNAIL
]
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)
}
}
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)
}
}
}
- return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
+ return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query)
}
static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
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'
return 'playlist-' + this.uuid + extension
}
getThumbnailUrl () {
- return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
+ if (!this.hasThumbnail()) return null
+
+ return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename
}
getThumbnailStaticPath () {
- return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
- }
+ if (!this.hasThumbnail()) return null
- removeThumbnail () {
- const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
- return remove(thumbnailPath)
- .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
+ return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename)
}
setAsRefreshed () {
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)
.then(o => {
return Object.assign(o, {
published: this.createdAt.toISOString(),
updated: this.updatedAt.toISOString(),
attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
- icon: {
- type: 'Image' as 'Image',
- url: this.getThumbnailUrl(),
- mediaType: 'image/jpeg' as 'image/jpeg',
- width: THUMBNAILS_SIZE.width,
- height: THUMBNAILS_SIZE.height
- }
+ icon
})
})
}
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { VideoPlaylistElementModel } from './video-playlist-element'
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
const indexes: Sequelize.DefineIndexesOptions[] = [
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
- WITH_USER_ID = 'WITH_USER_ID'
+ WITH_USER_ID = 'WITH_USER_ID',
+ WITH_THUMBNAILS = 'WITH_THUMBNAILS'
}
type ForAPIOptions = {
return query
},
+ [ ScopeNames.WITH_THUMBNAILS ]: {
+ include: [
+ {
+ model: () => ThumbnailModel,
+ required: false
+ }
+ ]
+ },
[ ScopeNames.WITH_USER_ID ]: {
include: [
{
})
Tags: TagModel[]
+ @HasMany(() => ThumbnailModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: true
+ },
+ hooks: true,
+ onDelete: 'cascade'
+ })
+ Thumbnails: ThumbnailModel[]
+
@HasMany(() => VideoPlaylistElementModel, {
foreignKey: {
name: 'videoId',
logger.info('Removing files of video %s.', instance.url)
- tasks.push(instance.removeThumbnail())
-
if (instance.isOwned()) {
if (!Array.isArray(instance.VideoFiles)) {
instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
}
- tasks.push(instance.removePreview())
-
// Remove physical files and torrents
instance.VideoFiles.forEach(file => {
tasks.push(instance.removeFile(file))
}
}
- 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) {
return Bluebird.all([
// 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 })
]).then(([ rows, totals ]) => {
// totals: totalVideos + totalVideoShares
})
}
- return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
- return {
- data: rows,
- total: count
- }
- })
+ return VideoModel.scope(ScopeNames.WITH_THUMBNAILS)
+ .findAndCountAll(query)
+ .then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
}
static async listForApi (options: {
transaction: t
}
- return VideoModel.findOne(options)
+ return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
}
static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
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) {
transaction: t
}
- return VideoModel.findOne(options)
+ return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
}
static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
- return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ])
- .findByPk(id, { transaction: t, logging })
+ return VideoModel.scope([
+ ScopeNames.WITH_FILES,
+ ScopeNames.WITH_STREAMING_PLAYLISTS,
+ ScopeNames.WITH_THUMBNAILS
+ ]).findByPk(id, { transaction: t, logging })
}
static loadByUUIDWithFile (uuid: string) {
}
}
- return VideoModel.findOne(options)
+ return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
}
static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
transaction
}
- return VideoModel.findOne(query)
+ return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
}
static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
return VideoModel.scope([
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_FILES,
- ScopeNames.WITH_STREAMING_PLAYLISTS
+ ScopeNames.WITH_STREAMING_PLAYLISTS,
+ ScopeNames.WITH_THUMBNAILS
]).findOne(query)
}
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_FILES,
- ScopeNames.WITH_STREAMING_PLAYLISTS
+ ScopeNames.WITH_STREAMING_PLAYLISTS,
+ ScopeNames.WITH_THUMBNAILS
]
if (userId) {
ScopeNames.WITH_BLACKLISTED,
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE,
+ ScopeNames.WITH_THUMBNAILS,
{ method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
]
}
// FIXME: typing
- const apiScope: any[] = []
+ const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ]
if (options.user) {
apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
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) {
return this.uuid + '-' + videoFile.resolution + videoFile.extname
}
- getThumbnailName () {
- const extension = '.jpg'
- return this.uuid + extension
+ generateThumbnailName () {
+ return this.uuid + '.jpg'
}
- getPreviewName () {
- const extension = '.jpg'
- return this.uuid + extension
+ getThumbnail () {
+ if (Array.isArray(this.Thumbnails) === false) return undefined
+
+ 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) {
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) {
return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
}
}
getThumbnailStaticPath () {
- return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
+ const thumbnail = this.getThumbnail()
+ if (!thumbnail) return null
+
+ return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
}
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 {
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) {
const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
}
- getThumbnailUrl (baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
- }
-
getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
}
totalItems: number
attributedTo: string[]
- icon: ActivityIconObject
+ icon?: ActivityIconObject
published: string
updated: string
--- /dev/null
+export enum ThumbnailType {
+ THUMBNAIL = 1,
+ PREVIEW = 2
+}