mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(xtream): use server timezone when constructing catchup/timeshift URLs
Previously the server timezone returned by the Xtream API (`server_info.timezone`) was fetched but never applied when formatting the start timestamp in catchup URLs. This caused timeshift requests to use the client's local time instead of the server's local time, resulting in wrong or missing archive content. Changes: - Store `server_info.timezone` on the playlist state after a successful portal status check (`with-portal.feature.ts`) - Thread `serverTimezone?` through `resolveCatchupUrl` → `constructCatchupUrl` → `formatCatchupStartTime` in `XtreamUrlService` - `formatCatchupStartTime` now uses `Intl.DateTimeFormat` with the server timezone when available, falling back to local time when not set - Pass `playlist.serverTimezone` from `LiveStreamLayoutComponent.playCatchup` - Add `serverTimezone?` field to `XtreamPlaylistData` interface Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
95e2a43626
commit
30b8533e3a
7 files changed
+92
-14
No files matched your search
@@ -65,3 +65,4 @@ testem.log
|
||||
Thumbs.db
|
||||
.angular
|
||||
.astro
|
||||
.claude/settings.local.json
|
||||
@@ -32,6 +32,7 @@ export interface XtreamPlaylistData {
|
||||
userAgent?: string;
|
||||
referrer?: string;
|
||||
origin?: string;
|
||||
serverTimezone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,4 +76,35 @@ describe('XtreamUrlService', () => {
|
||||
'legacy'
|
||||
);
|
||||
});
|
||||
|
||||
describe('formatCatchupStartTime (via constructCatchupUrl)', () => {
|
||||
// 2025-03-01 02:00:00 UTC = 2025-02-28 21:00:00 America/New_York
|
||||
const timestamp = 1740794400;
|
||||
|
||||
it('formats time in the server timezone when provided', () => {
|
||||
const url = service.constructCatchupUrl(
|
||||
credentials,
|
||||
101,
|
||||
timestamp,
|
||||
timestamp + 3600,
|
||||
'rest',
|
||||
'America/New_York'
|
||||
);
|
||||
expect(url).toContain('2025-02-28:21-00');
|
||||
});
|
||||
|
||||
it('falls back to client local time when no timezone is given', () => {
|
||||
const url = service.constructCatchupUrl(
|
||||
credentials,
|
||||
101,
|
||||
timestamp,
|
||||
timestamp + 3600,
|
||||
'rest'
|
||||
);
|
||||
const date = new Date(timestamp * 1000);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const expected = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}:${pad(date.getHours())}-${pad(date.getMinutes())}`;
|
||||
expect(url).toContain(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -102,13 +102,17 @@ export class XtreamUrlService {
|
||||
streamId: number,
|
||||
startTimestamp: number,
|
||||
stopTimestamp: number,
|
||||
scheme: XtreamCatchupScheme
|
||||
scheme: XtreamCatchupScheme,
|
||||
serverTimezone?: string
|
||||
): string {
|
||||
const durationMinutes = Math.max(
|
||||
1,
|
||||
Math.round((stopTimestamp - startTimestamp) / 60)
|
||||
);
|
||||
const timeString = this.formatCatchupStartTime(startTimestamp);
|
||||
const timeString = this.formatCatchupStartTime(
|
||||
startTimestamp,
|
||||
serverTimezone
|
||||
);
|
||||
|
||||
if (scheme === 'legacy') {
|
||||
const params = new URLSearchParams({
|
||||
@@ -129,14 +133,16 @@ export class XtreamUrlService {
|
||||
credentials: XtreamCredentials,
|
||||
streamId: number,
|
||||
startTimestamp: number,
|
||||
stopTimestamp: number
|
||||
stopTimestamp: number,
|
||||
serverTimezone?: string
|
||||
): Promise<string> {
|
||||
const scheme = await this.getCatchupScheme(
|
||||
playlistId,
|
||||
credentials,
|
||||
streamId,
|
||||
startTimestamp,
|
||||
stopTimestamp
|
||||
stopTimestamp,
|
||||
serverTimezone
|
||||
);
|
||||
|
||||
return this.constructCatchupUrl(
|
||||
@@ -144,7 +150,8 @@ export class XtreamUrlService {
|
||||
streamId,
|
||||
startTimestamp,
|
||||
stopTimestamp,
|
||||
scheme
|
||||
scheme,
|
||||
serverTimezone
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +160,8 @@ export class XtreamUrlService {
|
||||
credentials: XtreamCredentials,
|
||||
streamId: number,
|
||||
startTimestamp: number,
|
||||
stopTimestamp: number
|
||||
stopTimestamp: number,
|
||||
serverTimezone?: string
|
||||
): Promise<XtreamCatchupScheme> {
|
||||
const cacheKey = `${XTREAM_CATCHUP_SCHEME_KEY_PREFIX}${playlistId}`;
|
||||
const cached = this.catchupSchemeCache.get(cacheKey);
|
||||
@@ -177,7 +185,8 @@ export class XtreamUrlService {
|
||||
credentials,
|
||||
streamId,
|
||||
startTimestamp,
|
||||
stopTimestamp
|
||||
stopTimestamp,
|
||||
serverTimezone
|
||||
).finally(() => {
|
||||
this.catchupSchemeRequests.delete(cacheKey);
|
||||
});
|
||||
@@ -191,21 +200,24 @@ export class XtreamUrlService {
|
||||
credentials: XtreamCredentials,
|
||||
streamId: number,
|
||||
startTimestamp: number,
|
||||
stopTimestamp: number
|
||||
stopTimestamp: number,
|
||||
serverTimezone?: string
|
||||
): Promise<XtreamCatchupScheme> {
|
||||
const restUrl = this.constructCatchupUrl(
|
||||
credentials,
|
||||
streamId,
|
||||
startTimestamp,
|
||||
stopTimestamp,
|
||||
'rest'
|
||||
'rest',
|
||||
serverTimezone
|
||||
);
|
||||
const legacyUrl = this.constructCatchupUrl(
|
||||
credentials,
|
||||
streamId,
|
||||
startTimestamp,
|
||||
stopTimestamp,
|
||||
'legacy'
|
||||
'legacy',
|
||||
serverTimezone
|
||||
);
|
||||
|
||||
const restStatus = await this.probeCatchupUrl(restUrl);
|
||||
@@ -252,10 +264,28 @@ export class XtreamUrlService {
|
||||
);
|
||||
}
|
||||
|
||||
private formatCatchupStartTime(timestamp: number): string {
|
||||
private formatCatchupStartTime(
|
||||
timestamp: number,
|
||||
timezone?: string
|
||||
): string {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const pad = (value: number) => String(value).padStart(2, '0');
|
||||
|
||||
if (timezone) {
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: timezone,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).formatToParts(date);
|
||||
const get = (type: Intl.DateTimeFormatPartTypes) =>
|
||||
parts.find((p) => p.type === type)?.value ?? '00';
|
||||
return `${get('year')}-${get('month')}-${get('day')}:${get('hour')}-${get('minute')}`;
|
||||
}
|
||||
|
||||
const pad = (value: number) => String(value).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}:${pad(date.getHours())}-${pad(date.getMinutes())}`;
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,20 @@ export function withPortal() {
|
||||
const response =
|
||||
await apiService.getAccountInfo(credentials);
|
||||
const portalStatus = resolvePortalStatus(response);
|
||||
const serverTimezone =
|
||||
response?.server_info?.timezone ?? undefined;
|
||||
patchState(store, { portalStatus });
|
||||
if (serverTimezone) {
|
||||
const current = store.currentPlaylist();
|
||||
if (current) {
|
||||
patchState(store, {
|
||||
currentPlaylist: {
|
||||
...current,
|
||||
serverTimezone,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return portalStatus;
|
||||
} catch (error) {
|
||||
logger.error('Error checking portal status', error);
|
||||
|
||||
+2
-1
@@ -288,7 +288,8 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
},
|
||||
101,
|
||||
1775296800,
|
||||
1775300400
|
||||
1775300400,
|
||||
undefined
|
||||
);
|
||||
expect(xtreamStore.openPlayer).toHaveBeenCalledWith(
|
||||
'https://example.com/timeshift.ts',
|
||||
|
||||
+2
-1
@@ -403,7 +403,8 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
item.xtream_id,
|
||||
startTimestamp,
|
||||
stopTimestamp
|
||||
stopTimestamp,
|
||||
playlist.serverTimezone
|
||||
);
|
||||
|
||||
this.activeStreamUrl.set(catchupUrl);
|
||||
|
||||
Reference in new issue
Block a user