aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-12-11 14:52:50 +0100
committerChocobozzz <me@florianbigard.com>2018-12-11 15:11:09 +0100
commit14e2014acc1362cfbb770c051a7254b156cd8efb (patch)
tree8b7d5aedd9fe0beff8b971c9bae7781ba2069228
parent8923187455c5aa7167d813c5c745d3857f183fd7 (diff)
downloadPeerTube-14e2014acc1362cfbb770c051a7254b156cd8efb.tar.gz
PeerTube-14e2014acc1362cfbb770c051a7254b156cd8efb.tar.zst
PeerTube-14e2014acc1362cfbb770c051a7254b156cd8efb.zip
Support additional video extensions
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts3
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.html4
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.ts5
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html1
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts1
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.html3
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts9
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-update.component.ts7
-rw-r--r--config/default.yaml2
-rw-r--r--config/production.yaml.example2
-rw-r--r--config/test-2.yaml1
-rw-r--r--config/test.yaml1
-rw-r--r--server/controllers/api/config.ts5
-rw-r--r--server/controllers/api/users/me.ts4
-rw-r--r--server/controllers/api/video-channel.ts4
-rw-r--r--server/controllers/api/videos/captions.ts4
-rw-r--r--server/controllers/api/videos/import.ts11
-rw-r--r--server/controllers/api/videos/index.ts12
-rw-r--r--server/helpers/custom-validators/video-captions.ts4
-rw-r--r--server/helpers/custom-validators/video-imports.ts4
-rw-r--r--server/helpers/custom-validators/videos.ts13
-rw-r--r--server/initializers/checker-before-init.ts2
-rw-r--r--server/initializers/constants.ts96
-rw-r--r--server/initializers/migrations/0295-video-file-extname.ts49
-rw-r--r--server/lib/activitypub/actor.ts7
-rw-r--r--server/lib/activitypub/videos.ts7
-rw-r--r--server/models/redundancy/video-redundancy.ts6
-rw-r--r--server/models/video/video-file.ts4
-rw-r--r--server/models/video/video-format-utils.ts6
-rw-r--r--server/tests/api/check-params/config.ts1
-rw-r--r--server/tests/api/check-params/videos.ts7
-rw-r--r--server/tests/api/server/config.ts30
-rw-r--r--server/tests/api/videos/video-transcoder.ts31
-rw-r--r--server/tests/fixtures/video_short.avibin0 -> 584656 bytes
-rw-r--r--server/tests/fixtures/video_short.mkvbin0 -> 40642 bytes
-rw-r--r--shared/models/server/custom-config.model.ts1
-rw-r--r--shared/utils/server/config.ts1
39 files changed, 268 insertions, 90 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index fd4d3d9c9..6ece7e8bc 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -220,6 +220,14 @@
220 <ng-template [ngIf]="isTranscodingEnabled()"> 220 <ng-template [ngIf]="isTranscodingEnabled()">
221 221
222 <div class="form-group"> 222 <div class="form-group">
223 <my-peertube-checkbox
224 inputName="transcodingAllowAdditionalExtensions" formControlName="transcodingAllowAdditionalExtensions"
225 i18n-labelText labelText="Allow additional extensions"
226 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
227 ></my-peertube-checkbox>
228 </div>
229
230 <div class="form-group">
223 <label i18n for="transcodingThreads">Transcoding threads</label> 231 <label i18n for="transcodingThreads">Transcoding threads</label>
224 <div class="peertube-select-container"> 232 <div class="peertube-select-container">
225 <select id="transcodingThreads" formControlName="transcodingThreads"> 233 <select id="transcodingThreads" formControlName="transcodingThreads">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index f48b6fc1a..6eea1cd76 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -82,6 +82,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
82 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 82 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
83 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, 83 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY,
84 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 84 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS,
85 transcodingAllowAdditionalExtensions: null,
85 transcodingEnabled: null, 86 transcodingEnabled: null,
86 customizationJavascript: null, 87 customizationJavascript: null,
87 customizationCSS: null 88 customizationCSS: null
@@ -163,6 +164,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
163 }, 164 },
164 transcoding: { 165 transcoding: {
165 enabled: this.form.value['transcodingEnabled'], 166 enabled: this.form.value['transcodingEnabled'],
167 allowAdditionalExtensions: this.form.value['transcodingAllowAdditionalExtensions'],
166 threads: this.form.value['transcodingThreads'], 168 threads: this.form.value['transcodingThreads'],
167 resolutions: { 169 resolutions: {
168 '240p': this.form.value[this.getResolutionKey('240p')], 170 '240p': this.form.value[this.getResolutionKey('240p')],
@@ -221,6 +223,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
221 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily, 223 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
222 transcodingThreads: this.customConfig.transcoding.threads, 224 transcodingThreads: this.customConfig.transcoding.threads,
223 transcodingEnabled: this.customConfig.transcoding.enabled, 225 transcodingEnabled: this.customConfig.transcoding.enabled,
226 transcodingAllowAdditionalExtensions: this.customConfig.transcoding.allowAdditionalExtensions,
224 customizationJavascript: this.customConfig.instance.customizations.javascript, 227 customizationJavascript: this.customConfig.instance.customizations.javascript,
225 customizationCSS: this.customConfig.instance.customizations.css, 228 customizationCSS: this.customConfig.instance.customizations.css,
226 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled, 229 importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.html b/client/src/app/shared/forms/peertube-checkbox.component.html
index fb3006b53..7b8bcf601 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/forms/peertube-checkbox.component.html
@@ -1,10 +1,10 @@
1<div class="root"> 1<div class="root">
2 <label class="form-group-checkbox"> 2 <label class="form-group-checkbox">
3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="isDisabled" /> 3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" />
4 <span role="checkbox" [attr.aria-checked]="checked"></span> 4 <span role="checkbox" [attr.aria-checked]="checked"></span>
5 <span *ngIf="labelText">{{ labelText }}</span> 5 <span *ngIf="labelText">{{ labelText }}</span>
6 <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span> 6 <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span>
7 </label> 7 </label>
8 8
9 <my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help> 9 <my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help>
10</div> \ No newline at end of file 10</div>
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.ts b/client/src/app/shared/forms/peertube-checkbox.component.ts
index bbc9904df..c1a6915e8 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.ts
+++ b/client/src/app/shared/forms/peertube-checkbox.component.ts
@@ -19,8 +19,7 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor {
19 @Input() labelText: string 19 @Input() labelText: string
20 @Input() labelHtml: string 20 @Input() labelHtml: string
21 @Input() helpHtml: string 21 @Input() helpHtml: string
22 22 @Input() disabled = false
23 isDisabled = false
24 23
25 propagateChange = (_: any) => { /* empty */ } 24 propagateChange = (_: any) => { /* empty */ }
26 25
@@ -41,6 +40,6 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor {
41 } 40 }
42 41
43 setDisabledState (isDisabled: boolean) { 42 setDisabledState (isDisabled: boolean) {
44 this.isDisabled = isDisabled 43 this.disabled = isDisabled
45 } 44 }
46} 45}
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 33c766d87..bd52d686a 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
@@ -126,6 +126,7 @@
126 ></my-peertube-checkbox> 126 ></my-peertube-checkbox>
127 127
128 <my-peertube-checkbox 128 <my-peertube-checkbox
129 *ngIf="waitTranscodingEnabled"
129 inputName="waitTranscoding" formControlName="waitTranscoding" 130 inputName="waitTranscoding" formControlName="waitTranscoding"
130 i18n-labelText labelText="Wait transcoding before publishing the video" 131 i18n-labelText labelText="Wait transcoding before publishing the video"
131 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." 132 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
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 a56733e57..1d35b4ba8 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
@@ -27,6 +27,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] 27 @Input() userVideoChannels: { id: number, label: string, support: string }[] = []
28 @Input() schedulePublicationPossible = true 28 @Input() schedulePublicationPossible = true
29 @Input() videoCaptions: VideoCaptionEdit[] = [] 29 @Input() videoCaptions: VideoCaptionEdit[] = []
30 @Input() waitTranscodingEnabled = true
30 31
31 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent 32 @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent
32 33
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index a09f54dfc..289a28c66 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -6,7 +6,7 @@
6 <span i18n>Select the file to upload</span> 6 <span i18n>Select the file to upload</span>
7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> 7 <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" />
8 </div> 8 </div>
9 <span class="button-file-extension">(.mp4, .webm, .ogv)</span> 9 <span class="button-file-extension">({{ videoExtensions }})</span>
10 10
11 <div class="form-group form-group-channel"> 11 <div class="form-group form-group-channel">
12 <label i18n for="first-step-channel">Channel</label> 12 <label i18n for="first-step-channel">Channel</label>
@@ -47,6 +47,7 @@
47 <my-video-edit 47 <my-video-edit
48 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 48 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
49 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 49 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
50 [waitTranscodingEnabled]="waitTranscodingEnabled"
50 ></my-video-edit> 51 ></my-video-edit>
51 52
52 <div class="submit-container"> 53 <div class="submit-container">
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index 7ea3691fa..2180e22ab 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -44,6 +44,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
44 id: 0, 44 id: 0,
45 uuid: '' 45 uuid: ''
46 } 46 }
47 waitTranscodingEnabled = true
47 48
48 error: string 49 error: string
49 50
@@ -117,6 +118,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
117 const videofile = this.videofileInput.nativeElement.files[0] 118 const videofile = this.videofileInput.nativeElement.files[0]
118 if (!videofile) return 119 if (!videofile) return
119 120
121 // Check global user quota
120 const bytePipes = new BytesPipe() 122 const bytePipes = new BytesPipe()
121 const videoQuota = this.authService.getUser().videoQuota 123 const videoQuota = this.authService.getUser().videoQuota
122 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { 124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
@@ -132,6 +134,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
132 return 134 return
133 } 135 }
134 136
137 // Check daily user quota
135 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily 138 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
136 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { 139 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
137 const msg = this.i18n( 140 const msg = this.i18n(
@@ -146,6 +149,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
146 return 149 return
147 } 150 }
148 151
152 // Build name field
149 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') 153 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
150 let name: string 154 let name: string
151 155
@@ -153,6 +157,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
153 if (nameWithoutExtension.length < 3) name = videofile.name 157 if (nameWithoutExtension.length < 3) name = videofile.name
154 else name = nameWithoutExtension 158 else name = nameWithoutExtension
155 159
160 // Force user to wait transcoding for unsupported video types in web browsers
161 if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) {
162 this.waitTranscodingEnabled = false
163 }
164
156 const privacy = this.firstStepPrivacyId.toString() 165 const privacy = this.firstStepPrivacyId.toString()
157 const nsfw = false 166 const nsfw = false
158 const waitTranscoding = true 167 const waitTranscoding = true
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 9242c30a0..0457778c0 100644
--- a/client/src/app/videos/+video-edit/video-update.component.html
+++ b/client/src/app/videos/+video-edit/video-update.component.html
@@ -8,7 +8,7 @@
8 <my-video-edit 8 <my-video-edit
9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" 9 [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" 10 [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
11 [videoCaptions]="videoCaptions" 11 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled"
12 ></my-video-edit> 12 ></my-video-edit>
13 13
14 <div class="submit-container"> 14 <div class="submit-container">
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 3a0f3a39a..d99a02b18 100644
--- a/client/src/app/videos/+video-edit/video-update.component.ts
+++ b/client/src/app/videos/+video-edit/video-update.component.ts
@@ -12,6 +12,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 12import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
13import { VideoCaptionService } from '@app/shared/video-caption' 13import { VideoCaptionService } from '@app/shared/video-caption'
14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 14import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
15import { VideoDetails } from '@app/shared/video/video-details.model'
15 16
16@Component({ 17@Component({
17 selector: 'my-videos-update', 18 selector: 'my-videos-update',
@@ -26,6 +27,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
26 userVideoChannels: { id: number, label: string, support: string }[] = [] 27 userVideoChannels: { id: number, label: string, support: string }[] = []
27 schedulePublicationPossible = false 28 schedulePublicationPossible = false
28 videoCaptions: VideoCaptionEdit[] = [] 29 videoCaptions: VideoCaptionEdit[] = []
30 waitTranscodingEnabled = true
29 31
30 private updateDone = false 32 private updateDone = false
31 33
@@ -65,6 +67,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
65 67
66 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) 68 this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies)
67 69
70 const videoFiles = (video as VideoDetails).files
71 if (videoFiles.length > 1) { // Already transcoded
72 this.waitTranscodingEnabled = false
73 }
74
68 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout 75 // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout
69 setTimeout(() => this.hydrateFormFromVideo()) 76 setTimeout(() => this.hydrateFormFromVideo())
70 }, 77 },
diff --git a/config/default.yaml b/config/default.yaml
index d95fdc57b..080638a13 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -124,6 +124,8 @@ user:
124# Please, do not disable transcoding since many uploaded videos will not work 124# Please, do not disable transcoding since many uploaded videos will not work
125transcoding: 125transcoding:
126 enabled: true 126 enabled: true
127 # Allow your users to upload .mkv, .mov, .avi, .flv videos
128 allow_additional_extensions: true
127 threads: 1 129 threads: 1
128 resolutions: # Only created if the original video has a higher resolution, uses more storage! 130 resolutions: # Only created if the original video has a higher resolution, uses more storage!
129 240p: false 131 240p: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 4c50a550b..770bb97da 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -137,6 +137,8 @@ user:
137# Please, do not disable transcoding since many uploaded videos will not work 137# Please, do not disable transcoding since many uploaded videos will not work
138transcoding: 138transcoding:
139 enabled: true 139 enabled: true
140 # Allow your users to upload .mkv, .mov, .avi, .flv videos
141 allow_additional_extensions: true
140 threads: 1 142 threads: 1
141 resolutions: # Only created if the original video has a higher resolution, uses more storage! 143 resolutions: # Only created if the original video has a higher resolution, uses more storage!
142 240p: false 144 240p: false
diff --git a/config/test-2.yaml b/config/test-2.yaml
index a80ec6e54..b6d319394 100644
--- a/config/test-2.yaml
+++ b/config/test-2.yaml
@@ -29,3 +29,4 @@ signup:
29 29
30transcoding: 30transcoding:
31 enabled: true 31 enabled: true
32 allow_additional_extensions: true
diff --git a/config/test.yaml b/config/test.yaml
index 51a77e2fd..6e9c56e0a 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -51,6 +51,7 @@ signup:
51 51
52transcoding: 52transcoding:
53 enabled: true 53 enabled: true
54 allow_additional_extensions: false
54 threads: 2 55 threads: 2
55 resolutions: 56 resolutions:
56 240p: true 57 240p: true
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index d65e321e9..c75002aaf 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -172,7 +172,8 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
172 'instance.defaultClientRoute', 172 'instance.defaultClientRoute',
173 'instance.shortDescription', 173 'instance.shortDescription',
174 'cache.videoCaptions', 174 'cache.videoCaptions',
175 'signup.requiresEmailVerification' 175 'signup.requiresEmailVerification',
176 'transcoding.allowAdditionalExtensions'
176 ) 177 )
177 toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota 178 toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
178 toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily 179 toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
@@ -180,6 +181,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
180 toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription 181 toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
181 toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy 182 toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
182 toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification 183 toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
184 toUpdateJSON.transcoding['allow_additional_extensions'] = toUpdate.transcoding.allowAdditionalExtensions
183 185
184 await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) 186 await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
185 187
@@ -247,6 +249,7 @@ function customConfig (): CustomConfig {
247 }, 249 },
248 transcoding: { 250 transcoding: {
249 enabled: CONFIG.TRANSCODING.ENABLED, 251 enabled: CONFIG.TRANSCODING.ENABLED,
252 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
250 threads: CONFIG.TRANSCODING.THREADS, 253 threads: CONFIG.TRANSCODING.THREADS,
251 resolutions: { 254 resolutions: {
252 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], 255 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index d2456346b..f712b0f0b 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import 'multer' 2import 'multer'
3import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 3import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../../initializers' 5import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
6import { sendUpdateActor } from '../../../lib/activitypub/send' 6import { sendUpdateActor } from '../../../lib/activitypub/send'
7import { 7import {
8 asyncMiddleware, 8 asyncMiddleware,
@@ -42,7 +42,7 @@ import { AccountModel } from '../../../models/account/account'
42 42
43const auditLogger = auditLoggerFactory('users-me') 43const auditLogger = auditLoggerFactory('users-me')
44 44
45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 45const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
46 46
47const meRouter = express.Router() 47const meRouter = express.Router()
48 48
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index fd143a139..3d6a6af7f 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -22,7 +22,7 @@ import { createVideoChannel } from '../../lib/video-channel'
22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
23import { setAsyncActorKeys } from '../../lib/activitypub' 23import { setAsyncActorKeys } from '../../lib/activitypub'
24import { AccountModel } from '../../models/account/account' 24import { AccountModel } from '../../models/account/account'
25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 25import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
26import { logger } from '../../helpers/logger' 26import { logger } from '../../helpers/logger'
27import { VideoModel } from '../../models/video/video' 27import { VideoModel } from '../../models/video/video'
28import { updateAvatarValidator } from '../../middlewares/validators/avatar' 28import { updateAvatarValidator } from '../../middlewares/validators/avatar'
@@ -32,7 +32,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils'
32import { UserModel } from '../../models/account/user' 32import { UserModel } from '../../models/account/user'
33 33
34const auditLogger = auditLoggerFactory('channels') 34const auditLogger = auditLoggerFactory('channels')
35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 35const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
36 36
37const videoChannelRouter = express.Router() 37const videoChannelRouter = express.Router()
38 38
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 3ba918189..9b3661368 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' 3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
4import { createReqFiles } from '../../../helpers/express-utils' 4import { createReqFiles } from '../../../helpers/express-utils'
5import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 5import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
6import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
7import { VideoCaptionModel } from '../../../models/video/video-caption' 7import { VideoCaptionModel } from '../../../models/video/video-caption'
8import { VideoModel } from '../../../models/video/video' 8import { VideoModel } from '../../../models/video/video'
@@ -12,7 +12,7 @@ import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
12 12
13const reqVideoCaptionAdd = createReqFiles( 13const reqVideoCaptionAdd = createReqFiles(
14 [ 'captionfile' ], 14 [ 'captionfile' ],
15 VIDEO_CAPTIONS_MIMETYPE_EXT, 15 MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT,
16 { 16 {
17 captionfile: CONFIG.STORAGE.CAPTIONS_DIR 17 captionfile: CONFIG.STORAGE.CAPTIONS_DIR
18 } 18 }
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index f27d648c7..099ab7b8d 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -3,14 +3,7 @@ import * as magnetUtil from 'magnet-uri'
3import 'multer' 3import 'multer'
4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
6import { 6import { CONFIG, MIMETYPES, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers'
7 CONFIG,
8 IMAGE_MIMETYPE_EXT,
9 PREVIEWS_SIZE,
10 sequelizeTypescript,
11 THUMBNAILS_SIZE,
12 TORRENT_MIMETYPE_EXT
13} from '../../../initializers'
14import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' 7import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
15import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
16import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
@@ -35,7 +28,7 @@ const videoImportsRouter = express.Router()
35 28
36const reqVideoFileImport = createReqFiles( 29const reqVideoFileImport = createReqFiles(
37 [ 'thumbnailfile', 'previewfile', 'torrentfile' ], 30 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
38 Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 31 Object.assign({}, MIMETYPES.TORRENT.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
39 { 32 {
40 thumbnailfile: CONFIG.STORAGE.TMP_DIR, 33 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
41 previewfile: CONFIG.STORAGE.TMP_DIR, 34 previewfile: CONFIG.STORAGE.TMP_DIR,
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 4e4697ef4..00a1302d1 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -7,15 +7,13 @@ import { logger } from '../../../helpers/logger'
7import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
8import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 8import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
9import { 9import {
10 CONFIG, 10 CONFIG, MIMETYPES,
11 IMAGE_MIMETYPE_EXT,
12 PREVIEWS_SIZE, 11 PREVIEWS_SIZE,
13 sequelizeTypescript, 12 sequelizeTypescript,
14 THUMBNAILS_SIZE, 13 THUMBNAILS_SIZE,
15 VIDEO_CATEGORIES, 14 VIDEO_CATEGORIES,
16 VIDEO_LANGUAGES, 15 VIDEO_LANGUAGES,
17 VIDEO_LICENCES, 16 VIDEO_LICENCES,
18 VIDEO_MIMETYPE_EXT,
19 VIDEO_PRIVACIES 17 VIDEO_PRIVACIES
20} from '../../../initializers' 18} from '../../../initializers'
21import { 19import {
@@ -57,7 +55,7 @@ import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-u
57import { videoCaptionsRouter } from './captions' 55import { videoCaptionsRouter } from './captions'
58import { videoImportsRouter } from './import' 56import { videoImportsRouter } from './import'
59import { resetSequelizeInstance } from '../../../helpers/database-utils' 57import { resetSequelizeInstance } from '../../../helpers/database-utils'
60import { rename } from 'fs-extra' 58import { move } from 'fs-extra'
61import { watchingRouter } from './watching' 59import { watchingRouter } from './watching'
62 60
63const auditLogger = auditLoggerFactory('videos') 61const auditLogger = auditLoggerFactory('videos')
@@ -65,7 +63,7 @@ const videosRouter = express.Router()
65 63
66const reqVideoFileAdd = createReqFiles( 64const reqVideoFileAdd = createReqFiles(
67 [ 'videofile', 'thumbnailfile', 'previewfile' ], 65 [ 'videofile', 'thumbnailfile', 'previewfile' ],
68 Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 66 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
69 { 67 {
70 videofile: CONFIG.STORAGE.TMP_DIR, 68 videofile: CONFIG.STORAGE.TMP_DIR,
71 thumbnailfile: CONFIG.STORAGE.TMP_DIR, 69 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
@@ -74,7 +72,7 @@ const reqVideoFileAdd = createReqFiles(
74) 72)
75const reqVideoFileUpdate = createReqFiles( 73const reqVideoFileUpdate = createReqFiles(
76 [ 'thumbnailfile', 'previewfile' ], 74 [ 'thumbnailfile', 'previewfile' ],
77 IMAGE_MIMETYPE_EXT, 75 MIMETYPES.IMAGE.MIMETYPE_EXT,
78 { 76 {
79 thumbnailfile: CONFIG.STORAGE.TMP_DIR, 77 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
80 previewfile: CONFIG.STORAGE.TMP_DIR 78 previewfile: CONFIG.STORAGE.TMP_DIR
@@ -208,7 +206,7 @@ async function addVideo (req: express.Request, res: express.Response) {
208 // Move physical file 206 // Move physical file
209 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 207 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
210 const destination = join(videoDir, video.getVideoFilename(videoFile)) 208 const destination = join(videoDir, video.getVideoFilename(videoFile))
211 await rename(videoPhysicalFile.path, destination) 209 await move(videoPhysicalFile.path, destination)
212 // This is important in case if there is another attempt in the retry process 210 // This is important in case if there is another attempt in the retry process
213 videoPhysicalFile.filename = video.getVideoFilename(videoFile) 211 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
214 videoPhysicalFile.path = destination 212 videoPhysicalFile.path = destination
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index 177e9e86e..b33d90e18 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -1,4 +1,4 @@
1import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers' 1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers'
2import { exists, isFileValid } from './misc' 2import { exists, isFileValid } from './misc'
3import { Response } from 'express' 3import { Response } from 'express'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
@@ -8,7 +8,7 @@ function isVideoCaptionLanguageValid (value: any) {
8 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined 8 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined
9} 9}
10 10
11const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT) 11const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
12 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< 12 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream ><
13 .map(m => `(${m})`) 13 .map(m => `(${m})`)
14const videoCaptionTypesRegex = videoCaptionTypes.join('|') 14const videoCaptionTypesRegex = videoCaptionTypes.join('|')
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts
index 4d6ab1fa4..ce9e9193c 100644
--- a/server/helpers/custom-validators/video-imports.ts
+++ b/server/helpers/custom-validators/video-imports.ts
@@ -1,7 +1,7 @@
1import 'express-validator' 1import 'express-validator'
2import 'multer' 2import 'multer'
3import * as validator from 'validator' 3import * as validator from 'validator'
4import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers'
5import { exists, isFileValid } from './misc' 5import { exists, isFileValid } from './misc'
6import * as express from 'express' 6import * as express from 'express'
7import { VideoImportModel } from '../../models/video/video-import' 7import { VideoImportModel } from '../../models/video/video-import'
@@ -24,7 +24,7 @@ function isVideoImportStateValid (value: any) {
24 return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined 24 return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined
25} 25}
26 26
27const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`) 27const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`)
28const videoTorrentImportRegex = videoTorrentImportTypes.join('|') 28const videoTorrentImportRegex = videoTorrentImportTypes.join('|')
29function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 29function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
30 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) 30 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index a13b09ac8..e6f22e6c5 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -5,10 +5,9 @@ import 'multer'
5import * as validator from 'validator' 5import * as validator from 'validator'
6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
8 CONSTRAINTS_FIELDS, 8 CONSTRAINTS_FIELDS, MIMETYPES,
9 VIDEO_CATEGORIES, 9 VIDEO_CATEGORIES,
10 VIDEO_LICENCES, 10 VIDEO_LICENCES,
11 VIDEO_MIMETYPE_EXT,
12 VIDEO_PRIVACIES, 11 VIDEO_PRIVACIES,
13 VIDEO_RATE_TYPES, 12 VIDEO_RATE_TYPES,
14 VIDEO_STATES 13 VIDEO_STATES
@@ -83,10 +82,15 @@ function isVideoRatingTypeValid (value: string) {
83 return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 82 return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1
84} 83}
85 84
86const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) 85function isVideoFileExtnameValid (value: string) {
87const videoFileTypesRegex = videoFileTypes.join('|') 86 return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined
87}
88 88
89function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 89function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
90 const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
91 .map(m => `(${m})`)
92 .join('|')
93
90 return isFileValid(files, videoFileTypesRegex, 'videofile', null) 94 return isFileValid(files, videoFileTypesRegex, 'videofile', null)
91} 95}
92 96
@@ -221,6 +225,7 @@ export {
221 isVideoStateValid, 225 isVideoStateValid,
222 isVideoViewsValid, 226 isVideoViewsValid,
223 isVideoRatingTypeValid, 227 isVideoRatingTypeValid,
228 isVideoFileExtnameValid,
224 isVideoDurationValid, 229 isVideoDurationValid,
225 isVideoTagValid, 230 isVideoTagValid,
226 isVideoPrivacyValid, 231 isVideoPrivacyValid,
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index b51c7cfba..a7bc7eec8 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -19,7 +19,7 @@ function checkMissedConfig () {
19 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', 19 'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
20 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 20 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
21 'redundancy.videos.strategies', 'redundancy.videos.check_interval', 21 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
22 'transcoding.enabled', 'transcoding.threads', 22 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions',
23 'import.videos.http.enabled', 'import.videos.torrent.enabled', 23 'import.videos.http.enabled', 'import.videos.torrent.enabled',
24 'trending.videos.interval_days', 24 'trending.videos.interval_days',
25 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 25 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index d4496bc34..ad61bee73 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -16,7 +16,7 @@ let config: IConfig = require('config')
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
18 18
19const LAST_MIGRATION_VERSION = 290 19const LAST_MIGRATION_VERSION = 295
20 20
21// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
22 22
@@ -246,6 +246,7 @@ const CONFIG = {
246 }, 246 },
247 TRANSCODING: { 247 TRANSCODING: {
248 get ENABLED () { return config.get<boolean>('transcoding.enabled') }, 248 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
249 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
249 get THREADS () { return config.get<number>('transcoding.threads') }, 250 get THREADS () { return config.get<number>('transcoding.threads') },
250 RESOLUTIONS: { 251 RESOLUTIONS: {
251 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, 252 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
@@ -298,7 +299,7 @@ const CONFIG = {
298 299
299// --------------------------------------------------------------------------- 300// ---------------------------------------------------------------------------
300 301
301const CONSTRAINTS_FIELDS = { 302let CONSTRAINTS_FIELDS = {
302 USERS: { 303 USERS: {
303 NAME: { min: 1, max: 50 }, // Length 304 NAME: { min: 1, max: 50 }, // Length
304 DESCRIPTION: { min: 3, max: 1000 }, // Length 305 DESCRIPTION: { min: 3, max: 1000 }, // Length
@@ -357,7 +358,7 @@ const CONSTRAINTS_FIELDS = {
357 max: 2 * 1024 * 1024 // 2MB 358 max: 2 * 1024 * 1024 // 2MB
358 } 359 }
359 }, 360 },
360 EXTNAME: [ '.mp4', '.ogv', '.webm' ], 361 EXTNAME: buildVideosExtname(),
361 INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 362 INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
362 DURATION: { min: 0 }, // Number 363 DURATION: { min: 0 }, // Number
363 TAGS: { min: 0, max: 5 }, // Number of total tags 364 TAGS: { min: 0, max: 5 }, // Number of total tags
@@ -480,27 +481,31 @@ const VIDEO_ABUSE_STATES = {
480 [VideoAbuseState.ACCEPTED]: 'Accepted' 481 [VideoAbuseState.ACCEPTED]: 'Accepted'
481} 482}
482 483
483const VIDEO_MIMETYPE_EXT = { 484const MIMETYPES = {
484 'video/webm': '.webm', 485 VIDEO: {
485 'video/ogg': '.ogv', 486 MIMETYPE_EXT: buildVideoMimetypeExt(),
486 'video/mp4': '.mp4' 487 EXT_MIMETYPE: null as { [ id: string ]: string }
487} 488 },
488const VIDEO_EXT_MIMETYPE = invert(VIDEO_MIMETYPE_EXT) 489 IMAGE: {
489 490 MIMETYPE_EXT: {
490const IMAGE_MIMETYPE_EXT = { 491 'image/png': '.png',
491 'image/png': '.png', 492 'image/jpg': '.jpg',
492 'image/jpg': '.jpg', 493 'image/jpeg': '.jpg'
493 'image/jpeg': '.jpg' 494 }
494} 495 },
495 496 VIDEO_CAPTIONS: {
496const VIDEO_CAPTIONS_MIMETYPE_EXT = { 497 MIMETYPE_EXT: {
497 'text/vtt': '.vtt', 498 'text/vtt': '.vtt',
498 'application/x-subrip': '.srt' 499 'application/x-subrip': '.srt'
499} 500 }
500 501 },
501const TORRENT_MIMETYPE_EXT = { 502 TORRENT: {
502 'application/x-bittorrent': '.torrent' 503 MIMETYPE_EXT: {
504 'application/x-bittorrent': '.torrent'
505 }
506 }
503} 507}
508MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
504 509
505// --------------------------------------------------------------------------- 510// ---------------------------------------------------------------------------
506 511
@@ -526,7 +531,7 @@ const ACTIVITY_PUB = {
526 COLLECTION_ITEMS_PER_PAGE: 10, 531 COLLECTION_ITEMS_PER_PAGE: 10,
527 FETCH_PAGE_LIMIT: 100, 532 FETCH_PAGE_LIMIT: 100,
528 URL_MIME_TYPES: { 533 URL_MIME_TYPES: {
529 VIDEO: Object.keys(VIDEO_MIMETYPE_EXT), 534 VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT),
530 TORRENT: [ 'application/x-bittorrent' ], 535 TORRENT: [ 'application/x-bittorrent' ],
531 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] 536 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
532 }, 537 },
@@ -685,13 +690,12 @@ if (isTestInstance() === true) {
685 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' 690 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
686} 691}
687 692
688updateWebserverConfig() 693updateWebserverUrls()
689 694
690// --------------------------------------------------------------------------- 695// ---------------------------------------------------------------------------
691 696
692export { 697export {
693 API_VERSION, 698 API_VERSION,
694 VIDEO_CAPTIONS_MIMETYPE_EXT,
695 AVATARS_SIZE, 699 AVATARS_SIZE,
696 ACCEPT_HEADERS, 700 ACCEPT_HEADERS,
697 BCRYPT_SALT_SIZE, 701 BCRYPT_SALT_SIZE,
@@ -719,7 +723,6 @@ export {
719 FEEDS, 723 FEEDS,
720 JOB_TTL, 724 JOB_TTL,
721 NSFW_POLICY_TYPES, 725 NSFW_POLICY_TYPES,
722 TORRENT_MIMETYPE_EXT,
723 STATIC_MAX_AGE, 726 STATIC_MAX_AGE,
724 STATIC_PATHS, 727 STATIC_PATHS,
725 VIDEO_IMPORT_TIMEOUT, 728 VIDEO_IMPORT_TIMEOUT,
@@ -732,7 +735,6 @@ export {
732 VIDEO_LICENCES, 735 VIDEO_LICENCES,
733 VIDEO_STATES, 736 VIDEO_STATES,
734 VIDEO_RATE_TYPES, 737 VIDEO_RATE_TYPES,
735 VIDEO_MIMETYPE_EXT,
736 VIDEO_TRANSCODING_FPS, 738 VIDEO_TRANSCODING_FPS,
737 FFMPEG_NICE, 739 FFMPEG_NICE,
738 VIDEO_ABUSE_STATES, 740 VIDEO_ABUSE_STATES,
@@ -740,13 +742,12 @@ export {
740 USER_PASSWORD_RESET_LIFETIME, 742 USER_PASSWORD_RESET_LIFETIME,
741 MEMOIZE_TTL, 743 MEMOIZE_TTL,
742 USER_EMAIL_VERIFY_LIFETIME, 744 USER_EMAIL_VERIFY_LIFETIME,
743 IMAGE_MIMETYPE_EXT,
744 OVERVIEWS, 745 OVERVIEWS,
745 SCHEDULER_INTERVALS_MS, 746 SCHEDULER_INTERVALS_MS,
746 REPEAT_JOBS, 747 REPEAT_JOBS,
747 STATIC_DOWNLOAD_PATHS, 748 STATIC_DOWNLOAD_PATHS,
748 RATES_LIMIT, 749 RATES_LIMIT,
749 VIDEO_EXT_MIMETYPE, 750 MIMETYPES,
750 CRAWL_REQUEST_CONCURRENCY, 751 CRAWL_REQUEST_CONCURRENCY,
751 JOB_COMPLETED_LIFETIME, 752 JOB_COMPLETED_LIFETIME,
752 HTTP_SIGNATURE, 753 HTTP_SIGNATURE,
@@ -768,11 +769,43 @@ function getLocalConfigFilePath () {
768 return join(dirname(configSources[ 0 ].name), filename + '.json') 769 return join(dirname(configSources[ 0 ].name), filename + '.json')
769} 770}
770 771
771function updateWebserverConfig () { 772function buildVideoMimetypeExt () {
773 const data = {
774 'video/webm': '.webm',
775 'video/ogg': '.ogv',
776 'video/mp4': '.mp4'
777 }
778
779 if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
780 Object.assign(data, {
781 'video/quicktime': '.mov',
782 'video/x-msvideo': '.avi',
783 'video/x-flv': '.flv',
784 'video/x-matroska': '.mkv'
785 })
786 }
787
788 return data
789}
790
791function updateWebserverUrls () {
772 CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) 792 CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
773 CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) 793 CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
774} 794}
775 795
796function updateWebserverConfig () {
797 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname()
798
799 MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt()
800 MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
801}
802
803function buildVideosExtname () {
804 return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS
805 ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ]
806 : [ '.mp4', '.ogv', '.webm' ]
807}
808
776function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { 809function buildVideosRedundancy (objs: any[]): VideosRedundancy[] {
777 if (!objs) return [] 810 if (!objs) return []
778 811
@@ -854,4 +887,5 @@ export function reloadConfig () {
854 config = require('config') 887 config = require('config')
855 888
856 updateWebserverConfig() 889 updateWebserverConfig()
890 updateWebserverUrls()
857} 891}
diff --git a/server/initializers/migrations/0295-video-file-extname.ts b/server/initializers/migrations/0295-video-file-extname.ts
new file mode 100644
index 000000000..dbf249f66
--- /dev/null
+++ b/server/initializers/migrations/0295-video-file-extname.ts
@@ -0,0 +1,49 @@
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 await utils.queryInterface.renameColumn('videoFile', 'extname', 'extname_old')
11 }
12
13 {
14 const data = {
15 type: Sequelize.STRING,
16 defaultValue: null,
17 allowNull: true
18 }
19
20 await utils.queryInterface.addColumn('videoFile', 'extname', data)
21 }
22
23 {
24 const query = 'UPDATE "videoFile" SET "extname" = "extname_old"::text'
25 await utils.sequelize.query(query)
26 }
27
28 {
29 const data = {
30 type: Sequelize.STRING,
31 defaultValue: null,
32 allowNull: false
33 }
34 await utils.queryInterface.changeColumn('videoFile', 'extname', data)
35 }
36
37 {
38 await utils.queryInterface.removeColumn('videoFile', 'extname_old')
39 }
40}
41
42function down (options) {
43 throw new Error('Not implemented.')
44}
45
46export {
47 up,
48 down
49}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index bbe48833d..f7bf7c65a 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,5 +1,4 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { join } from 'path'
3import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
4import * as url from 'url' 3import * as url from 'url'
5import * as uuidv4 from 'uuid/v4' 4import * as uuidv4 from 'uuid/v4'
@@ -13,7 +12,7 @@ import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14import { doRequest, downloadImage } from '../../helpers/requests' 13import { doRequest, downloadImage } from '../../helpers/requests'
15import { getUrlFromWebfinger } from '../../helpers/webfinger' 14import { getUrlFromWebfinger } from '../../helpers/webfinger'
16import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 15import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
17import { AccountModel } from '../../models/account/account' 16import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../models/activitypub/actor'
19import { AvatarModel } from '../../models/avatar/avatar' 18import { AvatarModel } from '../../models/avatar/avatar'
@@ -172,10 +171,10 @@ async function fetchActorTotalItems (url: string) {
172 171
173async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { 172async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
174 if ( 173 if (
175 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && 174 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
176 isActivityPubUrlValid(actorJSON.icon.url) 175 isActivityPubUrlValid(actorJSON.icon.url)
177 ) { 176 ) {
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] 177 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
179 178
180 const avatarName = uuidv4() + extension 179 const avatarName = uuidv4() + extension
181 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE) 180 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 3d17e6846..379c2a0d7 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,7 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as sequelize from 'sequelize' 2import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { join } from 'path'
5import * as request from 'request' 4import * as request from 'request'
6import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index'
7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
@@ -11,7 +10,7 @@ import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos
11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 10import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
13import { doRequest, downloadImage } from '../../helpers/requests' 12import { doRequest, downloadImage } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers' 13import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
15import { ActorModel } from '../../models/activitypub/actor' 14import { ActorModel } from '../../models/activitypub/actor'
16import { TagModel } from '../../models/video/tag' 15import { TagModel } from '../../models/video/tag'
17import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
@@ -362,7 +361,7 @@ export {
362// --------------------------------------------------------------------------- 361// ---------------------------------------------------------------------------
363 362
364function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 363function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
365 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) 364 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
366 365
367 const urlMediaType = url.mediaType || url.mimeType 366 const urlMediaType = url.mediaType || url.mimeType
368 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 367 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
@@ -490,7 +489,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
490 489
491 const mediaType = fileUrl.mediaType || fileUrl.mimeType 490 const mediaType = fileUrl.mediaType || fileUrl.mimeType
492 const attribute = { 491 const attribute = {
493 extname: VIDEO_MIMETYPE_EXT[ mediaType ], 492 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
494 infoHash: parsed.infoHash, 493 infoHash: parsed.infoHash,
495 resolution: fileUrl.height, 494 resolution: fileUrl.height,
496 size: fileUrl.size, 495 size: fileUrl.size,
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index dd37dad22..8b6cd146a 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -15,7 +15,7 @@ import {
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers' 18import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { VideoModel } from '../video/video'
@@ -415,8 +415,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
415 expires: this.expiresOn.toISOString(), 415 expires: this.expiresOn.toISOString(),
416 url: { 416 url: {
417 type: 'Link', 417 type: 'Link',
418 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, 418 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
419 mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, 419 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
420 href: this.fileUrl, 420 href: this.fileUrl,
421 height: this.VideoFile.resolution, 421 height: this.VideoFile.resolution,
422 size: this.VideoFile.size, 422 size: this.VideoFile.size,
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index adebdf0c7..3fd2d5a99 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -14,6 +14,7 @@ import {
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { 16import {
17 isVideoFileExtnameValid,
17 isVideoFileInfoHashValid, 18 isVideoFileInfoHashValid,
18 isVideoFileResolutionValid, 19 isVideoFileResolutionValid,
19 isVideoFileSizeValid, 20 isVideoFileSizeValid,
@@ -58,7 +59,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
58 size: number 59 size: number
59 60
60 @AllowNull(false) 61 @AllowNull(false)
61 @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) 62 @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
63 @Column
62 extname: string 64 extname: string
63 65
64 @AllowNull(false) 66 @AllowNull(false)
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index e3f8d525b..de0747f22 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -2,7 +2,7 @@ import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file' 3import { VideoFileModel } from './video-file'
4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' 4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
5import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' 5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
6import { VideoCaptionModel } from './video-caption' 6import { VideoCaptionModel } from './video-caption'
7import { 7import {
8 getVideoCommentsActivityPubUrl, 8 getVideoCommentsActivityPubUrl,
@@ -207,8 +207,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
207 for (const file of video.VideoFiles) { 207 for (const file of video.VideoFiles) {
208 url.push({ 208 url.push({
209 type: 'Link', 209 type: 'Link',
210 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, 210 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
211 mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, 211 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
212 href: video.getVideoFileUrl(file, baseUrlHttp), 212 href: video.getVideoFileUrl(file, baseUrlHttp),
213 height: file.resolution, 213 height: file.resolution,
214 size: file.size, 214 size: file.size,
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index ffae380c1..b7bf41b58 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -54,6 +54,7 @@ describe('Test config API validators', function () {
54 }, 54 },
55 transcoding: { 55 transcoding: {
56 enabled: true, 56 enabled: true,
57 allowAdditionalExtensions: true,
57 threads: 1, 58 threads: 1,
58 resolutions: { 59 resolutions: {
59 '240p': false, 60 '240p': false,
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index d94eccf8e..f26b91435 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -320,10 +320,15 @@ describe('Test videos API validator', function () {
320 320
321 it('Should fail without an incorrect input file', async function () { 321 it('Should fail without an incorrect input file', async function () {
322 const fields = baseCorrectParams 322 const fields = baseCorrectParams
323 const attaches = { 323 let attaches = {
324 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm') 324 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm')
325 } 325 }
326 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 326 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
327
328 attaches = {
329 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mkv')
330 }
331 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
327 }) 332 })
328 333
329 it('Should fail with an incorrect thumbnail file', async function () { 334 it('Should fail with an incorrect thumbnail file', async function () {
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index c5c360a17..4c163d47d 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -17,6 +17,7 @@ import {
17 setAccessTokensToServers, 17 setAccessTokensToServers,
18 updateCustomConfig 18 updateCustomConfig
19} from '../../../../shared/utils' 19} from '../../../../shared/utils'
20import { ServerConfig } from '../../../../shared/models'
20 21
21const expect = chai.expect 22const expect = chai.expect
22 23
@@ -43,6 +44,7 @@ function checkInitialConfig (data: CustomConfig) {
43 expect(data.user.videoQuota).to.equal(5242880) 44 expect(data.user.videoQuota).to.equal(5242880)
44 expect(data.user.videoQuotaDaily).to.equal(-1) 45 expect(data.user.videoQuotaDaily).to.equal(-1)
45 expect(data.transcoding.enabled).to.be.false 46 expect(data.transcoding.enabled).to.be.false
47 expect(data.transcoding.allowAdditionalExtensions).to.be.false
46 expect(data.transcoding.threads).to.equal(2) 48 expect(data.transcoding.threads).to.equal(2)
47 expect(data.transcoding.resolutions['240p']).to.be.true 49 expect(data.transcoding.resolutions['240p']).to.be.true
48 expect(data.transcoding.resolutions['360p']).to.be.true 50 expect(data.transcoding.resolutions['360p']).to.be.true
@@ -74,6 +76,7 @@ function checkUpdatedConfig (data: CustomConfig) {
74 expect(data.user.videoQuotaDaily).to.equal(318742) 76 expect(data.user.videoQuotaDaily).to.equal(318742)
75 expect(data.transcoding.enabled).to.be.true 77 expect(data.transcoding.enabled).to.be.true
76 expect(data.transcoding.threads).to.equal(1) 78 expect(data.transcoding.threads).to.equal(1)
79 expect(data.transcoding.allowAdditionalExtensions).to.be.true
77 expect(data.transcoding.resolutions['240p']).to.be.false 80 expect(data.transcoding.resolutions['240p']).to.be.false
78 expect(data.transcoding.resolutions['360p']).to.be.true 81 expect(data.transcoding.resolutions['360p']).to.be.true
79 expect(data.transcoding.resolutions['480p']).to.be.true 82 expect(data.transcoding.resolutions['480p']).to.be.true
@@ -96,7 +99,7 @@ describe('Test config', function () {
96 99
97 it('Should have a correct config on a server with registration enabled', async function () { 100 it('Should have a correct config on a server with registration enabled', async function () {
98 const res = await getConfig(server.url) 101 const res = await getConfig(server.url)
99 const data = res.body 102 const data: ServerConfig = res.body
100 103
101 expect(data.signup.allowed).to.be.true 104 expect(data.signup.allowed).to.be.true
102 }) 105 })
@@ -111,11 +114,21 @@ describe('Test config', function () {
111 ]) 114 ])
112 115
113 const res = await getConfig(server.url) 116 const res = await getConfig(server.url)
114 const data = res.body 117 const data: ServerConfig = res.body
115 118
116 expect(data.signup.allowed).to.be.false 119 expect(data.signup.allowed).to.be.false
117 }) 120 })
118 121
122 it('Should have the correct video allowed extensions', async function () {
123 const res = await getConfig(server.url)
124 const data: ServerConfig = res.body
125
126 expect(data.video.file.extensions).to.have.lengthOf(3)
127 expect(data.video.file.extensions).to.contain('.mp4')
128 expect(data.video.file.extensions).to.contain('.webm')
129 expect(data.video.file.extensions).to.contain('.ogv')
130 })
131
119 it('Should get the customized configuration', async function () { 132 it('Should get the customized configuration', async function () {
120 const res = await getCustomConfig(server.url, server.accessToken) 133 const res = await getCustomConfig(server.url, server.accessToken)
121 const data = res.body as CustomConfig 134 const data = res.body as CustomConfig
@@ -165,6 +178,7 @@ describe('Test config', function () {
165 }, 178 },
166 transcoding: { 179 transcoding: {
167 enabled: true, 180 enabled: true,
181 allowAdditionalExtensions: true,
168 threads: 1, 182 threads: 1,
169 resolutions: { 183 resolutions: {
170 '240p': false, 184 '240p': false,
@@ -193,6 +207,18 @@ describe('Test config', function () {
193 checkUpdatedConfig(data) 207 checkUpdatedConfig(data)
194 }) 208 })
195 209
210 it('Should have the correct updated video allowed extensions', async function () {
211 const res = await getConfig(server.url)
212 const data: ServerConfig = res.body
213
214 expect(data.video.file.extensions).to.have.length.above(3)
215 expect(data.video.file.extensions).to.contain('.mp4')
216 expect(data.video.file.extensions).to.contain('.webm')
217 expect(data.video.file.extensions).to.contain('.ogv')
218 expect(data.video.file.extensions).to.contain('.flv')
219 expect(data.video.file.extensions).to.contain('.mkv')
220 })
221
196 it('Should have the configuration updated after a restart', async function () { 222 it('Should have the configuration updated after a restart', async function () {
197 this.timeout(10000) 223 this.timeout(10000)
198 224
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 68cf00194..eefd32ef8 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -20,9 +20,8 @@ import {
20 uploadVideo, 20 uploadVideo,
21 webtorrentAdd 21 webtorrentAdd
22} from '../../../../shared/utils' 22} from '../../../../shared/utils'
23import { join } from 'path' 23import { extname, join } from 'path'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 24import { waitJobs } from '../../../../shared/utils/server/jobs'
25import { pathExists } from 'fs-extra'
26import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' 25import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
27 26
28const expect = chai.expect 27const expect = chai.expect
@@ -322,6 +321,34 @@ describe('Test video transcoding', function () {
322 } 321 }
323 }) 322 })
324 323
324 it('Should accept and transcode additional extensions', async function () {
325 this.timeout(300000)
326
327 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
328 const videoAttributes = {
329 name: fixture,
330 fixture
331 }
332
333 await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
334
335 await waitJobs(servers)
336
337 for (const server of servers) {
338 const res = await getVideosList(server.url)
339
340 const video = res.body.data.find(v => v.name === videoAttributes.name)
341 const res2 = await getVideo(server.url, video.id)
342 const videoDetails = res2.body
343
344 expect(videoDetails.files).to.have.lengthOf(4)
345
346 const magnetUri = videoDetails.files[ 0 ].magnetUri
347 expect(magnetUri).to.contain('.mp4')
348 }
349 }
350 })
351
325 after(async function () { 352 after(async function () {
326 killallServers(servers) 353 killallServers(servers)
327 }) 354 })
diff --git a/server/tests/fixtures/video_short.avi b/server/tests/fixtures/video_short.avi
new file mode 100644
index 000000000..88979cab2
--- /dev/null
+++ b/server/tests/fixtures/video_short.avi
Binary files differ
diff --git a/server/tests/fixtures/video_short.mkv b/server/tests/fixtures/video_short.mkv
new file mode 100644
index 000000000..a67f4f806
--- /dev/null
+++ b/server/tests/fixtures/video_short.mkv
Binary files differ
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 3afd36fcd..028aafa1a 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -48,6 +48,7 @@ export interface CustomConfig {
48 48
49 transcoding: { 49 transcoding: {
50 enabled: boolean 50 enabled: boolean
51 allowAdditionalExtensions: boolean
51 threads: number 52 threads: number
52 resolutions: { 53 resolutions: {
53 '240p': boolean 54 '240p': boolean
diff --git a/shared/utils/server/config.ts b/shared/utils/server/config.ts
index 5b888b061..ff5288c82 100644
--- a/shared/utils/server/config.ts
+++ b/shared/utils/server/config.ts
@@ -86,6 +86,7 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
86 }, 86 },
87 transcoding: { 87 transcoding: {
88 enabled: true, 88 enabled: true,
89 allowAdditionalExtensions: true,
89 threads: 1, 90 threads: 1,
90 resolutions: { 91 resolutions: {
91 '240p': false, 92 '240p': false,