Files
CrawlFlix-Front/src/app/app.component.ts
Joris Bertomeu 847c30b112
All checks were successful
ci / Image build (push) Successful in 2m13s
ci / Deployment (push) Successful in 7s
Add Widevine Plugin support
2025-08-26 10:42:30 +02:00

335 lines
11 KiB
TypeScript

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
import { VideoProcessingService } from './video-processing.service';
import { MomentModule } from 'ngx-moment';
import { OrderModule } from 'ngx-order-pipe';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, MomentModule, OrderModule, FormsModule],
templateUrl: './app.component.html',
styles: []
})
export class AppComponent implements OnInit {
processingForm: FormGroup;
loadForm: FormGroup;
jobId: string | null = null;
jobStatus: string | null = null;
jobProgress: number = 0;
jobs: Array<any> = [];
lastJobSuccess: boolean = false;
welcomeHeader: any = null;
processRemux: boolean = true;
loaded: any = null;
Math: any = Math;
// Indicateur pour le préchargement depuis l'extension
isPreloadedFromExtension: boolean = false;
initForm() {
this.processingForm = this.fb.group({
keys: ['', Validators.required],
wantedResolution: ['1920x1080', Validators.required]
});
this.loadForm = this.fb.group({
mp4Filename: ['test', Validators.required],
mpdUrl: ['https://bakery.pplus.paramount.tech/l(de,it,no,fi,da,sv,es-MX,pt-BR,es-mx,pt-br)/paramountplus/2023/11/06/2279898691624/2416513_cenc_precon_dash/stream.mpd?CMCD=ot%3Dm%2Csf%3Dd%2Csid%3D%22295d7a15-c79d-4229-b593-7abdacd727c9%22%2Csu', Validators.required],
});
}
constructor(private fb: FormBuilder,
private videoProcessingService: VideoProcessingService,
private router: Router,
private route: ActivatedRoute) {
this.processingForm = this.fb.group({
keys: ['', Validators.required],
wantedResolution: ['1920x1080', Validators.required]
});
this.loadForm = this.fb.group({
mp4Filename: ['test', Validators.required],
mpdUrl: ['https://bakery.pplus.paramount.tech/l(de,it,no,fi,da,sv,es-MX,pt-BR,es-mx,pt-br)/paramountplus/2023/11/06/2279898691624/2416513_cenc_precon_dash/stream.mpd?CMCD=ot%3Dm%2Csf%3Dd%2Csid%3D%22295d7a15-c79d-4229-b593-7abdacd727c9%22%2Csu', Validators.required],
});
}
ngOnInit(): void {
this.startPollingJobsStatus();
this.sayHello();
this.checkForExtensionPreload();
const keysControl = this.processingForm.get('keys');
if (keysControl) {
keysControl.valueChanges.subscribe(value => {
const keys = this.parseKeys(value);
const videoTracks = this.loaded?.videoTracks || [];
const decryptableResolutions = videoTracks.filter((res: any) => {
if (!res.defaultKID || res.defaultKID.length === 0)
return false;
return keys.find(k => k.key.toLowerCase() === res.defaultKID.toLowerCase());
});
if (decryptableResolutions.length > 0) {
this.processingForm.get('wantedResolution')?.enable();
const currentResolution = this.processingForm.get('wantedResolution')?.value;
if (!decryptableResolutions.find((res: any) => res.name === currentResolution)) {
this.processingForm.get('wantedResolution')?.setValue(decryptableResolutions[0].name);
}
} else {
this.processingForm.get('wantedResolution')?.disable();
this.processingForm.get('wantedResolution')?.setValue(null);
}
});
}
}
/**
* Vérifie si l'application a été ouverte avec des paramètres depuis l'extension
*/
private checkForExtensionPreload(): void {
this.route.queryParams.subscribe(params => {
if (params['mpdUrl'] && params['keys'] && params['autoLoad']) {
this.preloadFromExtension(params['mpdUrl'], params['keys']);
// this.router.navigate([], {
// queryParams: {
// mpdUrl: null,
// keys: null,
// autoLoad: null
// },
// queryParamsHandling: 'merge'
// });
}
});
}
/**
* Précharge les données depuis l'extension Widevine
*/
private preloadFromExtension(mpdUrl: string, keys: string): void {
console.log('Preloading data from Widevine extension:', { mpdUrl, keys });
this.isPreloadedFromExtension = true;
const filename = this.generateFilenameFromMPD(mpdUrl);
this.loadForm.patchValue({
mpdUrl: mpdUrl,
mp4Filename: filename
});
this.processingForm.patchValue({
keys: keys
});
setTimeout(() => {
this.onSubmitLoad();
}, 500);
}
/**
* Génère un nom de fichier intelligent basé sur l'URL MPD
*/
private generateFilenameFromMPD(mpdUrl: string): string {
try {
const url = new URL(mpdUrl);
// Extraire des informations de l'URL
let filename = 'content';
// Patterns spécifiques pour différents CDNs
if (url.hostname.includes('canalplus') || url.hostname.includes('paramount')) {
// Exemple: paramplus.com_HDCTF601GLFR
const pathMatch = url.pathname.match(/([A-Z0-9_]+)\/\d+\//);
if (pathMatch) {
filename = pathMatch[1].toLowerCase().replace(/_/g, '-');
}
} else if (url.pathname.includes('/')) {
// Utiliser le dernier segment sans extension
const segments = url.pathname.split('/').filter(s => s);
const lastSegment = segments[segments.length - 1];
filename = lastSegment.replace('.mpd', '').replace(/[^a-zA-Z0-9-_]/g, '-');
}
// Ajouter un timestamp pour éviter les collisions
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
return `${filename}_${timestamp}`;
} catch (error) {
console.warn('Could not parse MPD URL for filename generation:', error);
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
return `widevine_content_${timestamp}`;
}
}
private clearURLParams(): void {
const url = new URL(window.location.href);
url.searchParams.delete('mpdUrl');
url.searchParams.delete('keys');
url.searchParams.delete('autoLoad');
window.history.replaceState({}, document.title, url.toString());
}
downloadFileFromAPI(filePath: string, filename: string, job: any) {
job.isDownloading = true;
this.videoProcessingService.downloadFile(filePath).subscribe({
next: (response) => {
const blob = new Blob([response], { type: 'video/mp4' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
job.isDownloading = false;
},
error: (error) => {
console.error('Error downloading file:', error);
job.isDownloading = false;
}
});
}
processUpdate(item: any, binType: string) {
item.isUpdating = true;
this.videoProcessingService.processUpdate(Object.assign(item, {binType})).subscribe({
next: (response) => {
item.isUpdating = false;
this.welcomeHeader = null;
this.sayHello();
},
error: (error) => {
console.error('Hello failed', error);
item.isUpdating = false;
}
});
}
sayHello() {
this.videoProcessingService.hello().subscribe({
next: (response) => {
console.log('Hello success', response);
this.welcomeHeader = response;
},
error: (error) => {
console.error('Hello failed', error);
}
});
}
flushQueue() {
this.videoProcessingService.flushQueue().subscribe({
next: (response) => {
},
error: (error) => {
}
});
}
humanFileSize(size: number) {
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return +((size / Math.pow(1024, i)).toFixed(2)) * 1 + ' ' + ['bps', 'kbps', 'mbps', 'gbps', 'tbps'][i];
}
onSubmit() {
if (this.processingForm.valid) {
const formData = this.processingForm.value;
const data = Object.assign(formData, {
keys: this.parseKeys(formData.keys),
wantedAudioTracks: this.loaded.audioTracks.filter((track: any) => track.selected),
wantedSubtitles: this.loaded.subtitles.filter((sub: any) => sub.selected),
wantedResolution: this.loaded.videoTracks.find((res: any) => res.name === formData.wantedResolution),
mpdUrl: this.loadForm.get('mpdUrl')?.value,
mp4Filename: this.loadForm.get('mp4Filename')?.value,
wantedRemux: this.processRemux
});
console.log(data);
this.videoProcessingService.startProcess(data).subscribe({
next: (response) => {
// Nettoyer les paramètres URL après avoir démarré le processus
if (this.isPreloadedFromExtension) {
this.clearURLParams();
this.isPreloadedFromExtension = false;
}
},
error: (error) => {
console.error('Error starting process:', error);
}
});
}
}
getFirstDecryptableResolution() {
const decryptableResolutions = this.loaded.videoTracks.filter((res: any) => this.isDecryptable(res));
return decryptableResolutions.length > 0 ? decryptableResolutions[0].name : null;
}
isDecryptable(res: any) {
if (!res.defaultKID || res.defaultKID.length === 0)
return false;
const keys = this.parseKeys(this.processingForm.value.keys);
return keys.find(k => k.key.toLowerCase() === res.defaultKID.toLowerCase());
}
displayJobAudio(tracks: any) {
if (tracks.length === 0)
return 'None';
return tracks.map((elem: any) => `${elem.name}${elem.attributes?.CODECS ? ` (${elem.attributes.CODECS || 'N/A'})` : ''}`).join(' + ');
}
onSubmitLoad() {
if (this.loadForm.valid) {
this.loaded = null;
const formData = this.loadForm.value;
this.videoProcessingService.load(formData).subscribe({
next: (response) => {
this.loaded = response;
if (this.isPreloadedFromExtension) {
console.log('MPD loaded successfully from Widevine extension');
const firstDecryptable = this.getFirstDecryptableResolution();
if (firstDecryptable) {
this.processingForm.patchValue({
wantedResolution: firstDecryptable
});
this.processingForm.get('wantedResolution')?.enable();
}
} else {
console.log('MPD loaded successfully', response);
// disable formData wantedResolution
this.processingForm.get('wantedResolution')?.disable();
}
},
error: (error) => {
console.error('Error starting process:', error);
}
});
}
}
parseKeys(keysString: string): { key: string, value: string }[] {
return keysString.split('\n').map(line => line.trim())
.filter(line => line.includes(':'))
.map(line => {
const [key, value] = line.split(':').map(part => part.trim());
return { key, value };
});
}
startPollingJobsStatus() {
const interval = setInterval(() => {
this.videoProcessingService.getJobsStatus().subscribe({
next: (response) => {
this.lastJobSuccess = true;
this.jobs = response;
},
error: (error) => {
console.error('Error fetching job status:', error);
this.lastJobSuccess = false;
}
});
}, 1500);
}
}