From 6b9e021d74833b3fcb2056fa9b713552f12997d2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 1 Mar 2025 22:00:04 +0100 Subject: [PATCH] feat(xtream): add connection testing functionality and portal status service --- .../xtream-code-import.component.html | 36 ++++-- .../xtream-code-import.component.ts | 81 ++++++++++++- src/app/services/portal-status.service.ts | 110 ++++++++++++++++++ src/app/services/pwa.service.ts | 75 ++++++------ src/app/services/tauri.service.ts | 14 ++- 5 files changed, 263 insertions(+), 53 deletions(-) create mode 100644 src/app/services/portal-status.service.ts diff --git a/src/app/home/xtream-code-import/xtream-code-import.component.html b/src/app/home/xtream-code-import/xtream-code-import.component.html index d58f59408..e08c7311b 100644 --- a/src/app/home/xtream-code-import/xtream-code-import.component.html +++ b/src/app/home/xtream-code-import/xtream-code-import.component.html @@ -33,12 +33,32 @@ }} - + +
+ + +
+ + @if (connectionStatus){ +
+ + {{ getStatusIcon() }} + + {{ getStatusMessage() }} +
+ } diff --git a/src/app/home/xtream-code-import/xtream-code-import.component.ts b/src/app/home/xtream-code-import/xtream-code-import.component.ts index cba30c835..599f05194 100644 --- a/src/app/home/xtream-code-import/xtream-code-import.component.ts +++ b/src/app/home/xtream-code-import/xtream-code-import.component.ts @@ -6,14 +6,18 @@ import { ReactiveFormsModule, Validators, } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; +import { MatButton } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIcon } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { v4 as uuid } from 'uuid'; import { Playlist } from '../../../../shared/playlist.interface'; -import { DataService } from '../../services/data.service'; +import { + PortalStatus, + PortalStatusService, +} from '../../services/portal-status.service'; import { addPlaylist } from '../../state/actions'; @Component({ @@ -22,8 +26,9 @@ import { addPlaylist } from '../../state/actions'; FormsModule, ReactiveFormsModule, MatFormFieldModule, + MatIcon, MatInputModule, - MatButtonModule, + MatButton, TranslateModule, ], selector: 'app-xtream-code-import', @@ -39,6 +44,34 @@ import { addPlaylist } from '../../state/actions'; form { width: 100%; } + + .status-active { + color: #4caf50; + } + + .status-inactive { + color: #f44336; + } + + .status-expired { + color: #ff9800; + } + + .status-unavailable { + color: #9e9e9e; + } + + .connection-status { + margin: 10px 0; + display: flex; + align-items: center; + gap: 8px; + } + + .button-row { + display: flex; + justify-content: space-between; + } `, ], }) @@ -58,8 +91,46 @@ export class XtreamCodeImportComponent { importDate: new FormControl(new Date().toISOString()), }); - dataService = inject(DataService); - store = inject(Store); + readonly store = inject(Store); + readonly portalStatusService = inject(PortalStatusService); + + connectionStatus: PortalStatus | null = null; + isTestingConnection = false; + + async testConnection(): Promise { + if (!this.form.valid) return; + + this.isTestingConnection = true; + const serverUrlAsString = this.form.value.serverUrl as string; + const url = new URL(serverUrlAsString); + const serverUrl = `${url.protocol}//${url.hostname}${url.port ? ':' + url.port : ''}`; + + try { + this.connectionStatus = + await this.portalStatusService.checkPortalStatus( + serverUrl, + this.form.value.username as string, + this.form.value.password as string + ); + } catch (error) { + console.error('Error testing connection:', error); + this.connectionStatus = 'unavailable'; + } finally { + this.isTestingConnection = false; + } + } + + getStatusMessage(): string { + return this.portalStatusService.getStatusMessage(this.connectionStatus); + } + + getStatusClass(): string { + return this.portalStatusService.getStatusClass(this.connectionStatus); + } + + getStatusIcon(): string { + return this.portalStatusService.getStatusIcon(this.connectionStatus); + } addPlaylist() { const serverUrlAsString = this.form.value.serverUrl as string; diff --git a/src/app/services/portal-status.service.ts b/src/app/services/portal-status.service.ts new file mode 100644 index 000000000..20c33f0bd --- /dev/null +++ b/src/app/services/portal-status.service.ts @@ -0,0 +1,110 @@ +import { Injectable, inject } from '@angular/core'; +import { XTREAM_REQUEST } from '../../../shared/ipc-commands'; +import { DataService } from './data.service'; + +export type PortalStatus = 'active' | 'inactive' | 'expired' | 'unavailable'; + +@Injectable({ + providedIn: 'root', +}) +export class PortalStatusService { + private readonly dataService = inject(DataService); + + /** + * Checks the status of an Xtream Code portal + * + * @param serverUrl The base URL of the server + * @param username The username for authentication + * @param password The password for authentication + * @returns A promise that resolves to the portal status + */ + async checkPortalStatus( + serverUrl: string, + username: string, + password: string + ): Promise { + try { + let normalizedUrl = serverUrl; + if (serverUrl && !serverUrl.endsWith('/')) { + normalizedUrl = serverUrl; + } + + let response = await this.dataService.sendIpcEvent(XTREAM_REQUEST, { + url: normalizedUrl, + params: { + password, + username, + action: 'get_account_info', + }, + }); + response = response?.payload; + + if (!response?.user_info?.status) { + return 'unavailable'; + } + + if (response.user_info.status === 'Active') { + const expDate = new Date( + parseInt(response.user_info.exp_date) * 1000 + ); + return expDate < new Date() ? 'expired' : 'active'; + } else { + return 'inactive'; + } + } catch (error) { + console.error('Error checking portal status:', error); + return 'unavailable'; + } + } + + /** + * Gets a user-friendly message based on the portal status + * + * @param status The portal status + * @returns A message describing the status + */ + getStatusMessage(status: PortalStatus | null): string { + switch (status) { + case 'active': + return 'Connection successful! Portal is active.'; + case 'inactive': + return 'Portal is inactive.'; + case 'expired': + return 'Portal subscription has expired.'; + case 'unavailable': + return 'Could not connect to the portal.'; + default: + return ''; + } + } + + /** + * Gets a CSS class name based on the portal status + * + * @param status The portal status + * @returns A CSS class name + */ + getStatusClass(status: PortalStatus | null): string { + return status ? `status-${status}` : ''; + } + + /** + * Gets an icon name based on the portal status + * + * @param status The portal status + * @returns A material icon name + */ + getStatusIcon(status: PortalStatus | null): string { + switch (status) { + case 'active': + return 'check_circle'; + case 'inactive': + return 'cancel'; + case 'expired': + return 'warning'; + case 'unavailable': + default: + return 'error'; + } + } +} diff --git a/src/app/services/pwa.service.ts b/src/app/services/pwa.service.ts index 77bfe1bb3..da42d88d9 100644 --- a/src/app/services/pwa.service.ts +++ b/src/app/services/pwa.service.ts @@ -5,7 +5,7 @@ import { Params } from '@angular/router'; import { SwUpdate } from '@angular/service-worker'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { catchError, EMPTY, throwError } from 'rxjs'; +import { catchError, firstValueFrom, throwError } from 'rxjs'; import { ERROR, PLAYLIST_PARSE_BY_URL, @@ -63,13 +63,13 @@ export class PwaService extends DataService { * @param type ipc command type * @param payload payload */ - sendIpcEvent(type: string, payload?: unknown): void { + sendIpcEvent(type: string, payload?: unknown) { if (type === PLAYLIST_PARSE_BY_URL) { this.fetchFromUrl(payload); } else if (type === PLAYLIST_UPDATE) { this.refreshPlaylist(payload); } else if (type === XTREAM_REQUEST) { - this.forwardXtreamRequest( + return this.forwardXtreamRequest( payload as { url: string; params: Record } ); } else if (type === STALKER_REQUEST) { @@ -151,7 +151,7 @@ export class PwaService extends DataService { return message; } - forwardXtreamRequest(payload: { + async forwardXtreamRequest(payload: { url: string; params: Record; macAddress?: string; @@ -163,39 +163,44 @@ export class PwaService extends DataService { }, } : {}; - return this.http - .get(`${this.corsProxyUrl}/xtream`, { - params: { - url: payload.url, - ...payload.params, - }, - ...headers, - }) - .pipe( - catchError((response) => { - window.postMessage({ - type: ERROR, - status: response.error.status, - message: response.error.message ?? 'Unknown error', - }); - return EMPTY; + try { + let result: any; + const response = await firstValueFrom( + this.http.get(`${this.corsProxyUrl}/xtream`, { + params: { + url: payload.url, + ...payload.params, + }, + ...headers, }) - ) - .subscribe((response) => { - if (!(response as any).payload) { - window.postMessage({ - type: ERROR, - status: (response as any).status, - message: (response as any).message ?? 'Unknown error', - }); - } else { - window.postMessage({ - type: XTREAM_RESPONSE, - payload: (response as any).payload, - action: payload.params.action, - }); - } + ); + + if (!(response as any).payload) { + if (payload.params.action === 'get_account_info') return; + + result = { + type: ERROR, + status: (response as any).status, + message: (response as any).message ?? 'Unknown error', + }; + window.postMessage(result); + } else { + result = { + type: XTREAM_RESPONSE, + payload: (response as any).payload, + action: payload.params.action, + }; + window.postMessage(result); + } + return result; + } catch (error: any) { + if (payload.params.action === 'get_account_info') return; + window.postMessage({ + type: ERROR, + status: error.error?.status, + message: error.error?.message ?? 'Unknown error', }); + } } forwardStalkerRequest(payload: { diff --git a/src/app/services/tauri.service.ts b/src/app/services/tauri.service.ts index 19fe9368b..af9107629 100644 --- a/src/app/services/tauri.service.ts +++ b/src/app/services/tauri.service.ts @@ -34,7 +34,7 @@ export class TauriService extends DataService { } else if (type === 'PLAYLIST_UPDATE') { console.log('PLAYLIST_UPDATE'); } else if (type === 'XTREAM_REQUEST') { - this.forwardXtreamRequest( + return await this.forwardXtreamRequest( payload as { url: string; params: Record } ); } else if (type === 'STALKER_REQUEST') { @@ -174,6 +174,7 @@ export class TauriService extends DataService { url: string; params: Record; }) { + let result: any; const url = new URL(`${payload.url}/player_api.php`); Object.entries(payload.params).forEach(([key, value]) => { url.searchParams.append(key, value); @@ -183,18 +184,21 @@ export class TauriService extends DataService { const responseBody = await response.json(); if (!responseBody) { - window.postMessage({ + result = { type: ERROR, status: response.status, message: responseBody.message ?? 'Unknown error', - }); + }; + window.postMessage(result); } else { - window.postMessage({ + result = { type: XTREAM_RESPONSE, payload: responseBody, action: payload.params.action, - }); + }; + window.postMessage(result); } + return result; } removeAllListeners(type: string): void {