mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
fix(portals): keep provider playback for a downloaded movie
Once a movie is downloaded the primary button plays the local file, and the sources popover only exists when another playlist carries the same film. The icon-only rework left those as the only two paths, so a downloaded movie in a single-playlist library had no way at all to stream the provider's copy — the labeled action that used to do it was gone. Restores it as an icon button beside the downloaded checkmark, under the same condition the old one used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
63ce638458
commit
ea505a6a9a
4 files changed
+101
No files matched your search
@@ -624,6 +624,36 @@ describe('VodDetailsRouteComponent fallback actions', () => {
|
||||
expect(openResolvedPlayback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still offers provider playback for a downloaded movie with no alternatives', async () => {
|
||||
// The primary button plays the local file once downloaded, and the
|
||||
// sources chip only appears when another playlist carries the film —
|
||||
// so without this control a single-playlist library loses every way
|
||||
// to stream the provider's copy.
|
||||
selectedItem.set(sparseItem());
|
||||
downloadsAvailable.set(true);
|
||||
isDownloaded.mockReturnValue(true);
|
||||
const playPinned = jest
|
||||
.spyOn(fixture.componentInstance.multiSource, 'playPinnedSource')
|
||||
.mockResolvedValue('played');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const host = fixture.nativeElement as HTMLElement;
|
||||
expect(fixture.componentInstance.multiSource.hasAlternatives()).toBe(
|
||||
false
|
||||
);
|
||||
const providerButton = host.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="vod-play-provider"]'
|
||||
);
|
||||
expect(providerButton).not.toBeNull();
|
||||
|
||||
providerButton?.click();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(playPinned).toHaveBeenCalled();
|
||||
expect(playDownload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reveals the finished file from the downloaded state button', async () => {
|
||||
selectedItem.set(sparseItem());
|
||||
downloadsAvailable.set(true);
|
||||
|
||||
@@ -109,6 +109,29 @@
|
||||
>
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
</button>
|
||||
<!-- The primary button plays the local file once a movie
|
||||
is downloaded, and the sources popover only exists
|
||||
when another playlist also carries the film. Without
|
||||
this, a downloaded movie in a single-playlist library
|
||||
offers no way to stream the provider's copy at all. -->
|
||||
@if (externalPrimaryButtonState() === 'idle') {
|
||||
<button
|
||||
type="button"
|
||||
class="icon-action-btn"
|
||||
(click)="playFromProviderSource(playableItem)"
|
||||
[matTooltip]="
|
||||
'PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE'
|
||||
| translate
|
||||
"
|
||||
[attr.aria-label]="
|
||||
'PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE'
|
||||
| translate
|
||||
"
|
||||
data-testid="vod-play-provider"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
}
|
||||
} @else if (isDownloading()) {
|
||||
<!-- Covers downloading, queued and paused: the ring holds
|
||||
the real percent, spins when the size is unknown, and
|
||||
|
||||
@@ -405,6 +405,35 @@ describe('VodMultiSourceHostService', () => {
|
||||
await Promise.all(pending);
|
||||
});
|
||||
|
||||
it('drops queued checks once the user opens another movie', async () => {
|
||||
const alts = [2, 3, 4, 5, 6, 7].map(alternative);
|
||||
await loadMovie(alts);
|
||||
|
||||
const gates = new Map<string, ReturnType<typeof createDeferred>>();
|
||||
resolver.resolve.mockImplementation((candidate) => {
|
||||
const gate = createDeferred<null>();
|
||||
gates.set((candidate as VodSourceCandidate).id, gate);
|
||||
return gate.promise;
|
||||
});
|
||||
|
||||
const pending = alts.map((alt) => service.check(alt.id));
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
// Four in flight, two still waiting for a slot.
|
||||
expect(resolver.resolve).toHaveBeenCalledTimes(4);
|
||||
|
||||
// The user navigates away while those two are still queued. They are
|
||||
// about a film that is no longer on screen, so they must not spend a
|
||||
// resolve against a foreign portal when a slot frees up.
|
||||
await loadMovie([], MOVIE_B);
|
||||
|
||||
for (const [, gate] of gates) {
|
||||
gate.resolve(null);
|
||||
}
|
||||
await Promise.all(pending);
|
||||
|
||||
expect(resolver.resolve).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('remembers verdicts across sessions instead of re-checking', async () => {
|
||||
probes.probe.mockResolvedValue({
|
||||
...PROBE_OK,
|
||||
|
||||
@@ -411,6 +411,25 @@ export class VodMultiSourceHostService {
|
||||
this.controller.setProbe(sourceId, { status: 'probing' });
|
||||
this.publish();
|
||||
|
||||
// Bound to the session that ASKED for the check, not to whatever is
|
||||
// current when a slot frees up. "Check all" can leave a dozen tasks
|
||||
// waiting, and the user is free to open another movie meanwhile —
|
||||
// those tasks are then about a film that is no longer on screen, so
|
||||
// they are dropped before spending a resolve on a foreign portal.
|
||||
// Both the controller and the session are read when a slot frees up,
|
||||
// NOT captured here — and that is deliberate.
|
||||
//
|
||||
// Capturing them would look safer and is worse: `sessionToken` also
|
||||
// moves when the movie identity is momentarily null, and that path
|
||||
// does NOT replace the controller (only a different film does). A
|
||||
// task dropped on a stale captured token would therefore abandon a
|
||||
// row it had already marked `probing` in the controller still on
|
||||
// screen, leaving it spinning for good.
|
||||
//
|
||||
// Read late, a queued task simply asks the CURRENT controller for the
|
||||
// source: gone when the user moved to another film, so `probeSource`
|
||||
// returns before it spends a request, and present when they came back
|
||||
// to the same one, where finishing the check is exactly right.
|
||||
return this.checkQueue(() =>
|
||||
probeSource(sourceId, {
|
||||
controller: this.controller,
|
||||
|
||||
Reference in new issue
Block a user