feat(stalker): discover portal connection on edit

This commit is contained in:
4gray committed 2026-08-08 22:06:27 +02:00
1 parent c95f826739
commit d2d91e322b
47 files changed
+2335 -497

No files matched your search

+6
View File
@@ -27,6 +27,7 @@ import {
PORTAL_PLAYER,
} from '@iptvnator/portal/shared/util';
import { PLAYLIST_PLAYER_ACTIONS } from '@iptvnator/playlist/shared/util';
import { STALKER_PLAYLIST_CONNECTION_EDITOR } from '@iptvnator/playlist/shared/ui';
import { provideXtreamDataSource } from '@iptvnator/portal/xtream/data-access';
import { DataService } from '@iptvnator/services';
import { dbConfig } from '@iptvnator/shared/interfaces';
@@ -42,6 +43,7 @@ import {
import { providePortalPlaybackPositions } from './services/portal-playback-positions.service';
import { PwaService } from './services/pwa.service';
import { shouldEnableServiceWorker } from './services/runtime-config';
import { AppStalkerPlaylistConnectionEditorService } from './services/stalker-playlist-connection-editor.service';
import { provideWorkspaceShellActions } from './services/workspace-shell-actions.service';
// AoT requires an exported function for factories
@@ -152,6 +154,10 @@ export const appConfig: ApplicationConfig = {
provide: PLAYLIST_PLAYER_ACTIONS,
useExisting: AppPortalNavigationActionsService,
},
{
provide: STALKER_PLAYLIST_CONNECTION_EDITOR,
useExisting: AppStalkerPlaylistConnectionEditorService,
},
...provideWorkspaceShellActions(),
...provideXtreamDataSource(),
{
@@ -0,0 +1,359 @@
import { TestBed } from '@angular/core/testing';
import { TranslateService } from '@ngx-translate/core';
import {
StalkerPortalDiscoveryService,
StalkerPortalError,
StalkerPortalRepairService,
StalkerSessionService,
StalkerStore,
stalkerSessionFingerprint,
} from '@iptvnator/portal/stalker/data-access';
import { STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS } from '@iptvnator/playlist/shared/ui';
import type { PlaylistMeta } from '@iptvnator/shared/interfaces';
import { AppStalkerPlaylistConnectionEditorService } from './stalker-playlist-connection-editor.service';
describe('AppStalkerPlaylistConnectionEditorService', () => {
const discovery = {
discover: jest.fn(),
};
const portalRepair = {
applyOverride: jest.fn((playlist: PlaylistMeta) => playlist),
retireForPlaylistEdit: jest.fn(),
};
const stalkerSession = {
replaceSessionAfterEdit: jest.fn().mockResolvedValue(undefined),
};
let activePlaylist: PlaylistMeta | undefined;
const stalkerStore = {
currentPlaylist: jest.fn(() => activePlaylist),
setCurrentPlaylist: jest.fn((playlist: PlaylistMeta) => {
activePlaylist = playlist;
}),
};
const translate = {
instant: jest.fn((key: string) => key),
};
const draft: PlaylistMeta = {
_id: 'stalker-1',
title: 'Stalker Portal',
count: 0,
importDate: '2026-08-08T00:00:00.000Z',
portalUrl: 'https://portal.example.com/c',
macAddress: '00:1A:79:AA:BB:CC',
username: 'subscriber',
password: 'secret',
stalkerSerialNumber: ' SERIAL ',
stalkerDeviceId1: ' DEVICE-1 ',
stalkerDeviceId2: '',
stalkerSignature1: ' SIGNATURE-1 ',
stalkerSignature2: '',
};
let service: AppStalkerPlaylistConnectionEditorService;
beforeEach(() => {
jest.clearAllMocks();
activePlaylist = undefined;
TestBed.configureTestingModule({
providers: [
AppStalkerPlaylistConnectionEditorService,
{
provide: StalkerPortalDiscoveryService,
useValue: discovery,
},
{
provide: StalkerPortalRepairService,
useValue: portalRepair,
},
{
provide: StalkerSessionService,
useValue: stalkerSession,
},
{ provide: StalkerStore, useValue: stalkerStore },
{ provide: TranslateService, useValue: translate },
],
});
service = TestBed.inject(AppStalkerPlaylistConnectionEditorService);
});
it('resolves a simple portal and clears the previous full session', async () => {
discovery.discover.mockResolvedValue({
status: 'resolved',
portalUrl: 'https://portal.example.com/portal.php',
isFullStalkerPortal: false,
});
const result = await service.resolveConnection(draft);
expect(discovery.discover).toHaveBeenCalledWith(
draft.portalUrl,
draft.macAddress,
{
serialNumber: 'SERIAL',
deviceId1: 'DEVICE-1',
signature1: 'SIGNATURE-1',
},
{
credentials: {
username: 'subscriber',
password: 'secret',
},
}
);
expect(result).toEqual({
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.RESOLVED,
playlist: {
...draft,
portalUrl: 'https://portal.example.com/portal.php',
isFullStalkerPortal: false,
stalkerSerialNumber: 'SERIAL',
stalkerDeviceId1: 'DEVICE-1',
stalkerDeviceId2: '',
stalkerSignature1: 'SIGNATURE-1',
stalkerSignature2: '',
stalkerSessionPatch: null,
},
});
});
it('replaces a full-portal session with the confirmed authorization result', async () => {
discovery.discover.mockResolvedValue({
status: 'resolved',
portalUrl: 'https://portal.example.com/server/load.php',
isFullStalkerPortal: true,
token: 'NEW_TOKEN',
watchdogTimeoutSeconds: 75,
timeslotSeconds: 8,
accountInfo: {
login: 'subscriber',
expire_date: 1800000000,
tariff_plan_name: 'Premium',
status: 0,
},
});
const result = await service.resolveConnection(draft);
const resolvedPlaylist = {
...draft,
portalUrl: 'https://portal.example.com/server/load.php',
isFullStalkerPortal: true,
stalkerSerialNumber: 'SERIAL',
stalkerDeviceId1: 'DEVICE-1',
stalkerDeviceId2: '',
stalkerSignature1: 'SIGNATURE-1',
stalkerSignature2: '',
};
expect(result).toEqual({
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.RESOLVED,
playlist: {
...resolvedPlaylist,
stalkerSessionPatch: {
stalkerToken: 'NEW_TOKEN',
stalkerSessionIdentity:
stalkerSessionFingerprint(resolvedPlaylist),
stalkerWatchdogTimeout: 75,
stalkerTimeslot: 8,
stalkerAccountInfo: {
login: 'subscriber',
expireDate: 1800000000,
tariffPlanName: 'Premium',
status: 0,
},
},
},
});
});
it('returns a user-facing portal refusal without producing an update', async () => {
discovery.discover.mockResolvedValue({
status: 'auth-rejected',
portalUrl: 'https://portal.example.com/server/load.php',
error: new StalkerPortalError(
'login-rejected',
'Subscription expired'
),
});
await expect(service.resolveConnection(draft)).resolves.toEqual({
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.AUTH_REJECTED,
message:
'HOME.STALKER_PORTAL.LOGIN_REJECTED HOME.STALKER_PORTAL.PORTAL_MESSAGE',
});
});
it('returns a dedicated error when no endpoint can be reached', async () => {
discovery.discover.mockResolvedValue({ status: 'unreachable' });
await expect(service.resolveConnection(draft)).resolves.toEqual({
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.UNREACHABLE,
message: 'HOME.STALKER_PORTAL.EDIT_UNREACHABLE',
});
});
it('replaces the active runtime playlist and session after a resolved full-portal edit', async () => {
activePlaylist = {
...draft,
portalUrl: 'https://old.example.com/portal.php',
isFullStalkerPortal: false,
};
const resolvedPlaylist = {
...draft,
portalUrl: 'https://new.example.com/server/load.php',
isFullStalkerPortal: true,
stalkerSessionPatch: {
stalkerToken: 'NEW_TOKEN',
stalkerSessionIdentity: 'new-fingerprint',
stalkerWatchdogTimeout: 90,
stalkerTimeslot: 5,
},
};
await service.applyResolvedConnection(resolvedPlaylist);
expect(portalRepair.retireForPlaylistEdit).toHaveBeenCalledWith(
draft._id
);
expect(stalkerSession.replaceSessionAfterEdit).toHaveBeenCalledWith(
expect.objectContaining({
portalUrl: 'https://new.example.com/server/load.php',
isFullStalkerPortal: true,
stalkerToken: 'NEW_TOKEN',
stalkerSessionIdentity: 'new-fingerprint',
stalkerWatchdogTimeout: 90,
stalkerTimeslot: 5,
})
);
expect(stalkerStore.setCurrentPlaylist).toHaveBeenCalledWith(
expect.objectContaining({
portalUrl: 'https://new.example.com/server/load.php',
isFullStalkerPortal: true,
})
);
expect(activePlaylist?.portalUrl).toBe(
'https://new.example.com/server/load.php'
);
});
it('clears the session and stops full-portal runtime behavior after a resolved simple edit', async () => {
activePlaylist = {
...draft,
portalUrl: 'https://old.example.com/server/load.php',
isFullStalkerPortal: true,
};
const resolvedPlaylist = {
...draft,
portalUrl: 'https://new.example.com/portal.php',
isFullStalkerPortal: false,
stalkerSessionPatch: null,
};
await service.applyResolvedConnection(resolvedPlaylist);
expect(portalRepair.retireForPlaylistEdit).toHaveBeenCalledWith(
draft._id
);
expect(stalkerSession.replaceSessionAfterEdit).toHaveBeenCalledWith(
expect.objectContaining({
portalUrl: 'https://new.example.com/portal.php',
isFullStalkerPortal: false,
stalkerToken: undefined,
stalkerSessionIdentity: undefined,
})
);
expect(activePlaylist).toEqual(
expect.objectContaining({
portalUrl: 'https://new.example.com/portal.php',
isFullStalkerPortal: false,
stalkerToken: undefined,
stalkerSessionIdentity: undefined,
})
);
});
it('repoints the active snapshot before an old authentication finishes draining', async () => {
activePlaylist = {
...draft,
portalUrl: 'https://old.example.com/server/load.php',
isFullStalkerPortal: true,
};
let finishReplacement: () => void = () => undefined;
stalkerSession.replaceSessionAfterEdit.mockReturnValueOnce(
new Promise<void>((resolve) => {
finishReplacement = resolve;
})
);
const resolvedPlaylist = {
...draft,
portalUrl: 'https://new.example.com/portal.php',
isFullStalkerPortal: false,
stalkerSessionPatch: null,
};
const applying = service.applyResolvedConnection(resolvedPlaylist);
expect(activePlaylist?.portalUrl).toBe(
'https://new.example.com/portal.php'
);
let settled = false;
void applying.then(() => (settled = true));
await Promise.resolve();
expect(settled).toBe(false);
finishReplacement();
await applying;
});
it.each([true, false])(
'keeps the exact credential-only Edit result when a prior endpoint repair exists (active=%s)',
async (isActive) => {
const resolvedPlaylist = {
...draft,
portalUrl: 'https://portal.example.com/server/load.php',
isFullStalkerPortal: true,
username: 'new-user',
stalkerSessionPatch: {
stalkerToken: 'NEW_TOKEN',
stalkerSessionIdentity: 'new-fingerprint',
stalkerWatchdogTimeout: 90,
stalkerTimeslot: 5,
},
};
// This is the remembered A→B repair that used to rewrite the
// newly proven A endpoint when only credentials changed.
portalRepair.applyOverride.mockReturnValue({
...resolvedPlaylist,
portalUrl: 'https://portal.example.com/portal.php',
});
activePlaylist = isActive
? {
...draft,
portalUrl: 'https://portal.example.com/portal.php',
}
: undefined;
await service.applyResolvedConnection(resolvedPlaylist);
expect(portalRepair.applyOverride).not.toHaveBeenCalled();
expect(portalRepair.retireForPlaylistEdit).toHaveBeenCalledWith(
draft._id
);
expect(stalkerSession.replaceSessionAfterEdit).toHaveBeenCalledWith(
expect.objectContaining({
portalUrl: 'https://portal.example.com/server/load.php',
username: 'new-user',
stalkerToken: 'NEW_TOKEN',
})
);
expect(stalkerStore.setCurrentPlaylist).toHaveBeenCalledTimes(
isActive ? 1 : 0
);
if (isActive) {
expect(activePlaylist?.portalUrl).toBe(
'https://portal.example.com/server/load.php'
);
}
}
);
});
@@ -0,0 +1,199 @@
import { inject, Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {
asStalkerPortalError,
StalkerPortalDiscoveryService,
StalkerPortalRepairService,
StalkerSessionService,
StalkerStore,
STALKER_WATCHDOG_DEFAULT_PERIOD_SECONDS,
stalkerSessionFingerprint,
type StalkerPortalErrorKind,
} from '@iptvnator/portal/stalker/data-access';
import {
STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS,
type StalkerPlaylistConnectionEditor,
type StalkerPlaylistConnectionResult,
} from '@iptvnator/playlist/shared/ui';
import {
normalizeStalkerPortalIdentity,
type Playlist,
type PlaylistMeta,
type PlaylistMetaUpdate,
} from '@iptvnator/shared/interfaces';
const STALKER_EDIT_ERROR_KEY_BY_KIND: Readonly<
Record<StalkerPortalErrorKind, string>
> = {
'login-required': 'HOME.STALKER_PORTAL.LOGIN_REQUIRED',
'login-rejected': 'HOME.STALKER_PORTAL.LOGIN_REJECTED',
'device-conflict': 'HOME.STALKER_PORTAL.DEVICE_CONFLICT',
blocked: 'HOME.STALKER_PORTAL.PORTAL_REFUSED',
'auth-failed': 'HOME.STALKER_PORTAL.AUTH_FAILED',
};
@Injectable({ providedIn: 'root' })
export class AppStalkerPlaylistConnectionEditorService implements StalkerPlaylistConnectionEditor {
private readonly discovery = inject(StalkerPortalDiscoveryService);
private readonly portalRepair = inject(StalkerPortalRepairService);
private readonly stalkerSession = inject(StalkerSessionService);
private readonly stalkerStore = inject(StalkerStore);
private readonly translate = inject(TranslateService);
async applyResolvedConnection(playlist: PlaylistMetaUpdate): Promise<void> {
// Edit discovery is newer and more authoritative than a lazy repair
// remembered for the previous connection. Applying that override to
// the resolved row could turn a credential-only A→A edit back into
// the repair's old A→B result.
const runtimePlaylist = this.toRuntimePlaylist(playlist);
this.portalRepair.retireForPlaylistEdit(runtimePlaylist._id);
const sessionReplacement =
this.stalkerSession.replaceSessionAfterEdit(runtimePlaylist);
let activePlaylistUpdate = Promise.resolve();
if (this.stalkerStore.currentPlaylist()?._id === runtimePlaylist._id) {
// `setCurrentPlaylist` patches the signal-store snapshot before
// its optional SQLite sync awaits, and re-points/stops the active
// watchdog synchronously. The next catalog/playback request can
// therefore no longer use the pre-edit endpoint or identity.
activePlaylistUpdate =
this.stalkerStore.setCurrentPlaylist(runtimePlaylist);
}
await Promise.all([sessionReplacement, activePlaylistUpdate]);
}
async resolveConnection(
playlist: PlaylistMeta
): Promise<StalkerPlaylistConnectionResult> {
const identity = normalizeStalkerPortalIdentity({
serialNumber: playlist.stalkerSerialNumber,
deviceId1: playlist.stalkerDeviceId1,
deviceId2: playlist.stalkerDeviceId2,
signature1: playlist.stalkerSignature1,
signature2: playlist.stalkerSignature2,
});
const normalizedPlaylist: PlaylistMetaUpdate = {
...playlist,
stalkerSerialNumber: identity.serialNumber ?? '',
stalkerDeviceId1: identity.deviceId1 ?? '',
stalkerDeviceId2: identity.deviceId2 ?? '',
stalkerSignature1: identity.signature1 ?? '',
stalkerSignature2: identity.signature2 ?? '',
};
const outcome = await this.discovery.discover(
playlist.portalUrl ?? '',
playlist.macAddress ?? '',
identity,
{
credentials: {
username: playlist.username ?? '',
password: playlist.password ?? '',
},
}
);
if (outcome.status === 'unreachable') {
return {
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.UNREACHABLE,
message: this.translate.instant(
'HOME.STALKER_PORTAL.EDIT_UNREACHABLE'
),
};
}
if (outcome.status === 'auth-rejected') {
return {
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.AUTH_REJECTED,
message: this.buildAuthErrorMessage(outcome.error),
};
}
const resolvedPlaylist: PlaylistMetaUpdate = {
...normalizedPlaylist,
portalUrl: outcome.portalUrl,
isFullStalkerPortal: outcome.isFullStalkerPortal,
};
if (!outcome.isFullStalkerPortal) {
return {
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.RESOLVED,
playlist: {
...resolvedPlaylist,
stalkerSessionPatch: null,
},
};
}
if (!outcome.token) {
return {
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.AUTH_REJECTED,
message: this.translate.instant(
'HOME.STALKER_PORTAL.AUTH_FAILED'
),
};
}
return {
status: STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS.RESOLVED,
playlist: {
...resolvedPlaylist,
stalkerSessionPatch: {
stalkerToken: outcome.token,
stalkerSessionIdentity:
stalkerSessionFingerprint(resolvedPlaylist),
stalkerWatchdogTimeout:
outcome.watchdogTimeoutSeconds ??
STALKER_WATCHDOG_DEFAULT_PERIOD_SECONDS,
stalkerTimeslot: outcome.timeslotSeconds ?? 0,
stalkerAccountInfo: outcome.accountInfo
? {
login: outcome.accountInfo.login,
expireDate: outcome.accountInfo.expire_date,
tariffPlanName:
outcome.accountInfo.tariff_plan_name,
status: outcome.accountInfo.status,
}
: undefined,
},
},
};
}
private buildAuthErrorMessage(error: unknown): string {
const portalError = asStalkerPortalError(error);
const headline = this.translate.instant(
portalError
? STALKER_EDIT_ERROR_KEY_BY_KIND[portalError.kind]
: 'HOME.STALKER_PORTAL.AUTH_FAILED'
);
if (!portalError?.portalText) {
return headline;
}
return `${headline} ${this.translate.instant(
'HOME.STALKER_PORTAL.PORTAL_MESSAGE',
{ message: portalError.portalText }
)}`;
}
private toRuntimePlaylist(update: PlaylistMetaUpdate): Playlist {
const { stalkerSessionPatch, ...metadata } = update;
return {
lastUsage: '',
...metadata,
...(stalkerSessionPatch !== undefined
? {
stalkerToken: stalkerSessionPatch?.stalkerToken,
stalkerSessionIdentity:
stalkerSessionPatch?.stalkerSessionIdentity,
stalkerWatchdogTimeout:
stalkerSessionPatch?.stalkerWatchdogTimeout,
stalkerTimeslot: stalkerSessionPatch?.stalkerTimeslot,
stalkerAccountInfo:
stalkerSessionPatch?.stalkerAccountInfo,
}
: {}),
} as Playlist;
}
}
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "التوقيع 2 (اختياري)",
"SIGNATURE_HINT": "64 حرفًا سداسيًا عشريًا - استخدمه إذا كان المزود يتطلب توقيعات للتحقق من الجهاز",
"SERVER_URL": "رابط الخادم",
"URL_VALIDATION_ERROR": "يجب أن يكون الرابط صالحًا مع بروتوكول (مثال: http://example.com/stalker_portal)",
"URL_HINT": "أدخل اسم مضيف أو عنوانًا ينتهي بـ /c أو portal.php أو server/load.php.",
"URL_VALIDATION_ERROR": "أدخل رابطًا صالحًا يبدأ بـ http:// أو https://.",
"ADD": "إضافة",
"VALIDATING": "جارٍ التحقق من البوابة...",
"CREDENTIALS_HINT": "مطلوب فقط عندما تطلب البوابة اسم مستخدم وكلمة مرور",
"AUTH_FAILED": "فشلت المصادقة مع البوابة. تحقق من الرابط وعنوان MAC.",
"EDIT_UNREACHABLE": "لم تستجب البوابة. لم يتم حفظ أي تغييرات. تحقق من الرابط وحاول مرة أخرى.",
"LOGIN_REQUIRED": "تتطلب هذه البوابة اسم مستخدم وكلمة مرور. املأ حقلي اسم المستخدم وكلمة المرور وأعد المحاولة.",
"LOGIN_REJECTED": "رفضت البوابة اسم المستخدم وكلمة المرور.",
"PORTAL_REFUSED": "رفضت البوابة الوصول لهذا الجهاز.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "التوقيع 2 (اختياري)",
"SIGNATURE_HINT": "64 حرف hex - استعملو إذا المزود كيتطلب توقيعات التحقق من الجهاز",
"SERVER_URL": "رابط الخادم",
"URL_VALIDATION_ERROR": "خاص يكون رابط صحيح مع البروتوكول (مثلا http://example.com/c ولا https://example.com/stalker_portal/c)",
"URL_HINT": "دخل الهوست ولا عنوان كيسالي بـ /c ولا portal.php ولا server/load.php.",
"URL_VALIDATION_ERROR": "دخل رابط صحيح كيبدا بـ http:// ولا https://.",
"ADD": "زيد",
"VALIDATING": "جاري التحقق من البوابة...",
"CREDENTIALS_HINT": "خاصين غير إذا البوابة كتطلب اسم المستخدم وكلمة المرور",
"AUTH_FAILED": "فشل تسجيل الدخول للبوابة. تحقق من الرابط وعنوان MAC.",
"EDIT_UNREACHABLE": "البوابة ما جاوباتش. ما تحفظ حتى تغيير. تأكد من الرابط وعاود المحاولة.",
"LOGIN_REQUIRED": "هاد البوابة كتطلب اسم المستخدم وكلمة المرور. عمّر خانات اسم المستخدم وكلمة المرور وحاول مرة أخرى.",
"LOGIN_REJECTED": "البوابة رفضات اسم المستخدم وكلمة المرور.",
"PORTAL_REFUSED": "البوابة رفضات الوصول لهاد الجهاز.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Подпіс 2 (неабавязкова)",
"SIGNATURE_HINT": "64 шаснаццатковыя сімвалы — выкарыстоўвайце, калі правайдэр патрабуе подпісы для верыфікацыі прылады",
"SERVER_URL": "URL сервера",
"URL_VALIDATION_ERROR": "Павінен быць сапраўдны URL-адрас з пратаколам (напрыклад, http://example.com/c або https://example.com/stalker_portal/c).",
"URL_HINT": "Увядзіце хост або адрас, які заканчваецца на /c, portal.php ці server/load.php.",
"URL_VALIDATION_ERROR": "Увядзіце правільны URL, які пачынаецца з http:// або https://.",
"ADD": "Дадаць",
"VALIDATING": "Праверка партала...",
"CREDENTIALS_HINT": "Патрэбна толькі тады, калі партал запытвае лагін і пароль",
"AUTH_FAILED": "Не ўдалося аўтэнтыфікавацца на партале. Праверце URL і MAC-адрас.",
"EDIT_UNREACHABLE": "Партал не адказаў. Змены не захаваны. Праверце URL і паўтарыце спробу.",
"LOGIN_REQUIRED": "Гэты партал патрабуе лагін і пароль. Запоўніце палі імя карыстальніка і пароля і паспрабуйце яшчэ раз.",
"LOGIN_REJECTED": "Партал адхіліў лагін і пароль.",
"PORTAL_REFUSED": "Партал адмовіў у доступе для гэтай прылады.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Signatur 2 (optional)",
"SIGNATURE_HINT": "64 Hex-Zeichen – verwenden, wenn der Anbieter Geräte-Verifizierungssignaturen verlangt",
"SERVER_URL": "Server-URL",
"URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com/c oder https://example.com/stalker_portal/c)",
"URL_HINT": "Geben Sie einen Host oder eine Adresse mit /c, portal.php oder server/load.php ein.",
"URL_VALIDATION_ERROR": "Geben Sie eine gültige URL mit http:// oder https:// ein.",
"ADD": "Hinzufügen",
"VALIDATING": "Portal wird überprüft …",
"CREDENTIALS_HINT": "Nur erforderlich, wenn das Portal Benutzername und Passwort verlangt",
"AUTH_FAILED": "Authentifizierung am Portal fehlgeschlagen. Überprüfen Sie die URL und die MAC-Adresse.",
"EDIT_UNREACHABLE": "Das Portal hat nicht geantwortet. Es wurden keine Änderungen gespeichert. Prüfen Sie die URL und versuchen Sie es erneut.",
"LOGIN_REQUIRED": "Dieses Portal erfordert Benutzername und Passwort. Füllen Sie die Felder Benutzername und Passwort aus und versuchen Sie es erneut.",
"LOGIN_REJECTED": "Das Portal hat Benutzername und Passwort abgelehnt.",
"PORTAL_REFUSED": "Das Portal hat den Zugriff für dieses Gerät verweigert.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Υπογραφή 2 (Προαιρετικό)",
"SIGNATURE_HINT": "64 δεκαεξαδικοί χαρακτήρες - χρησιμοποιήστε εάν ο πάροχος απαιτεί υπογραφές επαλήθευσης συσκευής",
"SERVER_URL": "Διεύθηνση URL σέρβερ",
"URL_VALIDATION_ERROR": "Θα πρέπει να είναι μια έγκυρη διεύθυνση URL με πρωτόκολλο (π.χ. http://example.com/c ή https://example.com/stalker_portal/c)",
"URL_HINT": "Εισαγάγετε έναν host ή μια διεύθυνση που τελειώνει σε /c, portal.php ή server/load.php.",
"URL_VALIDATION_ERROR": "Εισαγάγετε μια έγκυρη διεύθυνση URL που αρχίζει με http:// ή https://.",
"ADD": "Προσθήκη",
"VALIDATING": "Επικύρωση πύλης...",
"CREDENTIALS_HINT": "Απαιτείται μόνο όταν η πύλη ζητά όνομα χρήστη και κωδικό πρόσβασης",
"AUTH_FAILED": "Η ταυτοποίηση με την πύλη απέτυχε. Ελέγξτε τη διεύθυνση URL και τη διεύθυνση MAC.",
"EDIT_UNREACHABLE": "Η πύλη δεν απάντησε. Δεν αποθηκεύτηκαν αλλαγές. Ελέγξτε τη διεύθυνση URL και δοκιμάστε ξανά.",
"LOGIN_REQUIRED": "Αυτή η πύλη απαιτεί όνομα χρήστη και κωδικό πρόσβασης. Συμπληρώστε τα πεδία ονόματος χρήστη και κωδικού πρόσβασης και δοκιμάστε ξανά.",
"LOGIN_REJECTED": "Η πύλη απέρριψε το όνομα χρήστη και τον κωδικό πρόσβασης.",
"PORTAL_REFUSED": "Η πύλη αρνήθηκε την πρόσβαση για αυτήν τη συσκευή.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Signature 2 (Optional)",
"SIGNATURE_HINT": "64 hex characters - use if provider requires device verification signatures",
"SERVER_URL": "Server URL",
"URL_VALIDATION_ERROR": "Should be a valid url with protocol (e.g. http://example.com/c or https://example.com/stalker_portal/c)",
"URL_HINT": "Enter a host or an address ending in /c, portal.php, or server/load.php.",
"URL_VALIDATION_ERROR": "Enter a valid http:// or https:// URL.",
"ADD": "Add",
"VALIDATING": "Validating portal...",
"CREDENTIALS_HINT": "Only needed when the portal asks for a login and password",
"AUTH_FAILED": "Failed to authenticate with the portal. Check the URL and MAC address.",
"EDIT_UNREACHABLE": "The portal did not respond. No changes were saved. Check the URL and try again.",
"LOGIN_REQUIRED": "This portal requires a login and password. Fill in the username and password fields and try again.",
"LOGIN_REJECTED": "The portal rejected the login and password.",
"PORTAL_REFUSED": "The portal refused access for this device.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Firma 2 (opcional)",
"SIGNATURE_HINT": "64 caracteres hexadecimales — úsalo si el proveedor requiere firmas de verificación del dispositivo",
"SERVER_URL": "URL del servidor",
"URL_VALIDATION_ERROR": "Debe ser una URL válida con protocolo (por ejemplo, http://example.com/c o https://example.com/stalker_portal/c).",
"URL_HINT": "Introduce un host o una dirección que termine en /c, portal.php o server/load.php.",
"URL_VALIDATION_ERROR": "Introduce una URL válida que empiece por http:// o https://.",
"ADD": "Agregar",
"VALIDATING": "Validando portal…",
"CREDENTIALS_HINT": "Solo es necesario cuando el portal pide usuario y contraseña",
"AUTH_FAILED": "No se pudo autenticar con el portal. Verifica la URL y la dirección MAC.",
"EDIT_UNREACHABLE": "El portal no respondió. No se guardaron los cambios. Comprueba la URL e inténtalo de nuevo.",
"LOGIN_REQUIRED": "Este portal requiere usuario y contraseña. Completa los campos de nombre de usuario y contraseña e inténtalo de nuevo.",
"LOGIN_REJECTED": "El portal rechazó el usuario y la contraseña.",
"PORTAL_REFUSED": "El portal denegó el acceso a este dispositivo.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Signature 2 (optionnelle)",
"SIGNATURE_HINT": "64 caractères hexadécimaux — à utiliser si le fournisseur exige des signatures de vérification d'appareil",
"SERVER_URL": "URL du serveur",
"URL_VALIDATION_ERROR": "Doit être une URL valide avec un protocole (par exemple http://example.com/c ou https://example.com/stalker_portal/c)",
"URL_HINT": "Saisissez un hôte ou une adresse se terminant par /c, portal.php ou server/load.php.",
"URL_VALIDATION_ERROR": "Saisissez une URL valide commençant par http:// ou https://.",
"ADD": "Ajouter",
"VALIDATING": "Validation du portail…",
"CREDENTIALS_HINT": "Nécessaire uniquement lorsque le portail demande un identifiant et un mot de passe",
"AUTH_FAILED": "Échec de l'authentification auprès du portail. Vérifiez l'URL et l'adresse MAC.",
"EDIT_UNREACHABLE": "Le portail n'a pas répondu. Aucune modification n'a été enregistrée. Vérifiez l'URL et réessayez.",
"LOGIN_REQUIRED": "Ce portail nécessite un identifiant et un mot de passe. Renseignez les champs nom d'utilisateur et mot de passe, puis réessayez.",
"LOGIN_REJECTED": "Le portail a rejeté l'identifiant et le mot de passe.",
"PORTAL_REFUSED": "Le portail a refusé l'accès pour cet appareil.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "2. aláírás (nem kötelező)",
"SIGNATURE_HINT": "64 hexadecimális karakter – csak akkor adja meg, ha a szolgáltató eszközellenőrzési aláírást kér",
"SERVER_URL": "Kiszolgáló URL-címe",
"URL_VALIDATION_ERROR": "Adjon meg protokollt is tartalmazó, érvényes URL-címet (például http://example.com/c vagy https://example.com/stalker_portal/c)",
"URL_HINT": "Adjon meg egy gazdagépet vagy /c, portal.php vagy server/load.php végű címet.",
"URL_VALIDATION_ERROR": "Adjon meg egy http:// vagy https:// kezdetű érvényes URL-címet.",
"ADD": "Hozzáadás",
"VALIDATING": "Portál ellenőrzése…",
"CREDENTIALS_HINT": "Csak akkor szükséges, ha a portál felhasználónevet és jelszót kér",
"AUTH_FAILED": "Nem sikerült a hitelesítés a portálon. Ellenőrizze az URL-címet és a MAC-címet.",
"EDIT_UNREACHABLE": "A portál nem válaszolt. A módosítások nem lettek mentve. Ellenőrizze az URL-címet, és próbálja újra.",
"LOGIN_REQUIRED": "Ez a portál felhasználónevet és jelszót igényel. Töltse ki a felhasználónév és a jelszó mezőt, majd próbálja újra.",
"LOGIN_REJECTED": "A portál elutasította a felhasználónevet és a jelszót.",
"PORTAL_REFUSED": "A portál megtagadta a hozzáférést ettől az eszköztől.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Firma 2 (opzionale)",
"SIGNATURE_HINT": "64 caratteri esadecimali — usa se il fornitore richiede firme di verifica del dispositivo",
"SERVER_URL": "URL server",
"URL_VALIDATION_ERROR": "Dovrebbe essere una URL valida con protocollo (es. http://esempio.it/c o https://esempio.it/stalker_portal/c)",
"URL_HINT": "Inserisci un host o un indirizzo che termini con /c, portal.php o server/load.php.",
"URL_VALIDATION_ERROR": "Inserisci un URL valido che inizi con http:// o https://.",
"ADD": "Aggiungi",
"VALIDATING": "Validazione del portale...",
"CREDENTIALS_HINT": "Necessario solo se il portale richiede nome utente e password",
"AUTH_FAILED": "Impossibile autenticarsi con il portale. Controlla l'URL e l'indirizzo MAC.",
"EDIT_UNREACHABLE": "Il portale non ha risposto. Nessuna modifica è stata salvata. Controlla l'URL e riprova.",
"LOGIN_REQUIRED": "Questo portale richiede nome utente e password. Compila i campi nome utente e password e riprova.",
"LOGIN_REJECTED": "Il portale ha rifiutato il nome utente e la password.",
"PORTAL_REFUSED": "Il portale ha negato l'accesso a questo dispositivo.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "署名 2(任意)",
"SIGNATURE_HINT": "16進64文字。プロバイダーがデバイス検証署名を要求する場合に使用",
"SERVER_URL": "サーバーURL",
"URL_VALIDATION_ERROR": "プロトコルを含む有効なURLである必要があります(例:http://example.com/c または https://example.com/stalker_portal/c)",
"URL_HINT": "ホスト、または /c、portal.php、server/load.php で終わるアドレスを入力してください。",
"URL_VALIDATION_ERROR": "http:// または https:// で始まる有効なURLを入力してください。",
"ADD": "追加",
"VALIDATING": "ポータルを検証中...",
"CREDENTIALS_HINT": "ポータルがユーザー名とパスワードを要求する場合のみ必要",
"AUTH_FAILED": "ポータルでの認証に失敗しました。URLとMACアドレスを確認してください。",
"EDIT_UNREACHABLE": "ポータルが応答しませんでした。変更は保存されていません。URLを確認して再試行してください。",
"LOGIN_REQUIRED": "このポータルにはユーザー名とパスワードが必要です。ユーザー名とパスワードの欄を入力して、もう一度お試しください。",
"LOGIN_REJECTED": "ポータルがユーザー名とパスワードを拒否しました。",
"PORTAL_REFUSED": "ポータルがこのデバイスのアクセスを拒否しました。",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "서명 2 (선택 사항)",
"SIGNATURE_HINT": "16진수 64자 - 공급자가 기기 검증 서명을 요구할 때 사용",
"SERVER_URL": "서버 URL",
"URL_VALIDATION_ERROR": "프로토콜이 포함된 유효한 URL이어야 합니다(예: http://example.com/c 또는 https://example.com/stalker_portal/c).",
"URL_HINT": "호스트 또는 /c, portal.php, server/load.php로 끝나는 주소를 입력하세요.",
"URL_VALIDATION_ERROR": "http:// 또는 https://로 시작하는 올바른 URL을 입력하세요.",
"ADD": "추가하다",
"VALIDATING": "포털을 검증하는 중...",
"CREDENTIALS_HINT": "포털이 사용자 이름과 비밀번호를 요구하는 경우에만 필요",
"AUTH_FAILED": "포털 인증에 실패했습니다. URL과 MAC 주소를 확인하세요.",
"EDIT_UNREACHABLE": "포털이 응답하지 않았습니다. 변경 사항이 저장되지 않았습니다. URL을 확인하고 다시 시도하세요.",
"LOGIN_REQUIRED": "이 포털에는 사용자 이름과 비밀번호가 필요합니다. 사용자 이름과 비밀번호 필드를 입력한 후 다시 시도하세요.",
"LOGIN_REJECTED": "포털이 사용자 이름과 비밀번호를 거부했습니다.",
"PORTAL_REFUSED": "포털이 이 기기의 접근을 거부했습니다.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Handtekening 2 (optioneel)",
"SIGNATURE_HINT": "64 hex-tekens — gebruiken als de provider apparaatverificatiehandtekeningen vereist",
"SERVER_URL": "Server URL",
"URL_VALIDATION_ERROR": "Moet een geldige URL zijn met protocol (bijv. http://example.com/c o https://example.com/stalker_portal/c)",
"URL_HINT": "Voer een host of adres in dat eindigt op /c, portal.php of server/load.php.",
"URL_VALIDATION_ERROR": "Voer een geldige URL in die begint met http:// of https://.",
"ADD": "Toevoegen",
"VALIDATING": "Portaal valideren...",
"CREDENTIALS_HINT": "Alleen nodig als het portaal om een gebruikersnaam en wachtwoord vraagt",
"AUTH_FAILED": "Aanmelden bij het portaal is mislukt. Controleer de URL en het MAC-adres.",
"EDIT_UNREACHABLE": "Het portaal reageerde niet. Er zijn geen wijzigingen opgeslagen. Controleer de URL en probeer het opnieuw.",
"LOGIN_REQUIRED": "Dit portaal vereist een gebruikersnaam en wachtwoord. Vul de velden gebruikersnaam en wachtwoord in en probeer het opnieuw.",
"LOGIN_REJECTED": "Het portaal heeft de gebruikersnaam en het wachtwoord geweigerd.",
"PORTAL_REFUSED": "Het portaal heeft de toegang voor dit apparaat geweigerd.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Sygnatura 2 (opcjonalnie)",
"SIGNATURE_HINT": "64 znaki szesnastkowe – użyj, jeśli dostawca wymaga sygnatur weryfikacji urządzenia",
"SERVER_URL": "URL serwera",
"URL_VALIDATION_ERROR": "Powinien to być poprawny URL z protokołem (np. http://example.com/c lub https://example.com/stalker_portal/c)",
"URL_HINT": "Wprowadź host lub adres zakończony /c, portal.php albo server/load.php.",
"URL_VALIDATION_ERROR": "Wprowadź poprawny URL zaczynający się od http:// lub https://.",
"ADD": "Dodaj",
"VALIDATING": "Walidacja portalu...",
"CREDENTIALS_HINT": "Potrzebne tylko wtedy, gdy portal wymaga loginu i hasła",
"AUTH_FAILED": "Nie udało się uwierzytelnić w portalu. Sprawdź URL i adres MAC.",
"EDIT_UNREACHABLE": "Portal nie odpowiedział. Nie zapisano żadnych zmian. Sprawdź URL i spróbuj ponownie.",
"LOGIN_REQUIRED": "Ten portal wymaga loginu i hasła. Wypełnij pola nazwy użytkownika i hasła i spróbuj ponownie.",
"LOGIN_REJECTED": "Portal odrzucił login i hasło.",
"PORTAL_REFUSED": "Portal odmówił dostępu temu urządzeniu.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Assinatura 2 (opcional)",
"SIGNATURE_HINT": "64 caracteres hexadecimais - use se o provedor exigir assinaturas de verificação do dispositivo",
"SERVER_URL": "URL do servidor",
"URL_VALIDATION_ERROR": "Deve ser uma URL válida com protocolo (por exemplo, http://example.com/c ou https://example.com/stalker_portal/c)",
"URL_HINT": "Introduza um host ou endereço terminado em /c, portal.php ou server/load.php.",
"URL_VALIDATION_ERROR": "Introduza um URL válido que comece por http:// ou https://.",
"ADD": "Adicionar",
"VALIDATING": "Validando portal...",
"CREDENTIALS_HINT": "Necessário apenas quando o portal exige login e senha",
"AUTH_FAILED": "Falha na autenticação com o portal. Verifique a URL e o endereço MAC.",
"EDIT_UNREACHABLE": "O portal não respondeu. Nenhuma alteração foi guardada. Verifique o URL e tente novamente.",
"LOGIN_REQUIRED": "Este portal exige login e senha. Preencha os campos de nome de usuário e senha e tente novamente.",
"LOGIN_REJECTED": "O portal rejeitou o login e a senha.",
"PORTAL_REFUSED": "O portal recusou o acesso para este dispositivo.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "Подпись 2 (необязательно)",
"SIGNATURE_HINT": "64 шестнадцатеричных символа — используйте, если провайдер требует подписи проверки устройства",
"SERVER_URL": "URL сервера",
"URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com/c или https://example.com/stalker_portal/c).",
"URL_HINT": "Введите хост или адрес, оканчивающийся на /c, portal.php или server/load.php.",
"URL_VALIDATION_ERROR": "Введите корректный URL, начинающийся с http:// или https://.",
"ADD": "Добавить",
"VALIDATING": "Проверка портала…",
"CREDENTIALS_HINT": "Требуется, только если портал запрашивает логин и пароль",
"AUTH_FAILED": "Не удалось авторизоваться на портале. Проверьте URL и MAC-адрес.",
"EDIT_UNREACHABLE": "Портал не ответил. Изменения не сохранены. Проверьте URL и повторите попытку.",
"LOGIN_REQUIRED": "Этот портал требует логин и пароль. Заполните поля имени пользователя и пароля и повторите попытку.",
"LOGIN_REJECTED": "Портал отклонил логин и пароль.",
"PORTAL_REFUSED": "Портал отказал в доступе этому устройству.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "İmza 2 (İsteğe Bağlı)",
"SIGNATURE_HINT": "64 onaltılık karakter - sağlayıcı cihaz doğrulama imzaları gerektiriyorsa kullanın",
"SERVER_URL": "Sunucu URL'si",
"URL_VALIDATION_ERROR": "Geçerli bir URL olmalıdır (örn. http://example.com/c veya https://example.com/stalker_portal/c)",
"URL_HINT": "/c, portal.php veya server/load.php ile biten bir ana makine ya da adres girin.",
"URL_VALIDATION_ERROR": "http:// veya https:// ile başlayan geçerli bir URL girin.",
"ADD": "Ekle",
"VALIDATING": "Portal doğrulanıyor...",
"CREDENTIALS_HINT": "Yalnızca portal kullanıcı adı ve parola istediğinde gereklidir",
"AUTH_FAILED": "Portalda kimlik doğrulaması başarısız oldu. URL'yi ve MAC adresini kontrol edin.",
"EDIT_UNREACHABLE": "Portal yanıt vermedi. Hiçbir değişiklik kaydedilmedi. URL'yi kontrol edip tekrar deneyin.",
"LOGIN_REQUIRED": "Bu portal kullanıcı adı ve parola gerektiriyor. Kullanıcı adı ve parola alanlarını doldurup tekrar deneyin.",
"LOGIN_REJECTED": "Portal, kullanıcı adı ve parolayı reddetti.",
"PORTAL_REFUSED": "Portal bu cihaz için erişimi reddetti.",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "签名 2(可选)",
"SIGNATURE_HINT": "64 位十六进制字符 — 如提供商需要设备验证签名时使用",
"SERVER_URL": "服务器 URL",
"URL_VALIDATION_ERROR": "应该是带有协议的有效网址(例如 http://example.com/c 或 https://example.com/stalker_portal/c)",
"URL_HINT": "请输入主机或以 /c、portal.php、server/load.php 结尾的地址。",
"URL_VALIDATION_ERROR": "请输入以 http:// 或 https:// 开头的有效 URL。",
"ADD": "添加",
"VALIDATING": "正在验证门户…",
"CREDENTIALS_HINT": "仅在门户要求登录名和密码时才需要填写",
"AUTH_FAILED": "门户身份验证失败。请检查 URL 和 MAC 地址。",
"EDIT_UNREACHABLE": "门户没有响应。未保存任何更改。请检查 URL 后重试。",
"LOGIN_REQUIRED": "此门户需要登录名和密码。请填写用户名和密码字段后重试。",
"LOGIN_REJECTED": "门户拒绝了该登录名和密码。",
"PORTAL_REFUSED": "门户拒绝了此设备的访问。",
+3 -1
View File
@@ -199,11 +199,13 @@
"SIGNATURE_2": "簽章 2(選填)",
"SIGNATURE_HINT": "64 個十六進位字元——若供應商要求裝置驗證簽章時請填寫",
"SERVER_URL": "伺服器網址",
"URL_VALIDATION_ERROR": "必須是有效的網址並包含協定(例如 http://example.com/c 或 https://example.com/stalker_portal/c)",
"URL_HINT": "請輸入主機或以 /c、portal.php、server/load.php 結尾的位址。",
"URL_VALIDATION_ERROR": "請輸入以 http:// 或 https:// 開頭的有效網址。",
"ADD": "新增",
"VALIDATING": "正在驗證入口網站...",
"CREDENTIALS_HINT": "僅在入口網站要求登入名稱與密碼時才需填寫",
"AUTH_FAILED": "入口網站驗證失敗。請檢查網址與 MAC 位址。",
"EDIT_UNREACHABLE": "入口網站沒有回應。未儲存任何變更。請檢查網址後再試一次。",
"LOGIN_REQUIRED": "此入口網站需要登入名稱與密碼。請填寫使用者名稱與密碼欄位後重試。",
"LOGIN_REJECTED": "入口網站拒絕了此登入名稱與密碼。",
"PORTAL_REFUSED": "入口網站拒絕了此裝置的存取。",