From 847c30b11212b6cb4683d9964e98a6daf38d0a15 Mon Sep 17 00:00:00 2001 From: Joris Bertomeu Date: Tue, 26 Aug 2025 10:42:30 +0200 Subject: [PATCH] Add Widevine Plugin support --- public/favicon.ico | Bin 15086 -> 15086 bytes src/app/app.component.html | 18 ++--- src/app/app.component.ts | 160 ++++++++++++++++++++++++++++++++++--- 3 files changed, 159 insertions(+), 19 deletions(-) diff --git a/public/favicon.ico b/public/favicon.ico index 57614f9c967596fad0a3989bec2b1deff33034f6..a18bbb2381242ba1a508053f470ce01c10d8a507 100644 GIT binary patch literal 15086 zcmeHOU1%It6uwIW5e%k22(c)Mg5ZN-@Ihach}CC(P!uYr>Vt~_e9d0_KAP*JqoOhunGx&E=G>a(C{HmktiDU=hYqP<}$rMu#c+Ek5Qma0`D zZ+A*Dxn^42cra)j_V~M30{3^Ff?0+G^M6=22D>nQmBqF5GMNtK9r2C<7-u^Wr*HgL zdFeSZ2E@!u-GrU6;(A!N3&(oxhm0Om)t0*rzk`=+PXYU5U5VY91O0PgKavzb=J0Zj z>jx2k?<6Vk-62!eFDJI|$ZEAj@?5K&!+I;Op`_FvKZ-S5uOz~-p8YAWt4T3?h`27=cdnw7-I|09?miA!`pL8q!1y#G%;-x3M5kKbtdx}@u z!*IS5cMMw@KZu!?3ojnJR$Olu*I(`y{Maw*Sg-CB*agLP{A-*sY>EH5zEGRcZV{N> z)GiqP!2eg&F>EG&#LGPF?Y3<@=f>eVE_9{?RbJe(mtfoS$CU9IJ9|g4!{^7tiwp z*9kpw`62u7tk4^mw)?oXiO?8vDni5(w*J(K9OSq>>M<<+Yvb0+Ueq z!fs=5y<@1U`m+hmry|zO@5YZMt}j-U(vDO>q@aYZ=_unJ#>zzd7@Hy5)1=6D6saC~kaZk6+b)Jt%AQa^(o2-=5GA`r1&%@!7igUgK3*i6 z>mO;*$wB7ukM}nyblT2ei{_{fFntZ$=lExa4LG#Sh8()YMjcuw*P*|2#HaL~)-cN5 zDA{*fa6)!;ya(*(r~7c9_f4A5bZc~R?KPPr=)L`#zW%vC>GNY$d3v^VW2h?H&Zx3! z^#cZHi6tCq>5QwVmNyiRqm8+BjcuI$(mv#S-U{>6M%fqs+6wz-e(DZkzs9vkf-s~# z{zc^3gl$3b?(i933|#vX@6_*)S9Hj(~lC!>@P!MgafR}SxC9bIVqv32c3 zcTgQ|Yw}4ln=+o)KKb8PjRE2#SN0~!gwc46-dnqz+J4k5N3ftd?AHbeT%e3^Fxk#)g)9?;TpYad-$17F4KbVHs(SI;Av< zg?1y&8s#bQ6Ei1Q(G7VD`t1eQ!f3y_z;7flwAK-}hWfRW0<1Nue~aA$<~i}h;pKb8 zT2*g9s$<_?^DSOWz;oi27vIEr`5v)W)!h$amTNWO1b%G$_J-Q1zn3<3tocU9{q2QU zM%0CUzi%W&dkCE)eX@hzxyoc^eS>)PjtzU;NqVOi)UOlT8o5y9W09LJHfOZ2jD@OM dUipKu&kitlVh3`@$lI0+$kjUeQ_7t(_CF;cHSqud literal 15086 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 Nr__JTFzvMr^FKp4S3v*( diff --git a/src/app/app.component.html b/src/app/app.component.html index ca5ae50..da7989c 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -66,7 +66,7 @@
Downloader
- +
Installed binary @@ -76,7 +76,7 @@ No
- +
Update available
@@ -86,7 +86,7 @@ No - +
- +
Content Decrypter
- +
Installed binary @@ -113,7 +113,7 @@ No
- +
Update available
@@ -123,7 +123,7 @@ No - +
- +
@@ -166,7 +166,7 @@
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b262fc1..4b96a26 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,6 +4,7 @@ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule } 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', @@ -24,6 +25,9 @@ export class AppComponent implements OnInit { 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({ @@ -37,7 +41,9 @@ export class AppComponent implements OnInit { } constructor(private fb: FormBuilder, - private videoProcessingService: VideoProcessingService) { + private videoProcessingService: VideoProcessingService, + private router: Router, + private route: ActivatedRoute) { this.processingForm = this.fb.group({ keys: ['', Validators.required], wantedResolution: ['1920x1080', Validators.required] @@ -51,6 +57,117 @@ export class AppComponent implements OnInit { 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) { @@ -130,16 +247,31 @@ export class AppComponent implements OnInit { console.log(data); this.videoProcessingService.startProcess(data).subscribe({ next: (response) => { - //this.jobId = response.jobId; + // 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); } }); - //this.initForm(); } } + 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'; @@ -153,9 +285,22 @@ export class AppComponent implements OnInit { this.videoProcessingService.load(formData).subscribe({ next: (response) => { - console.log(response); this.loaded = response; - //this.jobId = response.jobId; + + 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); @@ -165,7 +310,6 @@ export class AppComponent implements OnInit { } parseKeys(keysString: string): { key: string, value: string }[] { - console.log('KS =', keysString); return keysString.split('\n').map(line => line.trim()) .filter(line => line.includes(':')) .map(line => { @@ -180,14 +324,10 @@ export class AppComponent implements OnInit { next: (response) => { this.lastJobSuccess = true; this.jobs = response; - // if (['completed', 'failed'].includes(response.state)) { - // clearInterval(interval); - // } }, error: (error) => { console.error('Error fetching job status:', error); this.lastJobSuccess = false; - //clearInterval(interval); } }); }, 1500);