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:
kontrollanten 2022-06-21 15:31:25 +02:00 committed by GitHub
parent dec4952155
commit 2e401e8575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 339 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 710 const LAST_MIGRATION_VERSION = 715
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import { VideoSourceModel } from '@server/models/video/video-source'
export type MVideoSource = Omit<VideoSourceModel, 'Video'>

View File

@ -0,0 +1,3 @@
export interface VideoSource {
filename: string
}

View File

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

View File

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