From 14198a84817a033bc02af71ac09e8cca3406aa6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jamie=20Bl=C3=A4si?= Date: Thu, 13 Nov 2025 11:03:51 +0100 Subject: [PATCH 1/3] implement mcstatus api --- src/app/home/home.html | 12 ++- src/app/home/home.scss | 4 + src/app/home/home.ts | 31 ++++++-- src/app/mcstatus.service.spec.ts | 16 ++++ src/app/mcstatus.service.ts | 38 ++++++++++ src/mcstatus.model.ts | 122 +++++++++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 src/app/mcstatus.service.spec.ts create mode 100644 src/app/mcstatus.service.ts create mode 100644 src/mcstatus.model.ts diff --git a/src/app/home/home.html b/src/app/home/home.html index b05a17c..b54a3e8 100644 --- a/src/app/home/home.html +++ b/src/app/home/home.html @@ -9,10 +9,12 @@
+ No Image Found - - {{ serverIp }} + + {{ serverIp }}
+ {{ version }}
@@ -39,7 +41,11 @@ {{ running ? "Stop" : "Start" }} Server @if (error) { -

{{ error }}

+

{{ error }}

+ } @if (playerCount != "undefined/undefined"){ +

Players: {{ playerCount }}

+ } @for (player of playerList; track $index) { +

{{ $index + ". " + player.name_clean }}

}
diff --git a/src/app/home/home.scss b/src/app/home/home.scss index 881f2ad..de09519 100644 --- a/src/app/home/home.scss +++ b/src/app/home/home.scss @@ -56,4 +56,8 @@ position: absolute; top: 10px; left: 80px; +} + +.icon { + width: 50px; } \ No newline at end of file diff --git a/src/app/home/home.ts b/src/app/home/home.ts index 9a8d54c..22df94a 100644 --- a/src/app/home/home.ts +++ b/src/app/home/home.ts @@ -7,6 +7,8 @@ import { MatChipsModule } from '@angular/material/chips'; import { ServerService } from '../server.service'; import { MatIconModule } from '@angular/material/icon'; import { AuthService } from '../auth.service'; +import { McstatusService } from '../mcstatus.service'; +import { MinecraftPlayerSample } from '../../mcstatus.model'; @Component({ selector: 'app-home', @@ -28,18 +30,37 @@ export class Home implements OnInit { serverIp = localStorage.getItem('serverIp'); error = ''; - private statusInterval?: ReturnType; + version: string | undefined = undefined; + playerCount: string | undefined = undefined; + playerList: MinecraftPlayerSample[] | undefined = undefined; + icon: string | null | undefined = undefined + + private infoInterval?: ReturnType; private serverService: ServerService = inject(ServerService); private authService: AuthService = inject(AuthService); + private mcstatusService: McstatusService = inject(McstatusService); ngOnInit() { this.loading = true; + this.fetchInfo(); + this.infoInterval = setInterval(() => this.fetchInfo(), 1000); + } + + private fetchInfo() { + this.mcstatusService.loadStatusInfo() this.fetchStatus(); - this.statusInterval = setInterval(() => this.fetchStatus(), 5000); + this.fetchMcstatusInfo(); + } + + private fetchMcstatusInfo() { + this.version = this.mcstatusService.getVersion(); + this.playerCount = this.mcstatusService.getPlayerCount(); + this.playerList = this.mcstatusService.getPlayerList(); + this.icon = this.mcstatusService.getServerIcon() } - fetchStatus() { + private fetchStatus() { this.serverService.getStatus().subscribe({ next: (res) => { this.running = res.running; @@ -55,7 +76,7 @@ export class Home implements OnInit { toggleServer() { if (this.running === null) return; - clearInterval(this.statusInterval); + clearInterval(this.infoInterval); this.loading = true; const action$ = this.running @@ -64,7 +85,7 @@ export class Home implements OnInit { action$.subscribe({ next: () => - (this.statusInterval = setInterval(() => this.fetchStatus(), 5000)), + (this.infoInterval = setInterval(() => this.fetchStatus(), 5000)), error: () => (this.loading = false), }); } diff --git a/src/app/mcstatus.service.spec.ts b/src/app/mcstatus.service.spec.ts new file mode 100644 index 0000000..123b5f5 --- /dev/null +++ b/src/app/mcstatus.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { McstatusService } from './mcstatus.service'; + +describe('McstatusService', () => { + let service: McstatusService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(McstatusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/mcstatus.service.ts b/src/app/mcstatus.service.ts new file mode 100644 index 0000000..374ca7a --- /dev/null +++ b/src/app/mcstatus.service.ts @@ -0,0 +1,38 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Mcstatus } from '../mcstatus.model'; + +@Injectable({ + providedIn: 'root', +}) +export class McstatusService { + // https://mcstatus.io/docs + + private readonly http = inject(HttpClient); + private readonly URL = 'https://api.mcstatus.io/v2/status/java/'; + private IP: string | null = null; + private status: Mcstatus | null = null; + + public loadStatusInfo() { + this.IP = localStorage.getItem('serverIp'); + this.http + .get(`${this.URL}/${this.IP?.replace("https://", "")}`) + .subscribe((res) => (this.status = res)); + } + + public getVersion() { + return this.status?.version?.name_clean; + } + + public getPlayerCount() { + return `${this.status?.players?.online}/${this.status?.players?.max}`; + } + + public getPlayerList() { + return this.status?.players?.list; + } + + public getServerIcon() { + return this.status?.icon + } +} diff --git a/src/mcstatus.model.ts b/src/mcstatus.model.ts new file mode 100644 index 0000000..73a69bb --- /dev/null +++ b/src/mcstatus.model.ts @@ -0,0 +1,122 @@ +export interface Mcstatus { + /** Whether the server is online or offline + * Not used since we get the status directly from the machine */ + online: boolean; + + /** Resolved hostname of the server */ + host: string; + + /** Resolved port of the server */ + port: number; + + /** Resolved IP address of the hostname (can be null if resolution failed) */ + ip_address: string | null; + + /** Whether this address is blocked by Mojang (EULA violation) */ + eula_blocked: boolean; + + /** Unix ms timestamp when the status was retrieved */ + retrieved_at: number; + + /** Unix ms timestamp when this status cache expires */ + expires_at: number; + + /** Server version data (missing if server is offline) */ + version?: MinecraftVersion; + + /** Player information (missing if server is offline) */ + players?: MinecraftPlayers; + + /** Message of the day / description (missing if server is offline) */ + motd?: MinecraftMotd; + + /** + * Base64-encoded PNG data of the 64x64 server icon. + * May be null if no icon is set. Missing if server is offline. + */ + icon?: string | null; + + /** + * Forge mods loaded on the server. + * Usually empty if no Forge is installed. + */ + mods: MinecraftMod[]; + + /** + * Software the server is running (null if query lookup fails). + * Missing if server is offline. + */ + software?: string | null; + + /** + * Plugins running on the server (missing if server is offline). + */ + plugins?: MinecraftPlugin[]; + + /** + * Result of SRV record lookup. + * Always present but can be null if no SRV record was found. + */ + srv_record: MinecraftSrvRecord | null; +} + +export interface MinecraftVersion { + /** Raw version name with formatting codes */ + name_raw: string; + /** Clean version name without formatting codes */ + name_clean: string; + /** HTML-formatted version name */ + name_html: string; + /** Protocol version used to identify supported client versions */ + protocol: number; +} + +export interface MinecraftPlayers { + /** Online player count */ + online: number; + /** Maximum allowed players */ + max: number; + /** Sample list of online players (may be empty) */ + list: MinecraftPlayerSample[]; +} + +export interface MinecraftPlayerSample { + /** UUID of the player */ + uuid: string; + /** Username with possible formatting codes */ + name_raw: string; + /** Username without formatting codes */ + name_clean: string; + /** HTML-formatted username */ + name_html: string; +} + +export interface MinecraftMotd { + /** Raw MOTD with formatting codes */ + raw: string; + /** Clean text-only MOTD */ + clean: string; + /** HTML representation of the MOTD */ + html: string; +} + +export interface MinecraftMod { + /** Name of the mod */ + name: string; + /** Version of the mod */ + version: string; +} + +export interface MinecraftPlugin { + /** Name of the plugin */ + name: string; + /** Semantic version of the plugin (can be null) */ + version: string | null; +} + +export interface MinecraftSrvRecord { + /** Hostname returned by the SRV lookup */ + host: string; + /** Port returned by the SRV lookup */ + port: number; +} From a0f44cc0fbde49a479989daff364c5dd116b9fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jamie=20Bl=C3=A4si?= Date: Thu, 13 Nov 2025 11:10:21 +0100 Subject: [PATCH 2/3] lint fix --- src/app/home/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/home/home.html b/src/app/home/home.html index b54a3e8..2874f72 100644 --- a/src/app/home/home.html +++ b/src/app/home/home.html @@ -42,7 +42,7 @@ @if (error) {

{{ error }}

- } @if (playerCount != "undefined/undefined"){ + } @if (playerCount !== "undefined/undefined"){

Players: {{ playerCount }}

} @for (player of playerList; track $index) {

{{ $index + ". " + player.name_clean }}

From 286e13d954f832b51374d09ce40570d79500c078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jamie=20Bl=C3=A4si?= Date: Thu, 13 Nov 2025 11:14:58 +0100 Subject: [PATCH 3/3] fix test --- src/app/mcstatus.service.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/mcstatus.service.spec.ts b/src/app/mcstatus.service.spec.ts index 123b5f5..c3a7e60 100644 --- a/src/app/mcstatus.service.spec.ts +++ b/src/app/mcstatus.service.spec.ts @@ -1,12 +1,15 @@ import { TestBed } from '@angular/core/testing'; import { McstatusService } from './mcstatus.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('McstatusService', () => { let service: McstatusService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + }); service = TestBed.inject(McstatusService); });