├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── css
└── index.css
├── img
├── discord.svg
├── empty-star.svg
├── question-mark.svg
├── refresh.svg
├── search.svg
├── star.svg
└── steam.svg
├── index.html
└── js
├── fetch.js
├── index.js
├── serach.js
├── server.js
└── utils
├── color.js
└── user.js
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .gitignore
3 | package.json
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "useTabs": true,
5 | "bracketSameLine": true
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 animekkk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fivem-player-list
2 |
3 | Simple website that will dipslay you all players connected to selected sever.
4 | It is available here: [https://igorovh.github.io/fivem-player-list/](https://igorovh.github.io/fivem-player-list/)
5 |
6 | It is using FiveM endpoint, which is used in their UI:
7 |
8 | ```
9 | https://servers-frontend.fivem.net/api/servers/single/serverId
10 | ```
11 |
12 | # How to get Server ID?
13 | ### Server Browser
14 | Visit [FiveM Server Browser](https://servers.fivem.net/) and click on chosen server.
15 | After that look into the URL and copy last fragment, which is **Server ID**. Example:
16 | `https://servers.fivem.net/servers/detail/vp4rxq` -> **vp4rxq**
17 | ### Client
18 | Find server in FiveM client and click on it.
19 | In the right side you will have "join URL", in which last fragment is **Server ID**. Example:
20 | `cfx.re/join/vp4rxq` -> **vp4rxq**
21 |
22 | 
23 |
--------------------------------------------------------------------------------
/css/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Mulish:wght@400;700&display=swap');
2 |
3 | body {
4 | margin: 0 !important;
5 | padding: 0 !important;
6 | background: #232323;
7 | display: flex;
8 | justify-content: center;
9 | color: white;
10 | }
11 |
12 | #wrapper {
13 | width: 75%;
14 | font-family: 'Mulish', sans-serif;
15 | background: #2C2C2C;
16 | height: min(100vh, max-content);
17 | position: relative;
18 | }
19 |
20 | header {
21 | display: flex;
22 | align-items: center;
23 | gap: 8px;
24 | font-weight: bold;
25 | background: #171717;
26 | }
27 |
28 | #title {
29 | display: flex;
30 | align-items: center;
31 | gap: 16px;
32 | padding: 16px 24px;
33 | font-size: 24px;
34 | flex-grow: 1;
35 | }
36 |
37 | header img {
38 | width: 64px;
39 | }
40 |
41 | input {
42 | width: 168px;
43 | height: 36px;
44 | background: #232323;
45 | border: 0px;
46 | text-align: center;
47 | font-size: 16px;
48 | outline: 0px;
49 | color: white;
50 | font-family: 'Mulish', sans-serif;
51 | padding: 4px 12px;
52 | }
53 |
54 | #server {
55 | display: flex;
56 | padding: 24px;
57 | }
58 |
59 | .icon {
60 | display: flex;
61 | align-items: center;
62 | justify-content: center;
63 | padding: 0 16px;
64 | background: #1E1E1E;
65 | gap: 12px;
66 | }
67 |
68 | .icon img {
69 | width: 22px;
70 | }
71 |
72 | .icon:hover {
73 | background: #252525;
74 | cursor: pointer;
75 | }
76 |
77 | .icon:active {
78 | background: #2e2e2e;
79 | }
80 |
81 | section {
82 | background: #171717;
83 | display: flex;
84 | gap: 16px;
85 | padding: 0 24px 24px 24px;
86 | }
87 |
88 | #search {
89 | text-align: left;
90 | flex-grow: 1;
91 | }
92 |
93 | .refresh-icon {
94 | width: 64px;
95 | text-align: right;
96 | }
97 |
98 | table {
99 | font-size: 16px;
100 | border-spacing: 0;
101 | width: 100%;
102 | }
103 |
104 | th {
105 | width: 100%;
106 | text-align: left;
107 | }
108 |
109 | tr {
110 | display: flex;
111 | padding: 16px 96px;
112 | justify-content: space-between;
113 | }
114 |
115 | td {
116 | font-weight: 400;
117 | }
118 |
119 | table tr:nth-child(odd) {
120 | background: #262626;
121 | }
122 |
123 | .table-favorite {
124 | width: 20%;
125 | }
126 |
127 | .table-favorite img:hover {
128 | cursor: pointer;
129 | opacity: 0.5;
130 | }
131 |
132 | .table-favorite img:active {
133 | opacity: 0.25;
134 | }
135 |
136 | .table-no {
137 | width: 15%;
138 | color: #939393;
139 | }
140 |
141 | .table-id {
142 | width: 15%;
143 | }
144 |
145 | .table-name {
146 | width: 40%;
147 | word-break: break-all;
148 | }
149 |
150 | .table-socials {
151 | width: 15%;
152 | display: flex;
153 | gap: 12px;
154 | align-items: center;
155 | }
156 |
157 | .table-ping {
158 | width: 15%;
159 | }
160 |
161 | .table-socials img:hover {
162 | cursor: pointer;
163 | opacity: 0.5;
164 | }
165 |
166 | .table-socials img:active {
167 | opacity: 0.25;
168 | }
169 |
170 | .table-footer {
171 | display: flex;
172 | justify-content: center;
173 | text-align: center;
174 | }
175 |
176 | .table-footer a {
177 | text-decoration: underline;
178 | color: white;
179 | }
180 |
181 | @media (max-width: 1200px) {
182 | #wrapper {
183 | width: 100%;
184 | }
185 |
186 | header {
187 | gap: 12px;
188 | align-items: flex-start;
189 | flex-direction: column;
190 | }
191 |
192 | #title {
193 | padding: 24px 24px 0 24px;
194 | }
195 |
196 | #server {
197 | padding: 0 24px 24px 24px;
198 | }
199 |
200 | tr {
201 | padding: 16px 16px;
202 | }
203 | }
204 |
205 | a {
206 | text-decoration: none;
207 | }
--------------------------------------------------------------------------------
/img/discord.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/empty-star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/question-mark.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/refresh.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/img/steam.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Waiting for Server ID...
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |

