feat(xtream): add connection testing functionality and portal status service

This commit is contained in:
4gray committed 2025-03-01 22:00:04 +01:00
1 parent d0c8e8259e
commit 6b9e021d74
5 files changed
+263 -53

No files matched your search

@@ -33,12 +33,32 @@
}}</mat-label>
<input matInput type="text" id="password" formControlName="password" />
</mat-form-field>
<button
mat-button
type="submit"
[disabled]="!form.valid"
(click)="addPlaylist()"
>
{{ 'HOME.XTREAM_PLAYLIST.ADD' | translate }}
</button>
<div class="button-row">
<button
mat-button
type="button"
[disabled]="!form.valid || isTestingConnection"
(click)="testConnection()"
>
{{ isTestingConnection ? 'Testing...' : 'Test Connection' }}
</button>
<button
mat-button
type="submit"
[disabled]="!form.valid"
(click)="addPlaylist()"
>
{{ 'HOME.XTREAM_PLAYLIST.ADD' | translate }}
</button>
</div>
@if (connectionStatus){
<div class="connection-status">
<mat-icon [class]="getStatusClass()">
{{ getStatusIcon() }}
</mat-icon>
<span [class]="getStatusClass()">{{ getStatusMessage() }}</span>
</div>
}
</form>
@@ -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<void> {
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;
+110
View File
@@ -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<PortalStatus> {
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';
}
}
}
+40 -35
View File
@@ -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<string, string> }
);
} 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<string, string>;
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: {
+9 -5
View File
@@ -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<string, string> }
);
} else if (type === 'STALKER_REQUEST') {
@@ -174,6 +174,7 @@ export class TauriService extends DataService {
url: string;
params: Record<string, string>;
}) {
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 {