aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html15
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html1
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts5
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts5
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts17
-rw-r--r--server/controllers/api/videos/index.ts13
-rw-r--r--server/controllers/api/videos/upload.ts7
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/migrations/0715-video-source.ts34
-rw-r--r--server/middlewares/validators/videos/index.ts1
-rw-r--r--server/middlewares/validators/videos/video-source.ts37
-rw-r--r--server/middlewares/validators/videos/videos.ts2
-rw-r--r--server/models/video/video-source.ts55
-rw-r--r--server/models/video/video.ts10
-rw-r--r--server/tests/api/check-params/index.ts11
-rw-r--r--server/tests/api/check-params/video-source.ts44
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/video-source.ts39
-rw-r--r--server/types/express.d.ts6
-rw-r--r--server/types/models/video/video-source.ts3
-rw-r--r--shared/models/videos/video-source.ts3
-rw-r--r--shared/server-commands/videos/videos-command.ts15
-rw-r--r--support/doc/api/openapi.yaml20
25 files changed, 339 insertions, 11 deletions
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 595200c3b..650448a74 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -340,6 +340,21 @@
340 </div> 340 </div>
341 341
342 <div class="col-md-12 col-xl-4"> 342 <div class="col-md-12 col-xl-4">
343
344 <div *ngIf="videoSource" class="form-group">
345 <label i18n for="filename">Filename</label>
346
347 <my-help>
348 <ng-template ptTemplate="preHtml">
349 <ng-container i18n>
350 Name of the uploaded file
351 </ng-container>
352 </ng-template>
353 </my-help>
354
355 <input type="text" [disabled]="true" id="filename" class="form-control" [value]="videoSource.filename" />
356 </div>
357
343 <div class="form-group originally-published-at"> 358 <div class="form-group originally-published-at">
344 <label i18n for="originallyPublishedAt">Original publication date</label> 359 <label i18n for="originallyPublishedAt">Original publication date</label>
345 <my-help> 360 <my-help>
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 16b964482..c74ef5731 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -37,6 +37,7 @@ import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
37import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 37import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
38import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component' 38import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component'
39import { VideoEditType } from './video-edit.type' 39import { VideoEditType } from './video-edit.type'
40import { VideoSource } from '@shared/models/videos/video-source'
40 41
41type VideoLanguages = VideoConstant<string> & { group?: string } 42type VideoLanguages = VideoConstant<string> & { group?: string }
42type PluginField = { 43type PluginField = {
@@ -61,6 +62,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
61 @Input() forbidScheduledPublication = true 62 @Input() forbidScheduledPublication = true
62 63
63 @Input() videoCaptions: VideoCaptionWithPathEdit[] = [] 64 @Input() videoCaptions: VideoCaptionWithPathEdit[] = []
65 @Input() videoSource: VideoSource
64 66
65 @Input() waitTranscodingEnabled = true 67 @Input() waitTranscodingEnabled = true
66 @Input() type: VideoEditType 68 @Input() type: VideoEditType
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
index 6a32f1477..ffd125695 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -12,6 +12,7 @@
12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()" 12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()"
13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" 13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
14 [liveVideo]="liveVideo" [videoToUpdate]="videoDetails" 14 [liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
15 [videoSource]="videoSource"
15 16
16 (formBuilt)="onFormBuilt()" 17 (formBuilt)="onFormBuilt()"
17 ></my-video-edit> 18 ></my-video-edit>
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index 9c4998f2e..43e8ba3e5 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -10,6 +10,7 @@ import { LiveVideoService } from '@app/shared/shared-video-live'
10import { LoadingBarService } from '@ngx-loading-bar/core' 10import { LoadingBarService } from '@ngx-loading-bar/core'
11import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' 11import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models'
12import { hydrateFormFromVideo } from './shared/video-edit-utils' 12import { hydrateFormFromVideo } from './shared/video-edit-utils'
13import { VideoSource } from '@shared/models/videos/video-source'
13 14
14@Component({ 15@Component({
15 selector: 'my-videos-update', 16 selector: 'my-videos-update',
@@ -19,6 +20,7 @@ import { hydrateFormFromVideo } from './shared/video-edit-utils'
19export class VideoUpdateComponent extends FormReactive implements OnInit { 20export class VideoUpdateComponent extends FormReactive implements OnInit {
20 video: VideoEdit 21 video: VideoEdit
21 videoDetails: VideoDetails 22 videoDetails: VideoDetails
23 videoSource: VideoSource
22 userVideoChannels: SelectChannelItem[] = [] 24 userVideoChannels: SelectChannelItem[] = []
23 videoCaptions: VideoCaptionEdit[] = [] 25 videoCaptions: VideoCaptionEdit[] = []
24 liveVideo: LiveVideo 26 liveVideo: LiveVideo
@@ -46,13 +48,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
46 this.buildForm({}) 48 this.buildForm({})
47 49
48 const { videoData } = this.route.snapshot.data 50 const { videoData } = this.route.snapshot.data
49 const { video, videoChannels, videoCaptions, liveVideo } = videoData 51 const { video, videoChannels, videoCaptions, videoSource, liveVideo } = videoData
50 52
51 this.video = new VideoEdit(video) 53 this.video = new VideoEdit(video)
52 this.videoDetails = video 54 this.videoDetails = video
53 55
54 this.userVideoChannels = videoChannels 56 this.userVideoChannels = videoChannels
55 this.videoCaptions = videoCaptions 57 this.videoCaptions = videoCaptions
58 this.videoSource = videoSource
56 this.liveVideo = liveVideo 59 this.liveVideo = liveVideo
57 60
58 this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE 61 this.forbidScheduledPublication = this.video.privacy !== VideoPrivacy.PRIVATE
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts
index 82dae5c1c..db5017340 100644
--- a/client/src/app/+videos/+video-edit/video-update.resolver.ts
+++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts
@@ -23,7 +23,8 @@ export class VideoUpdateResolver implements Resolve<any> {
23 return this.videoService.getVideo({ videoId: uuid }) 23 return this.videoService.getVideo({ videoId: uuid })
24 .pipe( 24 .pipe(
25 switchMap(video => forkJoin(this.buildVideoObservables(video))), 25 switchMap(video => forkJoin(this.buildVideoObservables(video))),
26 map(([ video, videoChannels, videoCaptions, liveVideo ]) => ({ video, videoChannels, videoCaptions, liveVideo })) 26 map(([ video, videoSource, videoChannels, videoCaptions, liveVideo ]) =>
27 ({ video, videoChannels, videoCaptions, videoSource, liveVideo }))
27 ) 28 )
28 } 29 }
29 30
@@ -33,6 +34,8 @@ export class VideoUpdateResolver implements Resolve<any> {
33 .loadCompleteDescription(video.descriptionPath) 34 .loadCompleteDescription(video.descriptionPath)
34 .pipe(map(description => Object.assign(video, { description }))), 35 .pipe(map(description => Object.assign(video, { description }))),
35 36
37 this.videoService.getSource(video.id),
38
36 listUserChannelsForSelect(this.authService), 39 listUserChannelsForSelect(this.authService),
37 40
38 this.videoCaptionService 41 this.videoCaptionService
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 142367506..83bc4eeb6 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -1,5 +1,5 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { from, Observable } from 'rxjs' 2import { from, Observable, of } from 'rxjs'
3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' 3import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' 4import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
@@ -24,6 +24,7 @@ import {
24 VideoTranscodingCreate, 24 VideoTranscodingCreate,
25 VideoUpdate 25 VideoUpdate
26} from '@shared/models' 26} from '@shared/models'
27import { VideoSource } from '@shared/models/videos/video-source'
27import { environment } from '../../../../environments/environment' 28import { environment } from '../../../../environments/environment'
28import { Account } from '../account/account.model' 29import { Account } from '../account/account.model'
29import { AccountService } from '../account/account.service' 30import { AccountService } from '../account/account.service'
@@ -323,6 +324,20 @@ export class VideoService {
323 ) 324 )
324 } 325 }
325 326
327 getSource (videoId: number) {
328 return this.authHttp
329 .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source')
330 .pipe(
331 catchError(err => {
332 if (err.status === 404) {
333 return of(undefined)
334 }
335
336 this.restExtractor.handleError(err)
337 })
338 )
339 }
340
326 setVideoLike (id: number) { 341 setVideoLike (id: number) {
327 return this.setVideoRate(id, 'like') 342 return this.setVideoRate(id, 'like')
328 } 343 }
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index be233722c..d4e08293e 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -26,6 +26,7 @@ import {
26 setDefaultVideosSort, 26 setDefaultVideosSort,
27 videosCustomGetValidator, 27 videosCustomGetValidator,
28 videosGetValidator, 28 videosGetValidator,
29 videoSourceGetValidator,
29 videosRemoveValidator, 30 videosRemoveValidator,
30 videosSortValidator 31 videosSortValidator
31} from '../../../middlewares' 32} from '../../../middlewares'
@@ -96,6 +97,14 @@ videosRouter.get('/:id/description',
96 asyncMiddleware(videosGetValidator), 97 asyncMiddleware(videosGetValidator),
97 asyncMiddleware(getVideoDescription) 98 asyncMiddleware(getVideoDescription)
98) 99)
100
101videosRouter.get('/:id/source',
102 openapiOperationDoc({ operationId: 'getVideoSource' }),
103 authenticate,
104 asyncMiddleware(videoSourceGetValidator),
105 getVideoSource
106)
107
99videosRouter.get('/:id', 108videosRouter.get('/:id',
100 openapiOperationDoc({ operationId: 'getVideo' }), 109 openapiOperationDoc({ operationId: 'getVideo' }),
101 optionalAuthenticate, 110 optionalAuthenticate,
@@ -155,6 +164,10 @@ async function getVideoDescription (req: express.Request, res: express.Response)
155 return res.json({ description }) 164 return res.json({ description })
156} 165}
157 166
167function getVideoSource (req: express.Request, res: express.Response) {
168 return res.json(res.locals.videoSource.toFormattedJSON())
169}
170
158async function listVideos (req: express.Request, res: express.Response) { 171async function listVideos (req: express.Request, res: express.Response) {
159 const serverActor = await getServerActor() 172 const serverActor = await getServerActor()
160 173
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 3afbedbb2..c5890691e 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -44,6 +44,7 @@ import {
44import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 44import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
45import { VideoModel } from '../../../models/video/video' 45import { VideoModel } from '../../../models/video/video'
46import { VideoFileModel } from '../../../models/video/video-file' 46import { VideoFileModel } from '../../../models/video/video-file'
47import { VideoSourceModel } from '@server/models/video/video-source'
47 48
48const lTags = loggerTagsFactory('api', 'video') 49const lTags = loggerTagsFactory('api', 'video')
49const auditLogger = auditLoggerFactory('videos') 50const auditLogger = auditLoggerFactory('videos')
@@ -151,6 +152,7 @@ async function addVideo (options: {
151 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 152 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
152 153
153 const videoFile = await buildNewFile(videoPhysicalFile) 154 const videoFile = await buildNewFile(videoPhysicalFile)
155 const originalFilename = videoPhysicalFile.originalname
154 156
155 // Move physical file 157 // Move physical file
156 const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) 158 const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
@@ -181,6 +183,11 @@ async function addVideo (options: {
181 183
182 video.VideoFiles = [ videoFile ] 184 video.VideoFiles = [ videoFile ]
183 185
186 await VideoSourceModel.create({
187 filename: originalFilename,
188 videoId: video.id
189 }, { transaction: t })
190
184 await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) 191 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
185 192
186 // Schedule an update in the future? 193 // Schedule an update in the future?
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index f54ce9506..0d7e7077d 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 710 27const LAST_MIGRATION_VERSION = 715
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 3576f444c..09786a91f 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -49,6 +49,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
49import { VideoTagModel } from '../models/video/video-tag' 49import { VideoTagModel } from '../models/video/video-tag'
50import { VideoViewModel } from '../models/view/video-view' 50import { VideoViewModel } from '../models/view/video-view'
51import { CONFIG } from './config' 51import { CONFIG } from './config'
52import { VideoSourceModel } from '@server/models/video/video-source'
52 53
53require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 54require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
54 55
@@ -126,6 +127,7 @@ async function initDatabaseModels (silent: boolean) {
126 VideoChannelModel, 127 VideoChannelModel,
127 VideoShareModel, 128 VideoShareModel,
128 VideoFileModel, 129 VideoFileModel,
130 VideoSourceModel,
129 VideoCaptionModel, 131 VideoCaptionModel,
130 VideoBlacklistModel, 132 VideoBlacklistModel,
131 VideoTagModel, 133 VideoTagModel,
diff --git a/server/initializers/migrations/0715-video-source.ts b/server/initializers/migrations/0715-video-source.ts
new file mode 100644
index 000000000..efcf77ebd
--- /dev/null
+++ b/server/initializers/migrations/0715-video-source.ts
@@ -0,0 +1,34 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `
11 CREATE TABLE IF NOT EXISTS "videoSource" (
12 "id" SERIAL ,
13 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
14 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
15 "filename" VARCHAR(255) DEFAULT NULL,
16 "videoId" INTEGER
17 REFERENCES "video" ("id")
18 ON DELETE CASCADE
19 ON UPDATE CASCADE,
20 PRIMARY KEY ("id")
21 );
22 `
23 await utils.sequelize.query(query)
24 }
25}
26
27function down (options) {
28 throw new Error('Not implemented.')
29}
30
31export {
32 up,
33 down
34}
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index bd2590bc5..1dd7b5d2e 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -9,6 +9,7 @@ export * from './video-ownership-changes'
9export * from './video-view' 9export * from './video-view'
10export * from './video-rates' 10export * from './video-rates'
11export * from './video-shares' 11export * from './video-shares'
12export * from './video-source'
12export * from './video-stats' 13export * from './video-stats'
13export * from './video-studio' 14export * from './video-studio'
14export * from './video-transcoding' 15export * from './video-transcoding'
diff --git a/server/middlewares/validators/videos/video-source.ts b/server/middlewares/validators/videos/video-source.ts
new file mode 100644
index 000000000..31a2f16b3
--- /dev/null
+++ b/server/middlewares/validators/videos/video-source.ts
@@ -0,0 +1,37 @@
1import express from 'express'
2import { getVideoWithAttributes } from '@server/helpers/video'
3import { VideoSourceModel } from '@server/models/video/video-source'
4import { MVideoFullLight } from '@server/types/models'
5import { HttpStatusCode, UserRight } from '@shared/models'
6import { logger } from '../../../helpers/logger'
7import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
8
9const videoSourceGetValidator = [
10 isValidVideoIdParam('id'),
11
12 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
13 logger.debug('Checking videoSourceGet parameters', { parameters: req.params })
14
15 if (areValidationErrors(req, res)) return
16 if (!await doesVideoExist(req.params.id, res, 'for-api')) return
17
18 const video = getVideoWithAttributes(res) as MVideoFullLight
19
20 res.locals.videoSource = await VideoSourceModel.loadByVideoId(video.id)
21 if (!res.locals.videoSource) {
22 return res.fail({
23 status: HttpStatusCode.NOT_FOUND_404,
24 message: 'Video source not found'
25 })
26 }
27
28 const user = res.locals.oauth.token.User
29 if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
30
31 return next()
32 }
33]
34
35export {
36 videoSourceGetValidator
37}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 0b6b8bfe5..c75c3640b 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -152,7 +152,7 @@ const videosAddResumableValidator = [
152 152
153 if (!await isVideoAccepted(req, res, file)) return cleanup() 153 if (!await isVideoAccepted(req, res, file)) return cleanup()
154 154
155 res.locals.videoFileResumable = file 155 res.locals.videoFileResumable = { ...file, originalname: file.filename }
156 156
157 return next() 157 return next()
158 } 158 }
diff --git a/server/models/video/video-source.ts b/server/models/video/video-source.ts
new file mode 100644
index 000000000..e306b160d
--- /dev/null
+++ b/server/models/video/video-source.ts
@@ -0,0 +1,55 @@
1import { Op } from 'sequelize'
2import {
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 ForeignKey,
8 Model,
9 Table,
10 UpdatedAt
11} from 'sequelize-typescript'
12import { AttributesOnly } from '@shared/typescript-utils'
13import { VideoModel } from './video'
14
15@Table({
16 tableName: 'videoSource',
17 indexes: [
18 {
19 fields: [ 'videoId' ],
20 where: {
21 videoId: {
22 [Op.ne]: null
23 }
24 }
25 }
26 ]
27})
28export class VideoSourceModel extends Model<Partial<AttributesOnly<VideoSourceModel>>> {
29 @CreatedAt
30 createdAt: Date
31
32 @UpdatedAt
33 updatedAt: Date
34
35 @AllowNull(false)
36 @Column
37 filename: string
38
39 @ForeignKey(() => VideoModel)
40 @Column
41 videoId: number
42
43 @BelongsTo(() => VideoModel)
44 Video: VideoModel
45
46 static loadByVideoId (videoId) {
47 return VideoSourceModel.findOne({ where: { videoId } })
48 }
49
50 toFormattedJSON () {
51 return {
52 filename: this.filename
53 }
54 }
55}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index e6a8d3f95..08adbced6 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -136,6 +136,7 @@ import { VideoPlaylistElementModel } from './video-playlist-element'
136import { VideoShareModel } from './video-share' 136import { VideoShareModel } from './video-share'
137import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 137import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
138import { VideoTagModel } from './video-tag' 138import { VideoTagModel } from './video-tag'
139import { VideoSourceModel } from './video-source'
139 140
140export enum ScopeNames { 141export enum ScopeNames {
141 FOR_API = 'FOR_API', 142 FOR_API = 'FOR_API',
@@ -597,6 +598,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
597 }) 598 })
598 VideoPlaylistElements: VideoPlaylistElementModel[] 599 VideoPlaylistElements: VideoPlaylistElementModel[]
599 600
601 @HasOne(() => VideoSourceModel, {
602 foreignKey: {
603 name: 'videoId',
604 allowNull: true
605 },
606 onDelete: 'CASCADE'
607 })
608 VideoSource: VideoSourceModel
609
600 @HasMany(() => VideoAbuseModel, { 610 @HasMany(() => VideoAbuseModel, {
601 foreignKey: { 611 foreignKey: {
602 name: 'videoId', 612 name: 'videoId',
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 259d7e783..a27bc8509 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -3,14 +3,14 @@ import './accounts'
3import './blocklist' 3import './blocklist'
4import './bulk' 4import './bulk'
5import './config' 5import './config'
6import './custom-pages'
7import './contact-form' 6import './contact-form'
7import './custom-pages'
8import './debug' 8import './debug'
9import './follows' 9import './follows'
10import './jobs' 10import './jobs'
11import './live'
11import './logs' 12import './logs'
12import './my-user' 13import './my-user'
13import './live'
14import './plugins' 14import './plugins'
15import './redundancy' 15import './redundancy'
16import './search' 16import './search'
@@ -25,12 +25,13 @@ import './video-blacklist'
25import './video-captions' 25import './video-captions'
26import './video-channels' 26import './video-channels'
27import './video-comments' 27import './video-comments'
28import './video-studio' 28import './video-files'
29import './video-imports' 29import './video-imports'
30import './video-playlists' 30import './video-playlists'
31import './videos' 31import './video-source'
32import './video-studio'
32import './videos-common-filters' 33import './videos-common-filters'
33import './video-files'
34import './videos-history' 34import './videos-history'
35import './videos-overviews' 35import './videos-overviews'
36import './videos'
36import './views' 37import './views'
diff --git a/server/tests/api/check-params/video-source.ts b/server/tests/api/check-params/video-source.ts
new file mode 100644
index 000000000..ca324bb9d
--- /dev/null
+++ b/server/tests/api/check-params/video-source.ts
@@ -0,0 +1,44 @@
1import { HttpStatusCode } from '@shared/models'
2import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
3
4describe('Test video sources API validator', function () {
5 let server: PeerTubeServer = null
6 let uuid: string
7 let userToken: string
8
9 before(async function () {
10 this.timeout(30000)
11
12 server = await createSingleServer(1)
13 await setAccessTokensToServers([ server ])
14
15 const created = await server.videos.quickUpload({ name: 'video' })
16 uuid = created.uuid
17
18 userToken = await server.users.generateUserAndToken('user')
19 })
20
21 it('Should fail without a valid uuid', async function () {
22 await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
23 })
24
25 it('Should receive 404 when passing a non existing video id', async function () {
26 await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
27 })
28
29 it('Should not get the source as unauthenticated', async function () {
30 await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
31 })
32
33 it('Should not get the source with another user', async function () {
34 await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken })
35 })
36
37 it('Should succeed with the correct parameters get the source as another user', async function () {
38 await server.videos.getSource({ id: uuid })
39 })
40
41 after(async function () {
42 await cleanupTests([ server ])
43 })
44})
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 27b119f30..a0b6b01cf 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -16,3 +16,4 @@ import './video-schedule-update'
16import './videos-common-filters' 16import './videos-common-filters'
17import './videos-history' 17import './videos-history'
18import './videos-overview' 18import './videos-overview'
19import './video-source'
diff --git a/server/tests/api/videos/video-source.ts b/server/tests/api/videos/video-source.ts
new file mode 100644
index 000000000..e34642300
--- /dev/null
+++ b/server/tests/api/videos/video-source.ts
@@ -0,0 +1,39 @@
1import 'mocha'
2import * as chai from 'chai'
3import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
4
5const expect = chai.expect
6
7describe('Test video source', () => {
8 let server: PeerTubeServer = null
9 const fixture = 'video_short.webm'
10
11 before(async function () {
12 this.timeout(30000)
13
14 server = await createSingleServer(1)
15 await setAccessTokensToServers([ server ])
16 })
17
18 it('Should get the source filename with legacy upload', async function () {
19 this.timeout(30000)
20
21 const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
22
23 const source = await server.videos.getSource({ id: uuid })
24 expect(source.filename).to.equal(fixture)
25 })
26
27 it('Should get the source filename with resumable upload', async function () {
28 this.timeout(30000)
29
30 const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
31
32 const source = await server.videos.getSource({ id: uuid })
33 expect(source.filename).to.equal(fixture)
34 })
35
36 after(async function () {
37 await cleanupTests([ server ])
38 })
39})
diff --git a/server/types/express.d.ts b/server/types/express.d.ts
index 7cc13f21d..27e532c31 100644
--- a/server/types/express.d.ts
+++ b/server/types/express.d.ts
@@ -42,6 +42,7 @@ import {
42 MVideoThumbnail 42 MVideoThumbnail
43} from './models' 43} from './models'
44import { Writable } from 'stream' 44import { Writable } from 'stream'
45import { MVideoSource } from './models/video/video-source'
45 46
46declare module 'express' { 47declare module 'express' {
47 export interface Request { 48 export interface Request {
@@ -68,7 +69,7 @@ declare module 'express' {
68 } | UploadFileForCheck[] 69 } | UploadFileForCheck[]
69 70
70 // Upload file with a duration added by our middleware 71 // Upload file with a duration added by our middleware
71 export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & { 72 export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size', 'originalname'> & {
72 duration: number 73 duration: number
73 } 74 }
74 75
@@ -85,6 +86,7 @@ declare module 'express' {
85 duration: number 86 duration: number
86 path: string 87 path: string
87 filename: string 88 filename: string
89 originalname: string
88 } 90 }
89 91
90 // Extends Response with added functions and potential variables passed by middlewares 92 // Extends Response with added functions and potential variables passed by middlewares
@@ -123,6 +125,8 @@ declare module 'express' {
123 125
124 videoShare?: MVideoShareActor 126 videoShare?: MVideoShareActor
125 127
128 videoSource?: MVideoSource
129
126 videoFile?: MVideoFile 130 videoFile?: MVideoFile
127 131
128 videoFileResumable?: EnhancedUploadXFile 132 videoFileResumable?: EnhancedUploadXFile
diff --git a/server/types/models/video/video-source.ts b/server/types/models/video/video-source.ts
new file mode 100644
index 000000000..0948f3b2e
--- /dev/null
+++ b/server/types/models/video/video-source.ts
@@ -0,0 +1,3 @@
1import { VideoSourceModel } from '@server/models/video/video-source'
2
3export type MVideoSource = Omit<VideoSourceModel, 'Video'>
diff --git a/shared/models/videos/video-source.ts b/shared/models/videos/video-source.ts
new file mode 100644
index 000000000..57e54fc7f
--- /dev/null
+++ b/shared/models/videos/video-source.ts
@@ -0,0 +1,3 @@
1export interface VideoSource {
2 filename: string
3}
diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts
index 1cceb58db..e952c9777 100644
--- a/shared/server-commands/videos/videos-command.ts
+++ b/shared/server-commands/videos/videos-command.ts
@@ -23,6 +23,7 @@ import {
23import { unwrapBody } from '../requests' 23import { unwrapBody } from '../requests'
24import { waitJobs } from '../server' 24import { waitJobs } from '../server'
25import { AbstractCommand, OverrideCommandOptions } from '../shared' 25import { AbstractCommand, OverrideCommandOptions } from '../shared'
26import { VideoSource } from '@shared/models/videos/video-source'
26 27
27export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & { 28export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
28 fixture?: string 29 fixture?: string
@@ -150,6 +151,20 @@ export class VideosCommand extends AbstractCommand {
150 }) 151 })
151 } 152 }
152 153
154 getSource (options: OverrideCommandOptions & {
155 id: number | string
156 }) {
157 const path = '/api/v1/videos/' + options.id + '/source'
158
159 return this.getRequestBody<VideoSource>({
160 ...options,
161
162 path,
163 implicitToken: true,
164 defaultExpectedStatus: HttpStatusCode.OK_200
165 })
166 }
167
153 async getId (options: OverrideCommandOptions & { 168 async getId (options: OverrideCommandOptions & {
154 uuid: number | string 169 uuid: number | string
155 }) { 170 }) {
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index bd36c41cd..afd310c0b 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -1903,6 +1903,22 @@ paths:
1903 example: | 1903 example: |
1904 **[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)** 1904 **[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)**
1905 1905
1906 '/videos/{id}/source':
1907 post:
1908 summary: Get video source file metadata
1909 operationId: getVideoSource
1910 tags:
1911 - Video
1912 parameters:
1913 - $ref: '#/components/parameters/idOrUUID'
1914 responses:
1915 '200':
1916 description: successful operation
1917 content:
1918 application/json:
1919 schema:
1920 $ref: '#/components/schemas/VideoSource'
1921
1906 '/videos/{id}/views': 1922 '/videos/{id}/views':
1907 post: 1923 post:
1908 summary: Notify user is watching a video 1924 summary: Notify user is watching a video
@@ -6141,6 +6157,10 @@ components:
6141 $ref: '#/components/schemas/VideoConstantString-Language' 6157 $ref: '#/components/schemas/VideoConstantString-Language'
6142 captionPath: 6158 captionPath:
6143 type: string 6159 type: string
6160 VideoSource:
6161 properties:
6162 filename:
6163 type: string
6144 ActorImage: 6164 ActorImage:
6145 properties: 6165 properties:
6146 path: 6166 path: