diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.html b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.html index 799b990943c7..299ddc2f7c93 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.html +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.html @@ -94,7 +94,7 @@ {{ 'artemisApp.videoUnit.createVideoUnit.sourceRequiredValidationError' | artemisTranslate }} } - @if (sourceControl?.errors?.invalidUrl) { + @if (sourceControl?.errors?.invalidVideoUrl) {
{{ 'artemisApp.videoUnit.createVideoUnit.sourceURLValidationError' | artemisTranslate }}
diff --git a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts index a872ce716815..0670285084c4 100644 --- a/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts +++ b/src/main/webapp/app/lecture/lecture-unit/lecture-unit-management/video-unit-form/video-unit-form.component.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs/esm'; import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; -import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import urlParser from 'js-video-url-parser'; import { faArrowLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; import { Competency } from 'app/entities/competency.model'; @@ -13,31 +13,44 @@ export interface VideoUnitFormData { competencies?: Competency[]; } -function videoUrlValidator(control: AbstractControl) { - if (control.value === undefined || control.value === null || control.value === '') { - return null; - } - - const videoInfo = urlParser.parse(control.value); - return videoInfo ? null : { invalidVideoUrl: true }; +function isTumLiveUrl(url: URL): boolean { + return url.host === 'live.rbg.tum.de'; } -function urlValidator(control: AbstractControl) { - let validUrl = true; +function isVideoOnlyTumUrl(url: URL): boolean { + return url?.searchParams.get('video_only') === '1'; +} - // for certain cases like embed links for vimeo - const regex = /^\/\/.*$/; - if (control.value && control.value.match(regex)) { - return null; +function videoSourceTransformUrlValidator(control: AbstractControl): ValidationErrors | undefined { + const urlValue = control.value; + if (!urlValue) { + return undefined; } - + let parsedUrl, url; try { - new URL(control.value); + url = new URL(urlValue); + parsedUrl = urlParser.parse(urlValue); } catch { - validUrl = false; + //intentionally empty + } + // The URL is valid if it's a TUM-Live URL or if it can be parsed by the js-video-url-parser. + if ((url && isTumLiveUrl(url)) || parsedUrl) { + return undefined; } + return { invalidVideoUrl: true }; +} - return validUrl ? null : { invalidUrl: true }; +function videoSourceUrlValidator(control: AbstractControl): ValidationErrors | undefined { + let url; + try { + url = new URL(control.value); + } catch { + // intentionally empty + } + if (url && !(isTumLiveUrl(url) && !isVideoOnlyTumUrl(url))) { + return undefined; + } + return { invalidVideoUrl: true }; } @Component({ @@ -61,8 +74,8 @@ export class VideoUnitFormComponent implements OnInit, OnChanges { faTimes = faTimes; - urlValidator = urlValidator; - videoUrlValidator = videoUrlValidator; + videoSourceUrlValidator = videoSourceUrlValidator; + videoSourceTransformUrlValidator = videoSourceTransformUrlValidator; // Icons faArrowLeft = faArrowLeft; @@ -108,8 +121,8 @@ export class VideoUnitFormComponent implements OnInit, OnChanges { name: [undefined as string | undefined, [Validators.required, Validators.maxLength(255)]], description: [undefined as string | undefined, [Validators.maxLength(1000)]], releaseDate: [undefined as dayjs.Dayjs | undefined], - source: [undefined as string | undefined, [Validators.required, this.urlValidator]], - urlHelper: [undefined as string | undefined, this.videoUrlValidator], + source: [undefined as string | undefined, [Validators.required, this.videoSourceUrlValidator]], + urlHelper: [undefined as string | undefined, this.videoSourceTransformUrlValidator], competencies: [undefined as Competency[] | undefined], }); } @@ -141,6 +154,11 @@ export class VideoUnitFormComponent implements OnInit, OnChanges { } extractEmbeddedUrl(videoUrl: string) { + const url = new URL(videoUrl); + if (isTumLiveUrl(url)) { + url.searchParams.set('video_only', '1'); + return url.toString(); + } return urlParser.create({ videoInfo: urlParser.parse(videoUrl)!, format: 'embed', diff --git a/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts index 7f2944879dde..5e00cff9c987 100644 --- a/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/video-unit/video-unit-form.component.spec.ts @@ -43,8 +43,8 @@ describe('VideoUnitFormComponent', () => { }); it('should not submit a form when name is missing', () => { - jest.spyOn(videoUnitFormComponent, 'urlValidator').mockReturnValue(null); - jest.spyOn(videoUnitFormComponent, 'videoUrlValidator').mockReturnValue(null); + jest.spyOn(videoUnitFormComponent, 'videoSourceUrlValidator').mockReturnValue(undefined); + jest.spyOn(videoUnitFormComponent, 'videoSourceTransformUrlValidator').mockReturnValue(undefined); videoUnitFormComponentFixture.detectChanges(); const exampleDescription = 'lorem ipsum'; videoUnitFormComponent.descriptionControl!.setValue(exampleDescription); @@ -67,8 +67,8 @@ describe('VideoUnitFormComponent', () => { }); it('should not submit a form when source is missing', () => { - jest.spyOn(videoUnitFormComponent, 'urlValidator').mockReturnValue(null); - jest.spyOn(videoUnitFormComponent, 'videoUrlValidator').mockReturnValue(null); + jest.spyOn(videoUnitFormComponent, 'videoSourceUrlValidator').mockReturnValue(undefined); + jest.spyOn(videoUnitFormComponent, 'videoSourceTransformUrlValidator').mockReturnValue(undefined); videoUnitFormComponentFixture.detectChanges(); const exampleName = 'test'; videoUnitFormComponent.nameControl!.setValue(exampleName); @@ -92,8 +92,8 @@ describe('VideoUnitFormComponent', () => { }); it('should submit valid form', () => { - jest.spyOn(videoUnitFormComponent, 'urlValidator').mockReturnValue(null); - jest.spyOn(videoUnitFormComponent, 'videoUrlValidator').mockReturnValue(null); + jest.spyOn(videoUnitFormComponent, 'videoSourceUrlValidator').mockReturnValue(undefined); + jest.spyOn(videoUnitFormComponent, 'videoSourceTransformUrlValidator').mockReturnValue(undefined); videoUnitFormComponentFixture.detectChanges(); const exampleName = 'test'; videoUnitFormComponent.nameControl!.setValue(exampleName); @@ -129,8 +129,8 @@ describe('VideoUnitFormComponent', () => { it('should correctly transform YouTube URL into embeddable format', () => { jest.spyOn(videoUnitFormComponent, 'extractEmbeddedUrl').mockReturnValue(validYouTubeUrlInEmbeddableFormat); - jest.spyOn(videoUnitFormComponent, 'urlValidator').mockReturnValue(null); - jest.spyOn(videoUnitFormComponent, 'videoUrlValidator').mockReturnValue(null); + jest.spyOn(videoUnitFormComponent, 'videoSourceUrlValidator').mockReturnValue(undefined); + jest.spyOn(videoUnitFormComponent, 'videoSourceTransformUrlValidator').mockReturnValue(undefined); videoUnitFormComponentFixture.detectChanges(); @@ -144,6 +144,22 @@ describe('VideoUnitFormComponent', () => { }); }); + it('should correctly transform TUM-Live URL without video only into embeddable format', () => { + const tumLiveUrl = 'https://live.rbg.tum.de/w/test/26'; + const expectedUrl = 'https://live.rbg.tum.de/w/test/26?video_only=1'; + + videoUnitFormComponentFixture.detectChanges(); + videoUnitFormComponent.urlHelperControl!.setValue(tumLiveUrl); + videoUnitFormComponentFixture.detectChanges(); + + const transformButton = videoUnitFormComponentFixture.debugElement.nativeElement.querySelector('#transformButton'); + transformButton.click(); + + return videoUnitFormComponentFixture.whenStable().then(() => { + expect(videoUnitFormComponent.sourceControl?.value).toEqual(expectedUrl); + }); + }); + it('should correctly set form values in edit mode', () => { videoUnitFormComponent.isEditMode = true; const formData: VideoUnitFormData = {