Skip to content

Commit 60de327

Browse files
authored
Ufal dtq sync 2025 05 14 (DSpace#855)
* Create Acknowledgment-ReadMe.md Acnkowledgment of NRP project (cherry picked from commit ad889b2) * Video files previews This uses the thumbnail as poster (if available) and correctly sets the source of the video currently only works for anonymously accessible files. (cherry picked from commit 4832c2f) * Handle video previews for restricted items append a shortlived token at the right time (error, seeking, stalled) (cherry picked from commit 2c12d7d) * Display only ORIGINAL bitstreams Thumbnails, when available, should be shown istead of the generic MIME_TYPE_IMAGE. Content of the TEXT bundle should not be shown at all this is usually automatically extracted "text layer" of a PDF, useful for indexing, but don't want people downloading it. (cherry picked from commit 3862442) * fix linter and test errors (cherry picked from commit f852096) * Code review follow up the listOfFiles should really not contain files from "TEXT" or "THUMBNAIL" bundles. * code review unsubscribe error$, seeking$ and $stalled * code review - thumbnail might be undefined * code review - consistent formatting
1 parent bc59ae5 commit 60de327

9 files changed

Lines changed: 190 additions & 13 deletions

Acknowledgment-ReadMe.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<p align="left">
2+
<img src="https://webcentrum.muni.cz/media/3831863/seda_eosc.png" alt="EOSC CZ Logo" height="90">
3+
</p>
4+
5+
---
6+
7+
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).
8+
9+
---
10+
11+
<p align="left">
12+
<img src="https://webcentrum.muni.cz/media/3832168/seda_eu-msmt_eng.png" alt="EU and MŠMT Logos" height="90">
13+
</p>

