mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(epg): let newer archive copy requests supersede pending work
This commit is contained in:
1 parent
dcaeb26849
commit
530c4db03f
5 files changed
+62
-19
No files matched your search
@@ -1134,8 +1134,9 @@ catch-up capability and therefore does not expose this action.
|
||||
|
||||
The URL can contain account credentials; the dialog explains that external
|
||||
players may also need the source's HTTP headers. Copy is explicit, errors use
|
||||
localized feedback without raw URLs, and repeated clicks share one pending
|
||||
copy operation. Timeline/list dialogs capture the host's source/channel
|
||||
localized feedback without raw URLs, and each explicit request supersedes older
|
||||
unresolved copies. Only the latest request may update the clipboard or feedback,
|
||||
so a slow resolver cannot block another channel or overwrite its copied URL. Timeline/list dialogs capture the host's source/channel
|
||||
`archiveContextKey`, refuse actions after it changes, and unsubscribe when
|
||||
the view is destroyed. A copied M3U start-over URL need not stop at the programme's
|
||||
end. Copying a URL does not download or preserve an expiring archive.
|
||||
|
||||
+5
@@ -1,3 +1,4 @@
|
||||
import { EpgArchiveCopyService } from '@iptvnator/ui/epg';
|
||||
import { computed, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
@@ -89,6 +90,10 @@ describe('Xtream live auto-open playback queue', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LiveStreamLayoutComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: EpgArchiveCopyService,
|
||||
useValue: { copy: jest.fn() },
|
||||
},
|
||||
{ provide: XtreamStore, useValue: store },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
|
||||
+9
-1
@@ -1,3 +1,4 @@
|
||||
import { EpgArchiveCopyService } from '@iptvnator/ui/epg';
|
||||
import { Component, Directive, input, output, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -130,6 +131,10 @@ describe('LiveStreamLayoutComponent sidebar levels', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LiveStreamLayoutComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: EpgArchiveCopyService,
|
||||
useValue: { copy: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
@@ -182,7 +187,10 @@ describe('LiveStreamLayoutComponent sidebar levels', () => {
|
||||
openExternalPlayback: jest.fn(),
|
||||
},
|
||||
},
|
||||
{ provide: LIVE_CATEGORIES_POPOVER, useValue: categoriesPopover },
|
||||
{
|
||||
provide: LIVE_CATEGORIES_POPOVER,
|
||||
useValue: categoriesPopover,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(LiveStreamLayoutComponent, {
|
||||
|
||||
@@ -57,19 +57,49 @@ describe('EpgArchiveCopyService', () => {
|
||||
'EPG.PROGRAM_DIALOG.ARCHIVE_URL_FAILED',
|
||||
]);
|
||||
});
|
||||
it('coalesces repeated clicks while the URL is resolving', async () => {
|
||||
let resolve!: (url: string) => void;
|
||||
const pending = service.copy(
|
||||
it.each([false, true])(
|
||||
'lets a new copy replace a slow request (old failure: %s)',
|
||||
async (failOld) => {
|
||||
let finish!: (url: string) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const first = service.copy(
|
||||
() =>
|
||||
new Promise<string>((resolve, fail) => {
|
||||
finish = resolve;
|
||||
reject = fail;
|
||||
})
|
||||
);
|
||||
const latest = jest.fn(async () => 'https://provider.test/new.ts');
|
||||
await service.copy(latest);
|
||||
expect(latest).toHaveBeenCalledTimes(1);
|
||||
if (failOld) reject(new Error('secret old request'));
|
||||
else finish('https://provider.test/old.ts');
|
||||
await first;
|
||||
expect(copy.mock.calls).toEqual([['https://provider.test/new.ts']]);
|
||||
expect(open).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
);
|
||||
it('discards an old result even while the newer request is still pending', async () => {
|
||||
let finishOld!: (url: string) => void,
|
||||
finishNew!: (url: string) => void;
|
||||
const first = service.copy(
|
||||
() =>
|
||||
new Promise<string>((done) => {
|
||||
resolve = done;
|
||||
new Promise<string>((resolve) => {
|
||||
finishOld = resolve;
|
||||
})
|
||||
);
|
||||
const duplicate = jest.fn();
|
||||
await service.copy(duplicate);
|
||||
expect(duplicate).not.toHaveBeenCalled();
|
||||
resolve('https://provider.test/archive.ts');
|
||||
await pending;
|
||||
expect(copy).toHaveBeenCalledTimes(1);
|
||||
const second = service.copy(
|
||||
() =>
|
||||
new Promise<string>((resolve) => {
|
||||
finishNew = resolve;
|
||||
})
|
||||
);
|
||||
finishOld('https://provider.test/old.ts');
|
||||
await first;
|
||||
expect(copy).not.toHaveBeenCalled();
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
finishNew('https://provider.test/new.ts');
|
||||
await second;
|
||||
expect(copy).toHaveBeenCalledWith('https://provider.test/new.ts');
|
||||
});
|
||||
});
|
||||
@@ -9,24 +9,23 @@ export class EpgArchiveCopyService {
|
||||
private readonly clipboard = inject(Clipboard);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private pending = false;
|
||||
private requestGeneration = 0;
|
||||
|
||||
async copy(
|
||||
resolve: () => string | null | Promise<string | null>
|
||||
): Promise<void> {
|
||||
if (this.pending) return;
|
||||
this.pending = true;
|
||||
const generation = ++this.requestGeneration;
|
||||
let copied = false;
|
||||
try {
|
||||
const url = await resolve();
|
||||
if (generation !== this.requestGeneration) return;
|
||||
if (url && ['http:', 'https:'].includes(new URL(url).protocol)) {
|
||||
copied = this.clipboard.copy(url);
|
||||
}
|
||||
} catch {
|
||||
// Provider URLs and error messages may contain credentials.
|
||||
} finally {
|
||||
this.pending = false;
|
||||
}
|
||||
if (generation !== this.requestGeneration) return;
|
||||
this.snackBar.open(
|
||||
this.translate.instant(
|
||||
copied
|
||||
|
||||
Reference in new issue
Block a user