diff --git a/index.js b/index.js new file mode 100644 index 0000000..341448f --- /dev/null +++ b/index.js @@ -0,0 +1,116 @@ +import express from "express"; +import dotenv from "dotenv"; +import path from "path"; + +const usernames = [ + {name: "rubylium__", status: "off", id: ""}, + {name: "lukkstv", status: "off", id: ""}, + {name: "havvenfr", status: "off", id: ""}, + {name: "olmanntv", status: "off", id: ""}, + {name: "nina_asmr", status: "off", id: ""}, + {name: "krw_aka", status: "off", id: ""}, + {name: "leetgamer54", status: "off", id: ""}, + {name: "coshiho", status: "off", id: ""}, + {name: "soramicia", status: "off", id: ""}, + {name: "entocraft", status: "off", id: ""}, + {name: "steakfritees_", status: "off", id: ""}, + {name: "dancyy_", status: "off", id: ""}, + {name: "tchoupyy_", status: "off", id: ""}, + {name: "thesollann", status: "off", id: ""}, +]; + +dotenv.config(); + +const app = express(); + +const port = process.env.PORT || 3000; + +let token = ""; +let config = {}; + +const checkEnvVars = () => { + const requiredEnvVars = ["CLIENT_ID", "CLIENT_SECRET"]; + let error = false; + requiredEnvVars.forEach((envVar) => { + if (process.env[envVar] === undefined) { + error = true; + console.log(`${envVar} is undefined`); + } else { + config[envVar] = process.env[envVar]; + } + }); + if (error) { + process.exit(1); + } +}; + +checkEnvVars(); + +function init() { + const url = `https://id.twitch.tv/oauth2/token?client_id=${config.CLIENT_ID}&client_secret=${config.CLIENT_SECRET}&grant_type=client_credentials`; + + fetch(url, { method: "POST" }) + .then((response) => response.json()) + .then((data) => { + token = data.access_token; + getAllUsersStatus(); + setInterval(getAllUsersStatus, 60000); + }); +} + +init(); + +async function getAllUsersStatus() { + for (let i = 0; i < usernames.length; i++) { + const userData = await getUserStatus(usernames[i].name, usernames[i].id); + usernames[i] = { ...usernames[i], ...userData }; + if (i === usernames.length - 1) { + console.log("Successfully updated all users"); + } + } +} + +app.use("/api/status", api); + +function api(req, res, next) { + res.json(usernames); +} + +async function getUserStatus(username, id) { + const userData = {}; + if (id === "") { + const userUrl = `https://api.twitch.tv/helix/users?login=${username}`; + const response = await fetch(userUrl, { + headers: { + Authorization: `Bearer ${token}`, + "Client-ID": config.CLIENT_ID, + }, + }); + const data = await response.json(); + userData.id = data.data[0].id; + userData.profilePicture = data.data[0].profile_image_url; + id = data.data[0].id; + } + const streamUrl = `https://api.twitch.tv/helix/streams?user_id=${id}`; + const streamResponse = await fetch(streamUrl, { + headers: { + Authorization: `Bearer ${token}`, + "Client-ID": config.CLIENT_ID, + }, + }); + const streamData = await streamResponse.json(); + if (streamData.data.length > 0) { + userData.status = "live"; + } else { + userData.status = "offline"; + } + return userData; +} + +const staticDist = express.static(path.join(path.resolve(), "./public")); + +app.use(staticDist); + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..94cfebc --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "donatien", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js" + }, + "entry": "index.js", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.21.1" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..82843e4 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,485 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + express: + specifier: ^4.21.1 + version: 4.21.1 + +packages: + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.21.1: + resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.10 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false diff --git a/public/images/.DS_Store b/public/images/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/public/images/.DS_Store differ diff --git a/public/images/favicon.png b/public/images/favicon.png new file mode 100644 index 0000000..db18558 Binary files /dev/null and b/public/images/favicon.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..93de5ed --- /dev/null +++ b/public/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + interface dnth + + + + +
+ +
+ +
+ +
+
+

En ligne

+ +
+ + Le premier chargement peut être long... +
+
+

Hors ligne

