diff --git a/Acknowledgment-ReadMe.md b/Acknowledgment-ReadMe.md new file mode 100644 index 00000000000..b2caaf86282 --- /dev/null +++ b/Acknowledgment-ReadMe.md @@ -0,0 +1,13 @@ +

+ EOSC CZ Logo +

+ +--- + +This project output was developed with financial contributions from the [EOSC CZ](https://www.eosc.cz/projekty/narodni-podpora-pro-eosc) initiative through the project **National Repository Platform for Research Data** (CZ.02.01.01/00/23_014/0008787), funded by the Programme Johannes Amos Comenius (P JAC) of the Ministry of Education, Youth and Sports of the Czech Republic (MEYS). + +--- + +

+ EU and MŠMT Logos +

diff --git a/src/app/core/metadata/metadata-bitstream.model.ts b/src/app/core/metadata/metadata-bitstream.model.ts index c5eed0594a4..eace9ba1121 100644 --- a/src/app/core/metadata/metadata-bitstream.model.ts +++ b/src/app/core/metadata/metadata-bitstream.model.ts @@ -31,7 +31,7 @@ export class MetadataBitstream extends ListableObject implements HALResource { * The identifier of this metadata field */ @autoserialize - id: number; + id: string; /** * The name of this bitstream diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts index f5e795d4d0e..a1455425ebf 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts @@ -23,7 +23,7 @@ describe('ClarinFilesSectionComponent', () => { let halService: HALEndpointService; // Set up the mock service's getMetadataBitstream method to return a simple stream const metadatabitstream = new MetadataBitstream(); - metadatabitstream.id = 123; + metadatabitstream.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe'; metadatabitstream.name = 'test'; metadatabitstream.description = 'test'; metadatabitstream.fileSize = 1024; diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index f9092be3020..cd7575d4d62 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -80,7 +80,7 @@ export class ClarinFilesSectionComponent implements OnInit { ngOnInit(): void { this.registryService - .getMetadataBitstream(this.itemHandle, 'ORIGINAL,TEXT,THUMBNAIL') + .getMetadataBitstream(this.itemHandle, 'ORIGINAL') .pipe(getAllSucceededRemoteListPayload()) .subscribe((data: MetadataBitstream[]) => { this.listOfFiles.next(data); diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html index 3d029dfef6c..91287138d49 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html @@ -1,13 +1,15 @@
-
{{'item.file.description.name' | translate}}
-
+
{{ fileInput.name }}
{{'item.file.description.size' | translate}}
@@ -29,7 +31,8 @@
Preview { let component: FileDescriptionComponent; let fixture: ComponentFixture; let halService: HALEndpointService; + let bitstreamDataService: BitstreamDataService; beforeEach(async () => { const configurationDataService = jasmine.createSpyObj('configurationDataService', { @@ -32,6 +41,10 @@ describe('FileDescriptionComponent', () => { getRootHref: 'root url', }); + bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { + findById: createSuccessfulRemoteDataObject$(new Bitstream()), + }); + await TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -42,7 +55,11 @@ describe('FileDescriptionComponent', () => { declarations: [FileDescriptionComponent, FileSizePipe], providers: [ { provide: ConfigurationDataService, useValue: configurationDataService }, - { provide: HALEndpointService, useValue: halService } + { provide: HALEndpointService, useValue: halService }, + { provide: AuthService, useClass: AuthServiceStub }, + { provide: FileService, useClass: FileServiceStub }, + { provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub }, + { provide: BitstreamDataService, useValue: bitstreamDataService }, ] }).compileComponents(); }); @@ -53,7 +70,7 @@ describe('FileDescriptionComponent', () => { // Mock the input value const fileInput = new MetadataBitstream(); - fileInput.id = 123; + fileInput.id = '66efe81e-2950-483d-a065-bbdacd689f95'; fileInput.name = 'testFile'; fileInput.description = 'test description'; fileInput.fileSize = 2048; diff --git a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts index c02d94f956b..f59479a9f3c 100644 --- a/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts +++ b/src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts @@ -1,9 +1,20 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model'; import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service'; import { Router } from '@angular/router'; import { ConfigurationDataService } from '../../../../../core/data/configuration-data.service'; -import { getFirstSucceededRemoteData } from '../../../../../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../../../../../core/shared/operators'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { Bitstream } from '../../../../../core/shared/bitstream.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { fromEvent, merge, Observable, of, Subscription } from 'rxjs'; +import { FileService } from '../../../../../core/shared/file.service'; +import { distinctUntilChanged, switchMap, take } from 'rxjs/operators'; +import { FeatureID } from '../../../../../core/data/feature-authorization/feature-id'; +import { hasValue } from '../../../../../shared/empty.util'; +import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service'; +import { AuthService } from '../../../../../core/auth/auth.service'; const allowedPreviewFormats = ['text/plain', 'text/html', 'application/zip', 'application/x-tar']; @Component({ @@ -11,17 +22,30 @@ const allowedPreviewFormats = ['text/plain', 'text/html', 'application/zip', 'ap templateUrl: './file-description.component.html', styleUrls: ['./file-description.component.scss'], }) -export class FileDescriptionComponent implements OnInit { +export class FileDescriptionComponent implements OnInit, OnDestroy { MIME_TYPE_IMAGES_PATH = './assets/images/mime/'; MIME_TYPE_DEFAULT_IMAGE_NAME = 'application-octet-stream.png'; @Input() fileInput: MetadataBitstream; + @ViewChild('videoPreview') videoElement: ElementRef; + emailToContact: string; + content_url$: Observable; + content_url: string; + thumbnail_url$: Observable; + handlers_added = false; + playPromise: Promise; + + private subscriptions: Subscription = new Subscription(); constructor(protected halService: HALEndpointService, private router: Router, + private bitstreamService: BitstreamDataService, + private auth: AuthService, + private authDataService: AuthorizationDataService, + private fileService: FileService, private configService: ConfigurationDataService) { } ngOnInit(): void { @@ -30,6 +54,122 @@ export class FileDescriptionComponent implements OnInit { .subscribe(remoteData => { this.emailToContact = remoteData?.payload?.values?.[0]; }); + this.content_url$ = this.bitstreamService.findById(this.fileInput.id, true, false, followLink('thumbnail')) + .pipe(getFirstCompletedRemoteData(), + switchMap((remoteData: RemoteData) => { + if (remoteData.hasSucceeded) { + if (remoteData.payload?.thumbnail){ + this.thumbnail_url$ = remoteData.payload?.thumbnail.pipe( + switchMap((thumbnailRD: RemoteData) => { + if (thumbnailRD.hasSucceeded) { + return this.buildUrl(thumbnailRD.payload?._links.content.href); + } else { + return of(''); + } + }), + ); + } else { + this.thumbnail_url$ = of(''); + } + return of(remoteData.payload?._links.content.href); + } + } + )); + this.content_url$.pipe(take(1)).subscribe((url) => { + this.content_url = url; + }); + } + + ngAfterViewInit() { + const video = this.videoElement?.nativeElement; + + if (video) { + const error$ = fromEvent(video, 'error'); + this.subscriptions.add( + error$.subscribe((event) => { + //console.log('error', video.error.message); + if (hasValue(video.src)) { + this.auth.isAuthenticated().pipe( + switchMap((isLoggedIn) => { + if (isLoggedIn) { + return this.authDataService.isAuthorized(FeatureID.CanDownload, this.content_url.replace('/content', '')); + } else { + return of(false); + } + }), + ).subscribe((isAuthorized: boolean) => { + if (isAuthorized) { + this.add_short_lived_token_handling_to_video_playback(video); + this.resetSource(); + } else { + video.src = null; + } + }); + } + }) + ); + } + } + + private add_short_lived_token_handling_to_video_playback(video: HTMLVideoElement) { + if (this.handlers_added) { + return; + } + + const seeking$ = fromEvent(video, 'seeking').pipe( + switchMap((event: Event) => { + //console.log('seeking'); + return of(video.currentTime); + }), + distinctUntilChanged(), + ); + const stalled$ = fromEvent(video, 'stalled').pipe( + switchMap((event: Event) => { + //console.log('stalled'); + return of(video.currentTime); + }), + distinctUntilChanged(), + ); + this.subscriptions.add( + merge(seeking$, stalled$).subscribe((currentTime) => { + this.resetSource(currentTime); + }) + ); + + this.handlers_added = true; + + } + + private resetSource(currentTime?) { + const video = this.videoElement?.nativeElement; + //console.log("networkState in resetSource", video.networkState); + if (this.playPromise) { + this.playPromise?.then(_ => { + //playback has started + // don't want to see The play() request was interrupted by... + // https://developer.chrome.com/blog/play-request-was-interrupted + this.updateSource(video, currentTime); + }).catch(_ => { + //do nothing + }); + } else { + this.updateSource(video, currentTime); + } + } + + private updateSource(video, currentTime) { + //console.log("Updating the src"); + this.buildUrl(this.content_url).subscribe(result => { + video.src = result; + if (currentTime) { + video.currentTime = currentTime; + } + this.playPromise = video.play(); + }); + } + + private buildUrl(url: string): Observable { + return url ? this.fileService.retrieveFileDownloadLink(url) : of(url); } public downloadFile() { @@ -68,4 +208,8 @@ export class FileDescriptionComponent implements OnInit { // this.fileInput.fileInfo.length === 0 means that the file has no preview return this.fileInput?.fileInfo?.length === 0; } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } } diff --git a/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts b/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts index 827b53a4cd6..1bac02083e7 100644 --- a/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts @@ -43,7 +43,7 @@ describe('PreviewSectionComponent', () => { // Set up the mock service's getMetadataBitstream method to return a simple stream const metadatabitstream = new MetadataBitstream(); - metadatabitstream.id = 123; + metadatabitstream.id = '5974f1cf-f2ef-4e4c-8f6d-85ad6c52efde'; metadatabitstream.name = 'test'; metadatabitstream.description = 'test'; metadatabitstream.fileSize = 1024; diff --git a/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts b/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts index 3f485e70f08..d8dd5faad53 100644 --- a/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts +++ b/src/app/item-page/simple/field-components/preview-section/preview-section.component.ts @@ -22,7 +22,7 @@ export class PreviewSectionComponent implements OnInit { ngOnInit(): void { this.registryService - .getMetadataBitstream(this.item.handle, 'ORIGINAL,TEXT,THUMBNAIL') + .getMetadataBitstream(this.item.handle, 'ORIGINAL') .pipe(getAllSucceededRemoteListPayload()) .subscribe((data: MetadataBitstream[]) => { this.listOfFiles.next(data);