mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #885 from sapireli/fix/xtream-catchup-server-timezone
fix(xtream): use server timezone when constructing catchup/timeshift URLs
This commit is contained in:
6 files changed
+110
-14
No files matched your search
@@ -32,6 +32,7 @@ export interface XtreamPlaylistData {
|
||||
userAgent?: string;
|
||||
referrer?: string;
|
||||
origin?: string;
|
||||
serverTimezone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,4 +76,50 @@ 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);
|
||||
});
|
||||
|
||||
it('falls back to client local time when an invalid timezone string is given', () => {
|
||||
const url = service.constructCatchupUrl(
|
||||
credentials,
|
||||
101,
|
||||
timestamp,
|
||||
timestamp + 3600,
|
||||
'rest',
|
||||
'UTC+5'
|
||||
);
|
||||
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,32 @@ 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) {
|
||||
try {
|
||||
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')}`;
|
||||
} catch {
|
||||
// Invalid timezone string — fall through to local-time formatting
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -477,7 +477,8 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
},
|
||||
101,
|
||||
1775296800,
|
||||
1775300400
|
||||
1775300400,
|
||||
undefined
|
||||
);
|
||||
expect(xtreamStore.openPlayer).toHaveBeenCalledWith(
|
||||
'https://example.com/timeshift.ts',
|
||||
|
||||
+2
-1
@@ -486,7 +486,8 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
item.xtream_id,
|
||||
startTimestamp,
|
||||
stopTimestamp
|
||||
stopTimestamp,
|
||||
playlist.serverTimezone
|
||||
);
|
||||
|
||||
this.activePlayback.set({
|
||||
|
||||
Reference in new issue
Block a user