Added support for uploading short videos

pull/6252/head
NC25012_Ankit Lal 2024-02-26 15:09:12 +05:30
parent eca039b97d
commit b2fa2e84f7
16 changed files with 186 additions and 12 deletions

View File

@ -67,6 +67,8 @@ export class Video implements VideoServerModel {
dislikes: number
nsfw: boolean
shortVideo: boolean
originInstanceUrl: string
originInstanceHost: string
@ -166,6 +168,8 @@ export class Video implements VideoServerModel {
this.nsfw = hash.nsfw
this.shortVideo = hash.nsfw
this.account = hash.account
this.channel = hash.channel

View File

@ -33,7 +33,7 @@ smtp:
password: null
log:
level: 'debug'
level: 'error'
open_telemetry:
metrics:

View File

@ -36,6 +36,9 @@ export interface VideosCommonQuery {
search?: string
excludeAlreadyWatched?: boolean
durationMax?: number /*EDITED*/
shortVideo?: boolean
}
export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery {

View File

@ -22,4 +22,6 @@ export interface VideoCreate {
thumbnailfile?: Blob | string
previewfile?: Blob | string
shortVideo?: boolean
}

View File

@ -57,6 +57,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
}
pluginData?: any
shortVideo: boolean
}
// Not included by default, needs query params

View File

@ -176,9 +176,11 @@ async function getVideoDescription (req: express.Request, res: express.Response)
}
async function listVideos (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const query = pickCommonVideoQuery(req.query)
const countVideos = getCountVideos(req)
const apiOptions = await Hooks.wrapObject({

View File

@ -100,6 +100,9 @@ export {
async function addVideoLegacy (req: express.Request, res: express.Response) {
// Uploading the video could be long
// Set timeout to 10 minutes, as Express's default is 2 minutes
req.setTimeout(1000 * 60 * 10, () => {
logger.error('Video upload has timed out.')
return res.fail({
@ -142,6 +145,7 @@ async function addVideo (options: {
let videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
videoData = await Hooks.wrapObject(videoData, 'filter:api.video.upload.video-attribute.result')
videoData.state = buildNextVideoState()
videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware

View File

@ -185,6 +185,12 @@ function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
return true
}
function isDurationValid(currDuration : number, requiredDuration: number){
return currDuration<= requiredDuration;
}
// ---------------------------------------------------------------------------
export {
@ -214,5 +220,6 @@ export {
isVideoImageValid,
isVideoSupportValid,
isPasswordValid,
isValidPasswordProtectedPrivacy
isValidPasswordProtectedPrivacy,
isDurationValid
}

View File

@ -26,7 +26,9 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
'hasWebtorrentFiles', // TODO: Remove in v7
'hasWebVideoFiles',
'search',
'excludeAlreadyWatched'
'excludeAlreadyWatched',
'durationMax',
'shortVideo'
])
}

View File

@ -41,7 +41,7 @@ import { cpus } from 'os'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 805
const LAST_MIGRATION_VERSION = 810
// ---------------------------------------------------------------------------

View File

@ -0,0 +1,25 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
const data = {
type: Sequelize.BOOLEAN,
allowNull: true,
defaultValue: false
}
await utils.queryInterface.addColumn('video', 'shortVideo', data)
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -38,7 +38,9 @@ export function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: numbe
channelId,
originallyPublishedAt: videoInfo.originallyPublishedAt
? new Date(videoInfo.originallyPublishedAt)
: null
: null,
shortVideo: videoInfo.shortVideo || false
}
}

View File

@ -33,7 +33,8 @@ import {
isVideoNameValid,
isVideoOriginallyPublishedAtValid,
isVideoPrivacyValid,
isVideoSupportValid
isVideoSupportValid,
isDurationValid
} from '../../../helpers/custom-validators/videos.js'
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
@ -54,6 +55,9 @@ import {
} from '../shared/index.js'
import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
// let duration: number
const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
body('videofile')
.custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
@ -71,6 +75,8 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
.isArray()
.withMessage('Video passwords should be an array.'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@ -81,13 +87,15 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) ||
!isValidPasswordProtectedPrivacy(req, res) ||
!await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) ||
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' })
) {
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' }) ) {
return cleanUpReqFiles(req)
}
return next()
}
},
])
/**
@ -353,8 +361,46 @@ const videosOverviewValidator = [
}
]
function getCommonVideoEditAttributes () {
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return cleanUpReqFiles(req);
const videoFile: express.VideoUploadFile = req.files['videofile'][0];
const user = res.locals.oauth.token.User;
if (
!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) ||
!isValidPasswordProtectedPrivacy(req, res) ||
!await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) ||
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' })
) {
return cleanUpReqFiles(req);
}
};
return [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return cleanUpReqFiles(req);
const videoFile: express.VideoUploadFile = req.files['videofile'][0];
const user = res.locals.oauth.token.User;
if (
!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) ||
!isValidPasswordProtectedPrivacy(req, res) ||
!await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) ||
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' })
) {
return cleanUpReqFiles(req);
}
return next();
},
body('thumbnailfile')
.custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage(
'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
@ -427,7 +473,29 @@ function getCommonVideoEditAttributes () {
body('scheduleUpdate.privacy')
.optional()
.customSanitizer(toIntOrNull)
.custom(isScheduleVideoUpdatePrivacyValid)
.custom(isScheduleVideoUpdatePrivacyValid),
body('shortVideo')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(() => true).withMessage('Should have shortVideo boolean'),
body('shortVideo')
.optional()
.customSanitizer(toBooleanOrNull)
.custom((value, { req }) => {
if (value) {
const videoFile: express.VideoUploadFile = req.files['videofile'][0]
return isDurationValid(videoFile.duration, 20);
}
return true;
})
.withMessage('Video duration must be less than 60 seconds for short videos'),
] as (ValidationChain | ExpressPromiseHandler)[]
}
@ -459,6 +527,14 @@ const commonVideosFiltersValidator = [
query('nsfw')
.optional()
.custom(isBooleanBothQueryValid),
query('shortVideo')
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid shortVideo boolean'),
query('isLive')
.optional()
.customSanitizer(toBooleanOrNull)

View File

@ -114,6 +114,8 @@ export function videoModelToFormattedJSON (video: MVideoFormattable, options: Vi
// Can be added by external plugins
pluginData: (video as any).pluginData,
shortVideo : video.shortVideo,
...buildAdditionalAttributes(video, options)
}

View File

@ -32,6 +32,9 @@ export type BuildVideosListQueryOptions = {
sort: string
nsfw?: boolean
shortVideo?: boolean
host?: string
isLive?: boolean
isLocal?: boolean
@ -211,6 +214,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
this.whereSFW()
}
if(options.shortVideo === true){
this.whereShortVideo()
}
else if(options.shortVideo == false){
this.whereLongVideo()
}
if (options.isLive === true) {
this.whereLive()
} else if (options.isLive === false) {
@ -305,6 +316,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
this.sort + ' ' +
this.limit + ' ' +
this.offset
}
private setCountAttribute () {
@ -513,6 +525,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
this.and.push('"video"."nsfw" IS FALSE')
}
private whereShortVideo(){
this.and.push('"video"."shortVideo" IS TRUE')
}
private whereLongVideo(){
this.and.push('"video"."shortVideo" IS FALSE')
}
private whereLive () {
this.and.push('"video"."isLive" IS TRUE')
}

View File

@ -430,6 +430,15 @@ export type ForAPIOptions = {
nsfw: true
}
},
{
fields: [ 'shortVideo' ],
where: {
shortVideo: false
}
},
{
fields: [ 'isLive' ], // Most of the videos are VOD
where: {
@ -550,6 +559,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
@Column
downloadEnabled: boolean
@AllowNull(true)
@Column
shortVideo: boolean
@AllowNull(false)
@Column
waitTranscoding: boolean
@ -1157,6 +1170,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
search?: string
excludeAlreadyWatched?: boolean
durationMax?:number
shortVideo? : boolean
}) {
VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
@ -1197,7 +1214,9 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'hasWebtorrentFiles',
'hasWebVideoFiles',
'search',
'excludeAlreadyWatched'
'excludeAlreadyWatched',
'durationMax',
'shortVideo'
]),
serverAccountIdForBlock: serverActor.Account.id,
@ -1249,6 +1268,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
excludeAlreadyWatched?: boolean
countVideos?: boolean
shortVideo?: boolean
}) {
VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
@ -1284,7 +1305,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
'uuids',
'search',
'displayOnlyForFollower',
'excludeAlreadyWatched'
'excludeAlreadyWatched',
'shortVideo'
]),
serverAccountIdForBlock: serverActor.Account.id
}
@ -1624,6 +1646,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
options: BuildVideosListQueryOptions,
countVideos = true
): Promise<ResultList<VideoModel>> {
const span = tracer.startSpan('peertube.VideoModel.getAvailableForApi')
function getCount () {