+
+ +
+
+
+ +
+ + + + + + + + \ No newline at end of file diff --git a/public/scripts/infos.js b/public/scripts/infos.js new file mode 100644 index 0000000..11dee9d --- /dev/null +++ b/public/scripts/infos.js @@ -0,0 +1,245 @@ +// Fonction pour obtenir l'access token Twitch +async function getAccessToken() { + const clientId = 'z2tcrq9oeynzf4ijgzhv6br5feoprr'; + const clientSecret = 'ybm8e512lrsylqqkl78ow08nv3jgx9'; + const url = `https://id.twitch.tv/oauth2/token?client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`; + + const response = await fetch(url, { method: 'POST' }); + const data = await response.json(); + return data.access_token; +} + +// Fonction pour obtenir les informations utilisateur et le statut du stream +async function getUserInfoAndStreamStatus(username, accessToken, clientId) { + const userUrl = `https://api.twitch.tv/helix/users?login=${username}`; + const userResponse = await fetch(userUrl, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Client-ID': clientId, + }, + }); + + if (userResponse.status !== 200) { + console.error('Erreur lors de la récupération des informations utilisateur.'); + return null; + } + + const userData = await userResponse.json(); + if (userData.data.length === 0) { + console.error('Utilisateur non trouvé.'); + return null; + } + + const userId = userData.data[0].id; + const profileImageUrl = userData.data[0].profile_image_url; + const displayName = userData.data[0].display_name; + + const streamUrl = `https://api.twitch.tv/helix/streams?user_id=${userId}`; + const streamResponse = await fetch(streamUrl, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Client-ID': clientId, + }, + }); + + const streamData = await streamResponse.json(); + let streamType = 'offline'; + + if (streamData.data.length > 0) { + streamType = streamData.data[0].type; + } + + return { + profileImageUrl, + displayName, + streamType, + }; +} + +// Lire le fichier streamers.json depuis le localStorage +function readStreamersFile() { + const fileContent = localStorage.getItem('streamers'); + return fileContent ? JSON.parse(fileContent) : { streamers: [] }; +} + +// Sauvegarder dans streamers.json dans le localStorage +function saveStreamersFile(streamersData) { + localStorage.setItem('streamers', JSON.stringify(streamersData, null, 2)); +} + +// Vérifier et ajouter les streamers manquants +async function updateMissingStreamers(usernames) { + let streamersData = readStreamersFile(); + + const currentStreamers = streamersData.streamers.map(streamer => streamer.username); + + const missingUsernames = usernames.filter(username => !currentStreamers.includes(username)); + + if (missingUsernames.length === 0) { + console.log("Tous les streamers sont déjà présents."); + return; + } + + console.log(`Les streamers suivants sont manquants: ${missingUsernames.join(', ')}`); + + const accessToken = await getAccessToken(); + const clientId = 'z2tcrq9oeynzf4ijgzhv6br5feoprr'; + + for (const username of missingUsernames) { + const data = await getUserInfoAndStreamStatus(username, accessToken, clientId); + if (data) { + streamersData.streamers.push({ + username: username, + displayName: data.displayName, + profileImageUrl: data.profileImageUrl, + streamType: data.streamType + }); + } + } + + saveStreamersFile(streamersData); + console.log("Les streamers manquants ont été ajoutés."); +} + +// Fonction pour afficher les streamers +async function displayStreamers(usernames) { + const accessToken = await getAccessToken(); + const clientId = 'z2tcrq9oeynzf4ijgzhv6br5feoprr'; + + const streamersData = readStreamersFile(); + const onlineStreamers = []; + const offlineStreamers = []; + + for (const username of usernames) { + const streamerInfo = streamersData.streamers.find(streamer => streamer.username === username); + + if (streamerInfo && streamerInfo.streamType === 'live') { + onlineStreamers.push(streamerInfo); + } else { + offlineStreamers.push(streamerInfo); + } + } + + // Trier les streamers par nom + onlineStreamers.sort((a, b) => a.displayName.localeCompare(b.displayName)); + offlineStreamers.sort((a, b) => a.displayName.localeCompare(b.displayName)); + + // Mettre à jour le nombre de streamers en ligne et hors ligne + document.getElementById('onlineCount').textContent = onlineStreamers.length; + document.getElementById('offlineCount').textContent = offlineStreamers.length; + + const onlineContainer = document.getElementById('onlineStreamers'); + const offlineContainer = document.getElementById('offlineStreamers'); + + // Réinitialiser les conteneurs + onlineContainer.innerHTML = ''; + offlineContainer.innerHTML = ''; + + if (onlineStreamers.length > 0) { + // Supprimer le texte de chargement + const loaderText = document.getElementById('loadertext'); + if (loaderText) { + loaderText.style.display = 'none'; + } + } else { + // Ajouter le texte de chargement si aucun streamer en ligne + const loaderText = document.getElementById('loadertext'); + if (loaderText) { + loaderText.textContent = "Il n'y a aucun streamer en ligne actuellement"; + loaderText.style.display = 'block'; + } + } + + onlineStreamers.forEach(data => { + const profileLink = `https://twitch.tv/${data.displayName}`; + + const image = document.createElement('img'); + image.src = data.profileImageUrl; + image.alt = data.displayName; + image.className = 'streamer-image'; + + const liveIndicator = document.createElement('div'); + liveIndicator.className = 'live-indicator'; + liveIndicator.style.display = 'block'; + + const name = document.createElement('div'); + name.className = 'streamer-name'; + name.textContent = data.displayName; + + const link = document.createElement('a'); + link.href = profileLink; + link.target = '_blank'; + link.appendChild(image); + link.appendChild(liveIndicator); + + const infoDiv = document.createElement('div'); + infoDiv.className = 'streamer-info'; + infoDiv.appendChild(link); + infoDiv.appendChild(name); + + onlineContainer.appendChild(infoDiv); + }); + + // Réinitialiser et remplir le conteneur des streamers hors ligne + if (offlineStreamers.length === 0) { + // Ajouter le texte de chargement si aucun streamer hors ligne + const loaderText = document.getElementById('loadertext'); + if (loaderText) { + loaderText.textContent = "Il n'y a aucun streamer hors ligne actuellement"; + loaderText.style.display = 'block'; + } + } else { + // Supprimer le texte de chargement + const loaderText = document.getElementById('loadertext'); + if (loaderText) { + loaderText.style.display = 'none'; + } + } + + offlineStreamers.forEach(data => { + const profileLink = `https://twitch.tv/${data.displayName}`; + + const image = document.createElement('img'); + image.src = data.profileImageUrl; + image.alt = data.displayName; + image.className = 'streamer-image'; + image.classList.toggle('offline', data.streamType !== 'live'); + + const liveIndicator = document.createElement('div'); + liveIndicator.className = 'live-indicator'; + liveIndicator.style.display = 'none'; + + const name = document.createElement('div'); + name.className = 'streamer-name'; + name.textContent = data.displayName; + + const link = document.createElement('a'); + link.href = profileLink; + link.target = '_blank'; + link.appendChild(image); + link.appendChild(liveIndicator); + + const infoDiv = document.createElement('div'); + infoDiv.className = 'streamer-info'; + infoDiv.appendChild(link); + infoDiv.appendChild(name); + + offlineContainer.appendChild(infoDiv); + }); + + if (onlineStreamers.length > 0) { + // Supprimer le texte "aucun streamer en ligne" et le texte de chargement + const loaderText = document.getElementById('onlineLoaderText'); + if (loaderText) { + loaderText.style.display = 'none'; + } + } else { + // Ajouter le texte "aucun streamer en ligne" + const loaderText = document.getElementById('onlineLoaderText'); + if (loaderText) { + loaderText.textContent = "Il n'y a aucun streamer en ligne actuellement"; + loaderText.style.display = 'block'; // Assurez-vous que le texte est visible + } + } + +} diff --git a/public/scripts/twitch.js b/public/scripts/twitch.js new file mode 100644 index 0000000..dc8625a --- /dev/null +++ b/public/scripts/twitch.js @@ -0,0 +1,11 @@ +console.log("twitch.js chargé"); + +const usernames = ['rubylium__', 'lukkstv', 'havvenfr', 'olmanntv', 'nina_asmr', 'krw_aka', 'leetgamer54', 'coshiho', 'soramicia', 'entocraft', 'steakfritees_', 'dancyy_', 'tchoupyy_', 'thesollann']; + +(async function() { + // Vérifier et ajouter les streamers manquants dans streamers.json + await updateMissingStreamers(usernames); + + // Une fois que tout est à jour, afficher les streamers + displayStreamers(usernames); +})(); diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..7c1987e --- /dev/null +++ b/public/styles.css @@ -0,0 +1,207 @@ +/* work-sans-regular - latin */ +@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap'); +.atkinson-hyperlegible-regular { + font-family: "Atkinson Hyperlegible", sans-serif; + font-weight: 400; + font-style: normal; + } + +.atkinson-hyperlegible-bold { + font-family: "Atkinson Hyperlegible", sans-serif; + font-weight: 700; + font-style: normal; + } + +.atkinson-hyperlegible-regular-italic { + font-family: "Atkinson Hyperlegible", sans-serif; + font-weight: 400; + font-style: italic; + } + +.atkinson-hyperlegible-bold-italic { + font-family: "Atkinson Hyperlegible", sans-serif; + font-weight: 700; + font-style: italic; + } +/* Contenu principal */ +/* Contenu principal */ +/* Contenu principal */ + +body { + background: rgb(29, 25, 27); + color: rgba(161, 208, 252); + font-family: "Atkinson Hyperlegible"; + width: 100%; + margin: 0; +} + +html { + width: 100%; + justify-content: center; +} + +/* Catégories */ +/* Catégories */ +/* Catégories */ + +h1 { + margin-top: 0; + margin-bottom: 1rem; + font-weight: bold; + font-style: italic; + font-size: 3rem; +} + +h2 { + margin-bottom: 0.5rem; +} + +h2, .small { + display: inline; + margin: 0; /* Supprime les marges par défaut */ +} + +.small { + margin-bottom: 0.5rem; + font-style: italic; + color: rgba(161, 208, 252, 0.49); +} + +strong { + font-weight: bold; +} + +/* Throbber */ +/* HTML:
*/ +/* HTML:
*/ +.loadertext { + width: 140px; + font-style: italic; + color: rgba(161, 208, 252, 0.49); +} + +#loadertext { + margin-left: 20px; + width: 140px; + font-style: italic; + color: rgba(161, 208, 252, 0.49); +} + +.loader { + width: 60px; + display: flex; + align-items: flex-start; + aspect-ratio: 1; +} +.loader:before, +.loader:after { + content: ""; + flex: 1; + aspect-ratio: 1; + --g: conic-gradient(from -90deg at 10px 10px, #a1d0fc 90deg,#0000 0); + background: var(--g), var(--g), var(--g); + filter: drop-shadow(30px 30px 0 #A1D0FC); + animation: l20 1s infinite; +} +.loader:after { + transform: scaleX(-1); +} +@keyframes l20 { + 0% {background-position:0 0, 10px 10px, 20px 20px} + 33% {background-position:10px 10px} + 66% {background-position:0 20px,10px 10px,20px 0 } + 100% {background-position:0 0, 10px 10px, 20px 20px} +} + +/* Contenu */ +/* Contenu */ +/* Contenu */ + +main { + display: flex; + justify-content: center; +} + +#streamersContainer { + margin: 4rem; + width: 50%; + padding: 1rem; + border: 1px solid #A1D0FC; + border-radius: 0.5rem; + box-sizing: border-box; +} + +.streamers-container { + margin-right: 1rem; + display: flex; + flex-direction: column; + gap: 20px; +} + +.streamers-category { + display: flex; + flex-wrap: wrap; + gap: 10px; /* Espace entre les images */ +} + +.streamer-info { + position: relative; + text-align: center; + width: 60px; /* Assurez-vous que la largeur est fixe */ + margin: 0; /* Supprime les marges éventuelles */ +} + +.streamer-image { + width: 60px; + height: 60px; + border-radius: 50%; /* Pour une image ronde */ + transition: filter 0.3s ease, transform 0.3s ease; + justify-content: center; /* Centre les images horizontalement */ + transform: scale(1); /* Échelle normale par défaut */ + z-index: 1; /* Assure que l'image est au-dessus des autres éléments */ +} + +.streamer-info:hover .streamer-image { + filter: grayscale(0%); /* Réduit le niveaux de gris à 0% lors du survol */ + transform: scale(1.2); /* Zoom de 10% sur l'image lors du survol */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); /* Optionnel : ajout d'une ombre portée pour plus d'effet */ +} + +.offline:hover { + filter: grayscale(0%); /* Réduit le niveaux de gris à 0% lors du survol */ +} + +.offline { + filter: grayscale(100%); +} + +.live-indicator { + position: absolute; + top: 0; + right: 0; + width: 7px; + height: 7px; + background-color: red; + border-radius: 50%; + border: 0.25rem solid rgb(29, 25, 27); +} + +.streamer-name { + z-index: 999; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.7); + color: white; + padding: 5px; + border-radius: 99999px; + opacity: 0; + transition: opacity 0.3s ease; + white-space: nowrap; + pointer-events: none; +} + +.streamer-info:hover .streamer-name { + opacity: 1; +}