</div>
<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">
<label i18n for="originallyPublishedAt">Original publication date</label>
<my-help>
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
import { VideoEditType } from './video-edit.type'
+import { VideoSource } from '@shared/models/videos/video-source'
type VideoLanguages = VideoConstant<string> & { group?: string }
type PluginField = {
@Input() forbidScheduledPublication = true
@Input() videoCaptions: VideoCaptionWithPathEdit[] = []
+ @Input() videoSource: VideoSource
@Input() waitTranscodingEnabled = true
@Input() type: VideoEditType
[videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()"
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
+ [videoSource]="videoSource"
(formBuilt)="onFormBuilt()"
></my-video-edit>
import { LoadingBarService } from '@ngx-loading-bar/core'
import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
import { hydrateFormFromVideo } from './shared/video-edit-utils'
+import { VideoSource } from '@shared/models/videos/video-source'
@Component({
selector: 'my-videos-update',
export class VideoUpdateComponent extends FormReactive implements OnInit {
video: VideoEdit
videoDetails: VideoDetails
+ videoSource: VideoSource
userVideoChannels: SelectChannelItem[] = []
videoCaptions: VideoCaptionEdit[] = []
liveVideo: LiveVideo
this.buildForm({})
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.videoDetails = video
this.userVideoChannels = videoChannels
this.videoCaptions = videoCaptions
+ this.videoSource = videoSource
this.liveVideo = liveVideo
this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE
return this.videoService.getVideo({ videoId: uuid })
.pipe(
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 }))
)
}
.loadCompleteDescription(video.descriptionPath)
.pipe(map(description => Object.assign(video, { description }))),
+ this.videoService.getSource(video.id),
+
listUserChannelsForSelect(this.authService),
this.videoCaptionService
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 { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
import { Injectable } from '@angular/core'
VideoTranscodingCreate,
VideoUpdate
} from '@shared/models'
+import { VideoSource } from '@shared/models/videos/video-source'
import { environment } from '../../../../environments/environment'
import { Account } from '../account/account.model'
import { AccountService } from '../account/account.service'
)
}
+ 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) {
return this.setVideoRate(id, 'like')
}
setDefaultVideosSort,
videosCustomGetValidator,
videosGetValidator,
+ videoSourceGetValidator,
videosRemoveValidator,
videosSortValidator
} from '../../../middlewares'
asyncMiddleware(videosGetValidator),
asyncMiddleware(getVideoDescription)
)
+
+videosRouter.get('/:id/source',
+ openapiOperationDoc({ operationId: 'getVideoSource' }),
+ authenticate,
+ asyncMiddleware(videoSourceGetValidator),
+ getVideoSource
+)
+
videosRouter.get('/:id',
openapiOperationDoc({ operationId: 'getVideo' }),
optionalAuthenticate,
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) {
const serverActor = await getServerActor()
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/video'
import { VideoFileModel } from '../../../models/video/video-file'
+import { VideoSourceModel } from '@server/models/video/video-source'
const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
const videoFile = await buildNewFile(videoPhysicalFile)
+ const originalFilename = videoPhysicalFile.originalname
// Move physical file
const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
video.VideoFiles = [ videoFile ]
+ await VideoSourceModel.create({
+ filename: originalFilename,
+ videoId: video.id
+ }, { transaction: t })
+
await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
// Schedule an update in the future?
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 710
+const LAST_MIGRATION_VERSION = 715
// ---------------------------------------------------------------------------
import { VideoTagModel } from '../models/video/video-tag'
import { VideoViewModel } from '../models/view/video-view'
import { CONFIG } from './config'
+import { VideoSourceModel } from '@server/models/video/video-source'
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
VideoChannelModel,
VideoShareModel,
VideoFileModel,
+ VideoSourceModel,
VideoCaptionModel,
VideoBlacklistModel,
VideoTagModel,
--- /dev/null
+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
+}
export * from './video-view'
export * from './video-rates'
export * from './video-shares'
+export * from './video-source'
export * from './video-stats'
export * from './video-studio'
export * from './video-transcoding'
--- /dev/null
+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
+}
if (!await isVideoAccepted(req, res, file)) return cleanup()
- res.locals.videoFileResumable = file
+ res.locals.videoFileResumable = { ...file, originalname: file.filename }
return next()
}
--- /dev/null
+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
+ }
+ }
+}
import { VideoShareModel } from './video-share'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { VideoTagModel } from './video-tag'
+import { VideoSourceModel } from './video-source'
export enum ScopeNames {
FOR_API = 'FOR_API',
})
VideoPlaylistElements: VideoPlaylistElementModel[]
+ @HasOne(() => VideoSourceModel, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: true
+ },
+ onDelete: 'CASCADE'
+ })
+ VideoSource: VideoSourceModel
+
@HasMany(() => VideoAbuseModel, {
foreignKey: {
name: 'videoId',
import './blocklist'
import './bulk'
import './config'
-import './custom-pages'
import './contact-form'
+import './custom-pages'
import './debug'
import './follows'
import './jobs'
+import './live'
import './logs'
import './my-user'
-import './live'
import './plugins'
import './redundancy'
import './search'
import './video-captions'
import './video-channels'
import './video-comments'
-import './video-studio'
+import './video-files'
import './video-imports'
import './video-playlists'
-import './videos'
+import './video-source'
+import './video-studio'
import './videos-common-filters'
-import './video-files'
import './videos-history'
import './videos-overviews'
+import './videos'
import './views'
--- /dev/null
+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 ])
+ })
+})
import './videos-common-filters'
import './videos-history'
import './videos-overview'
+import './video-source'
--- /dev/null
+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 ])
+ })
+})
MVideoThumbnail
} from './models'
import { Writable } from 'stream'
+import { MVideoSource } from './models/video/video-source'
declare module 'express' {
export interface Request {
} | UploadFileForCheck[]
// 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
path: string
filename: string
+ originalname: string
}
// Extends Response with added functions and potential variables passed by middlewares
videoShare?: MVideoShareActor
+ videoSource?: MVideoSource
+
videoFile?: MVideoFile
videoFileResumable?: EnhancedUploadXFile
--- /dev/null
+import { VideoSourceModel } from '@server/models/video/video-source'
+
+export type MVideoSource = Omit<VideoSourceModel, 'Video'>
--- /dev/null
+export interface VideoSource {
+ filename: string
+}
import { unwrapBody } from '../requests'
import { waitJobs } from '../server'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
+import { VideoSource } from '@shared/models/videos/video-source'
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
fixture?: string
})
}
+ 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 & {
uuid: number | string
}) {
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)**
+ '/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':
post:
summary: Notify user is watching a video
$ref: '#/components/schemas/VideoConstantString-Language'
captionPath:
type: string
+ VideoSource:
+ properties:
+ filename:
+ type: string
ActorImage:
properties:
path: