├── .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 | ![image](https://github.com/igorovh/fivem-player-list/assets/37638480/cc4427f2-9fb0-4a9a-822b-db3344845b21) 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/empty-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/question-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /img/steam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Waiting for Server ID... 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | Server Icon 16 | Waiting for Server ID... 17 | 18 |
19 |
20 | 21 |
22 | Search 23 |
24 |
25 | 29 | Search 30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 | Search 38 | 30s 39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 |
No.IdNameSocialsPing
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 = `Add to Favorites`; 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 = 'Steam'; 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 = 'Discord'; 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 | 160 | 161 | This page is not affiliated with FiveM or any other server.
162 | Created by igorovh. 163 | 164 | `; 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 | --------------------------------------------------------------------------------