fix(epg): remove cached XMLTV data after source deletion

This commit is contained in:
4gray committed 2026-09-05 22:15:20 +02:00
1 parent b55a3cc322
commit ffcbee8140
32 files changed
+1051 -82

No files matched your search

+37 -1
View File
@@ -14,7 +14,12 @@ import {
} from '@iptvnator/workspace/shell/util';
import { MockProvider } from 'ng-mocks';
import { EMPTY, of } from 'rxjs';
import { DataService, RuntimeCapabilitiesService } from '@iptvnator/services';
import {
DataService,
EpgSourceSettingsService,
SettingsStore,
RuntimeCapabilitiesService,
} from '@iptvnator/services';
import {
Language,
Settings,
@@ -235,6 +240,37 @@ describe('AppComponent', () => {
expect(snackBar.open).not.toHaveBeenCalled();
});
it('does not reimport a deleted source from a late startup freshness response', async () => {
await TestBed.inject(SettingsStore).loadSettings();
let finishFreshness!: (value: {
freshUrls: string[];
staleUrls: string[];
}) => void;
epgBridge.checkFreshness = jest.fn(
() =>
new Promise((resolve) => {
finishFreshness = resolve;
})
);
const pending = (
component as unknown as {
fetchStaleEpgData(urls: string[]): Promise<void>;
}
).fetchStaleEpgData(['https://removed.example/guide.xml']);
await Promise.resolve();
const sources = TestBed.inject(EpgSourceSettingsService);
sources.revision.update((value) => value + 1);
sources.changed$.next();
finishFreshness({
freshUrls: [],
staleUrls: ['https://removed.example/guide.xml'],
});
await pending;
expect(epgService.fetchEpg).not.toHaveBeenCalledWith([
'https://removed.example/guide.xml',
]);
});
it('does not fetch EPG settings when the EPG bridge cannot import EPG', async () => {
const settings: Settings = {
...DEFAULT_SETTINGS,
+13 -4
View File
@@ -20,6 +20,7 @@ import {
DataService,
RuntimeCapabilitiesService,
SettingsStore,
EpgSourceSettingsService,
} from '@iptvnator/services';
import {
AUTO_UPDATE_PLAYLISTS,
@@ -63,6 +64,7 @@ export class AppComponent implements OnInit {
private translate = inject(TranslateService);
private settingsService = inject(SettingsService);
private settingsStore = inject(SettingsStore);
private readonly epgSources = inject(EpgSourceSettingsService);
private playbackKeepAwake = inject(PlaybackKeepAwakeService);
private playlistOpenRequests = inject(PlaylistOpenRequestService);
private runtime = inject(RuntimeCapabilitiesService);
@@ -188,8 +190,15 @@ export class AppComponent implements OnInit {
* Data is considered fresh if updated within the last 12 hours.
*/
private async fetchStaleEpgData(urls: string[]): Promise<void> {
await this.settingsStore.loadSettings();
const revision = this.epgSources.revision();
const fetchCurrentSources = (sources: string[]) => {
this.epgService.fetchEpg(
this.epgSources.retainCurrentSources(sources, revision)
);
};
if (!this.epgBridge.supportsSourceFreshness) {
this.epgService.fetchEpg(urls);
fetchCurrentSources(urls);
return;
}
@@ -197,7 +206,7 @@ export class AppComponent implements OnInit {
const result = await this.epgBridge.checkFreshness(urls, 12);
if (!result) {
this.epgService.fetchEpg(urls);
fetchCurrentSources(urls);
return;
}
@@ -219,12 +228,12 @@ export class AppComponent implements OnInit {
debugAppComponent(
`EPG: Fetching ${result.staleUrls.length} stale source(s)`
);
this.epgService.fetchEpg(result.staleUrls);
fetchCurrentSources(result.staleUrls);
}
} catch (error) {
console.error('Error checking EPG freshness, fetching all:', error);
// Fallback: fetch all URLs if freshness check fails
this.epgService.fetchEpg(urls);
fetchCurrentSources(urls);
}
}
@@ -16,7 +16,10 @@ import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute, Router } from '@angular/router';
import { SettingsContextService } from '@iptvnator/workspace/shell/util';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { RuntimeCapabilitiesService } from '@iptvnator/services';
import {
EpgSourceReconciliationError,
RuntimeCapabilitiesService,
} from '@iptvnator/services';
import { VodSourceDiscoveryService } from '@iptvnator/portal/shared/data-access';
import { Language, StreamFormat } from '@iptvnator/shared/interfaces';
import { firstValueFrom, map } from 'rxjs';
@@ -307,7 +310,13 @@ export class SettingsComponent
try {
await this.form.save(() => this.applyChangedSettings());
return true;
} catch {
} catch (error) {
if (error instanceof EpgSourceReconciliationError) {
this.settingsSnackbar.open(
this.translate.instant('SETTINGS.EPG_DATA_CLEAR_FAILED')
);
return false;
}
// The store already applied the change in memory, so without
// this the save looks successful until the next restart. The
// unsaved-changes bar stays visible so it can be retried.