mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 17:36:15 -08:00
fix(downloads): keep offline Xtream playback available
This commit is contained in:
1 parent
5511d66651
commit
26b20658e3
4 files changed
+154
-82
No files matched your search
@@ -143,6 +143,30 @@ describe('VodDetailsRouteComponent — playback actions', () => {
|
||||
expect(playDownload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stops external playback without a usable provider item', async () => {
|
||||
currentPlaylist.set({ id: 'playlist-1' });
|
||||
downloadsAvailable.set(true);
|
||||
isDownloaded.mockReturnValue(true);
|
||||
getDownloadedFilePath.mockReturnValue('/downloads/example.mp4');
|
||||
activeSession.set({
|
||||
player: 'mpv',
|
||||
status: 'playing',
|
||||
contentInfo: {
|
||||
playlistId: 'playlist-1',
|
||||
contentXtreamId: 650020,
|
||||
contentType: 'vod',
|
||||
},
|
||||
});
|
||||
|
||||
const component = fixture.componentInstance;
|
||||
expect(component.isExternalStopAction()).toBe(true);
|
||||
|
||||
await component.onPrimaryAction(null);
|
||||
|
||||
expect(closeSession).toHaveBeenCalled();
|
||||
expect(playDownload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('plays a completed download before provider resume or pin resolution', async () => {
|
||||
currentPlaylist.set({ id: 'playlist-1' });
|
||||
downloadsAvailable.set(true);
|
||||
|
||||
@@ -107,6 +107,15 @@ describe('VodDetailsRouteComponent fallback actions', () => {
|
||||
container_extension: 'mp4',
|
||||
},
|
||||
}) as XtreamVodDetails;
|
||||
const unplayableRichItem = (): XtreamVodDetails =>
|
||||
({
|
||||
...richItem(),
|
||||
movie_data: {
|
||||
stream_id: 650020,
|
||||
name: 'Metadata movie',
|
||||
container_extension: '',
|
||||
},
|
||||
}) as XtreamVodDetails;
|
||||
beforeEach(async () => {
|
||||
selectedItem.set(null);
|
||||
downloadsAvailable.set(false);
|
||||
@@ -430,6 +439,34 @@ describe('VodDetailsRouteComponent fallback actions', () => {
|
||||
).toContain('DOWNLOADS.PLAY_LOCAL');
|
||||
});
|
||||
|
||||
it('plays a rich downloaded movie locally without a usable provider source', async () => {
|
||||
selectedItem.set(unplayableRichItem());
|
||||
downloadsAvailable.set(true);
|
||||
isDownloaded.mockReturnValue(true);
|
||||
getDownloadedFilePath.mockReturnValue('/downloads/metadata-movie.mp4');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const host = fixture.nativeElement as HTMLElement;
|
||||
const primary =
|
||||
host.querySelector<HTMLButtonElement>('button.play-btn');
|
||||
expect(fixture.componentInstance.playableVodItem()).toBeNull();
|
||||
expect(host.textContent).toContain('DOWNLOADS.OFFLINE');
|
||||
expect(primary?.textContent).toContain('DOWNLOADS.PLAY_LOCAL');
|
||||
expect(host.textContent).not.toContain(
|
||||
'PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE'
|
||||
);
|
||||
|
||||
primary?.click();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(playDownload).toHaveBeenCalledWith(
|
||||
'/downloads/metadata-movie.mp4'
|
||||
);
|
||||
expect(constructVodStreamUrl).not.toHaveBeenCalled();
|
||||
expect(openResolvedPlayback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps pinned provider playback behind the downloaded secondary action', async () => {
|
||||
selectedItem.set(sparseItem());
|
||||
downloadsAvailable.set(true);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<ng-template #vodActions>
|
||||
@if (playableVodItem(); as playableItem) {
|
||||
@let playableItem = playableVodItem();
|
||||
@if (playableItem || isDownloaded()) {
|
||||
@if (hasPlaybackPosition()) {
|
||||
<button
|
||||
class="play-btn play-btn--resume"
|
||||
@@ -73,96 +74,106 @@
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
@if (!isFavorite()) {
|
||||
<button class="favorite-btn" (click)="toggleFavorite()">
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
class="favorite-btn favorite-btn--active"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>check</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (isElectron()) {
|
||||
@if (isDownloaded()) {
|
||||
@if (externalPrimaryButtonState() === 'idle') {
|
||||
<button
|
||||
class="download-btn"
|
||||
(click)="playFromProviderSource(playableItem)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{
|
||||
'PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE' | translate
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
} @else if (isPausedDownload()) {
|
||||
<button
|
||||
class="download-btn download-btn--paused"
|
||||
(click)="resumePausedDownload()"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'DOWNLOADS.RESUME' | translate }}
|
||||
</button>
|
||||
} @else if (isDownloading()) {
|
||||
<button class="download-btn download-btn--downloading" disabled>
|
||||
<mat-icon>downloading</mat-icon>
|
||||
{{ 'DOWNLOADS.STATUS.DOWNLOADING' | translate }}
|
||||
@if (playableItem) {
|
||||
@if (!isFavorite()) {
|
||||
<button class="favorite-btn" (click)="toggleFavorite()">
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
class="download-btn"
|
||||
(click)="downloadVod(playableItem)"
|
||||
class="favorite-btn favorite-btn--active"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
{{ 'DOWNLOADS.DOWNLOAD' | translate }}
|
||||
<mat-icon>check</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@if (multiSource.hasAlternatives()) {
|
||||
<app-vod-sources-chip
|
||||
[sources]="multiSource.sources()"
|
||||
[title]="multiSourceTitle()"
|
||||
[matchKind]="multiSource.matchKind()"
|
||||
[count]="multiSource.alternativeCount()"
|
||||
[autoFailoverEnabled]="multiSource.autoFailoverEnabled()"
|
||||
[autoFailoverSupported]="autoFailoverSupported()"
|
||||
[playbackLive]="playbackLive()"
|
||||
(playRequested)="playFromSource($event)"
|
||||
(pinRequested)="pinSource($event)"
|
||||
(checkRequested)="checkSource($event)"
|
||||
(autoFailoverToggled)="setAutoFailover($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (activeSourceCaption(); as caption) {
|
||||
<p class="source-caption">
|
||||
<span class="source-caption__label">
|
||||
{{ 'PORTALS.MULTI_SOURCE.PLAYING_FROM' | translate }}
|
||||
</span>
|
||||
<span class="source-caption__source">{{ caption.source }}</span>
|
||||
@if (multiSource.hasAlternatives()) {
|
||||
<span class="source-caption__extra">
|
||||
@if (caption.alternativeCount === 1) {
|
||||
@if (isElectron()) {
|
||||
@if (isDownloaded()) {
|
||||
@if (externalPrimaryButtonState() === 'idle') {
|
||||
<button
|
||||
class="download-btn"
|
||||
(click)="playFromProviderSource(playableItem)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{
|
||||
'PORTALS.MULTI_SOURCE.ALSO_FOUND_IN_ONE'
|
||||
'PORTALS.MULTI_SOURCE.PLAY_FROM_SOURCE'
|
||||
| translate
|
||||
}}
|
||||
} @else {
|
||||
{{
|
||||
'PORTALS.MULTI_SOURCE.ALSO_FOUND_IN_OTHER'
|
||||
| translate
|
||||
: { count: caption.alternativeCount }
|
||||
}}
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
} @else if (isPausedDownload()) {
|
||||
<button
|
||||
class="download-btn download-btn--paused"
|
||||
(click)="resumePausedDownload()"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'DOWNLOADS.RESUME' | translate }}
|
||||
</button>
|
||||
} @else if (isDownloading()) {
|
||||
<button
|
||||
class="download-btn download-btn--downloading"
|
||||
disabled
|
||||
>
|
||||
<mat-icon>downloading</mat-icon>
|
||||
{{ 'DOWNLOADS.STATUS.DOWNLOADING' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
class="download-btn"
|
||||
(click)="downloadVod(playableItem)"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
{{ 'DOWNLOADS.DOWNLOAD' | translate }}
|
||||
</button>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (multiSource.hasAlternatives()) {
|
||||
<app-vod-sources-chip
|
||||
[sources]="multiSource.sources()"
|
||||
[title]="multiSourceTitle()"
|
||||
[matchKind]="multiSource.matchKind()"
|
||||
[count]="multiSource.alternativeCount()"
|
||||
[autoFailoverEnabled]="multiSource.autoFailoverEnabled()"
|
||||
[autoFailoverSupported]="autoFailoverSupported()"
|
||||
[playbackLive]="playbackLive()"
|
||||
(playRequested)="playFromSource($event)"
|
||||
(pinRequested)="pinSource($event)"
|
||||
(checkRequested)="checkSource($event)"
|
||||
(autoFailoverToggled)="setAutoFailover($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (activeSourceCaption(); as caption) {
|
||||
<p class="source-caption">
|
||||
<span class="source-caption__label">
|
||||
{{ 'PORTALS.MULTI_SOURCE.PLAYING_FROM' | translate }}
|
||||
</span>
|
||||
<span class="source-caption__source">{{
|
||||
caption.source
|
||||
}}</span>
|
||||
@if (multiSource.hasAlternatives()) {
|
||||
<span class="source-caption__extra">
|
||||
@if (caption.alternativeCount === 1) {
|
||||
{{
|
||||
'PORTALS.MULTI_SOURCE.ALSO_FOUND_IN_ONE'
|
||||
| translate
|
||||
}}
|
||||
} @else {
|
||||
{{
|
||||
'PORTALS.MULTI_SOURCE.ALSO_FOUND_IN_OTHER'
|
||||
| translate
|
||||
: {
|
||||
count: caption.alternativeCount,
|
||||
}
|
||||
}}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -503,7 +503,7 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy {
|
||||
// make the control do the opposite of what it says — launching a
|
||||
// second player while the first keeps running.
|
||||
if (this.playback.isExternalStopAction()) {
|
||||
this.playback.onPrimaryAction(vodItem);
|
||||
await this.playback.stopExternalPlayback();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user