16 |
Waiting for Server ID...
17 |
18 |
19 |
33 |
34 |
41 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/js/fetch.js:
--------------------------------------------------------------------------------
1 | import { setServerInfo, setTitle } from './server.js';
2 | import { getDiscordId, getSteamId } from './utils/user.js';
3 | import { isSearching, serachPlayers } from './serach.js';
4 |
5 | const refreshButton = document.querySelector('#refresh-button');
6 |
7 | let currentPlayers;
8 |
9 | export const getPlayers = () => currentPlayers;
10 |
11 | export const fetchServer = (serverId) => {
12 | setTitle('Loading server data from FiveM API...');
13 | seconds = 30;
14 | refreshButton.onclick = () => fetchServer(serverId);
15 | const url = `https://servers-frontend.fivem.net/api/servers/single/${serverId}`;
16 | console.info(`Fetching server info`, serverId, url);
17 | fetch(url, {
18 | headers: {
19 | Accept: '*/*',
20 | 'Content-Type': 'application/json',
21 | 'User-Agent':
22 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
23 | },
24 | })
25 | .then((response) => response.json())
26 | .then((json) => {
27 | setServerInfo(serverId, json.Data);
28 | let playersFetch = false; //Todo
29 | let url = `https://servers-frontend.fivem.net/api/servers/single/${serverId}`;
30 | fetchPlayers(url, playersFetch);
31 | startFetcher(serverId);
32 | })
33 | .catch((error) => console.error(error));
34 | };
35 |
36 | const refreshTimer = document.querySelector('#refresh-timer');
37 | let fetcher;
38 | let seconds = 30;
39 |
40 | const startFetcher = (serverId) => {
41 | console.log(`Starting fetcher at ${seconds} seconds`);
42 | if (fetcher) clearInterval(fetcher);
43 | fetcher = setInterval(() => {
44 | refreshTimer.textContent = seconds + 's';
45 | if (seconds < 1) {
46 | clearInterval(fetcher);
47 | fetchServer(serverId);
48 | }
49 | seconds--;
50 | }, 1000);
51 | };
52 |
53 | const fetchPlayers = (url, playersFetch = false) => {
54 | console.info('Fetching players with method:', playersFetch ? 'players.json' : 'normal', url);
55 | fetch(url, {
56 | headers: {
57 | Accept: '*/*',
58 | 'Content-Type': 'application/json',
59 | 'User-Agent':
60 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
61 | },
62 | })
63 | .then((response) => response.json())
64 | .then((json) => {
65 | let players = playersFetch ? json : json.Data.players;
66 | players = formatPlayers(players);
67 | currentPlayers = players;
68 | renderPlayers(players);
69 | })
70 | .catch((error) => console.error(error));
71 | };
72 |
73 | const formatPlayers = (players) => {
74 | const formattedPlayers = [];
75 | players.forEach((player) => {
76 | const socials = {};
77 |
78 | if (player.identifiers) {
79 | const steamIdentifier = getSteamId(player.identifiers);
80 | if (steamIdentifier) socials.steam = steamIdentifier;
81 |
82 | const discordIdentifier = getDiscordId(player.identifiers);
83 | if (discordIdentifier) socials.discord = discordIdentifier;
84 | }
85 |
86 | formattedPlayers.push({
87 | name: player.name,
88 | id: player.id,
89 | socials,
90 | ping: player.ping,
91 | });
92 | });
93 | return formattedPlayers.sort((a, b) => a.id - b.id);
94 | };
95 |
96 | const table = document.querySelector('table');
97 |
98 | const resetTable = () => {
99 | [...table.querySelectorAll('tr')].filter((tr) => tr.id !== 'table-header').forEach((tr) => tr.remove());
100 | };
101 |
102 | const STEAM_LINK = 'https://steamcommunity.com/profiles/%id%';
103 | const DISCORD_LINK = 'https://discord.com/users/%id%';
104 |
105 | export const renderPlayers = (players, search = false) => {
106 | resetTable();
107 |
108 | console.info('Rendering new players', players.length);
109 | let index = 1;
110 | players.forEach((player) => {
111 | const tr = document.createElement('tr');
112 |
113 | const no = document.createElement('td');
114 | // const star = document.createElement('td');
115 | const id = document.createElement('td');
116 | const name = document.createElement('td');
117 | const socials = document.createElement('td');
118 | const ping = document.createElement('td');
119 |
120 | no.className = 'table-no';
121 | // star.className = 'table-favorite';
122 | id.className = 'table-id';
123 | name.className = 'table-name';
124 | socials.className = 'table-socials';
125 | ping.className = 'table-ping';
126 |
127 | no.textContent = index++ + '.';
128 | // star.innerHTML = `
`;
129 | id.textContent = player.id;
130 | name.textContent = player.name;
131 | ping.textContent = `${player.ping}ms`;
132 |
133 | if (player.socials.steam) {
134 | const link = document.createElement('a');
135 | link.href = STEAM_LINK.replace('%id%', player.socials.steam);
136 | link.target = '_blank';
137 | link.innerHTML = '
';
138 | socials.appendChild(link);
139 | }
140 | if (player.socials.discord) {
141 | const link = document.createElement('a');
142 | link.href = DISCORD_LINK.replace('%id%', player.socials.discord);
143 | link.target = '_blank';
144 | link.innerHTML = '
';
145 | socials.appendChild(link);
146 | }
147 |
148 | tr.appendChild(no);
149 | // tr.appendChild(star);
150 | tr.appendChild(id);
151 | tr.appendChild(name);
152 | tr.appendChild(socials);
153 | tr.appendChild(ping);
154 |
155 | table.appendChild(tr);
156 | });
157 | // Footer
158 | table.innerHTML += `
159 | `;
165 | if (isSearching() && !search) serachPlayers();
166 | };
167 |
--------------------------------------------------------------------------------
/js/index.js:
--------------------------------------------------------------------------------
1 | import { fetchServer } from './fetch.js';
2 | import { initializeSearch } from './serach.js';
3 |
4 | // FiveCity vp4rxq
5 | // Pixa vqkdxx
6 |
7 | window.addEventListener('DOMContentLoaded', () => {
8 | // Old ID Fix
9 | if (localStorage.getItem('lastId')) {
10 | localStorage.setItem('serverId', localStorage.getItem('lastId'));
11 | localStorage.removeItem('lastId');
12 | }
13 |
14 | initializeSearch(); // Player Search
15 |
16 | // Server Id Serach
17 | const serverIdSearch = document.querySelector('#server-id');
18 | serverIdSearch.addEventListener('keyup', (event) => {
19 | if (event.key === 'Enter' || event.keyCode === 13) {
20 | const value = serverIdSearch.value;
21 | if (value.length < 1) return;
22 | fetchServer(value);
23 | setId(value);
24 | console.info('Fetching by input.');
25 | }
26 | });
27 | document.querySelector('#server-id-button').onclick = () => {
28 | const value = serverIdSearch.value;
29 | if (value.length < 1) return;
30 | fetchServer(value);
31 | setId(value);
32 | console.info('Fetching by input.');
33 | };
34 |
35 | const url = new URL(window.location.href);
36 | if (url.searchParams.has('serverId')) {
37 | const serverId = url.searchParams.get('serverId');
38 | fetchServer(serverId);
39 | setId(serverId);
40 | console.info('Fetching by URL.');
41 | return;
42 | }
43 |
44 | const storageServerId = localStorage.getItem('serverId');
45 | if (storageServerId) {
46 | fetchServer(storageServerId);
47 | setId(storageServerId);
48 | console.info('Fetching by localStorage.');
49 | }
50 | });
51 |
52 | const setId = (serverId) => {
53 | const url = new URL(window.location.href);
54 | url.searchParams.set('serverId', serverId);
55 | window.history.replaceState(null, null, url);
56 | localStorage.setItem('serverId', serverId);
57 | };
58 |
--------------------------------------------------------------------------------
/js/serach.js:
--------------------------------------------------------------------------------
1 | import { getPlayers, renderPlayers } from './fetch.js';
2 |
3 | let search;
4 | let searching = false;
5 |
6 | export const initializeSearch = () => {
7 | search = document.querySelector('#search');
8 | search.addEventListener('keyup', (event) => serachPlayers());
9 | };
10 |
11 | export const serachPlayers = () => {
12 | const value = search.value;
13 | let players = getPlayers();
14 | if (value.length < 1) {
15 | searching = false;
16 | return renderPlayers(players, true);
17 | }
18 |
19 | searching = true;
20 | players = players.filter(
21 | (player) => player.id.toString().startsWith(value) || player.name.toLowerCase().includes(value.toLowerCase())
22 | );
23 | renderPlayers(players, true);
24 | };
25 |
26 | export const isSearching = () => searching;
27 |
--------------------------------------------------------------------------------
/js/server.js:
--------------------------------------------------------------------------------
1 | import { fixColors } from './utils/color.js';
2 |
3 | const favicon = document.querySelector('#favicon');
4 | const serverIcon = document.querySelector('#server-icon');
5 | const serverName = document.querySelector('#server-name');
6 | const serverIdInput = document.querySelector('#server-id');
7 | const serverPlayers = document.querySelector('#server-players');
8 |
9 | export const setServerInfo = (serverId, data) => {
10 | serverIdInput.value = serverId;
11 | const title = fixColors(data.hostname);
12 | const icon = `https://servers-live.fivem.net/servers/icon/${serverId}/${data.iconVersion}.png`;
13 | document.title = title;
14 | favicon.href = icon;
15 | setTitle(title);
16 | serverIcon.src = icon;
17 | serverPlayers.textContent = `[${data.clients}/${data.svMaxclients ?? data.sv_maxclients ?? 0}]`;
18 | };
19 |
20 | export const setTitle = (title) => {
21 | serverName.textContent = title;
22 | serverName.title = title;
23 | };
24 |
--------------------------------------------------------------------------------
/js/utils/color.js:
--------------------------------------------------------------------------------
1 | export const fixColors = (title) => {
2 | return title.replace(/\^\d+/g, '');
3 | };
4 |
--------------------------------------------------------------------------------
/js/utils/user.js:
--------------------------------------------------------------------------------
1 | export const getSteamId = (ids) => {
2 | const filteredIdentifiers = ids.filter((identifier) => identifier.startsWith('steam:'));
3 | if (filteredIdentifiers.length > 0) {
4 | return hexToDecimal(filteredIdentifiers[0].substring(filteredIdentifiers[0].indexOf(':') + 1));
5 | }
6 | };
7 |
8 | export const getDiscordId = (ids) => {
9 | const filteredIdentifiers = ids.filter((identifier) => identifier.startsWith('discord:'));
10 | if (filteredIdentifiers.length > 0) {
11 | return filteredIdentifiers[0].substring(filteredIdentifiers[0].indexOf(':') + 1);
12 | }
13 | };
14 |
15 | export const hexToDecimal = (s) => {
16 | var i,
17 | j,
18 | digits = [0],
19 | carry;
20 | for (i = 0; i < s.length; i += 1) {
21 | carry = parseInt(s.charAt(i), 16);
22 | for (j = 0; j < digits.length; j += 1) {
23 | digits[j] = digits[j] * 16 + carry;
24 | carry = (digits[j] / 10) | 0;
25 | digits[j] %= 10;
26 | }
27 | while (carry > 0) {
28 | digits.push(carry % 10);
29 | carry = (carry / 10) | 0;
30 | }
31 | }
32 | return digits.reverse().join('');
33 | };
34 |
--------------------------------------------------------------------------------