diff options
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' | |||
37 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 37 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
38 | import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component' | 38 | import { VideoCaptionEditModalComponent } from './video-caption-edit-modal/video-caption-edit-modal.component' |
39 | import { VideoEditType } from './video-edit.type' | 39 | import { VideoEditType } from './video-edit.type' |
40 | import { VideoSource } from '@shared/models/videos/video-source' | ||
40 | 41 | ||
41 | type VideoLanguages = VideoConstant<string> & { group?: string } | 42 | type VideoLanguages = VideoConstant<string> & { group?: string } |
42 | type PluginField = { | 43 | type 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' | |||
10 | import { LoadingBarService } from '@ngx-loading-bar/core' | 10 | import { LoadingBarService } from '@ngx-loading-bar/core' |
11 | import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' | 11 | import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' |
12 | import { hydrateFormFromVideo } from './shared/video-edit-utils' | 12 | import { hydrateFormFromVideo } from './shared/video-edit-utils' |
13 | import { 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' | |||
19 | export class VideoUpdateComponent extends FormReactive implements OnInit { | 20 | export 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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { from, Observable } from 'rxjs' | 2 | import { from, Observable, of } from 'rxjs' |
3 | import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' | 3 | import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' |
4 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' | 4 | import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' |
5 | import { Injectable } from '@angular/core' | 5 | import { 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' |
27 | import { VideoSource } from '@shared/models/videos/video-source' | ||
27 | import { environment } from '../../../../environments/environment' | 28 | import { environment } from '../../../../environments/environment' |
28 | import { Account } from '../account/account.model' | 29 | import { Account } from '../account/account.model' |
29 | import { AccountService } from '../account/account.service' | 30 | import { 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 | |||
101 | videosRouter.get('/:id/source', | ||
102 | openapiOperationDoc({ operationId: 'getVideoSource' }), | ||
103 | authenticate, | ||
104 | asyncMiddleware(videoSourceGetValidator), | ||
105 | getVideoSource | ||
106 | ) | ||
107 | |||
99 | videosRouter.get('/:id', | 108 | videosRouter.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 | ||
167 | function getVideoSource (req: express.Request, res: express.Response) { | ||
168 | return res.json(res.locals.videoSource.toFormattedJSON()) | ||
169 | } | ||
170 | |||
158 | async function listVideos (req: express.Request, res: express.Response) { | 171 | async 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 { | |||
44 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 44 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
45 | import { VideoModel } from '../../../models/video/video' | 45 | import { VideoModel } from '../../../models/video/video' |
46 | import { VideoFileModel } from '../../../models/video/video-file' | 46 | import { VideoFileModel } from '../../../models/video/video-file' |
47 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
47 | 48 | ||
48 | const lTags = loggerTagsFactory('api', 'video') | 49 | const lTags = loggerTagsFactory('api', 'video') |
49 | const auditLogger = auditLoggerFactory('videos') | 50 | const 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 | ||
27 | const LAST_MIGRATION_VERSION = 710 | 27 | const 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 | |||
49 | import { VideoTagModel } from '../models/video/video-tag' | 49 | import { VideoTagModel } from '../models/video/video-tag' |
50 | import { VideoViewModel } from '../models/view/video-view' | 50 | import { VideoViewModel } from '../models/view/video-view' |
51 | import { CONFIG } from './config' | 51 | import { CONFIG } from './config' |
52 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
52 | 53 | ||
53 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 54 | require('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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
27 | function down (options) { | ||
28 | throw new Error('Not implemented.') | ||
29 | } | ||
30 | |||
31 | export { | ||
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' | |||
9 | export * from './video-view' | 9 | export * from './video-view' |
10 | export * from './video-rates' | 10 | export * from './video-rates' |
11 | export * from './video-shares' | 11 | export * from './video-shares' |
12 | export * from './video-source' | ||
12 | export * from './video-stats' | 13 | export * from './video-stats' |
13 | export * from './video-studio' | 14 | export * from './video-studio' |
14 | export * from './video-transcoding' | 15 | export * 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 @@ | |||
1 | import express from 'express' | ||
2 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
3 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
4 | import { MVideoFullLight } from '@server/types/models' | ||
5 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
8 | |||
9 | const 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 | |||
35 | export { | ||
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 @@ | |||
1 | import { Op } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | ForeignKey, | ||
8 | Model, | ||
9 | Table, | ||
10 | UpdatedAt | ||
11 | } from 'sequelize-typescript' | ||
12 | import { AttributesOnly } from '@shared/typescript-utils' | ||
13 | import { 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 | }) | ||
28 | export 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' | |||
136 | import { VideoShareModel } from './video-share' | 136 | import { VideoShareModel } from './video-share' |
137 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 137 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
138 | import { VideoTagModel } from './video-tag' | 138 | import { VideoTagModel } from './video-tag' |
139 | import { VideoSourceModel } from './video-source' | ||
139 | 140 | ||
140 | export enum ScopeNames { | 141 | export 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' | |||
3 | import './blocklist' | 3 | import './blocklist' |
4 | import './bulk' | 4 | import './bulk' |
5 | import './config' | 5 | import './config' |
6 | import './custom-pages' | ||
7 | import './contact-form' | 6 | import './contact-form' |
7 | import './custom-pages' | ||
8 | import './debug' | 8 | import './debug' |
9 | import './follows' | 9 | import './follows' |
10 | import './jobs' | 10 | import './jobs' |
11 | import './live' | ||
11 | import './logs' | 12 | import './logs' |
12 | import './my-user' | 13 | import './my-user' |
13 | import './live' | ||
14 | import './plugins' | 14 | import './plugins' |
15 | import './redundancy' | 15 | import './redundancy' |
16 | import './search' | 16 | import './search' |
@@ -25,12 +25,13 @@ import './video-blacklist' | |||
25 | import './video-captions' | 25 | import './video-captions' |
26 | import './video-channels' | 26 | import './video-channels' |
27 | import './video-comments' | 27 | import './video-comments' |
28 | import './video-studio' | 28 | import './video-files' |
29 | import './video-imports' | 29 | import './video-imports' |
30 | import './video-playlists' | 30 | import './video-playlists' |
31 | import './videos' | 31 | import './video-source' |
32 | import './video-studio' | ||
32 | import './videos-common-filters' | 33 | import './videos-common-filters' |
33 | import './video-files' | ||
34 | import './videos-history' | 34 | import './videos-history' |
35 | import './videos-overviews' | 35 | import './videos-overviews' |
36 | import './videos' | ||
36 | import './views' | 37 | import './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 @@ | |||
1 | import { HttpStatusCode } from '@shared/models' | ||
2 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
3 | |||
4 | describe('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' | |||
16 | import './videos-common-filters' | 16 | import './videos-common-filters' |
17 | import './videos-history' | 17 | import './videos-history' |
18 | import './videos-overview' | 18 | import './videos-overview' |
19 | import './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 @@ | |||
1 | import 'mocha' | ||
2 | import * as chai from 'chai' | ||
3 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
4 | |||
5 | const expect = chai.expect | ||
6 | |||
7 | describe('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' |
44 | import { Writable } from 'stream' | 44 | import { Writable } from 'stream' |
45 | import { MVideoSource } from './models/video/video-source' | ||
45 | 46 | ||
46 | declare module 'express' { | 47 | declare 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 @@ | |||
1 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
2 | |||
3 | export 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 @@ | |||
1 | export 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 { | |||
23 | import { unwrapBody } from '../requests' | 23 | import { unwrapBody } from '../requests' |
24 | import { waitJobs } from '../server' | 24 | import { waitJobs } from '../server' |
25 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | 25 | import { AbstractCommand, OverrideCommandOptions } from '../shared' |
26 | import { VideoSource } from '@shared/models/videos/video-source' | ||
26 | 27 | ||
27 | export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & { | 28 | export 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: |