feat(downloads): download completed Xtream catch-up programmes as TS (#1572)

* feat(epg): copy catch-up programme URLs without changing playback

* feat(downloads): save completed Xtream archive programmes as TS

* fix(epg): let newer archive copy requests supersede pending work

* fix(downloads): protect archive partials and independent submissions

* fix(downloads): verify archive identity through finalization

* fix(downloads): bound archive storage and capture cleanup entries

* fix(downloads): preserve archive ownership across failure paths

* fix(downloads): recover explicitly verified archive completions

* fix(downloads): journal archive promotion before publishing files

* fix(downloads): reset archive proof before an explicit restart

* fix(downloads): preserve archive recovery ownership and interruption

* fix(downloads): verify durable archive identity at resume open

* fix(downloads): fence archive commands during completion commit

* fix(downloads): persist archive ownership throughout its lifecycle

* fix(downloads): protect archive removal and missing-file recovery

* fix(downloads): journal private cleanup captures for recovery

* fix(downloads): journal active archive cleanup before removal

* fix(downloads): clean settled archives before deleting stale rows

* fix(downloads): preserve archive ownership on removal and resubmission

* fix(downloads): recover proven archive completions before retry

* fix(downloads): recover local archives before remote transfer checks

* test(downloads): resolve archive fixture from workspace root

* fix(downloads): distinguish reused archive inodes by creation time

* fix(downloads): bind fresh archive reservations to owned files

* fix(downloads): clean reservations when ownership writes fail

* fix(downloads): commit archive reservation and ownership atomically

* fix(downloads): retain captures until replacement restoration succeeds

* fix(downloads): require durable ownership before cleanup relocation

* fix(downloads): preserve 64-bit archive file identities on Windows

* refactor(release): keep capture fixture constants in their shared module

* fix(downloads): preserve the last link of captured foreign files

* fix(downloads): expose retained archive recovery files

* fix(downloads): keep recovery instructions open while copying
This commit is contained in:
4gray authored and GitHub committed 2026-09-08 20:33:05 +02:00
1 parent a7f3860102
commit bad8a0991e
117 files changed
+7510 -880

No files matched your search

@@ -0,0 +1,54 @@
import { TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { ConfirmDialogComponent } from './confirm-dialog.component';
describe('ConfirmDialogComponent actions', () => {
it.each([false, true])(
'keeps the action in place only when requested: %s',
async (keepOpenOnConfirm) => {
const onConfirm = jest.fn();
const close = jest.fn();
await TestBed.configureTestingModule({
imports: [
ConfirmDialogComponent,
NoopAnimationsModule,
TranslateModule.forRoot(),
],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: {
title: 'Recovery',
message: '/saved/entry',
confirmLabel: 'Copy',
cancelLabel: 'Close',
keepOpenOnConfirm,
onConfirm,
},
},
{ provide: MatDialogRef, useValue: { close } },
],
}).compileComponents();
const fixture = TestBed.createComponent(ConfirmDialogComponent);
fixture.detectChanges();
const buttons = fixture.nativeElement.querySelectorAll(
'button'
) as NodeListOf<HTMLButtonElement>;
buttons[1].click();
if (keepOpenOnConfirm) {
expect(onConfirm).toHaveBeenCalledTimes(1);
expect(close).not.toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain(
'/saved/entry'
);
} else {
expect(onConfirm).not.toHaveBeenCalled();
expect(close).toHaveBeenCalledWith(true);
}
buttons[0].click();
expect(close).toHaveBeenCalled();
}
);
});
@@ -9,6 +9,8 @@ export interface ConfirmDialogData {
width?: string;
confirmLabel?: string;
cancelLabel?: string;
/** Run the action in the dialog; only Close/backdrop/Escape dismiss it. */
keepOpenOnConfirm?: boolean;
onConfirm: () => void;
}
@@ -25,9 +27,23 @@ export interface ConfirmDialogData {
<button mat-button mat-dialog-close cdkFocusInitial>
{{ dialogData.cancelLabel || 'NO' | translate }}
</button>
<button mat-flat-button [mat-dialog-close]="true" color="primary">
{{ dialogData.confirmLabel || 'YES' | translate }}
</button>
@if (dialogData.keepOpenOnConfirm) {
<button
mat-flat-button
(click)="dialogData.onConfirm()"
color="primary"
>
{{ dialogData.confirmLabel || 'YES' | translate }}
</button>
} @else {
<button
mat-flat-button
[mat-dialog-close]="true"
color="primary"
>
{{ dialogData.confirmLabel || 'YES' | translate }}
</button>
}
</mat-dialog-actions>
`,
})
@@ -83,4 +83,15 @@ describe('DialogService', () => {
expect(onConfirm).toHaveBeenCalledTimes(1);
});
it('does not repeat a persistent dialog action on programmatic close', () => {
const onConfirm = jest.fn();
dialog.open.mockReturnValue({ afterClosed: () => of(true) });
service.openConfirmDialog({
title: 'Recovery',
message: 'Path',
keepOpenOnConfirm: true,
onConfirm,
});
expect(onConfirm).not.toHaveBeenCalled();
});
});
@@ -28,6 +28,8 @@ export class DialogService {
});
dialogRef
.afterClosed()
.subscribe((result) => (result ? data.onConfirm() : null));
.subscribe((result) =>
result && !data.keepOpenOnConfirm ? data.onConfirm() : null
);
}
}