src/app/core/metadata/metadata-bitstream.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class MetadataBitstream extends ListableObject implements HALResource {
3131
* The identifier of this metadata field
3232
*/
3333
@autoserialize
34-
id: number;
34+
id: string;
3535

3636
/**
3737
* The name of this bitstream

src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('ClarinFilesSectionComponent', () => {
2323
let halService: HALEndpointService;
2424
// Set up the mock service's getMetadataBitstream method to return a simple stream
2525
const metadatabitstream = new MetadataBitstream();
26-
metadatabitstream.id = 123;
26+
metadatabitstream.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe';
2727
metadatabitstream.name = 'test';
2828
metadatabitstream.description = 'test';
2929
metadatabitstream.fileSize = 1024;

src/app/item-page/clarin-files-section/clarin-files-section.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class ClarinFilesSectionComponent implements OnInit {
8080

8181
ngOnInit(): void {
8282
this.registryService
83-
.getMetadataBitstream(this.itemHandle, 'ORIGINAL,TEXT,THUMBNAIL')
83+
.getMetadataBitstream(this.itemHandle, 'ORIGINAL')
8484
.pipe(getAllSucceededRemoteListPayload())
8585
.subscribe((data: MetadataBitstream[]) => {
8686
this.listOfFiles.next(data);

src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<div class="file-preview-box rounded pt-2">
22
<div style="text-align: left" *ngIf="fileInput.format === 'video/mp4'">
3-
<video preload="none" controls="controls" height="240">
3+
<video #videoPreview preload="none" controls="controls" [src]="content_url" [poster]="thumbnail_url$ | async"
4+
height="240">
5+
<!--source [src]="content_url" type="video/mp4"-->
46
{{'item.file.description.not.supported.video' | translate}}
57
</video>
68
</div>
79
<div class="file-content">
810
<dl class="dl-horizontal">
911
<dt>{{'item.file.description.name' | translate}}</dt>
10-
<dd title="ud-treebanks-v2.12.tgz">
12+
<dd>
1113
{{ fileInput.name }}
1214
</dd>
1315
<dt>{{'item.file.description.size' | translate}}</dt>
@@ -29,7 +31,8 @@
2931
</dl>
3032
<img
3133
*ngIf="fileInput.format !== 'video/mp4'"
32-
[src]="MIME_TYPE_IMAGES_PATH + fileInput.format?.replace('/','-') + '.png'"
34+
[src]="(thumbnail_url$ | async) ? (thumbnail_url$ | async) : MIME_TYPE_IMAGES_PATH +
35+
fileInput.format?.replace('/','-') + '.png'"
3336
(error)="handleImageError($event)"
3437
(click)="downloadFile()"
3538
alt="Preview"

src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.spec.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,20 @@ import { RouterTestingModule } from '@angular/router/testing';
1212
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
1313
import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service';
1414
import { FileSizePipe } from '../../../../../shared/utils/file-size-pipe';
15+
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
16+
import { AuthService } from '../../../../../core/auth/auth.service';
17+
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
18+
import { FileService } from '../../../../../core/shared/file.service';
19+
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
20+
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
21+
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
22+
import { Bitstream } from '../../../../../core/shared/bitstream.model';
1523

1624
describe('FileDescriptionComponent', () => {
1725
let component: FileDescriptionComponent;
1826
let fixture: ComponentFixture<FileDescriptionComponent>;
1927
let halService: HALEndpointService;
28+
let bitstreamDataService: BitstreamDataService;
2029

2130
beforeEach(async () => {
2231
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
@@ -32,6 +41,10 @@ describe('FileDescriptionComponent', () => {
3241
getRootHref: 'root url',
3342
});
3443

44+
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
45+
findById: createSuccessfulRemoteDataObject$(new Bitstream()),
46+
});
47+
3548
await TestBed.configureTestingModule({
3649
imports: [TranslateModule.forRoot({
3750
loader: {
@@ -42,7 +55,11 @@ describe('FileDescriptionComponent', () => {
4255
declarations: [FileDescriptionComponent, FileSizePipe],
4356
providers: [
4457
{ provide: ConfigurationDataService, useValue: configurationDataService },
45-
{ provide: HALEndpointService, useValue: halService }
58+
{ provide: HALEndpointService, useValue: halService },
59+
{ provide: AuthService, useClass: AuthServiceStub },
60+
{ provide: FileService, useClass: FileServiceStub },
61+
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
62+
{ provide: BitstreamDataService, useValue: bitstreamDataService },
4663
]
4764
}).compileComponents();
4865
});
@@ -53,7 +70,7 @@ describe('FileDescriptionComponent', () => {
5370

5471
// Mock the input value
5572
const fileInput = new MetadataBitstream();
56-
fileInput.id = 123;
73+
fileInput.id = '66efe81e-2950-483d-a065-bbdacd689f95';
5774
fileInput.name = 'testFile';
5875
fileInput.description = 'test description';
5976
fileInput.fileSize = 2048;

src/app/item-page/simple/field-components/preview-section/file-description/file-description.component.ts

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
1-
import { Component, Input, OnInit } from '@angular/core';
1+
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
22
import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model';
33
import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service';
44
import { Router } from '@angular/router';
55
import { ConfigurationDataService } from '../../../../../core/data/configuration-data.service';
6-
import { getFirstSucceededRemoteData } from '../../../../../core/shared/operators';
6+
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../../../../../core/shared/operators';
7+
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
8+
import { Bitstream } from '../../../../../core/shared/bitstream.model';
9+
import { RemoteData } from '../../../../../core/data/remote-data';
10+
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
11+
import { fromEvent, merge, Observable, of, Subscription } from 'rxjs';
12+
import { FileService } from '../../../../../core/shared/file.service';
13+
import { distinctUntilChanged, switchMap, take } from 'rxjs/operators';
14+
import { FeatureID } from '../../../../../core/data/feature-authorization/feature-id';
15+
import { hasValue } from '../../../../../shared/empty.util';
16+
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
17+
import { AuthService } from '../../../../../core/auth/auth.service';
718

819
const allowedPreviewFormats = ['text/plain', 'text/html', 'application/zip', 'application/x-tar'];
920
@Component({
1021
selector: 'ds-file-description',
1122
templateUrl: './file-description.component.html',
1223
styleUrls: ['./file-description.component.scss'],
1324
})
14-
export class FileDescriptionComponent implements OnInit {
25+
export class FileDescriptionComponent implements OnInit, OnDestroy {
1526
MIME_TYPE_IMAGES_PATH = './assets/images/mime/';
1627
MIME_TYPE_DEFAULT_IMAGE_NAME = 'application-octet-stream.png';
1728

1829
@Input()
1930
fileInput: MetadataBitstream;
2031

32+
@ViewChild('videoPreview') videoElement: ElementRef;
33+
2134
emailToContact: string;
35+
content_url$: Observable<string>;
36+
content_url: string;
37+
thumbnail_url$: Observable<string>;
38+
handlers_added = false;
39+
playPromise: Promise<void>;
40+
41+
private subscriptions: Subscription = new Subscription();
2242

2343
constructor(protected halService: HALEndpointService,
2444
private router: Router,
45+
private bitstreamService: BitstreamDataService,
46+
private auth: AuthService,
47+
private authDataService: AuthorizationDataService,
48+
private fileService: FileService,
2549
private configService: ConfigurationDataService) { }
2650

2751
ngOnInit(): void {
@@ -30,6 +54,122 @@ export class FileDescriptionComponent implements OnInit {
3054
.subscribe(remoteData => {
3155
this.emailToContact = remoteData?.payload?.values?.[0];
3256
});
57+
this.content_url$ = this.bitstreamService.findById(this.fileInput.id, true, false, followLink('thumbnail'))
58+
.pipe(getFirstCompletedRemoteData(),
59+
switchMap((remoteData: RemoteData<Bitstream>) => {
60+
if (remoteData.hasSucceeded) {
61+
if (remoteData.payload?.thumbnail){
62+
this.thumbnail_url$ = remoteData.payload?.thumbnail.pipe(
63+
switchMap((thumbnailRD: RemoteData<Bitstream>) => {
64+
if (thumbnailRD.hasSucceeded) {
65+
return this.buildUrl(thumbnailRD.payload?._links.content.href);
66+
} else {
67+
return of('');
68+
}
69+
}),
70+
);
71+
} else {
72+
this.thumbnail_url$ = of('');
73+
}
74+
return of(remoteData.payload?._links.content.href);
75+
}
76+
}
77+
));
78+
this.content_url$.pipe(take(1)).subscribe((url) => {
79+
this.content_url = url;
80+
});
81+
}
82+
83+
ngAfterViewInit() {
84+
const video = this.videoElement?.nativeElement;
85+
86+
if (video) {
87+
const error$ = fromEvent(video, 'error');
88+
this.subscriptions.add(
89+
error$.subscribe((event) => {
90+
//console.log('error', video.error.message);
91+
if (hasValue(video.src)) {
92+
this.auth.isAuthenticated().pipe(
93+
switchMap((isLoggedIn) => {
94+
if (isLoggedIn) {
95+
return this.authDataService.isAuthorized(FeatureID.CanDownload, this.content_url.replace('/content', ''));
96+
} else {
97+
return of(false);
98+
}
99+
}),
100+
).subscribe((isAuthorized: boolean) => {
101+
if (isAuthorized) {
102+
this.add_short_lived_token_handling_to_video_playback(video);
103+
this.resetSource();
104+
} else {
105+
video.src = null;
106+
}
107+
});
108+
}
109+
})
110+
);
111+
}
112+
}
113+
114+
private add_short_lived_token_handling_to_video_playback(video: HTMLVideoElement) {
115+
if (this.handlers_added) {
116+
return;
117+
}
118+
119+
const seeking$ = fromEvent(video, 'seeking').pipe(
120+
switchMap((event: Event) => {
121+
//console.log('seeking');
122+
return of(video.currentTime);
123+
}),
124+
distinctUntilChanged(),
125+
);
126+
const stalled$ = fromEvent(video, 'stalled').pipe(
127+
switchMap((event: Event) => {
128+
//console.log('stalled');
129+
return of(video.currentTime);
130+
}),
131+
distinctUntilChanged(),
132+
);
133+
this.subscriptions.add(
134+
merge(seeking$, stalled$).subscribe((currentTime) => {
135+
this.resetSource(currentTime);
136+
})
137+
);
138+
139+
this.handlers_added = true;
140+
141+
}
142+
143+
private resetSource(currentTime?) {
144+
const video = this.videoElement?.nativeElement;
145+
//console.log("networkState in resetSource", video.networkState);
146+
if (this.playPromise) {
147+
this.playPromise?.then(_ => {
148+
//playback has started
149+
// don't want to see The play() request was interrupted by...
150+
// https://developer.chrome.com/blog/play-request-was-interrupted
151+
this.updateSource(video, currentTime);
152+
}).catch(_ => {
153+
//do nothing
154+
});
155+
} else {
156+
this.updateSource(video, currentTime);
157+
}
158+
}
159+
160+
private updateSource(video, currentTime) {
161+
//console.log("Updating the src");
162+
this.buildUrl(this.content_url).subscribe(result => {
163+
video.src = result;
164+
if (currentTime) {
165+
video.currentTime = currentTime;
166+
}
167+
this.playPromise = video.play();
168+
});
169+
}
170+
171+
private buildUrl(url: string): Observable<string> {
172+
return url ? this.fileService.retrieveFileDownloadLink(url) : of(url);
33173
}
34174

35175
public downloadFile() {
@@ -68,4 +208,8 @@ export class FileDescriptionComponent implements OnInit {
68208
// this.fileInput.fileInfo.length === 0 means that the file has no preview
69209
return this.fileInput?.fileInfo?.length === 0;
70210
}
211+
212+
ngOnDestroy(): void {
213+
this.subscriptions.unsubscribe();
214+
}
71215
}

src/app/item-page/simple/field-components/preview-section/preview-section.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('PreviewSectionComponent', () => {
4343

4444
// Set up the mock service's getMetadataBitstream method to return a simple stream
4545
const metadatabitstream = new MetadataBitstream();
46-
metadatabitstream.id = 123;
46+
metadatabitstream.id = '5974f1cf-f2ef-4e4c-8f6d-85ad6c52efde';
4747
metadatabitstream.name = 'test';
4848
metadatabitstream.description = 'test';
4949
metadatabitstream.fileSize = 1024;

src/app/item-page/simple/field-components/preview-section/preview-section.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class PreviewSectionComponent implements OnInit {
2222

2323
ngOnInit(): void {
2424
this.registryService
25-
.getMetadataBitstream(this.item.handle, 'ORIGINAL,TEXT,THUMBNAIL')
25+
.getMetadataBitstream(this.item.handle, 'ORIGINAL')
2626
.pipe(getAllSucceededRemoteListPayload())
2727
.subscribe((data: MetadataBitstream[]) => {
2828
this.listOfFiles.next(data);

0 commit comments

Comments
 (0)