store uploaded video filename (#4885)
* store uploaded video filename closes #4731 * dont crash if videos channel exist * migration: use raw query * video source: fixes after code review * cleanup * bump migration * updates after code review * refactor: use checkUserCanManageVideo * videoSource: add openapi doc * test(check-params/video-source): fix timeout * Styling * Correctly set original filename as source Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
dec4952155
commit
2e401e8575
|
@ -340,6 +340,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-12 col-xl-4">
|
<div class="col-md-12 col-xl-4">
|
||||||
|
|
||||||
|
<div *ngIf="videoSource" class="form-group">
|
||||||
|
<label i18n for="filename">Filename</label>
|
||||||
|
|
||||||
|
<my-help>
|
||||||
|
<ng-template ptTemplate="preHtml">
|
||||||
|
<ng-container i18n>
|
||||||
|
Name of the uploaded file
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</my-help>
|
||||||
|
|
||||||
|
<input type="text" [disabled]="true" id="filename" class="form-control" [value]="videoSource.filename" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group originally-published-at">
|
<div class="form-group originally-published-at">
|
||||||
<label i18n for="originallyPublishedAt">Original publication date</label>
|
<label i18n for="originallyPublishedAt">Original publication date</label>
|
||||||
<my-help>
|
<my-help>
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||||
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
|
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
|
||||||
import { VideoEditType } from './video-edit.type'
|
import { VideoEditType } from './video-edit.type'
|
||||||
|
import { VideoSource } from '@shared/models/videos/video-source'
|
||||||
|
|
||||||
type VideoLanguages = VideoConstant<string> & { group?: string }
|
type VideoLanguages = VideoConstant<string> & { group?: string }
|
||||||
type PluginField = {
|
type PluginField = {
|
||||||
|
@ -61,6 +62,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
@Input() forbidScheduledPublication = true
|
@Input() forbidScheduledPublication = true
|
||||||
|
|
||||||
@Input() videoCaptions: VideoCaptionWithPathEdit[] = []
|
@Input() videoCaptions: VideoCaptionWithPathEdit[] = []
|
||||||
|
@Input() videoSource: VideoSource
|
||||||
|
|
||||||
@Input() waitTranscodingEnabled = true
|
@Input() waitTranscodingEnabled = true
|
||||||
@Input() type: VideoEditType
|
@Input() type: VideoEditType
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()"
|
[videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()"
|
||||||
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
|
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
|
||||||
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
|
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
|
||||||
|
[videoSource]="videoSource"
|
||||||
|
|
||||||
(formBuilt)="onFormBuilt()"
|
(formBuilt)="onFormBuilt()"
|
||||||
></my-video-edit>
|
></my-video-edit>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||||
import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
|
import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
|
||||||
import { hydrateFormFromVideo } from './shared/video-edit-utils'
|
import { hydrateFormFromVideo } from './shared/video-edit-utils'
|
||||||
|
import { VideoSource } from '@shared/models/videos/video-source'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-update',
|
selector: 'my-videos-update',
|
||||||
|
@ -19,6 +20,7 @@ import { hydrateFormFromVideo } from './shared/video-edit-utils'
|
||||||
export class VideoUpdateComponent extends FormReactive implements OnInit {
|
export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
video: VideoEdit
|
video: VideoEdit
|
||||||
videoDetails: VideoDetails
|
videoDetails: VideoDetails
|
||||||
|
videoSource: VideoSource
|
||||||
userVideoChannels: SelectChannelItem[] = []
|
userVideoChannels: SelectChannelItem[] = []
|
||||||
videoCaptions: VideoCaptionEdit[] = []
|
videoCaptions: VideoCaptionEdit[] = []
|
||||||
liveVideo: LiveVideo
|
liveVideo: LiveVideo
|
||||||
|
@ -46,13 +48,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
this.buildForm({})
|
this.buildForm({})
|
||||||
|
|
||||||
const { videoData } = this.route.snapshot.data
|
const { videoData } = this.route.snapshot.data
|
||||||
const { video, videoChannels, videoCaptions, liveVideo } = videoData
|
const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData
|
||||||
|
|
||||||
this.video = new VideoEdit(video)
|
this.video = new VideoEdit(video)
|
||||||
this.videoDetails = video
|
this.videoDetails = video
|
||||||
|
|
||||||
this.userVideoChannels = videoChannels
|
this.userVideoChannels = videoChannels
|
||||||
this.videoCaptions = videoCaptions
|
this.videoCaptions = videoCaptions
|
||||||
|
this.videoSource = videoSource
|
||||||
this.liveVideo = liveVideo
|
this.liveVideo = liveVideo
|
||||||
|
|
||||||
this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE
|
this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE
|
||||||
|
|
|
@ -23,7 +23,8 @@ export class VideoUpdateResolver implements Resolve<any> {
|
||||||
return this.videoService.getVideo({ videoId: uuid })
|
return this.videoService.getVideo({ videoId: uuid })
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(video => forkJoin(this.buildVideoObservables(video))),
|
switchMap(video => forkJoin(this.buildVideoObservables(video))),
|
||||||
map(([ video, videoChannels, videoCaptions, liveVideo ]) => ({ video, videoChannels, videoCaptions, liveVideo }))
|
map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) =>
|
||||||
|
({ video, videoChannels, videoCaptions, videoSource, liveVideo }))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +34,8 @@ export class VideoUpdateResolver implements Resolve<any> {
|
||||||
.loadCompleteDescription(video.descriptionPath)
|
.loadCompleteDescription(video.descriptionPath)
|
||||||
.pipe(map(description => Object.assign(video, { description }))),
|
.pipe(map(description => Object.assign(video, { description }))),
|
||||||
|
|
||||||
|
this.videoService.getSource(video.id),
|
||||||
|
|
||||||
listUserChannelsForSelect(this.authService),
|
listUserChannelsForSelect(this.authService),
|
||||||
|
|
||||||
this.videoCaptionService
|
this.videoCaptionService
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { from, Observable } from 'rxjs'
|
import { from, Observable, of } from 'rxjs'
|
||||||
import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
|
import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
|
import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
|
@ -24,6 +24,7 @@ import {
|
||||||
VideoTranscodingCreate,
|
VideoTranscodingCreate,
|
||||||
VideoUpdate
|
VideoUpdate
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
|
import { VideoSource } from '@shared/models/videos/video-source'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
import { Account } from '../account/account.model'
|
import { Account } from '../account/account.model'
|
||||||
import { AccountService } from '../account/account.service'
|
import { AccountService } from '../account/account.service'
|
||||||
|
@ -323,6 +324,20 @@ export class VideoService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSource (videoId: number) {
|
||||||
|
return this.authHttp
|
||||||
|
.get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
|
||||||
|
.pipe(
|
||||||
|
catchError(err => {
|
||||||
|
if (err.status === 404) {
|
||||||
|
return of(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restExtractor.handleError(err)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
setVideoLike (id: number) {
|
setVideoLike (id: number) {
|
||||||
return this.setVideoRate(id, 'like')
|
return this.setVideoRate(id, 'like')
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
setDefaultVideosSort,
|
setDefaultVideosSort,
|
||||||
videosCustomGetValidator,
|
videosCustomGetValidator,
|
||||||
videosGetValidator,
|
videosGetValidator,
|
||||||
|
videoSourceGetValidator,
|
||||||
videosRemoveValidator,
|
videosRemoveValidator,
|
||||||
videosSortValidator
|
videosSortValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
|
@ -96,6 +97,14 @@ videosRouter.get('/:id/description',
|
||||||
asyncMiddleware(videosGetValidator),
|
asyncMiddleware(videosGetValidator),
|
||||||
asyncMiddleware(getVideoDescription)
|
asyncMiddleware(getVideoDescription)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
videosRouter.get('/:id/source',
|
||||||
|
openapiOperationDoc({ operationId: 'getVideoSource' }),
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(videoSourceGetValidator),
|
||||||
|
getVideoSource
|
||||||
|
)
|
||||||
|
|
||||||
videosRouter.get('/:id',
|
videosRouter.get('/:id',
|
||||||
openapiOperationDoc({ operationId: 'getVideo' }),
|
openapiOperationDoc({ operationId: 'getVideo' }),
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
|
@ -155,6 +164,10 @@ async function getVideoDescription (req: express.Request, res: express.Response)
|
||||||
return res.json({ description })
|
return res.json({ description })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVideoSource (req: express.Request, res: express.Response) {
|
||||||
|
return res.json(res.locals.videoSource.toFormattedJSON())
|
||||||
|
}
|
||||||
|
|
||||||
async function listVideos (req: express.Request, res: express.Response) {
|
async function listVideos (req: express.Request, res: express.Response) {
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
|
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoFileModel } from '../../../models/video/video-file'
|
import { VideoFileModel } from '../../../models/video/video-file'
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('api', 'video')
|
const lTags = loggerTagsFactory('api', 'video')
|
||||||
const auditLogger = auditLoggerFactory('videos')
|
const auditLogger = auditLoggerFactory('videos')
|
||||||
|
@ -151,6 +152,7 @@ async function addVideo (options: {
|
||||||
video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
|
video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
|
||||||
|
|
||||||
const videoFile = await buildNewFile(videoPhysicalFile)
|
const videoFile = await buildNewFile(videoPhysicalFile)
|
||||||
|
const originalFilename = videoPhysicalFile.originalname
|
||||||
|
|
||||||
// Move physical file
|
// Move physical file
|
||||||
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
|
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
|
||||||
|
@ -181,6 +183,11 @@ async function addVideo (options: {
|
||||||
|
|
||||||
video.VideoFiles = [ videoFile ]
|
video.VideoFiles = [ videoFile ]
|
||||||
|
|
||||||
|
await VideoSourceModel.create({
|
||||||
|
filename: originalFilename,
|
||||||
|
videoId: video.id
|
||||||
|
}, { transaction: t })
|
||||||
|
|
||||||
await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
|
await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
|
||||||
|
|
||||||
// Schedule an update in the future?
|
// Schedule an update in the future?
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 710
|
const LAST_MIGRATION_VERSION = 715
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
|
||||||
import { VideoTagModel } from '../models/video/video-tag'
|
import { VideoTagModel } from '../models/video/video-tag'
|
||||||
import { VideoViewModel } from '../models/view/video-view'
|
import { VideoViewModel } from '../models/view/video-view'
|
||||||
import { CONFIG } from './config'
|
import { CONFIG } from './config'
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source'
|
||||||
|
|
||||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -126,6 +127,7 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
VideoChannelModel,
|
VideoChannelModel,
|
||||||
VideoShareModel,
|
VideoShareModel,
|
||||||
VideoFileModel,
|
VideoFileModel,
|
||||||
|
VideoSourceModel,
|
||||||
VideoCaptionModel,
|
VideoCaptionModel,
|
||||||
VideoBlacklistModel,
|
VideoBlacklistModel,
|
||||||
VideoTagModel,
|
VideoTagModel,
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const query = `
|
||||||
|
CREATE TABLE IF NOT EXISTS "videoSource" (
|
||||||
|
"id" SERIAL ,
|
||||||
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"filename" VARCHAR(255) DEFAULT NULL,
|
||||||
|
"videoId" INTEGER
|
||||||
|
REFERENCES "video" ("id")
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
`
|
||||||
|
await utils.sequelize.query(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ export * from './video-ownership-changes'
|
||||||
export * from './video-view'
|
export * from './video-view'
|
||||||
export * from './video-rates'
|
export * from './video-rates'
|
||||||
export * from './video-shares'
|
export * from './video-shares'
|
||||||
|
export * from './video-source'
|
||||||
export * from './video-stats'
|
export * from './video-stats'
|
||||||
export * from './video-studio'
|
export * from './video-studio'
|
||||||
export * from './video-transcoding'
|
export * from './video-transcoding'
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import express from 'express'
|
||||||
|
import { getVideoWithAttributes } from '@server/helpers/video'
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source'
|
||||||
|
import { MVideoFullLight } from '@server/types/models'
|
||||||
|
import { HttpStatusCode, UserRight } from '@shared/models'
|
||||||
|
import { logger } from '../../../helpers/logger'
|
||||||
|
import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
|
||||||
|
|
||||||
|
const videoSourceGetValidator = [
|
||||||
|
isValidVideoIdParam('id'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking videoSourceGet parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
if (!await doesVideoExist(req.params.id, res, 'for-api')) return
|
||||||
|
|
||||||
|
const video = getVideoWithAttributes(res) as MVideoFullLight
|
||||||
|
|
||||||
|
res.locals.videoSource = await VideoSourceModel.loadByVideoId(video.id)
|
||||||
|
if (!res.locals.videoSource) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.NOT_FOUND_404,
|
||||||
|
message: 'Video source not found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = res.locals.oauth.token.User
|
||||||
|
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export {
|
||||||
|
videoSourceGetValidator
|
||||||
|
}
|
|
@ -152,7 +152,7 @@ const videosAddResumableValidator = [
|
||||||
|
|
||||||
if (!await isVideoAccepted(req, res, file)) return cleanup()
|
if (!await isVideoAccepted(req, res, file)) return cleanup()
|
||||||
|
|
||||||
res.locals.videoFileResumable = file
|
res.locals.videoFileResumable = { ...file, originalname: file.filename }
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Op } from 'sequelize'
|
||||||
|
import {
|
||||||
|
AllowNull,
|
||||||
|
BelongsTo,
|
||||||
|
Column,
|
||||||
|
CreatedAt,
|
||||||
|
ForeignKey,
|
||||||
|
Model,
|
||||||
|
Table,
|
||||||
|
UpdatedAt
|
||||||
|
} from 'sequelize-typescript'
|
||||||
|
import { AttributesOnly } from '@shared/typescript-utils'
|
||||||
|
import { VideoModel } from './video'
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 'videoSource',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: [ 'videoId' ],
|
||||||
|
where: {
|
||||||
|
videoId: {
|
||||||
|
[Op.ne]: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class VideoSourceModel extends Model<Partial<AttributesOnly<VideoSourceModel>>> {
|
||||||
|
@CreatedAt
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Column
|
||||||
|
filename: string
|
||||||
|
|
||||||
|
@ForeignKey(() => VideoModel)
|
||||||
|
@Column
|
||||||
|
videoId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => VideoModel)
|
||||||
|
Video: VideoModel
|
||||||
|
|
||||||
|
static loadByVideoId (videoId) {
|
||||||
|
return VideoSourceModel.findOne({ where: { videoId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
toFormattedJSON () {
|
||||||
|
return {
|
||||||
|
filename: this.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -136,6 +136,7 @@ import { VideoPlaylistElementModel } from './video-playlist-element'
|
||||||
import { VideoShareModel } from './video-share'
|
import { VideoShareModel } from './video-share'
|
||||||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||||
import { VideoTagModel } from './video-tag'
|
import { VideoTagModel } from './video-tag'
|
||||||
|
import { VideoSourceModel } from './video-source'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
FOR_API = 'FOR_API',
|
FOR_API = 'FOR_API',
|
||||||
|
@ -597,6 +598,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
|
||||||
})
|
})
|
||||||
VideoPlaylistElements: VideoPlaylistElementModel[]
|
VideoPlaylistElements: VideoPlaylistElementModel[]
|
||||||
|
|
||||||
|
@HasOne(() => VideoSourceModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'videoId',
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
VideoSource: VideoSourceModel
|
||||||
|
|
||||||
@HasMany(() => VideoAbuseModel, {
|
@HasMany(() => VideoAbuseModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'videoId',
|
name: 'videoId',
|
||||||
|
|
|
@ -3,14 +3,14 @@ import './accounts'
|
||||||
import './blocklist'
|
import './blocklist'
|
||||||
import './bulk'
|
import './bulk'
|
||||||
import './config'
|
import './config'
|
||||||
import './custom-pages'
|
|
||||||
import './contact-form'
|
import './contact-form'
|
||||||
|
import './custom-pages'
|
||||||
import './debug'
|
import './debug'
|
||||||
import './follows'
|
import './follows'
|
||||||
import './jobs'
|
import './jobs'
|
||||||
|
import './live'
|
||||||
import './logs'
|
import './logs'
|
||||||
import './my-user'
|
import './my-user'
|
||||||
import './live'
|
|
||||||
import './plugins'
|
import './plugins'
|
||||||
import './redundancy'
|
import './redundancy'
|
||||||
import './search'
|
import './search'
|
||||||
|
@ -25,12 +25,13 @@ import './video-blacklist'
|
||||||
import './video-captions'
|
import './video-captions'
|
||||||
import './video-channels'
|
import './video-channels'
|
||||||
import './video-comments'
|
import './video-comments'
|
||||||
import './video-studio'
|
import './video-files'
|
||||||
import './video-imports'
|
import './video-imports'
|
||||||
import './video-playlists'
|
import './video-playlists'
|
||||||
import './videos'
|
import './video-source'
|
||||||
|
import './video-studio'
|
||||||
import './videos-common-filters'
|
import './videos-common-filters'
|
||||||
import './video-files'
|
|
||||||
import './videos-history'
|
import './videos-history'
|
||||||
import './videos-overviews'
|
import './videos-overviews'
|
||||||
|
import './videos'
|
||||||
import './views'
|
import './views'
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { HttpStatusCode } from '@shared/models'
|
||||||
|
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
|
||||||
|
|
||||||
|
describe('Test video sources API validator', function () {
|
||||||
|
let server: PeerTubeServer = null
|
||||||
|
let uuid: string
|
||||||
|
let userToken: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1)
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
|
const created = await server.videos.quickUpload({ name: 'video' })
|
||||||
|
uuid = created.uuid
|
||||||
|
|
||||||
|
userToken = await server.users.generateUserAndToken('user')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail without a valid uuid', async function () {
|
||||||
|
await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should receive 404 when passing a non existing video id', async function () {
|
||||||
|
await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not get the source as unauthenticated', async function () {
|
||||||
|
await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not get the source with another user', async function () {
|
||||||
|
await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct parameters get the source as another user', async function () {
|
||||||
|
await server.videos.getSource({ id: uuid })
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -16,3 +16,4 @@ import './video-schedule-update'
|
||||||
import './videos-common-filters'
|
import './videos-common-filters'
|
||||||
import './videos-history'
|
import './videos-history'
|
||||||
import './videos-overview'
|
import './videos-overview'
|
||||||
|
import './video-source'
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'mocha'
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test video source', () => {
|
||||||
|
let server: PeerTubeServer = null
|
||||||
|
const fixture = 'video_short.webm'
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1)
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get the source filename with legacy upload', async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
|
||||||
|
|
||||||
|
const source = await server.videos.getSource({ id: uuid })
|
||||||
|
expect(source.filename).to.equal(fixture)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get the source filename with resumable upload', async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
|
||||||
|
|
||||||
|
const source = await server.videos.getSource({ id: uuid })
|
||||||
|
expect(source.filename).to.equal(fixture)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -42,6 +42,7 @@ import {
|
||||||
MVideoThumbnail
|
MVideoThumbnail
|
||||||
} from './models'
|
} from './models'
|
||||||
import { Writable } from 'stream'
|
import { Writable } from 'stream'
|
||||||
|
import { MVideoSource } from './models/video/video-source'
|
||||||
|
|
||||||
declare module 'express' {
|
declare module 'express' {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
|
@ -68,7 +69,7 @@ declare module 'express' {
|
||||||
} | UploadFileForCheck[]
|
} | UploadFileForCheck[]
|
||||||
|
|
||||||
// Upload file with a duration added by our middleware
|
// Upload file with a duration added by our middleware
|
||||||
export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & {
|
export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size', 'originalname'> & {
|
||||||
duration: number
|
duration: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ declare module 'express' {
|
||||||
duration: number
|
duration: number
|
||||||
path: string
|
path: string
|
||||||
filename: string
|
filename: string
|
||||||
|
originalname: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extends Response with added functions and potential variables passed by middlewares
|
// Extends Response with added functions and potential variables passed by middlewares
|
||||||
|
@ -123,6 +125,8 @@ declare module 'express' {
|
||||||
|
|
||||||
videoShare?: MVideoShareActor
|
videoShare?: MVideoShareActor
|
||||||
|
|
||||||
|
videoSource?: MVideoSource
|
||||||
|
|
||||||
videoFile?: MVideoFile
|
videoFile?: MVideoFile
|
||||||
|
|
||||||
videoFileResumable?: EnhancedUploadXFile
|
videoFileResumable?: EnhancedUploadXFile
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { VideoSourceModel } from '@server/models/video/video-source'
|
||||||
|
|
||||||
|
export type MVideoSource = Omit<VideoSourceModel, 'Video'>
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface VideoSource {
|
||||||
|
filename: string
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import {
|
||||||
import { unwrapBody } from '../requests'
|
import { unwrapBody } from '../requests'
|
||||||
import { waitJobs } from '../server'
|
import { waitJobs } from '../server'
|
||||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||||
|
import { VideoSource } from '@shared/models/videos/video-source'
|
||||||
|
|
||||||
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
|
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
|
||||||
fixture?: string
|
fixture?: string
|
||||||
|
@ -150,6 +151,20 @@ export class VideosCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSource (options: OverrideCommandOptions & {
|
||||||
|
id: number | string
|
||||||
|
}) {
|
||||||
|
const path = '/api/v1/videos/' + options.id + '/source'
|
||||||
|
|
||||||
|
return this.getRequestBody<VideoSource>({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async getId (options: OverrideCommandOptions & {
|
async getId (options: OverrideCommandOptions & {
|
||||||
uuid: number | string
|
uuid: number | string
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -1903,6 +1903,22 @@ paths:
|
||||||
example: |
|
example: |
|
||||||
**[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**
|
**[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**
|
||||||
|
|
||||||
|
'/videos/{id}/source':
|
||||||
|
post:
|
||||||
|
summary: Get video source file metadata
|
||||||
|
operationId: getVideoSource
|
||||||
|
tags:
|
||||||
|
- Video
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/idOrUUID'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/VideoSource'
|
||||||
|
|
||||||
'/videos/{id}/views':
|
'/videos/{id}/views':
|
||||||
post:
|
post:
|
||||||
summary: Notify user is watching a video
|
summary: Notify user is watching a video
|
||||||
|
@ -6141,6 +6157,10 @@ components:
|
||||||
$ref: '#/components/schemas/VideoConstantString-Language'
|
$ref: '#/components/schemas/VideoConstantString-Language'
|
||||||
captionPath:
|
captionPath:
|
||||||
type: string
|
type: string
|
||||||
|
VideoSource:
|
||||||
|
properties:
|
||||||
|
filename:
|
||||||
|
type: string
|
||||||
ActorImage:
|
ActorImage:
|
||||||
properties:
|
properties:
|
||||||
path:
|
path:
|
||||||
|
|
Loading…
Reference in New Issue