mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(m3u): guard the guide's initial group scope and track language changes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
07af71e49d
commit
e20b9cc7a0
2 files changed
+58
-3
No files matched your search
+38
-1
@@ -2,6 +2,7 @@ import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access';
|
||||
import { ChannelActions } from '@iptvnator/m3u-state';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
@@ -49,12 +50,16 @@ describe('M3uEpgGuideSourceService', () => {
|
||||
const getProgramCoverage = jest.fn();
|
||||
const searchPrograms = jest.fn();
|
||||
let service: M3uEpgGuideSourceService;
|
||||
let translateStub: { instant: (key: string) => string };
|
||||
let onLangChange: Subject<{ lang: string }>;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch.mockReset();
|
||||
getProgramsForChannels.mockReset();
|
||||
getProgramCoverage.mockReset();
|
||||
searchPrograms.mockReset();
|
||||
onLangChange = new Subject<{ lang: string }>();
|
||||
translateStub = { instant: (key: string) => key };
|
||||
channels.set([
|
||||
makeChannel('a', { tvgId: 'a.tv', group: 'News' }),
|
||||
makeChannel('b', { name: 'Beta', group: 'Sports' }),
|
||||
@@ -80,7 +85,12 @@ describe('M3uEpgGuideSourceService', () => {
|
||||
},
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: { instant: (key: string) => key },
|
||||
useValue: {
|
||||
get instant() {
|
||||
return translateStub.instant;
|
||||
},
|
||||
onLangChange,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -165,6 +175,33 @@ describe('M3uEpgGuideSourceService', () => {
|
||||
expect(service.scopeId()).toBe('all');
|
||||
});
|
||||
|
||||
it("falls back to the all scope when the active channel's group has no eligible channels", () => {
|
||||
const sportsOnlyChannels = signal<Channel[]>([
|
||||
makeChannel('b', { name: 'Beta', group: 'Sports' }),
|
||||
]);
|
||||
const newsActiveChannel = signal<Channel | null>(
|
||||
makeChannel('a', { tvgId: 'a.tv', group: 'News' })
|
||||
);
|
||||
service.bind({
|
||||
channels: sportsOnlyChannels,
|
||||
favoriteIds,
|
||||
activeChannel: newsActiveChannel,
|
||||
});
|
||||
|
||||
service.applyInitialScope('groups');
|
||||
|
||||
expect(service.scopeId()).toBe('all');
|
||||
});
|
||||
|
||||
it('re-labels scopes when the active language changes', () => {
|
||||
expect(service.scopes()[0].label).toBe('CHANNELS.ALL_CHANNELS');
|
||||
|
||||
translateStub.instant = (key: string) => `${key}!`;
|
||||
onLangChange.next({ lang: 'de' });
|
||||
|
||||
expect(service.scopes()[0].label).toBe('CHANNELS.ALL_CHANNELS!');
|
||||
});
|
||||
|
||||
it('forwards programme search to the bridge', async () => {
|
||||
searchPrograms.mockResolvedValue([program('a.tv'), program('x')]);
|
||||
const hits = await service.searchPrograms('news');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { computed, inject, Injectable, Signal, signal } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access';
|
||||
import { resolveChannelEpgLookupKey } from '@iptvnator/m3u-state';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
@@ -43,6 +45,10 @@ export class M3uEpgGuideSourceService implements EpgGuideSource {
|
||||
|
||||
private readonly inputs = signal<M3uEpgGuideInputs | null>(null);
|
||||
private readonly scope = signal(SCOPE_ALL);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
readonly scopeId = this.scope.asReadonly();
|
||||
|
||||
@@ -51,6 +57,7 @@ export class M3uEpgGuideSourceService implements EpgGuideSource {
|
||||
);
|
||||
|
||||
readonly scopes = computed<EpgGuideScope[]>(() => {
|
||||
this.languageTick();
|
||||
const groups: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const channel of this.allChannels()) {
|
||||
@@ -123,8 +130,15 @@ export class M3uEpgGuideSourceService implements EpgGuideSource {
|
||||
const activeGroup = this.inputs()
|
||||
?.activeChannel()
|
||||
?.group?.title?.trim();
|
||||
if (view === 'groups' && activeGroup) {
|
||||
this.scope.set(`${GROUP_PREFIX}${activeGroup}`);
|
||||
const groupScopeId = activeGroup
|
||||
? `${GROUP_PREFIX}${activeGroup}`
|
||||
: null;
|
||||
if (
|
||||
view === 'groups' &&
|
||||
groupScopeId &&
|
||||
this.scopes().some((scope) => scope.id === groupScopeId)
|
||||
) {
|
||||
this.scope.set(groupScopeId);
|
||||
return;
|
||||
}
|
||||
this.scope.set(SCOPE_ALL);
|
||||
@@ -178,6 +192,10 @@ export class M3uEpgGuideSourceService implements EpgGuideSource {
|
||||
return covered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the channel in the current scope because the guide only ever
|
||||
* passes back ids it rendered from `channels()`.
|
||||
*/
|
||||
activate(channelId: string): void {
|
||||
const channel = this.scopedChannels().find(
|
||||
(candidate) => candidate.id === channelId
|
||||
|
||||
Reference in new issue
Block a user