mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
feat(xtream): add connection testing functionality and portal status service
This commit is contained in:
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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user