Add Widevine Plugin support
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -66,7 +66,7 @@
|
|||||||
<i class="fas fa-download text-primary me-2"></i>
|
<i class="fas fa-download text-primary me-2"></i>
|
||||||
<h6 class="mb-0 fw-semibold">Downloader</h6>
|
<h6 class="mb-0 fw-semibold">Downloader</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<small class="text-muted d-block mb-1">Installed binary</small>
|
<small class="text-muted d-block mb-1">Installed binary</small>
|
||||||
<span *ngIf="welcomeHeader.downloader.filsIsPresent" class="badge bg-success bg-opacity-10 text-success border border-success">
|
<span *ngIf="welcomeHeader.downloader.filsIsPresent" class="badge bg-success bg-opacity-10 text-success border border-success">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<i class="fas fa-exclamation-triangle me-1"></i>No
|
<i class="fas fa-exclamation-triangle me-1"></i>No
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<small class="text-muted d-block mb-1">Update available</small>
|
<small class="text-muted d-block mb-1">Update available</small>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
<span *ngIf="!welcomeHeader.downloader.newReleaseAvailable" class="badge bg-success bg-opacity-10 text-success border border-success">
|
<span *ngIf="!welcomeHeader.downloader.newReleaseAvailable" class="badge bg-success bg-opacity-10 text-success border border-success">
|
||||||
<i class="fas fa-circle-check me-1"></i>No
|
<i class="fas fa-circle-check me-1"></i>No
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button *ngIf="welcomeHeader.downloader.newReleaseAvailable" [disabled]="welcomeHeader.downloader.isUpdating" (click)="processUpdate(welcomeHeader.downloader, 'downloader')" class="btn btn-sm btn-primary">
|
<button *ngIf="welcomeHeader.downloader.newReleaseAvailable" [disabled]="welcomeHeader.downloader.isUpdating" (click)="processUpdate(welcomeHeader.downloader, 'downloader')" class="btn btn-sm btn-primary">
|
||||||
<i *ngIf="welcomeHeader.downloader.isUpdating" class="fas fa-circle-notch fa-spin me-1"></i>
|
<i *ngIf="welcomeHeader.downloader.isUpdating" class="fas fa-circle-notch fa-spin me-1"></i>
|
||||||
<i *ngIf="!welcomeHeader.downloader.isUpdating" class="fas fa-arrow-up me-1"></i>
|
<i *ngIf="!welcomeHeader.downloader.isUpdating" class="fas fa-arrow-up me-1"></i>
|
||||||
@@ -96,14 +96,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="border-start border-4 border-info ps-3 h-100">
|
<div class="border-start border-4 border-info ps-3 h-100">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<i class="fas fa-key text-info me-2"></i>
|
<i class="fas fa-key text-info me-2"></i>
|
||||||
<h6 class="mb-0 fw-semibold">Content Decrypter</h6>
|
<h6 class="mb-0 fw-semibold">Content Decrypter</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<small class="text-muted d-block mb-1">Installed binary</small>
|
<small class="text-muted d-block mb-1">Installed binary</small>
|
||||||
<span *ngIf="welcomeHeader.mp4decrypt.filsIsPresent" class="badge bg-success bg-opacity-10 text-success border border-success">
|
<span *ngIf="welcomeHeader.mp4decrypt.filsIsPresent" class="badge bg-success bg-opacity-10 text-success border border-success">
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<i class="fas fa-exclamation-triangle me-1"></i>No
|
<i class="fas fa-exclamation-triangle me-1"></i>No
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<small class="text-muted d-block mb-1">Update available</small>
|
<small class="text-muted d-block mb-1">Update available</small>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
<span *ngIf="!welcomeHeader.mp4decrypt.newReleaseAvailable" class="badge bg-success bg-opacity-10 text-success border border-success">
|
<span *ngIf="!welcomeHeader.mp4decrypt.newReleaseAvailable" class="badge bg-success bg-opacity-10 text-success border border-success">
|
||||||
<i class="fas fa-circle-check me-1"></i>No
|
<i class="fas fa-circle-check me-1"></i>No
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button *ngIf="welcomeHeader.mp4decrypt.newReleaseAvailable" [disabled]="welcomeHeader.mp4decrypt.isUpdating" (click)="processUpdate(welcomeHeader.mp4decrypt, 'mp4decrypt')" class="btn btn-sm btn-primary">
|
<button *ngIf="welcomeHeader.mp4decrypt.newReleaseAvailable" [disabled]="welcomeHeader.mp4decrypt.isUpdating" (click)="processUpdate(welcomeHeader.mp4decrypt, 'mp4decrypt')" class="btn btn-sm btn-primary">
|
||||||
<i *ngIf="welcomeHeader.mp4decrypt.isUpdating" class="fas fa-circle-notch fa-spin me-1"></i>
|
<i *ngIf="welcomeHeader.mp4decrypt.isUpdating" class="fas fa-circle-notch fa-spin me-1"></i>
|
||||||
<i *ngIf="!welcomeHeader.mp4decrypt.isUpdating" class="fas fa-arrow-up me-1"></i>
|
<i *ngIf="!welcomeHeader.mp4decrypt.isUpdating" class="fas fa-arrow-up me-1"></i>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" *ngIf="!welcomeHeader">
|
<div class="row" *ngIf="!welcomeHeader">
|
||||||
<div class="col-12 text-center py-4">
|
<div class="col-12 text-center py-4">
|
||||||
<i class="fas fa-circle-notch fa-2x fa-spin text-primary"></i>
|
<i class="fas fa-circle-notch fa-2x fa-spin text-primary"></i>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
<div class="flex flex-col mt-2">
|
<div class="flex flex-col mt-2">
|
||||||
<label>Wanted Resolution</label>
|
<label>Wanted Resolution</label>
|
||||||
<select formControlName="wantedResolution" class="form-select">
|
<select formControlName="wantedResolution" class="form-select">
|
||||||
<option [value]="res.name" *ngFor="let res of loaded.videoTracks">{{res.name}} ({{res.codec}} - {{humanFileSize(res.bandwidth)}})</option>
|
<option [disabled]="!isDecryptable(res)" [value]="res.name" *ngFor="let res of loaded.videoTracks">{{res.name}} ({{res.codec}} - {{humanFileSize(res.bandwidth)}})</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule }
|
|||||||
import { VideoProcessingService } from './video-processing.service';
|
import { VideoProcessingService } from './video-processing.service';
|
||||||
import { MomentModule } from 'ngx-moment';
|
import { MomentModule } from 'ngx-moment';
|
||||||
import { OrderModule } from 'ngx-order-pipe';
|
import { OrderModule } from 'ngx-order-pipe';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -24,6 +25,9 @@ export class AppComponent implements OnInit {
|
|||||||
processRemux: boolean = true;
|
processRemux: boolean = true;
|
||||||
loaded: any = null;
|
loaded: any = null;
|
||||||
Math: any = Math;
|
Math: any = Math;
|
||||||
|
|
||||||
|
// Indicateur pour le préchargement depuis l'extension
|
||||||
|
isPreloadedFromExtension: boolean = false;
|
||||||
|
|
||||||
initForm() {
|
initForm() {
|
||||||
this.processingForm = this.fb.group({
|
this.processingForm = this.fb.group({
|
||||||
@@ -37,7 +41,9 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(private fb: FormBuilder,
|
constructor(private fb: FormBuilder,
|
||||||
private videoProcessingService: VideoProcessingService) {
|
private videoProcessingService: VideoProcessingService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute) {
|
||||||
this.processingForm = this.fb.group({
|
this.processingForm = this.fb.group({
|
||||||
keys: ['', Validators.required],
|
keys: ['', Validators.required],
|
||||||
wantedResolution: ['1920x1080', Validators.required]
|
wantedResolution: ['1920x1080', Validators.required]
|
||||||
@@ -51,6 +57,117 @@ export class AppComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.startPollingJobsStatus();
|
this.startPollingJobsStatus();
|
||||||
this.sayHello();
|
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) {
|
downloadFileFromAPI(filePath: string, filename: string, job: any) {
|
||||||
@@ -130,16 +247,31 @@ export class AppComponent implements OnInit {
|
|||||||
console.log(data);
|
console.log(data);
|
||||||
this.videoProcessingService.startProcess(data).subscribe({
|
this.videoProcessingService.startProcess(data).subscribe({
|
||||||
next: (response) => {
|
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) => {
|
error: (error) => {
|
||||||
console.error('Error starting process:', 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) {
|
displayJobAudio(tracks: any) {
|
||||||
if (tracks.length === 0)
|
if (tracks.length === 0)
|
||||||
return 'None';
|
return 'None';
|
||||||
@@ -153,9 +285,22 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
this.videoProcessingService.load(formData).subscribe({
|
this.videoProcessingService.load(formData).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
console.log(response);
|
|
||||||
this.loaded = 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) => {
|
error: (error) => {
|
||||||
console.error('Error starting process:', error);
|
console.error('Error starting process:', error);
|
||||||
@@ -165,7 +310,6 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseKeys(keysString: string): { key: string, value: string }[] {
|
parseKeys(keysString: string): { key: string, value: string }[] {
|
||||||
console.log('KS =', keysString);
|
|
||||||
return keysString.split('\n').map(line => line.trim())
|
return keysString.split('\n').map(line => line.trim())
|
||||||
.filter(line => line.includes(':'))
|
.filter(line => line.includes(':'))
|
||||||
.map(line => {
|
.map(line => {
|
||||||
@@ -180,14 +324,10 @@ export class AppComponent implements OnInit {
|
|||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.lastJobSuccess = true;
|
this.lastJobSuccess = true;
|
||||||
this.jobs = response;
|
this.jobs = response;
|
||||||
// if (['completed', 'failed'].includes(response.state)) {
|
|
||||||
// clearInterval(interval);
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error fetching job status:', error);
|
console.error('Error fetching job status:', error);
|
||||||
this.lastJobSuccess = false;
|
this.lastJobSuccess = false;
|
||||||
//clearInterval(interval);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|||||||
Reference in New Issue
Block a user