moved api from monorepo

This commit is contained in:
John O'Keefe 2024-09-12 15:48:27 -04:00
parent 734bb0a0d2
commit d575a4efc5
29 changed files with 2904 additions and 0 deletions

960
_data/games.json Normal file
View File

@ -0,0 +1,960 @@
[
{
"os": {
"windows": true,
"mac": false,
"linux": false
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Windows 7 x64<br></li><li><strong>Processor:</strong> Intel Core i5-3470 (4 * 3200) or equivalent<br></li><li><strong>Memory:</strong> 6 GB RAM<br></li><li><strong>Graphics:</strong> GeForce GTX 960 (4096 VRAM)<br></li><li><strong>Storage:</strong> 12 GB available space</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Windows 10 x64<br></li><li><strong>Processor:</strong> Intel Core i7-6700 (8 * 3400) or equivalent<br></li><li><strong>Memory:</strong> 12 GB RAM<br></li><li><strong>Graphics:</strong> GeForce RTX 2060 Super (6144VRAM)<br></li><li><strong>Storage:</strong> 12 GB available space</li></ul>"
},
"mac": {
"minimum": "",
"recommended": ""
},
"linux": {
"minimum": "",
"recommended": ""
}
},
"_id": "6143fb8c9d57ff50939bef40",
"title": "I Am Fish",
"series": "",
"steamId": "1472560",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/header.jpg?t=1631811463",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_448c5e50d0f663660dceb92279f0480b1a55806b.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_448c5e50d0f663660dceb92279f0480b1a55806b.600x338.jpg?t=1631811463"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_30de2e5d50bcb4a0460f5ef6858019665b9bc8d9.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_30de2e5d50bcb4a0460f5ef6858019665b9bc8d9.600x338.jpg?t=1631811463"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_0480024d03a454dbd12178b35b5b10e8619c83e1.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_0480024d03a454dbd12178b35b5b10e8619c83e1.600x338.jpg?t=1631811463"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_1afa229bd43a56ddc114983fb6e2c824f1dd3c3d.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_1afa229bd43a56ddc114983fb6e2c824f1dd3c3d.600x338.jpg?t=1631811463"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_839c9b1028afb8226febb1e258260d3f92c7e125.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_839c9b1028afb8226febb1e258260d3f92c7e125.600x338.jpg?t=1631811463"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_fd9aa604a0c61a0ed5f86da7e1ca70e72f27aa6b.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_fd9aa604a0c61a0ed5f86da7e1ca70e72f27aa6b.600x338.jpg?t=1631811463"
},
{
"id": 6,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_858e3528167517a8b2f49d9efd1691b2ee0c6681.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_858e3528167517a8b2f49d9efd1691b2ee0c6681.600x338.jpg?t=1631811463"
},
{
"id": 7,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_4be849859bec3bcdf49bd6af15f4036db0a5e8cb.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_4be849859bec3bcdf49bd6af15f4036db0a5e8cb.600x338.jpg?t=1631811463"
},
{
"id": 8,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_ba1ad6a5f9ae0625c8d65d5d59eaecfaeed00ffa.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_ba1ad6a5f9ae0625c8d65d5d59eaecfaeed00ffa.600x338.jpg?t=1631811463"
},
{
"id": 9,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_043e2dfcedd39c3d2a46526054036bc406c275bb.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_043e2dfcedd39c3d2a46526054036bc406c275bb.600x338.jpg?t=1631811463"
},
{
"id": 10,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_1d6e4efe8e674ec1bcc0cea7f2781b0e8ee106f7.1920x1080.jpg?t=1631811463",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1472560/ss_1d6e4efe8e674ec1bcc0cea7f2781b0e8ee106f7.600x338.jpg?t=1631811463"
}
],
"genre": [
"Adventure",
"Simulation"
],
"store": [
"Steam"
],
"playStatus": "Never Played",
"wine": "Not Tested",
"controller": "Full Controller Support",
"developer": [
"Bossa Studios"
],
"publisher": [
"Curve Digital"
],
"releaseDate": "Sep 16, 2021",
"shortDesc": "I Am Fish is a charming, physics-based adventure starring four intrepid fish friends, forcibly separated from their home in a pet shop fish tank. Swim, fly, roll and chomp your way to the open ocean in a bid for freedom and to re-unite once again.",
"reviews": "“One of the most unique, compelling titles Ive had the luxury of playing recently... The best game that Bossa Studios has made to date... I really hope theres a sequel.”<br>8.5 <a href=\"https://steamcommunity.com/linkfilter/?url=https://www.pcinvasion.com/i-am-fish-review/\" target=\"_blank\" rel=\"noopener\" >PC Invasion</a><br><br>“Immediately fun... One of those games that anyone can play... Home to some genius moments that you won't want to miss.”<br><a href=\"https://www.rockpapershotgun.com/i-am-fish-review\" target=\"_blank\" rel=\"noreferrer\" >Rock Paper Shotgun</a><br><br>“I really enjoyed it... I cried tears of joy... Kept me laughing and shaking my head in disbelief.”<br>7.0 <a href=\"https://www.ign.com/articles/i-am-fish-review\" target=\"_blank\" rel=\"noreferrer\" >IGN</a><br>",
"summary": "<img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/Goldfish_GIF_9.gif?t=1631811463\" /><br><br>I Am Fish is a charming, physics-based adventure starring four intrepid fish friends, forcibly separated from their home in a pet shop fish tank. Over the course of the game you join them as they swim, fly, roll and chomp their way to the open ocean from the far-flung corners of Barnardshire (the smallest county in England) in their bid for freedom and to re-unite once again.<br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/Piranha_GIF_2.gif?t=1631811463\" /><br><br><strong>FINTASTIC FRIENDS</strong><br>Meet our heroes! Goldfish - cheerful, brave, and adventurous, a natural born swimmer! Pufferfish a little slow but kind-hearted who can also puff up into a ball and roll across land. Piranha - wild, chaotic, loud, unpredictable, and loves to bite - obviously. Flying Fish a little aloof at times but a real softy at heart, with the ability to glide through the air! These plucky heroes will leave no bowl unturned, putting their heart and shoal into the mission to re-unite!<br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/All_Fish_GIF_8.gif?t=1631811463\" /><br><br><strong>NO NEED TO BE A BRAIN STURGEON</strong><br>Swim, roll, glide, chomp, flip flop, inflate, fly and bite your way through enthralling challenges. A simple, intuitive control scheme leaves no excuses should your fish perish leaving you feeling very gill-ty. <br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/Flying_GIF_4.gif?t=1631811463\" /><br><br><strong>THINK OUTSIDE THE BOWL</strong><br>With its idyllic coastline and quaint villages Barnardshire might seem like a perfect slice of quintessential tranquility, but to our aquatic adventurers its alive with the very real threats presented by crossing roads, traversing rooftops, dodging deep fat fryers, avoiding wildlife including the cantankerous locals, not to mention fragile fishbowls and the very real problem that fish cant breathe out of water. <br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/Goldfish_GIF_3.gif?t=1631811463\" /><br><br><strong>LEAVE NO FISH BEHIND</strong><br>We wont dwell on grizzly fish death, but should your fish fall too far in whatever make-shift fishbowl it finds itself in, run out of air or generally flounder you will be popped back to the most recent checkpoint to refine your approach.<br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/Puffer_GIF_1.gif?t=1631811463\" /><br><br><strong>SWIM TO FREEDOM</strong><br>Our four fin-tastic friends will be hitching rides in all manner of ad hoc water carriers including jars, mop buckets on wheels and the occasional pint glass. Cheers! Navigating in each make-shift aquatic vehicle poses its own unique challenge, but dont worry, the relative freedom of open water is never too far away with inviting fountains, swimming pools and err, sewers full of hazardous materials to splash around in, before reaching the final goal the shimmering, open ocean. <br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/Swim_to_freedom_2.gif?t=1631811463\" /><br><br><strong>BE CAREFUL YOU DONT FLOUNDER</strong><br>Whilst I Am Fish may look like a piece of hake to play, actshoally the controls are deliberately designed to be challenging and with an additional layer of added Bossa Style controls, it wont be like shooting fish in a barrel. Even if you are a dab-handed player, you might find yourself in deep waters - we dare you to dive in!<br><br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1472560/extras/WDC_IAMFISH.jpg?t=1631811463\" /><br><br><strong>Whale and Dolphin Conservation</strong><br>WDC is the leading global charity dedicated to the protection of whales and dolphins. With a vision of a world where every whale and dolphin is safe and free, WDC fights for the survival of these beautiful creatures by ending captivity, stopping whaling, preventing deaths in nets and creating healthy seas. The equivalent of £0.125 GBP from each copy will be donated to WDC, Whale and Dolphin Conservation. UK registered charity no. 1014705.",
"soundtrack": "No",
"intel": "No",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "6143fb7244277443078d7d44",
"soundtrack": "No",
"store": [
"Steam"
],
"playStatus": "Never Played",
"rating": 5
}
],
"createDate": "2021-09-17T02:21:01.667Z",
"lastUpdateDate": "2021-09-17T02:21:01.667Z"
},
{
"os": {
"windows": true,
"mac": false,
"linux": false
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 8.1 and 10<br></li><li><strong>Processor:</strong> Intel Core i5-8265U<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> Intel UHD 620<br></li><li><strong>DirectX:</strong> Version 11<br></li><li><strong>Storage:</strong> 2 GB available space</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 8.1 and 10<br></li><li><strong>Processor:</strong> Intel Core i5-6600K @ 3.50GHz<br></li><li><strong>Memory:</strong> 16 GB RAM<br></li><li><strong>Graphics:</strong> NVIDIA GeForce GTX 970<br></li><li><strong>DirectX:</strong> Version 11<br></li><li><strong>Storage:</strong> 2 GB available space</li></ul>"
},
"mac": {
"minimum": "",
"recommended": ""
},
"linux": {
"minimum": "",
"recommended": ""
}
},
"_id": "6143fba39d57ff50939bef41",
"title": "STORY OF SEASONS: Pioneers of Olive Town",
"series": "",
"steamId": "1392960",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/header.jpg?t=1631749858",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_2f4235399f9b09f648e5cb4e617c12c0a445a550.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_2f4235399f9b09f648e5cb4e617c12c0a445a550.600x338.jpg?t=1631749858"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_444b300288436ec1276c0afebbe3ba0166edc52d.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_444b300288436ec1276c0afebbe3ba0166edc52d.600x338.jpg?t=1631749858"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_ee1ccb62083c6b90f115640e450f5d46a32c86d7.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_ee1ccb62083c6b90f115640e450f5d46a32c86d7.600x338.jpg?t=1631749858"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_7bbbfdac759ab21c20dbbff4f46e84d396e06664.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_7bbbfdac759ab21c20dbbff4f46e84d396e06664.600x338.jpg?t=1631749858"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_85f92a0ba308253a767b9f24744cc5969e270816.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_85f92a0ba308253a767b9f24744cc5969e270816.600x338.jpg?t=1631749858"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_0118d0ecbd8aa7d3c31eb18067d6af46e429b8f4.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_0118d0ecbd8aa7d3c31eb18067d6af46e429b8f4.600x338.jpg?t=1631749858"
},
{
"id": 6,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_fbcbaaf87cce9490a525eef27bd7be8a81163024.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_fbcbaaf87cce9490a525eef27bd7be8a81163024.600x338.jpg?t=1631749858"
},
{
"id": 7,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_efd758b3a731a7c15d1a9bc960babc6b3b30c2cb.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_efd758b3a731a7c15d1a9bc960babc6b3b30c2cb.600x338.jpg?t=1631749858"
},
{
"id": 8,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_95688d934cd8d8b5dff0dcbe957fb7b98b81fb08.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_95688d934cd8d8b5dff0dcbe957fb7b98b81fb08.600x338.jpg?t=1631749858"
},
{
"id": 9,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_90532bfbfa8568ead58d2787dc3a66b294ec57a8.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_90532bfbfa8568ead58d2787dc3a66b294ec57a8.600x338.jpg?t=1631749858"
},
{
"id": 10,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_447461dc39cbce0e1f16760948d4408cf658dc79.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_447461dc39cbce0e1f16760948d4408cf658dc79.600x338.jpg?t=1631749858"
},
{
"id": 11,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_3c3837973b5beded3bd9c8ad1b0ff200057943fd.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_3c3837973b5beded3bd9c8ad1b0ff200057943fd.600x338.jpg?t=1631749858"
},
{
"id": 12,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_74c9e3b5a52344ee6f28c624410043d11b00c22f.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_74c9e3b5a52344ee6f28c624410043d11b00c22f.600x338.jpg?t=1631749858"
},
{
"id": 13,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_7eee35894df1d32db1ace312578d9a8190570c55.1920x1080.jpg?t=1631749858",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1392960/ss_7eee35894df1d32db1ace312578d9a8190570c55.600x338.jpg?t=1631749858"
}
],
"genre": [
"Casual",
"RPG",
"Simulation"
],
"wine": "Not Tested",
"controller": "Partial Controller Support",
"developer": [
"Marvelous Inc."
],
"publisher": [
"XSEED Games",
"Marvelous USA, Inc.",
"Marvelous"
],
"releaseDate": "Sep 15, 2021",
"shortDesc": "Welcome to Olive Town, a peaceful community established by your trailblazing grandfather and his friends. Now that you've taken over his farm, it's your job to carry on his legacy. Plant crops, raise animals, build relationships, and get to know the residents of your new home!",
"reviews": "",
"summary": "<img src=\"https://cdn.akamai.steamstatic.com/steam/apps/1392960/extras/PoOT-Store-Watering.gif?t=1631749858\" /><br><br><i><h2 class=\"bb_tag\">Build Your Farm From the Ground Up!</h2></i><br>Welcome to Olive Town, a peaceful community established by your trailblazing grandfather and his friends. Now that you've taken over his farm, it's your job to carry on his legacy.<br><br>Plant crops, raise animals, build relationships, and get to know the residents of your new home in this brand-new entry in the STORY OF SEASONS series!<br><br><img src=\"https://cdn.akamai.steamstatic.com/steam/apps/1392960/extras/PoOT-Store-Fishing.gif?t=1631749858\" /><br><br><strong>Cultivate Your Farm, Cultivate Your Town</strong><br>Tame the wilderness and build your farm from the ground up! Gather and process materials to fulfill requests and improve Olive Town's infrastructure, upgrade tools, or commission new outfits and accessories.<br><br><strong>A Farm of Endless Possibilities</strong><br>Clear the land, repair old facilities, and place new ones wherever you see fit. Level up your farming skills and craft a variety of decorations and facilities, from fences and automatic feeders for livestock to sprinklers for crops!<br><br><strong>New Adventures Off the Beaten Path</strong><br>Finding Earth Sprites while exploring your farmland may lead you to mysterious, fantastical lands such as gardens where the seasons never change, an island in the sky, or even the inside of a volcano!<br><br><strong>There's Always Something Going on in Olive Town!</strong><br>Participate in local festivals and watch the town come to life! Get to know your neighbors better with over 200 unique events, and you may even find love with a special someone!<br><br><img src=\"https://cdn.akamai.steamstatic.com/steam/apps/1392960/extras/PoOT-Store-DogEvent.gif?t=1631749858\" />",
"intel": "No",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "6143fb7244277443078d7d44",
"soundtrack": "No",
"store": [
"Steam"
],
"playStatus": "Never Played"
}
],
"createDate": "2021-09-17T02:21:23.962Z",
"lastUpdateDate": "2021-09-17T02:21:23.962Z"
},
{
"os": {
"windows": true,
"mac": false,
"linux": false
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 7<br></li><li><strong>Processor:</strong> Intel Core i3-540 | AMD FX-4350<br></li><li><strong>Memory:</strong> 4 GB RAM<br></li><li><strong>Graphics:</strong> Nvidia GeForce GTX 650, 1 GB | AMD Radeon HD 7870, 2 GB<br></li><li><strong>DirectX:</strong> Version 9.0c<br></li><li><strong>Storage:</strong> 7 GB available space</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 10<br></li><li><strong>Processor:</strong> Intel Core i5-6600 | AMD FX-8350<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> Nvidia GeForce GTX 1070, 8 GB | AMD Radeon RX 580, 4 GB<br></li><li><strong>DirectX:</strong> Version 11<br></li><li><strong>Storage:</strong> 7 GB available space</li></ul>"
},
"mac": {
"minimum": "",
"recommended": ""
},
"linux": {
"minimum": "",
"recommended": ""
}
},
"_id": "6143fbb19d57ff50939bef42",
"title": "The Artful Escape",
"series": "",
"steamId": "1122680",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/header.jpg?t=1631203144",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_ddbd9919d189a909af3cc874714b8f01a6872da8.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_ddbd9919d189a909af3cc874714b8f01a6872da8.600x338.jpg?t=1631203144"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_8ba7c3cc7b4a2210b3c4a823b9b956427ddb6e51.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_8ba7c3cc7b4a2210b3c4a823b9b956427ddb6e51.600x338.jpg?t=1631203144"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_96f03593dadecbc8a63b59bd667d3b6fab8ea7fb.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_96f03593dadecbc8a63b59bd667d3b6fab8ea7fb.600x338.jpg?t=1631203144"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_ab8d9cc3efe4d51ac4387feca4cd856869c2b65e.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_ab8d9cc3efe4d51ac4387feca4cd856869c2b65e.600x338.jpg?t=1631203144"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_999deed56165d386911ce17c520da9d2355ca6d7.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_999deed56165d386911ce17c520da9d2355ca6d7.600x338.jpg?t=1631203144"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_6f06e1d46d986e1601a3dc214273955d9d754b07.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_6f06e1d46d986e1601a3dc214273955d9d754b07.600x338.jpg?t=1631203144"
},
{
"id": 6,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_d0550df3b2b649fff4f74203ec389a76ea9eca05.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_d0550df3b2b649fff4f74203ec389a76ea9eca05.600x338.jpg?t=1631203144"
},
{
"id": 7,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_9972926970943a7679fb3fe278e1a0abfd044651.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_9972926970943a7679fb3fe278e1a0abfd044651.600x338.jpg?t=1631203144"
},
{
"id": 8,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_c96f8ade705102a89f019bb7b9c8ff248af5cb8a.1920x1080.jpg?t=1631203144",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1122680/ss_c96f8ade705102a89f019bb7b9c8ff248af5cb8a.600x338.jpg?t=1631203144"
}
],
"genre": [
"Action",
"Adventure",
"Indie"
],
"wine": "Not Tested",
"controller": "Full Controller Support",
"developer": [
"Beethoven and Dinosaur"
],
"publisher": [
"Annapurna Interactive"
],
"releaseDate": "Sep 9, 2021",
"shortDesc": "A teenage guitar prodigy sets out on a psychedelic journey to inspire his stage persona and confront the legacy of a dead folk legend. Starring voice performances by Michael Johnston, Caroline Kinley, Lena Headey, Jason Schwartzman, Mark Strong, and Carl Weathers.",
"reviews": "",
"summary": "On the eve of his first performance, Francis Vendetti struggles with the legacy of a dead folk legend and the cosmic wanderings of his own imagination.<br><br>In an attempt to escape the musical legacy of his uncle, a teenage guitar prodigy embarks on a psychedelic journey to inspire his new stage persona, searching for who he isnt in an adventure spanning stolen opera houses, melodic alien landscapes, and the impossible depths of the <strong><i>Cosmic Extraordinary</i></strong>.<br><br>Starring voice performances by Michael Johnston, Caroline Kinley, Lena Headey, Jason Schwartzman, Mark Strong, and Carl Weathers.<br><br><strong>KEY FEATURES:</strong><br>* A story about great expectations, towering legacies, aliens, folk music, guitar solos, making stuff up, and living your dreams like memories.<br>* Musical jams. Theyre visceral. They traverse dimensions.<br>* Craft your own stage persona from the sci-fi beginnings of your backstory to the trim on your moonboots.<br>* Converse, consult and chill with all manner of beings: disenchanted publicans, nostalgic villagers, lumbering alien wildlife, and reality-defying behemoths.<br>* Shred, soar and dance across the multiverse. Traverse landscapes made of sound -- composed by your movement -- as if the world itself were an instrument.",
"intel": "No",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "6143fb7244277443078d7d44",
"soundtrack": "No",
"store": [
"Steam"
],
"playStatus": "Never Played"
}
],
"createDate": "2021-09-17T02:21:38.590Z",
"lastUpdateDate": "2021-09-17T02:21:38.590Z"
},
{
"os": {
"windows": true,
"mac": true,
"linux": true
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Windows Vista<br></li><li><strong>Processor:</strong> Dual Core 2.0GHz or equivalent<br></li><li><strong>Memory:</strong> 2 GB RAM<br></li><li><strong>Graphics:</strong> ATI or NVidia card w/ 512 MB RAM (not recommended for Intel HD Graphics cards)<br></li><li><strong>DirectX:</strong> Version 9.0<br></li><li><strong>Storage:</strong> 5 GB available space</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Windows 7<br></li><li><strong>Processor:</strong> Dual Core 3.0GHz or equivalent<br></li><li><strong>Memory:</strong> 2 GB RAM<br></li><li><strong>Graphics:</strong> ATI or NVidia card w/ 1024 MB RAM (NVIDIA GeForce GTX 260 or ATI HD 4890)<br></li><li><strong>DirectX:</strong> Version 9.0<br></li><li><strong>Storage:</strong> 14 GB available space</li></ul>"
},
"mac": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> macOS 10.11<br></li><li><strong>Processor:</strong> 1.8GHz Intel or greater<br></li><li><strong>Memory:</strong> 4 GB RAM<br></li><li><strong>Graphics:</strong> 512Mb AMD 4850, 512Mb Nvidia GT130, Intel HD4000 (See Notes for more details)<br></li><li><strong>Storage:</strong> 15 GB available space<br></li><li><strong>Additional Notes:</strong> <strong>The game is supported on the following Macs. To check your Mac model and when it was released, select About This Mac from the Apple menu on your menu bar.</strong> <ul class=\"bb_ul\"> <li>* All MacBook Airs released since Mid 2012 (1) </li><li>* All 13” MacBook Pros released since Mid 2012 (1) </li><li>* All 15” &amp; 17&quot; MacBook Pros released since Mid 2010 with a 512Mb graphics card or better </li><li>* All Mac Minis released since Late 2012 (1) </li><li>* All iMacs released since Early 2009 with a 512Mb graphics card or better </li><li>* All Mac Pros released since early 2009 </li></ul> <ul class=\"bb_ul\"> <li>(1) HD4000 &amp; HD5000 cards require at least 8GB of System Memory </li></ul> ——— <strong>The following Macs are capable of running the game but do not consistently meet the standards required for official support.</strong> <ul class=\"bb_ul\"> <li>* All MacBooks released since Early 2016 </li></ul> Please note for your computer to meet the minimum requirements it must match or better all elements of the listed spec. For more detailed specifications check the Feral website.</li></ul>",
"recommended": ""
},
"linux": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Ubuntu 16.04 / SteamOS 2.0<br></li><li><strong>Processor:</strong> Intel i3 or AMD FX6300<br></li><li><strong>Memory:</strong> 4 GB RAM<br></li><li><strong>Graphics:</strong> NVIDIA 640 (1GB), AMD R9 270, Intel Iris Pro 6200 or better<br></li><li><strong>Additional Notes:</strong> NVIDIA GPU's require Driver version 367.27 (tested). AMD and Intel GPU's require MESA 11.2 (tested). AMD GPUs are not supported on SteamOS</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Ubuntu 16.04 / SteamOS 2.0<br></li><li><strong>Processor:</strong> Intel Core i5 or better<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> NVIDIA 7xx series or better<br></li><li><strong>Storage:</strong> 16 GB available space<br></li><li><strong>Additional Notes:</strong> NVIDIA GPU's require Driver version 367.27 (tested). AMD and Intel GPU's require MESA 11.2 (tested). AMD GPUs are not supported on SteamOS</li></ul>"
}
},
"_id": "6143fbb69d57ff50939bef43",
"title": "Life is Strange - Episode 3",
"series": "Life is Strange",
"steamId": "329910",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/329910/header.jpg?t=1521537755",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_0e9be011d0a997c4e6ed09291bce9a625530762e.1920x1080.jpg?t=1521537755",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_0e9be011d0a997c4e6ed09291bce9a625530762e.600x338.jpg?t=1521537755"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_82c36037ed7ed0037f71a285f5eb50ed754c6bd6.1920x1080.jpg?t=1521537755",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_82c36037ed7ed0037f71a285f5eb50ed754c6bd6.600x338.jpg?t=1521537755"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_a055d8315a360382645b6766490ee7911cae9fb2.1920x1080.jpg?t=1521537755",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_a055d8315a360382645b6766490ee7911cae9fb2.600x338.jpg?t=1521537755"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_8cba744ebebd36ffa6cd1c38973f1216a46fc02a.1920x1080.jpg?t=1521537755",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_8cba744ebebd36ffa6cd1c38973f1216a46fc02a.600x338.jpg?t=1521537755"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_827b3e27ff69e0b4dd2a36ff14ddae4fbb0363e4.1920x1080.jpg?t=1521537755",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/329910/ss_827b3e27ff69e0b4dd2a36ff14ddae4fbb0363e4.600x338.jpg?t=1521537755"
}
],
"genre": [
"Action",
"Adventure"
],
"wine": "Not Tested",
"controller": "Full Controller Support",
"developer": [
"DONTNOD ENTERTAINMENT",
"Feral Interactive (Mac)",
"Feral Interactive (Linux)"
],
"publisher": [
"Square Enix",
"Feral interactive (Mac)",
"Feral Interactive (Linux)"
],
"releaseDate": "",
"shortDesc": "Max and Chloes investigation into Rachel Ambers disappearance lead them to break into Blackwell Academy after dark, searching for answers. Its here they discover that Rachel kept many secrets and was not the person Chloe thought she knew. Max meanwhile discovers she has a new power that brings with it some devastating consequences.",
"reviews": "",
"summary": "Max and Chloes investigation into Rachel Ambers disappearance lead them to break into Blackwell Academy after dark, searching for answers. Its here they discover that Rachel kept many secrets and was not the person Chloe thought she knew. Max meanwhile discovers she has a new power that brings with it some devastating consequences.<br><br>Life Is Strange Chaos Theory is part three of a five part series that sets out to revolutionise story based choice and consequence games by allowing the player to rewind time and affect the past, present and future. <br><br><strong>Features</strong><br><ul class=\"bb_ul\"><li>A beautifully written modern adventure game<br></li><li>Rewind time to change the course of events<br></li><li>Multiple endings depending on the choices you make <br></li><li>Striking, hand-painted visuals<br></li><li>Distinct, licensed indie soundtrack</li></ul>",
"intel": "No",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "6143fb7244277443078d7d44",
"soundtrack": "No",
"store": [
"Steam"
],
"playStatus": "Never Played"
}
],
"createDate": "2021-09-17T02:21:43.249Z",
"lastUpdateDate": "2021-09-17T02:21:43.249Z"
},
{
"os": {
"windows": true,
"mac": true,
"linux": true
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Windows Vista<br></li><li><strong>Processor:</strong> Dual Core 2.0GHz or equivalent<br></li><li><strong>Memory:</strong> 2 GB RAM<br></li><li><strong>Graphics:</strong> ATI or NVidia card w/ 512 MB RAM (not recommended for Intel HD Graphics cards)<br></li><li><strong>DirectX:</strong> Version 9.0<br></li><li><strong>Storage:</strong> 5 GB available space</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Windows 7<br></li><li><strong>Processor:</strong> Dual Core 3.0GHz or equivalent<br></li><li><strong>Memory:</strong> 2 GB RAM<br></li><li><strong>Graphics:</strong> ATI or NVidia card w/ 1024 MB RAM (NVIDIA GeForce GTX 260 or ATI HD 4890)<br></li><li><strong>DirectX:</strong> Version 9.0<br></li><li><strong>Storage:</strong> 14 GB available space</li></ul>"
},
"mac": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> macOS 10.11<br></li><li><strong>Processor:</strong> 1.8GHz Intel or greater<br></li><li><strong>Memory:</strong> 4 GB RAM<br></li><li><strong>Graphics:</strong> 512Mb AMD 4850, 512Mb Nvidia GT130, Intel HD4000 (See Notes for more details)<br></li><li><strong>Storage:</strong> 15 GB available space<br></li><li><strong>Additional Notes:</strong> <strong>The game is supported on the following Macs. To check your Mac model and when it was released, select About This Mac from the Apple menu on your menu bar.</strong> <ul class=\"bb_ul\"> <li>* All MacBook Airs released since Mid 2012 (1) </li><li>* All 13” MacBook Pros released since Mid 2012 (1) </li><li>* All 15” &amp; 17&quot; MacBook Pros released since Mid 2010 with a 512Mb graphics card or better </li><li>* All Mac Minis released since Late 2012 (1) </li><li>* All iMacs released since Early 2009 with a 512Mb graphics card or better </li><li>* All Mac Pros released since early 2009 </li></ul> <ul class=\"bb_ul\"> <li>(1) HD4000 &amp; HD5000 cards require at least 8GB of System Memory </li></ul> ——— <strong>The following Macs are capable of running the game but do not consistently meet the standards required for official support.</strong> <ul class=\"bb_ul\"> <li>* All MacBooks released since Early 2016 </li></ul> Please note for your computer to meet the minimum requirements it must match or better all elements of the listed spec. For more detailed specifications check the Feral website.</li></ul>",
"recommended": ""
},
"linux": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Ubuntu 16.04 / SteamOS 2.0<br></li><li><strong>Processor:</strong> Intel i3 or AMD FX6300<br></li><li><strong>Memory:</strong> 4 GB RAM<br></li><li><strong>Graphics:</strong> NVIDIA 640 (1GB), AMD R9 270, Intel Iris Pro 6200 or better<br></li><li><strong>Storage:</strong> 16 GB available space<br></li><li><strong>Additional Notes:</strong> NVIDIA GPU's require Driver version 367.27 (tested). AMD and Intel GPU's require MESA 11.2 (tested). AMD GPUs are not supported on SteamOS</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li><strong>OS:</strong> Ubuntu 16.04 / SteamOS 2.0<br></li><li><strong>Processor:</strong> Intel Core i5 or better<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> NVIDIA 7xx series or better<br></li><li><strong>Storage:</strong> 16 GB available space<br></li><li><strong>Additional Notes:</strong> NVIDIA GPU's require Driver version 367.27 (tested). AMD and Intel GPU's require MESA 11.2 (tested). AMD GPUs are not supported on SteamOS</li></ul>"
}
},
"_id": "6143fbbc9d57ff50939bef44",
"title": "Life is Strange - Episode 1",
"series": "Life is Strange",
"steamId": "319630",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/319630/header.jpg?t=1592488448",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_d74ebed52e05e7c22937e53fcf6c7bf1de70ada1.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_d74ebed52e05e7c22937e53fcf6c7bf1de70ada1.600x338.jpg?t=1592488448"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_f0cbce81ce638fca6fa8154d6b5f0178e67eb87f.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_f0cbce81ce638fca6fa8154d6b5f0178e67eb87f.600x338.jpg?t=1592488448"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_2abb901703c73f9230d0ad42846c29d263825807.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_2abb901703c73f9230d0ad42846c29d263825807.600x338.jpg?t=1592488448"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_f071f2da3d45953de69f00e05c6e333954ecdf26.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_f071f2da3d45953de69f00e05c6e333954ecdf26.600x338.jpg?t=1592488448"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_a856ba29b6a4eeb12cea337d6804d3a177c86e1c.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_a856ba29b6a4eeb12cea337d6804d3a177c86e1c.600x338.jpg?t=1592488448"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_351f0026c4ca89eef429a095750814aaf6b5ebc0.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_351f0026c4ca89eef429a095750814aaf6b5ebc0.600x338.jpg?t=1592488448"
},
{
"id": 6,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_8df8236403f5aad45eeedd33d2bd545e45435b39.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_8df8236403f5aad45eeedd33d2bd545e45435b39.600x338.jpg?t=1592488448"
},
{
"id": 7,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_d0648fc4ed2aa5b671e5bf11819df99b72a219ff.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_d0648fc4ed2aa5b671e5bf11819df99b72a219ff.600x338.jpg?t=1592488448"
},
{
"id": 8,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_dc4879bb7a8305411f089fc4fb9a605d1881a862.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_dc4879bb7a8305411f089fc4fb9a605d1881a862.600x338.jpg?t=1592488448"
},
{
"id": 9,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_bf32315f4ee26c933967b8b5189baa90281fb7c6.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_bf32315f4ee26c933967b8b5189baa90281fb7c6.600x338.jpg?t=1592488448"
},
{
"id": 10,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_bd2379094bf433c9376ba5047ab54c3a601b74ef.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_bd2379094bf433c9376ba5047ab54c3a601b74ef.600x338.jpg?t=1592488448"
},
{
"id": 11,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_0e9be011d0a997c4e6ed09291bce9a625530762e.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_0e9be011d0a997c4e6ed09291bce9a625530762e.600x338.jpg?t=1592488448"
},
{
"id": 12,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_a055d8315a360382645b6766490ee7911cae9fb2.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_a055d8315a360382645b6766490ee7911cae9fb2.600x338.jpg?t=1592488448"
},
{
"id": 13,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_8cba744ebebd36ffa6cd1c38973f1216a46fc02a.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_8cba744ebebd36ffa6cd1c38973f1216a46fc02a.600x338.jpg?t=1592488448"
},
{
"id": 14,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_827b3e27ff69e0b4dd2a36ff14ddae4fbb0363e4.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_827b3e27ff69e0b4dd2a36ff14ddae4fbb0363e4.600x338.jpg?t=1592488448"
},
{
"id": 15,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_82c36037ed7ed0037f71a285f5eb50ed754c6bd6.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_82c36037ed7ed0037f71a285f5eb50ed754c6bd6.600x338.jpg?t=1592488448"
},
{
"id": 16,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_08f0871f59f213497304739ee675bae9f2b77883.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_08f0871f59f213497304739ee675bae9f2b77883.600x338.jpg?t=1592488448"
},
{
"id": 17,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_4bb34c58857d28cf10de6b9c08c956d8d3b190f0.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_4bb34c58857d28cf10de6b9c08c956d8d3b190f0.600x338.jpg?t=1592488448"
},
{
"id": 18,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_4b6de0ccb365a09b49f0d7ac9feecb299422142c.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_4b6de0ccb365a09b49f0d7ac9feecb299422142c.600x338.jpg?t=1592488448"
},
{
"id": 19,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_e96d229f90e743bb8855a417b81ee229fc83e96d.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_e96d229f90e743bb8855a417b81ee229fc83e96d.600x338.jpg?t=1592488448"
},
{
"id": 20,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_582d8b31d940aa4ab675382be36090fd8b9ed903.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_582d8b31d940aa4ab675382be36090fd8b9ed903.600x338.jpg?t=1592488448"
},
{
"id": 21,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_d6ab80f511623688f0497fc1b85137f453279bc7.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_d6ab80f511623688f0497fc1b85137f453279bc7.600x338.jpg?t=1592488448"
},
{
"id": 22,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_2f717a78e3777d04363c45490e4a26a235c2af8a.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_2f717a78e3777d04363c45490e4a26a235c2af8a.600x338.jpg?t=1592488448"
},
{
"id": 23,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_8146f6c8fb9afaabbdd49b7bafe3aebdf6dc9053.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_8146f6c8fb9afaabbdd49b7bafe3aebdf6dc9053.600x338.jpg?t=1592488448"
},
{
"id": 24,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_f7dbabdf90d2f5c558bdceeac52be468027a2896.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_f7dbabdf90d2f5c558bdceeac52be468027a2896.600x338.jpg?t=1592488448"
},
{
"id": 25,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_c170fbe46876e8ee01b711eab94f628c33b977d2.1920x1080.jpg?t=1592488448",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/319630/ss_c170fbe46876e8ee01b711eab94f628c33b977d2.600x338.jpg?t=1592488448"
}
],
"genre": [
"Action",
"Adventure"
],
"wine": "Not Tested",
"controller": "Full Controller Support",
"developer": [
"DONTNOD Entertainment",
"Feral Interactive (Mac)",
"Feral Interactive (Linux)"
],
"publisher": [
"Square Enix",
"Feral interactive (Mac)",
"Feral Interactive (Linux)"
],
"releaseDate": "Jan 29, 2015",
"shortDesc": "Episode 1 now FREE! Life is Strange is an award-winning and critically acclaimed episodic adventure game that allows the player to rewind time and affect the past, present and future.",
"reviews": "“The climax of Episode Two is one of the most compelling — and devastating — things Ive ever experienced in a game, because its so real, so understandable. Dontnod nails it.”<br>8.5/10 <a href=\"https://steamcommunity.com/linkfilter/?url=http://www.polygon.com/2015/1/29/7945795/life-is-strange-review-xbox-one-ps4-360-ps3-PC-episode-one\" target=\"_blank\" rel=\"noopener\" >Polygon</a><br><br>“Essential”<br>9/10 GamesTM<br><br>“Best episodic adventure game out there.”<br>5/5 <a href=\"https://steamcommunity.com/linkfilter/?url=http://blogcritics.org/playstation-4-review-life-is-strange-episode-2-out-of-time\" target=\"_blank\" rel=\"noopener\" >Blogcritics</a><br>",
"summary": "<img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/319630/extras/Steam_banner_616.png?t=1592488448\" /><br><strong>Episode 1 now FREE!</strong><br><br>Life is Strange is an award-winning and critically acclaimed episodic adventure game that allows the player to rewind time and affect the past, present and future. <br><br>Follow the story of Max Caulfield, a photography senior who discovers she can rewind time while saving her best friend Chloe Price.<br>The pair soon find themselves investigating the mysterious disappearance of fellow student Rachel Amber, uncovering a dark side to life in Arcadia Bay. Meanwhile, Max must quickly learn that changing the past can sometimes lead to a devastating future.<h2 class=\"bb_tag\">Key Features:</h2><br><ul class=\"bb_ul\"><li>A beautifully written modern adventure game.<br></li><li>Rewind time to change the course of events.<br></li><li>Multiple endings depending on the choices you make.<br></li><li>Striking, hand-painted visuals.<br></li><li>Distinct, licensed indie soundtrack.</li></ul>",
"intel": "No",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "6143fb7244277443078d7d44",
"soundtrack": "No",
"store": [
"Steam"
],
"playStatus": "Never Played"
}
],
"createDate": "2021-09-17T02:21:49.127Z",
"lastUpdateDate": "2021-09-17T02:21:49.127Z"
},
{
"os": {
"windows": true,
"mac": true,
"linux": true
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 7x64 / Windows 8.1x64 / Windows 10x64<br></li><li><strong>Processor:</strong> Intel Core i3 2.5 Ghz or AMD Phenom II 2.6 Ghz or greater<br></li><li><strong>Memory:</strong> 4 GB RAM<br></li><li><strong>Graphics:</strong> 1 GB &amp; AMD 5570 or nVidia 450 or Intel Integrated Graphics 530<br></li><li><strong>DirectX:</strong> Version 11<br></li><li><strong>Storage:</strong> 12 GB available space<br></li><li><strong>Sound Card:</strong> DirectX Compatible Sound Device<br></li><li><strong>Additional Notes:</strong> Initial installation requires one-time Internet connection for Steam authentication; software installations required (included with the game) include Steam Client, Microsoft Visual C++ 2012 and 2015 Runtime Libraries, and Microsoft DirectX. Internet connection and acceptance of Steam™ Subscriber Agreement required for activation. See <a href=\"http://www.steampowered.com/agreement\" target=\"_blank\" rel=\"noreferrer\" >www.steampowered.com/agreement</a> for details.</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 7x64 / Windows 8.1x64 / Windows 10x64<br></li><li><strong>Processor:</strong> Fourth Generation Intel Core i5 2.5 Ghz or AMD FX8350 4.0 Ghz or greater<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> 2GB &amp; AMD 7970 or nVidia 770 or greater<br></li><li><strong>DirectX:</strong> Version 11<br></li><li><strong>Storage:</strong> 12 GB available space<br></li><li><strong>Sound Card:</strong> DirectX Compatible Sound Device</li></ul>"
},
"mac": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> 10.12.6 (Sierra)<br></li><li><strong>Processor:</strong> Intel Core i5 2.7Ghz<br></li><li><strong>Memory:</strong> 6 GB RAM<br></li><li><strong>Graphics:</strong> 1 GB GPU Minimum - GeForce 775M | Radeon HD 6970 | Intel Iris Pro<br></li><li><strong>Storage:</strong> 15 GB available space<br></li><li><strong>Additional Notes:</strong> <strong>NOTICE:</strong> It is possible for Mac and PC to become out of sync during updates or patches. Within this time period, Mac users will only be able to play other Mac users.</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system</li></ul>"
},
"linux": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Ubuntu 16.04 (64bit)<br></li><li><strong>Processor:</strong> Intel Core i3 530 or AMD A8-3870<br></li><li><strong>Memory:</strong> 6 GB RAM<br></li><li><strong>Graphics:</strong> 1 GB VRAM Minimum - NVIDIA GeForce 650<br></li><li><strong>Storage:</strong> 15 GB available space<br></li><li><strong>Additional Notes:</strong> <strong>IMPORTANT NOTICE: Some Intel i3 Processors may require an additional 2 GB Swap Partition.</strong> IMPORTANT NOTICE: ATI and INTEL chipsets are NOT supported to run Civilization VI LINUX. <strong> Don't meet the above requirements? Running on a unique distro? That doesn't mean your configuration wont run Civ VI! Visit the Civilization VI community page to share your experience with other Linux players and learn about how to send bugs to Aspyr. Your feedback will help us improve Civ VI Linux and future AAA Linux releases!</strong></li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system</li></ul>"
}
},
"_id": "6143fbc99d57ff50939bef45",
"title": "Sid Meiers Civilization® VI",
"series": "Sid Meier's Civilization",
"steamId": "289070",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/289070/header.jpg?t=1631817221",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_36c63ebeb006b246cb740fdafeb41bb20e3b330d.1920x1080.jpg?t=1631817221",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_36c63ebeb006b246cb740fdafeb41bb20e3b330d.600x338.jpg?t=1631817221"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_cf53258cb8c4d283e52cf8dce3edf8656f83adc6.1920x1080.jpg?t=1631817221",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_cf53258cb8c4d283e52cf8dce3edf8656f83adc6.600x338.jpg?t=1631817221"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_f501156a69223131ee8b12452f3003698334e964.1920x1080.jpg?t=1631817221",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_f501156a69223131ee8b12452f3003698334e964.600x338.jpg?t=1631817221"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_2be9153a2633e671c283e2dbcec64e2e4543f66f.1920x1080.jpg?t=1631817221",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_2be9153a2633e671c283e2dbcec64e2e4543f66f.600x338.jpg?t=1631817221"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_a4b07a0fbdd09e35b5ec3a4726239b884f1f1f7d.1920x1080.jpg?t=1631817221",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_a4b07a0fbdd09e35b5ec3a4726239b884f1f1f7d.600x338.jpg?t=1631817221"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_fd6bbe6791ee8ab68f8a91455fa3c25b4dd9bca7.1920x1080.jpg?t=1631817221",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/289070/ss_fd6bbe6791ee8ab68f8a91455fa3c25b4dd9bca7.600x338.jpg?t=1631817221"
}
],
"genre": [
"Strategy"
],
"wine": "Not Tested",
"controller": "No Controller Support",
"developer": [
"Firaxis Games",
"Aspyr (Mac)",
"Aspyr (Linux)"
],
"publisher": [
"2K",
"Aspyr (Mac)",
"Aspyr (Linux)"
],
"releaseDate": "Oct 20, 2016",
"shortDesc": "Civilization VI offers new ways to interact with your world, expand your empire across the map, advance your culture, and compete against historys greatest leaders to build a civilization that will stand the test of time. Play as one of 20 historical leaders including Roosevelt (America) and Victoria (England).",
"reviews": "“Ill never need another Civ game in my life besides this one”<br>93 / 100 <a href=\"http://www.pcgamer.com/civilization-6-review/\" target=\"_blank\" rel=\"noreferrer\" >PC Gamer</a><br><br>“Possibly the biggest and deepest game in the series' 25-year history.”<br>9.4 / 10 <a href=\"http://www.ign.com/articles/2016/10/26/sid-meiers-civilization-vi-review\" target=\"_blank\" rel=\"noreferrer\" >IGN</a><br><br>“One of the most rewarding 4X experiences to date”<br>9.5 / 10 <a href=\"https://steamcommunity.com/linkfilter/?url=http://www.gameinformer.com/games/sid_meiers_civilization_vi/b/pc/archive/2016/10/25/civilization-vi-game-informer-review.aspx\" target=\"_blank\" rel=\"noopener\" >Game Informer</a><br>",
"summary": "<img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/289070/extras/2KGMKT_CIV6_DETAILS_BG_IMAGE_615x210.png?t=1631817221\" /><br>Originally created by legendary game designer Sid Meier, <i>Civilization</i> is a turn-based strategy game in which you attempt to build an empire to stand the test of time. Become Ruler of the World by establishing and leading a civilization from the Stone Age to the Information Age. Wage war, conduct diplomacy, advance your culture, and go head-to-head with historys greatest leaders as you attempt to build the greatest civilization the world has ever known.<br><br><i>Civilization VI</i> offers new ways to engage with your world: cities now physically expand across the map, active research in technology and culture unlocks new potential, and competing leaders will pursue their own agendas based on their historical traits as you race for one of five ways to achieve victory in the game.<br><br><ul class=\"bb_ul\"><li><strong><h2 class=\"bb_tag\">EXPANSIVE EMPIRES: </h2></strong>See the marvels of your empire spread across the map like never before. Each city spans multiple tiles so you can custom build your cities to take full advantage of the local terrain.<br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/289070/extras/wonder_1.gif?t=1631817221\" /><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/289070/extras/wonder_2.gif?t=1631817221\" /><br></li><li><strong><h2 class=\"bb_tag\">ACTIVE RESEARCH: </h2></strong>Unlock boosts that speed your civilizations progress through history. To advance more quickly, use your units to actively explore, develop your environment, and discover new cultures. <br></li><li><strong><h2 class=\"bb_tag\">DYNAMIC DIPLOMACY:</h2></strong> Interactions with other civilizations change over the course of the game, from primitive first interactions where conflict is a fact of life, to late game alliances and negotiations.<br> <br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/289070/extras/tomyris.gif?t=1631817221\" /><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/289070/extras/gilgamesh.gif?t=1631817221\" /><br></li><li><strong><h2 class=\"bb_tag\">COMBINED ARMS:</h2></strong> Expanding on the “one unit per tile” design, support units can now be embedded with other units, like anti-tank support with infantry, or a warrior with settlers. Similar units can also be combined to form powerful “Corps” units.<br><br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/289070/extras/Civ6-GIF-CannonFire1a.gif?t=1631817221\" /><br></li><li><strong><h2 class=\"bb_tag\">ENHANCED MULTIPLAYER:</h2></strong> In addition to traditional multiplayer modes, cooperate and compete with your friends in a wide variety of situations all designed to be easily completed in a single session. <br></li><li><strong><h2 class=\"bb_tag\">A CIV FOR ALL PLAYERS:</h2></strong> <i>Civilization VI</i> provides veteran players new ways to build and tune their civilization for the greatest chance of success. New tutorial systems introduce new players to the underlying concepts so they can easily get started.</li></ul>",
"intel": "No",
"createdBy": "61876dddcbf9050cf108a658",
"accessedBy": [
{
"user": "61876dddcbf9050cf108a658",
"store": [
"Steam"
],
"playStatus": "Never Played",
"soundtrack": "No"
}
],
"createDate": "2021-09-17T02:22:02.347Z",
"lastUpdateDate": "2021-09-17T02:22:02.347Z"
},
{
"os": {
"windows": true,
"mac": false,
"linux": false
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> 64 bit Windows 10 version 1909 or higher<br></li><li><strong>Processor:</strong> Intel Core i5-8400 @ 2.80GHz or AMD Ryzen 5 1600<br></li><li><strong>Memory:</strong> 12 GB RAM<br></li><li><strong>Graphics:</strong> Nvidia GTX 1060 (6GB) or AMD Radeon RX 580 (8GB)<br></li><li><strong>DirectX:</strong> Version 12<br></li><li><strong>Storage:</strong> 30 GB available space</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> 64 bit Windows 10 version 1909 or higher<br></li><li><strong>Processor:</strong> Intel Core i7-9700K @ 3.60GHz or AMD Ryzen 7 2700X<br></li><li><strong>Memory:</strong> 16 GB RAM<br></li><li><strong>Graphics:</strong> Nvidia RTX 2060 (6GB) or AMD Radeon RX 5700 (8GB)<br></li><li><strong>DirectX:</strong> Version 12<br></li><li><strong>Network:</strong> Broadband Internet connection<br></li><li><strong>Storage:</strong> 30 GB available space</li></ul>"
},
"mac": {
"minimum": "",
"recommended": ""
},
"linux": {
"minimum": "",
"recommended": ""
}
},
"_id": "614674967beedada7a335484",
"title": "DEATHLOOP",
"series": "",
"steamId": "1252330",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/header.jpg?t=1631723461",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_3701b17b1907b1db4c7cd226875e5be7425b4187.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_3701b17b1907b1db4c7cd226875e5be7425b4187.600x338.jpg?t=1631723461"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_fdabbcbfcd0684a8c27d7d34a489d309cb739fe4.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_fdabbcbfcd0684a8c27d7d34a489d309cb739fe4.600x338.jpg?t=1631723461"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_dfa0f824063ae4d2b89aaf893070238f51c2a10d.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_dfa0f824063ae4d2b89aaf893070238f51c2a10d.600x338.jpg?t=1631723461"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_400dcbac5adb1973496cab8539cbfcff5e57c262.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_400dcbac5adb1973496cab8539cbfcff5e57c262.600x338.jpg?t=1631723461"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_7dcb3b38463527e9b4ff79e08bb90506bef48668.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_7dcb3b38463527e9b4ff79e08bb90506bef48668.600x338.jpg?t=1631723461"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_fe103526b4a8ea25533de7ab9597892786222f98.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_fe103526b4a8ea25533de7ab9597892786222f98.600x338.jpg?t=1631723461"
},
{
"id": 6,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_b870f4114030f0b9c64154ca20cf4eecbf5d54fa.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_b870f4114030f0b9c64154ca20cf4eecbf5d54fa.600x338.jpg?t=1631723461"
},
{
"id": 7,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_b1e15dc9d723ced88108654ceefb0fbb6a684081.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_b1e15dc9d723ced88108654ceefb0fbb6a684081.600x338.jpg?t=1631723461"
},
{
"id": 8,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_238c2fc57b844026d216cb446ddad62a7f041551.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_238c2fc57b844026d216cb446ddad62a7f041551.600x338.jpg?t=1631723461"
},
{
"id": 9,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_0f52ec120227b3810ff8566c30a70b04513c44a1.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_0f52ec120227b3810ff8566c30a70b04513c44a1.600x338.jpg?t=1631723461"
},
{
"id": 10,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_4e90b2abfceca33f96fce415580aab5eb96e47fd.1920x1080.jpg?t=1631723461",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1252330/ss_4e90b2abfceca33f96fce415580aab5eb96e47fd.600x338.jpg?t=1631723461"
}
],
"genre": [
"Action"
],
"wine": "Not Tested",
"controller": "Full Controller Support",
"developer": [
"Arkane Studios"
],
"publisher": [
"Bethesda Softworks"
],
"releaseDate": "Sep 13, 2021",
"shortDesc": "DEATHLOOP is a next-gen FPS from Arkane Lyon, the award-winning studio behind Dishonored. In DEATHLOOP, two rival assassins are trapped in a mysterious timeloop on the island of Blackreef, doomed to repeat the same day for eternity.",
"reviews": "",
"summary": "<img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1252330/extras/DL_First_Paragraph_616x275.gif?t=1631723461\" /><br>DEATHLOOP is a next-gen first person shooter from Arkane Lyon, the award-winning studio behind Dishonored. In DEATHLOOP, two rival assassins are trapped in a mysterious timeloop on the island of Blackreef, doomed to repeat the same day for eternity. As Colt, the only chance for escape is to end the cycle by assassinating eight key targets before the day resets. Learn from each cycle - try new paths, gather intel, and find new weapons and abilities. Do whatever it takes to break the loop.<h2 class=\"bb_tag\">If At First You Dont Succeed... Die, Die Again </h2>Every new loop is an opportunity to change things up. Use the knowledge you gain from each attempt to change up your playstyle, stealthily sneaking through levels or barreling into the fight, guns-blazing. In each loop youll discover new secrets, gather intel on your targets as well as the island of Blackreef, and expand your arsenal. Armed with a host of otherworldly abilities and savage weaponry, youll utilize every tool at your command to execute takedowns that are as striking as they are devastating. Customize your loadout wisely to survive this deadly game of hunter vs hunted.<br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1252330/extras/DL_If_at_first_you_don_t_succeed_616x275.gif?t=1631723461\" /><h2 class=\"bb_tag\">SINGLE PLAYER GAMEPLAY INJECTED WITH DEADLY MULTIPLAYER </h2>Are you the hero or the villain? Youll experience DEATHLOOPs main story as Colt, hunting down targets across the island of Blackreef to break the loop and earn your freedom. All the while, youll be hunted by your rival Julianna, who can be controlled by another player. So if youre feeling devious, you, too, can step into Juliannas stylish sneakers and invade another players campaign to kill Colt. The multiplayer experience is completely optional, and players can choose to have Julianna controlled by AI within their campaign.<br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1252330/extras/DL_Single_Player_Gameplay_616x275.gif?t=1631723461\" /><h2 class=\"bb_tag\">The island Of Blackreef Paradise Or Prison </h2>Arkane is renowned for magnificently artistic worlds with multiple pathways and emergent gameplay. DEATHLOOP will present a stunning, retro-future, 60s-inspired environment, that feels like a character within itself. While Blackreef may be a stylish wonderland, for Colt it is his prison, a world ruled by decadence where death has no meaning, and delinquents party forever while keeping him captive.<br><img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1252330/extras/DL_Island_of_Blackreef_616x275.gif?t=1631723461\" />",
"intel": "No",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "61876dddcbf9050cf108a658",
"playStatus": "Never Played",
"soundtrack": "No",
"store": [
"Steam", "Humble Bundle"
]
},
{
"user": "6143fb7244277443078d7d44",
"playStatus": "Playing",
"soundtrack": "Yes",
"store": [
"Steam", "GOG"
]
}
],
"createDate": "2021-09-18T23:21:58.649Z",
"lastUpdateDate": "2021-09-18T23:21:58.649Z"
},
{
"os": {
"windows": true,
"mac": false,
"linux": false
},
"systemRequirements": {
"windows": {
"minimum": "<strong>Minimum:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 10<br></li><li><strong>Processor:</strong> Intel Core i5-3470 | AMD FX-8350<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> Nvidia GeForce GTX 660, 2 GB | AMD Radeon HD 7870, 2 GB<br></li><li><strong>Storage:</strong> 40 GB available space<br></li><li><strong>Additional Notes:</strong> Requires a CPU which supports the AVX and SSE4.2 instruction set</li></ul>",
"recommended": "<strong>Recommended:</strong><br><ul class=\"bb_ul\"><li>Requires a 64-bit processor and operating system<br></li><li><strong>OS:</strong> Windows 10<br></li><li><strong>Processor:</strong> Intel Core i7-6700 | AMD Ryzen 5 1400<br></li><li><strong>Memory:</strong> 8 GB RAM<br></li><li><strong>Graphics:</strong> Nvidia GeForce GTX 1060, 3 GB | AMD Radeon RX 580, 4 GB<br></li><li><strong>Storage:</strong> 60 GB available space<br></li><li><strong>Additional Notes:</strong> Requires a CPU which supports the AVX and SSE4.2 instruction set</li></ul>"
},
"mac": {
"minimum": "",
"recommended": ""
},
"linux": {
"minimum": "",
"recommended": ""
}
},
"_id": "614fc6bd22da13b9bbcddcf8",
"title": "Yakuza: Like a Dragon",
"series": "Yakuza",
"steamId": "1235140",
"frontImage": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/header.jpg?t=1632304741",
"screenshots": [
{
"id": 0,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_3672df00523861cd37b0f969d80604003ba14fd4.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_3672df00523861cd37b0f969d80604003ba14fd4.600x338.jpg?t=1632304741"
},
{
"id": 1,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_9cecfb713527a480f607bbde54c01763b18bf354.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_9cecfb713527a480f607bbde54c01763b18bf354.600x338.jpg?t=1632304741"
},
{
"id": 2,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_d8bcb8c72368ec09506d3a60d42ff2a1901e39f7.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_d8bcb8c72368ec09506d3a60d42ff2a1901e39f7.600x338.jpg?t=1632304741"
},
{
"id": 3,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_23d607ec8ceaca26edc88b6445d92ddcd111fea4.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_23d607ec8ceaca26edc88b6445d92ddcd111fea4.600x338.jpg?t=1632304741"
},
{
"id": 4,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_66151ac4259635b4f90e7945706498dacf6e98c1.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_66151ac4259635b4f90e7945706498dacf6e98c1.600x338.jpg?t=1632304741"
},
{
"id": 5,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_b7336fc083ba36c6073c25659fe23075234b179a.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_b7336fc083ba36c6073c25659fe23075234b179a.600x338.jpg?t=1632304741"
},
{
"id": 6,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_8562245f8ca68976ffb5404023d148fb0cbc13e9.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_8562245f8ca68976ffb5404023d148fb0cbc13e9.600x338.jpg?t=1632304741"
},
{
"id": 7,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_fe349bb52fa4a8243ba8d9b861bb52c3a4f4b5b0.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_fe349bb52fa4a8243ba8d9b861bb52c3a4f4b5b0.600x338.jpg?t=1632304741"
},
{
"id": 8,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_a46dfebb4726aa042c76636131f8bbaa1a079fb9.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_a46dfebb4726aa042c76636131f8bbaa1a079fb9.600x338.jpg?t=1632304741"
},
{
"id": 9,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_5a17b24cc9582e458a3db0f5f9ecc3f956ded072.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_5a17b24cc9582e458a3db0f5f9ecc3f956ded072.600x338.jpg?t=1632304741"
},
{
"id": 10,
"full": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_d3fe630add22247a497f0c2d22e568df66a39f48.1920x1080.jpg?t=1632304741",
"thumbnail": "https://cdn.akamai.steamstatic.com/steam/apps/1235140/ss_d3fe630add22247a497f0c2d22e568df66a39f48.600x338.jpg?t=1632304741"
}
],
"genre": [
"Action",
"Adventure",
"RPG"
],
"wine": "Not Tested",
"controller": "Full Controller Support",
"developer": [
"Ryu Ga Gotoku Studio"
],
"publisher": [
"SEGA"
],
"releaseDate": "Nov 10, 2020",
"shortDesc": "Become Ichiban Kasuga, a low-ranking yakuza grunt left on the brink of death by the man he trusted most. Take up your legendary bat and get ready to crack some underworld skulls in dynamic RPG combat set against the backdrop of modern-day Japan.",
"reviews": "",
"summary": "<img src=\"https://cdn.cloudflare.steamstatic.com/steam/apps/1235140/extras/YLAD_STEAM_REVIEW_SCORES_616.jpg?t=1632304741\" /><br><br><br><strong>Yakuza: Like a Dragons Hero Edition </strong>includes a selection of the games DLC, Job Set, and Management Mode Set.<br><br><strong>Yakuza: Like a Dragons Legendary Hero Edition </strong>includes ALL of the games DLC. This DLC adds a wide variety of in-game bonus content, including the Job Set, which unlocks the Devil Rocker and Matriarch Jobs, as well as the Management Mode Set, Crafting Set, Karaoke Set, Ultimate Costume Set, and Stat Boost Set.<h2 class=\"bb_tag\"><strong>RISE LIKE A DRAGON </strong></h2><br><br>Ichiban Kasuga, a low-ranking grunt of a low-ranking yakuza family in Tokyo, faces an 18-year prison sentence after taking the fall for a crime he didn't commit. Never losing faith, he loyally serves his time and returns to society to discover that no one was waiting for him on the outside, and his clan has been destroyed by the man he respected most.<br>Ichiban sets out to discover the truth behind his family's betrayal and take his life back, drawing a ragtag group of societys outcasts to his side: Adachi, a rogue cop, Nanba, a homeless ex-nurse, and Saeko, a hostess on a mission. Together, they are drawn into a conflict brewing beneath the surface in Yokohama and must rise to become the heroes they never expected to be.<h2 class=\"bb_tag\"><strong>LEVEL UP FROM UNDERDOG TO DRAGON IN DYNAMIC RPG COMBAT</strong></h2><br><br>Experience dynamic RPG combat like none other. Switch between 19 unique Jobs ranging from Bodyguard to Musician, using the battlefield as your weapon. Take up bats, umbrellas, bikes, signs, and everything else at your disposal to clean up the streets!<h2 class=\"bb_tag\"><strong>ENTER THE UNDERWORLD PLAYGROUND</strong></h2><br><br>When you're not busy bashing heads, relax by hitting up the local arcade for some classic SEGA games, compete with locals in a no holds barred go-kart race around Yokohama, complete 50 unique substories, or just take in the scenery of a modern-day Japanese city. Theres always something new around the corner.",
"intel": "Yes",
"createdBy": "6143fb7244277443078d7d44",
"accessedBy": [
{
"user": "61876dddcbf9050cf108a658",
"soundtrack": "No",
"store": [
"Steam"
],
"playStatus": "Playing"
}
],
"createDate": "2021-09-26T01:02:53.899Z",
"lastUpdateDate": "2021-09-26T01:02:53.899Z"
}
]

34
_data/users.json Normal file
View File

@ -0,0 +1,34 @@
[
{
"_id": "613eb7adcbb6b9eb52b40d72",
"name": "Admin",
"email": "admin@gamesdatabase.com",
"password": "123456",
"displayName": "Administrator",
"role": "admin"
},
{
"_id": "6188775eb0af643c865b28fb",
"name": "User",
"email": "user@gamesdatabase.com",
"password": "123456",
"displayName": "User",
"role": "user"
},
{
"_id": "6143fb7244277443078d7d44",
"name": "John",
"email": "jokeefe@fastmail.com",
"password": "123456",
"displayName": "LinuxHG",
"role": "user"
},
{
"_id": "61876dddcbf9050cf108a658",
"name": "John O'Keefe",
"email": "nymusicman@gmail.com",
"password": "123456",
"displayName": "John",
"role": "admin"
}
]

57
app.js Normal file
View File

@ -0,0 +1,57 @@
import 'dotenv/config'
import 'colors'
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import cookieParser from 'cookie-parser'
import mongoSanitize from 'express-mongo-sanitize'
import xss from 'xss-clean'
import rateLimit from 'express-rate-limit'
import hpp from 'hpp'
import morgan from 'morgan'
import errorHandler from './middleware/error.js'
import games from './routes/games.js'
import adminGames from './routes/adminGames.js'
import tags from './routes/tags.js'
import auth from './routes/auth.js'
import users from './routes/users.js'
import createAdmin from './scripts/adminUser.js'
import connectDB from './config/db.js'
connectDB().then(x => x)
const app = express()
const whitelist = ['http://localhost:3000', 'http://localhost:5173','https://games.linuxhg.com', 'http://localhost:8000']
const corsOptions = {
origin: (origin, callback) => {
if (whitelist.indexOf(origin) !== -1 || !origin) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
},
}
const limiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
max: 100
})
app.use(express.json(), cookieParser(), morgan('dev'), mongoSanitize(), helmet(), xss(), limiter, hpp(), cors(corsOptions))
app.use('/api/admin/games', adminGames)
app.use('/api/games', games)
app.use('/api/tags', tags)
app.use('/api/auth', auth)
app.use('/api/admin/users', users)
app.use(errorHandler)
createAdmin()
export default app

86
bin/www.js Normal file
View File

@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from '../app.js'
import debug from 'debug'
import http from 'http'
/**
* Normalize a port into a number, string, or false.
*/
const normalizePort = (val) => {
const port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
const onError = (error) => {
if (error.syscall !== 'listen') {
throw error
}
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
break
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
break
default:
throw error
}
}
/**
* Event listener for HTTP server "listening" event.
*/
const onListening = () => {
const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
debug('Listening on ' + bind)
}
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(Bun.env.PORT || '5000')
app.set('port', port)
/**
* Create HTTP server.
*/
const server = http.createServer(app)
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)

BIN
bun.lockb Executable file

Binary file not shown.

18
config/db.js Normal file
View File

@ -0,0 +1,18 @@
import mongoose from 'mongoose'
const MONGO_URI =
Bun.env.NODE_ENV === 'development'
? Bun.env.MONGO_DEV_URI
: Bun.env.MONGO_URI
const connectDB = async () => {
mongoose.set('strictQuery', true)
const conn = await mongoose.connect(MONGO_URI)
console.log(
`MongoDB Connected to ${
conn.connection.host
} using ${conn.connection.name.toUpperCase()} database`.cyan.underline.bold
)
}
export default connectDB

135
controllers/adminGames.js Normal file
View File

@ -0,0 +1,135 @@
import Game from '../models/Game.js'
import steamScraper from '../scripts/scraper.js'
import asyncHandler from '../middleware/async.js'
import ErrorResponse from '../utils/errorResponse.js'
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
import {decode, isValid} from 'js-base64'
/**
* games.js
*
* @description :: Server-side logic for managing games.
*/
/**
* gameController.list()
*/
export const list = asyncHandler(async (req, res, next) => {
return res.status(200).json(res.advancedResults)
})
/**
* gameController.show()
*/
export const show = asyncHandler(async (req, res, next) => {
const { id } = req.params
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
const game = await Game.findOne({ [gameId]: id }).select('-__v')
return res.status(200).json({
success: true,
data: game,
})
})
/**
* gameController.create()
*/
export const create = asyncHandler(async (req, res, next) => {
let oldGame
req.body.steamId.length > 0
? (oldGame = await Game.findOne({ steamId: req.body.steamId }))
: (oldGame = await Game.findOne({ title: req.body.title }))
if (oldGame)
return next(
new ErrorResponse(`The game ${oldGame.title} already exists`, 400)
)
req.body.createdBy = req.user.id
req.body.lastModifiedBy = req.user.id
req.body.accessedBy[0].user = req.user.id
const data =
req.body.scrape === true ? await steamScraper(req.body) : req.body
if (req.body.scrape === false) {
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
isValid(req.body.systemRequirements?.windows?.minimum) ? req.body.systemRequirements.windows.minimum = decode(req.body.systemRequirements.windows.minimum) : ''
isValid(req.body.systemRequirements?.windows?.recommended) ? req.body.systemRequirements.windows.recommended = decode(req.body.systemRequirements.windows.recommended) : ''
isValid(req.body.systemRequirements?.mac?.minimum) ? req.body.systemRequirements.mac.minimum = decode(req.body.systemRequirements.mac.minimum) : ''
isValid(req.body.systemRequirements?.mac?.recommended) ? req.body.systemRequirements.mac.recommended = decode(req.body.systemRequirements.mac.recommended) : ''
isValid(req.body.systemRequirements?.linux?.minimum) ? req.body.systemRequirements.linux.minimum = decode(req.body.systemRequirements.linux.minimum) : ''
isValid(req.body.systemRequirements?.linux?.recommended) ? req.body.systemRequirements.linux.recommended = decode(req.body.systemRequirements.linux.recommended) : ''
}
const game = await Game.create(data)
res.status(200).json({
success: true,
data: game,
})
})
/**
* gameController.update()
*/
export const update = asyncHandler(async (req, res, next) => {
const { id } = req.params
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
let game = await Game.findOne({ [gameId]: id })
if (!game)
return next(
ErrorResponse(`A game with the id of ${id} does not exist`, 401),
)
req.body.lastModifiedBy = req.user.id
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
isValid(req.body.systemRequirements?.windows?.minimum) ? req.body.systemRequirements.windows.minimum = decode(req.body.systemRequirements.windows.minimum) : ''
isValid(req.body.systemRequirements?.windows?.recommended) ? req.body.systemRequirements.windows.recommended = decode(req.body.systemRequirements.windows.recommended) : ''
isValid(req.body.systemRequirements?.mac?.minimum) ? req.body.systemRequirements.mac.minimum = decode(req.body.systemRequirements.mac.minimum) : ''
isValid(req.body.systemRequirements?.mac?.recommended) ? req.body.systemRequirements.mac.recommended = decode(req.body.systemRequirements.mac.recommended) : ''
isValid(req.body.systemRequirements?.linux?.minimum) ? req.body.systemRequirements.linux.minimum = decode(req.body.systemRequirements.linux.minimum) : ''
isValid(req.body.systemRequirements?.linux?.recommended) ? req.body.systemRequirements.linux.recommended = decode(req.body.systemRequirements.linux.recommended) : ''
const data =
req.body.scrape === true ? await steamScraper(req.body) : req.body
game = await Game.findOneAndUpdate({ [gameId]: id }, data, {
new: true,
runValidators: true,
})
res.status(200).json({
success: true,
data: game,
})
})
/**
* gameController.remove()
*/
export const remove = asyncHandler(async (req, res, next) => {
const { id } = req.params
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
await Game.findOneAndDelete({ [gameId]: id })
res.status(200).json({ success: true, data: {} })
})

223
controllers/auth.js Normal file
View File

@ -0,0 +1,223 @@
// noinspection SpellCheckingInspection
import User from '../models/User.js'
import asyncHandler from '../middleware/async.js'
import ErrorResponse from '../utils/errorResponse.js'
import sendEmail from '../utils/sendEmail.js'
import crypto from 'crypto'
// @desc Register user
// @route POST /api/v1/auth/register
// @access Public
export const register = asyncHandler(async (req, res, next) => {
const { name, displayName, email, password, role } = req.body
//Create user
const user = await User.create({
name,
displayName,
email,
password,
role,
gamesCreated: [],
gamesToAccess: [],
})
sendTokenResponse(user, 200, res)
})
// @desc Login user
// @route POST /api/v1/auth/login
// @access Public
export const login = asyncHandler(async (req, res, next) => {
const { email, password } = req.body
// Validate email & password
if (!email || !password)
return next(new ErrorResponse('Please provide an email and password', 400))
// Check for user
const user = await User.findOne({ email }).select('+password')
if (!user) return next(new ErrorResponse('Invalid credentials', 401))
// Check is password matches
const isMatched = await user.matchPassword(password)
if (!isMatched) return next(new ErrorResponse('Invalid credentials', 401))
sendTokenResponse(user, 200, res)
})
// @desc Logout User / Clear Cookie
// @route GET /api/v1/auth/logout
// @access Private
export const logout = asyncHandler(async (req, res, next) => {
res.cookie('token', 'none', {
expires: new Date(Date.now() + 10 * 1000),
httpOnly: true,
})
res.status(200).json({
success: true,
data: {},
})
})
// @desc Get current logged-in user
// @route POST /api/v1/auth/me
// @access Private
export const getMe = asyncHandler(async (req, res, next) => {
const user = await User.findById(req.user.id)
res.status(200).json({
success: true,
data: user,
})
})
// @desc Update user details
// @route PUT /api/v1/auth/updatedetails
// @access Private
export const updateDetails = asyncHandler(async (req, res, next) => {
const fieldsToUpdate = {
name: req.body.name,
displayName: req.body.displayName,
email: req.body.email,
role: req.body.role,
updateDate: Date.now(),
}
const user = await User.findByIdAndUpdate(req.user.id, fieldsToUpdate, {
new: true,
runValidators: true,
})
res.status(200).json({
success: true,
data: user,
})
})
// @desc Update password
// @route PUT /api/v1/auth/updatepassword
// @access Private
export const updatePassword = asyncHandler(async (req, res, next) => {
const user = await User.findById(req.user.id).select('+password')
// Check current password
if (!(await user.matchPassword(req.body.currentPassword)))
return next(new ErrorResponse('Password is incorrect', 401))
user.password = req.body.newPassword
await user.save()
sendTokenResponse(user, 200, res)
})
// @desc Forgot password
// @route POST /api/v1/auth/forgotpassword
// @access Public
export const forgotPassword = asyncHandler(async (req, res, next) => {
const user = await User.findOne({ email: req.body.email })
if (!user) {
console.error('user does not exist')
return res.status(200).json({
success: true,
data: 'Email has been sent',
})
}
// Get reset token
const resetToken = await user.getResetPasswordToken()
await user.save({ validateBeforeSave: false })
// Create reset url
let resetUrl
let message
if (req.get('Referrer') === undefined) {
const protocol = req.secure ? 'https' : 'http'
const host = await req.get('host')
resetUrl = `${protocol}://${host}/api/auth/resetpassword/${resetToken}`
message = {
text: `You are receiving this email because you (or someone else) has requested the reset of a password. Please make a PUT request to: \n\n${resetUrl}`,
html: `<body><p>You are receiving this email because you (or someone else) has requested the reset of a password. <br />Please make a PUT request to:</p> <p>${resetUrl}</p></body>`
}
} else {
resetUrl = `${req.get('Referrer')}resetpassword/${resetToken}`
message = {
text: `You are receiving this email because you (or someone else) has requested the reset of a password. \nPlease visit the below URL to continue this request. \n\n${resetUrl}`,
html: `<body><p>You are receiving this email because you (or someone else) has requested the reset of a password. <br />Please visit the below URL to continue this request.</p> <p><a href=${resetUrl}>${resetUrl}</a></p></body>`,
}
}
try {
await sendEmail({
email: user.email,
subject: 'Games Database Password Reset Token',
message,
})
res.status(200).json({
success: true,
data: 'Email has been sent',
})
} catch (err) {
console.log(err)
user.resetPasswordToken = undefined
user.resetPasswordExpire = undefined
await user.save({ validateBeforeSave: false })
return next(new ErrorResponse('Email could not be sent', 500))
}
})
// @desc Reset password
// @route PUT /api/v1/auth/resetpassword/:resettoken
// @access Public
export const resetPassword = asyncHandler(async (req, res, next) => {
// Get hashed token
const resetPasswordToken = crypto
.createHash('sha256')
.update(req.params.resettoken)
.digest('hex')
const user = await User.findOne({
resetPasswordToken,
resetPasswordExpire: { $gt: Date.now() },
})
if (!user) return next(new ErrorResponse('Invalid token', 400))
// Set new password
user.password = req.body.password
user.resetPasswordToken = undefined
user.resetPasswordExpire = undefined
await user.save()
sendTokenResponse(user, 200, res)
})
// Get token from model, create cookie and send response
const sendTokenResponse = (user, statusCode, res) => {
// Create token
const token = user.getSignedJwtToken()
const options = {
expires: new Date(
Date.now() + 24 * 60 * 60 * 1000,
),
httpOnly: true,
}
if (Bun.env.NODE_ENV === 'production') options.secure = true
res.status(statusCode).cookie('token', token, options).json({
success: true,
token,
})
}

238
controllers/games.js Normal file
View File

@ -0,0 +1,238 @@
// noinspection DuplicatedCode
import Game from '../models/Game.js'
import steamScraper from '../scripts/scraper.js'
import asyncHandler from '../middleware/async.js'
import ErrorResponse from '../utils/errorResponse.js'
import {decode, isValid} from "js-base64";
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$')
const checkForTwelveRegExp = new RegExp('^[0-9a-fA-F]{12}$')
/**
* games.js
*
* @description :: Server-side logic for managing games.
*/
/**
* gameController.list()
*/
export const list = asyncHandler(async (req, res, next) => {
const data = res.advancedResults.data
if (data[0]?.accessedBy) {
for (let i = 0; i < data.length; i++) {
for (let x = 0; x < data[i].accessedBy.length; x++) {
if (data[i].accessedBy[x].user.toString() !== req.user.id)
data[i].accessedBy.splice(x, 1)
}
}
}
return res.status(200).json(res.advancedResults)
})
/**
* gameController.show()
*/
export const show = asyncHandler(async (req, res, next) => {
const {id} = req.params
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
const game = await Game.findOne({[gameId]: id})
if (!game)
return next(
new ErrorResponse(`Game not found with id of ${req.params.id}`, 404),
)
if (
!game.accessedBy
.map((x) => x.user.toString() === req.user.id)
.includes(true)
)
return next(
new ErrorResponse(
`You do not have permission to access Game ID ${req.params.id}`,
401,
),
)
const userGame = await Game.findOne({[gameId]: id}).select(
'-createdBy -__v',
)
for (let i = 0; i < userGame.accessedBy.length; i++) {
if (userGame.accessedBy[i].user.toString() !== req.user.id)
userGame.accessedBy.splice(i, 1)
}
res.status(200).json({success: true, data: userGame})
})
/**
* gameController.create()
*/
export const create = asyncHandler(async (req, res, next) => {
let oldGame
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
isValid(req.body.systemRequirements?.windows?.minimum) ? req.body.systemRequirements.windows.minimum = decode(req.body.systemRequirements.windows.minimum) : ''
isValid(req.body.systemRequirements?.windows?.recommended) ? req.body.systemRequirements.windows.recommended = decode(req.body.systemRequirements.windows.recommended) : ''
isValid(req.body.systemRequirements?.mac?.minimum) ? req.body.systemRequirements.mac.minimum = decode(req.body.systemRequirements.mac.minimum) : ''
isValid(req.body.systemRequirements?.mac?.recommended) ? req.body.systemRequirements.mac.recommended = decode(req.body.systemRequirements.mac.recommended) : ''
isValid(req.body.systemRequirements?.linux?.minimum) ? req.body.systemRequirements.linux.minimum = decode(req.body.systemRequirements.linux.minimum) : ''
isValid(req.body.systemRequirements?.linux?.recommended) ? req.body.systemRequirements.linux.recommended = decode(req.body.systemRequirements.linux.recommended) : ''
req.body.steamId.length > 0
? (oldGame = await Game.findOne({
steamId: req.body.steamId,
'accessedBy.user': req.user.id,
}))
: (oldGame = await Game.findOne({
title: req.body.title,
'accessedBy.user': req.user.id,
}))
if (oldGame)
return next(
new ErrorResponse(`The game ${oldGame.title} already exists in users account.`, 400)
)
req.body.steamId.length > 0
? (oldGame = await Game.findOne({steamId: req.body.steamId}))
: (oldGame = await Game.findOne({title: req.body.title}))
if (oldGame) {
oldGame.accessedBy.push({
user: req.user.id,
store: req.body.accessedBy[0].store,
playStatus: req.body.accessedBy[0].playStatus,
soundtrack: req.body.accessedBy[0].soundtrack,
rating: req.body.accessedBy[0].rating,
})
oldGame.lastModifiedBy = req.user.id
await oldGame.save()
return res.status(200).json({
success: true,
data: oldGame,
})
}
req.body.createdBy = req.user.id
req.body.lastModifiedBy = req.user.id
req.body.accessedBy[0].user = req.user.id
const data =
req.body.scrape === true ? await steamScraper(req.body) : req.body
const game = await Game.create(data)
res.status(200).json({
success: true,
data: game,
})
})
/**
* gameController.update()
*/
export const update = asyncHandler(async (req, res, next) => {
const {id} = req.params
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
if (req.body.shortDesc && isValid(req.body.shortDesc)) req.body.shortDesc = decode(req.body.shortDesc)
if (req.body.reviews && isValid(req.body.reviews)) req.body.reviews = decode(req.body.reviews)
if (req.body.summary && isValid(req.body.summary)) req.body.summary = decode(req.body.summary)
isValid(req.body.systemRequirements?.windows?.minimum) ? req.body.systemRequirements.windows.minimum = decode(req.body.systemRequirements.windows.minimum) : ''
isValid(req.body.systemRequirements?.windows?.recommended) ? req.body.systemRequirements.windows.recommended = decode(req.body.systemRequirements.windows.recommended) : ''
isValid(req.body.systemRequirements?.mac?.minimum) ? req.body.systemRequirements.mac.minimum = decode(req.body.systemRequirements.mac.minimum) : ''
isValid(req.body.systemRequirements?.mac?.recommended) ? req.body.systemRequirements.mac.recommended = decode(req.body.systemRequirements.mac.recommended) : ''
isValid(req.body.systemRequirements?.linux?.minimum) ? req.body.systemRequirements.linux.minimum = decode(req.body.systemRequirements.linux.minimum) : ''
isValid(req.body.systemRequirements?.linux?.recommended) ? req.body.systemRequirements.linux.recommended = decode(req.body.systemRequirements.linux.recommended) : ''
let game = await Game.findOne({
[gameId]: id,
'accessedBy.user': req.user.id,
})
if (!game)
return next(
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
)
game = await Game.findOneAndUpdate(
{[gameId]: id, 'accessedBy.user': req.user.id},
{
$set: {
series: req.body.series,
intel: req.body.intel,
genre: req.body.genre,
wine: req.body.wine,
lastModifiedBy: req.user.id,
'accessedBy.$.store': req.body.accessedBy[0].store,
'accessedBy.$.playStatus': req.body.accessedBy[0].playStatus,
'accessedBy.$.soundtrack': req.body.accessedBy[0].soundtrack,
'accessedBy.$.rating': req.body.accessedBy[0].rating,
},
},
{new: true, runValidators: true},
)
res.status(200).json({
success: true,
data: game,
})
})
/**
* gameController.remove()
*/
export const remove = asyncHandler(async (req, res, next) => {
const {id} = req.params
const gameId =
id === id.match(checkForTwelveRegExp) || id.match(checkForHexRegExp)
? '_id'
: 'steamId'
let game = await Game.findOne({
[gameId]: id,
'accessedBy.user': req.user.id,
})
if (!game)
return next(
new ErrorResponse(`A game with the id of ${id} does not exist`, 401),
)
if (game.accessedBy.length > 1) {
await Game.findOneAndUpdate(
{[gameId]: id, 'accessedBy.user': req.user.id},
{
$pull: {
accessedBy: {user: req.user.id},
},
},
{new: true, runValidators: true},
)
} else {
await Game.findOneAndDelete({[gameId]: id})
}
res.status(200).json({
success: true,
data: {},
})
})

64
controllers/tags.js Normal file
View File

@ -0,0 +1,64 @@
import getTag from '../utils/getTag.js'
import asyncHandler from '../middleware/async.js'
/**
* gameController.tagList()
* Finds all genres used distinctly
*/
export const genreList = asyncHandler(async (req, res, next) => {
const genre = await getTag('genre')
return res.status(200).json({
success: true,
genre,
})
})
export const seriesList = asyncHandler(async (req, res, next) => {
const series = await getTag('series')
return res.status(200).json({
success: true,
series,
})
})
export const storeList = asyncHandler(async (req, res, next) => {
const store = await getTag('accessedBy.store')
return res.status(200).json({
success: true,
store,
})
})
export const developerList = asyncHandler(async (req, res, next) => {
const developer = await getTag('developer')
return res.status(200).json({
success: true,
developer,
})
})
export const publisherList = asyncHandler(async (req, res, next) => {
const publisher = await getTag('publisher')
return res.status(200).json({
success: true,
publisher,
})
})
export const tags = async (req, res, next) => {
const genre = await getTag('genre')
const series = await getTag('series')
const store = await getTag('accessedBy.store')
const developer = await getTag('developer')
const publisher = await getTag('publisher')
const tags = { genre, series, store, developer, publisher }
return res.status(200).json({
success: true,
data: tags,
})
}

79
controllers/users.js Normal file
View File

@ -0,0 +1,79 @@
import User from '../models/User.js'
import asyncHandler from '../middleware/async.js'
import ErrorResponse from '../utils/errorResponse.js'
// @desc Get all users
// @route GET /api/admin/users
// @access Private/Admin
export const getUsers = asyncHandler(async (req, res, next) => {
res.status(200).json({
success: true,
data: res.advancedResults,
})
})
// @desc Get single user
// @route GET /api/admin/users/:id
// @access Private/Admin
export const getUser = asyncHandler(async (req, res, next) => {
console.log('reached route.')
const user = await User.findById(req.params.id)
res.status(200).json({
success: true,
data: user,
})
})
// @desc Create user
// @route POST /api/admin/users
// @access Private/Admin
export const createUser = asyncHandler(async (req, res, next) => {
const user = await User.create(req.body)
res.status(201).json({
success: true,
data: user,
})
})
// @desc Update user
// @route PUT /api/admin/users/:id
// @access Private/Admin
export const updateUser = asyncHandler(async (req, res, next) => {
const { password, ...updateFields } = req.body
let user = await User.findById(req.params.id).select('+password')
if (!user)
return next(new ErrorResponse('User does not exist in database', 404))
await user.updateOne(updateFields, {
runValidators: true,
})
if (password) {
user.password = password
await user.save()
user = await User.findById(req.params.id).select('+password')
} else {
user = await User.findById(req.params.id)
}
res.status(200).json({
success: true,
data: user,
})
})
// @desc Delete user
// @route DELETE /api/admin/users/:id
// @access Private/Admin
export const deleteUser = asyncHandler(async (req, res, next) => {
await User.findByIdAndDelete(req.params.id)
res.status(200).json({
success: true,
data: {},
})
})

View File

@ -0,0 +1,240 @@
const advancedResults = (model, populate) => async (req, res, next) => {
if (req.originalUrl.indexOf('admin') === -1)
req.query = {
...req.query,
$and: [{accessedBy: {$elemMatch: {user: req.user.id}}}],
}
if (req.query.search === undefined) req.query.search = ''
if (req.query.search)
req.query = {...req.query, $text: {$search: req.query.search}}
const filterAndInGame = ['genre', 'os', 'developer', 'publisher', 'series', 'intel', 'wine', 'controller', 'store', 'soundtrack', 'playStatus', 'rating', 'ratinggte', 'ratinglte', 'steamRating', 'steamRatinggte', 'steamRatinglte']
filterAndInGame.map((filter) => {
if (req.query[filter] === undefined) req.query[filter] = ''
if (req.query[filter]) {
let newFilter
if (filter === 'os') {
newFilter = req.query[filter].split(',').reduce((prev, curr, index, arr) => {
const key = `${[filter]}.${arr[index]}`.toLowerCase()
prev.push({[key]: true})
return prev
}, [])
} else if (filter === 'store' || filter === 'soundtrack' || filter === 'playStatus') {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({accessedBy: {$elemMatch: {[filter]: curr}}})
return prev
}, [])
} else if (filter === 'rating') {
if (Object.hasOwn(req.query[filter], 'gte')) {
newFilter = req.query[filter]['gte'].split(',').reduce((prev, curr) => {
prev.push({accessedBy: {$elemMatch: {rating: {$gte: Number(curr)}}}})
return prev
}, [])
req.query.rating = `gte${req.query[filter]['gte']}`
} else if (Object.hasOwn(req.query[filter], 'lte')) {
newFilter = req.query[filter]['lte'].split(',').reduce((prev, curr) => {
prev.push({accessedBy: {$elemMatch: {rating: {$lte: Number(curr)}}}})
return prev
}, [])
req.query.rating = `lte${req.query[filter]['lte']}`
} else {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({accessedBy: {$elemMatch: {rating: Number(curr)}}})
return prev
}, [])
}
} else if (filter === 'ratinggte') {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({accessedBy: {$elemMatch: {rating: {$gte: Number(curr)}}}})
return prev
}, [])
req.query.rating = req.query.ratinggte
} else if (filter === 'ratinglte') {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({accessedBy: {$elemMatch: {rating: {$lte: Number(curr)}}}})
return prev
}, [])
req.query.rating = req.query.ratinglte
} else if (filter === 'steamRating') {
if (Object.hasOwn(req.query[filter], 'gte')) {
newFilter = req.query[filter]['gte'].split(',').reduce((prev, curr) => {
prev.push({steamRating: {$gte: Number(curr)}})
return prev
}, [])
req.query.steamRating = `gte${req.query[filter]['gte']}`
} else if (Object.hasOwn(req.query[filter], 'lte')) {
newFilter = req.query[filter]['lte'].split(',').reduce((prev, curr) => {
prev.push({steamRating: {$lte: Number(curr)}})
return prev
}, [])
req.query.steamRating = `lte${req.query[filter]['lte']}`
} else {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({steamRating: Number(curr)})
return prev
}, [])
}
} else if (filter === 'steamRatinggte') {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({steamRating: {$gte: Number(curr)}})
return prev
}, [])
req.query.steamRating = req.query.steamRatinggte
} else if (filter === 'steamRatinglte') {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({steamRating: {$lte: Number(curr)}})
return prev
}, [])
req.query.steamRating = req.query.steamRatinglte
} else {
newFilter = req.query[filter].split(',').reduce((prev, curr) => {
prev.push({[filter]: curr})
return prev
}, [])
}
newFilter.forEach(element => req.query.$and.push(element))
}
})
let query
// Copy req.query
const reqQuery = {...req.query}
// Fields to exclude
const removeFields = [
'select',
'series',
'developer',
'publisher',
'sort',
'limit',
'page',
'search',
'os',
'playStatus',
'store',
'soundtrack',
'genre',
'controller',
'rating',
'ratinggte',
'ratinglte',
'steamRating',
'steamRatinggte',
'steamRatinglte',
]
// Loop over removeFields and delete them from reqQuery
removeFields.forEach((param) => delete reqQuery[param])
filterAndInGame.forEach((param) => {
if (req.query[param] === '') delete reqQuery[param]
})
// Create query String
let queryStr = JSON.stringify(reqQuery)
// Create operators like gt, gte, etc...
queryStr = queryStr.replace(
/\b(gt|gte|lt|lte|in|all)\b/g,
(match) => `$${match}`,
)
queryStr = queryStr.replace(/\$\$/g, '$')
const total = await model.countDocuments(reqQuery)
// Finding resource
query = model.find(JSON.parse(queryStr))
// Select Fields
if (req.query.select) {
const fields = req.query.select.split(',').join(' ')
query = query.select(fields)
}
// Sort
if (req.query.sort) {
const sortBy = req.query.sort.split(',').join(' ')
query = query.sort(sortBy)
} else {
query = query.sort('series title')
}
// Pagination
let page = parseInt(req.query.page, 10) || 1
const limit = parseInt(req.query.limit, 10) || 10
// This block figures out how many total pages there will be and if
// the client is calling a page larger than the total pages
let pages
if (limit === 1) pages = total
else if (total === 0) pages = 1
else if (total % limit === 0) pages = (total - (total % limit)) / limit
else pages = (total - (total % limit)) / limit + 1
if (page > pages) page = pages
const startIndex = (page - 1) * limit
const endIndex = page * limit
query = query.skip(startIndex).limit(limit)
if (populate) {
query = query.populate(populate)
}
// Executing query
const results = await query
// Pagination result
const pagination = {}
if (endIndex < total) {
pagination.next = {
page: page + 1,
}
}
if (startIndex > 0) {
pagination.prev = {
page: page - 1,
}
}
res.advancedResults = {
success: true,
searchParams: req.query.search,
count: {
gamesPerPage: results.length,
totalGames: total,
currentPage: page,
pages,
limit,
},
filters: {
series: req.query.series,
playStatus: req.query.playStatus,
genre: req.query.genre,
os: req.query.os,
store: req.query.store,
developer: req.query.developer,
controller: req.query.controller,
publisher: req.query.publisher,
soundtrack: req.query.soundtrack,
intel: req.query.intel,
wine: req.query.wine,
rating: req.query.rating,
steamRating: req.query.steamRating,
},
pagination,
data: results,
}
next()
}
export default advancedResults

4
middleware/async.js Normal file
View File

@ -0,0 +1,4 @@
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next)
export default asyncHandler

48
middleware/auth.js Normal file
View File

@ -0,0 +1,48 @@
import jwt from 'jsonwebtoken'
import asyncHandler from './async.js'
import ErrorResponse from '../utils/errorResponse.js'
import User from '../models/User.js'
// Protect route
export const protect = asyncHandler(async (req, res, next) => {
let token
// Set token from header
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
)
token = req.headers.authorization.split(' ')[1]
// Set token from cookie
// else if (req.cookies.token) token = req.cookies.token
// Make sure token exists
if (!token)
return next(new ErrorResponse('Not authorized to access this route', 401))
try {
// Verify token
const decoded = jwt.verify(token, Bun.env.ACCESS_TOKEN_SECRET)
req.user = await User.findById(decoded.id)
next()
} catch (err) {
return next(new ErrorResponse('Not authorized to access this route', 401))
}
})
// Grants access to specific roles
export const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return next(
new ErrorResponse(
`User role ${req.user.role} is not authorized to access this route.`,
403
)
)
}
next()
}
}

31
middleware/error.js Normal file
View File

@ -0,0 +1,31 @@
import ErrorResponse from '../utils/errorResponse.js'
const errorHandler = (err, req, res, next) => {
let error = { ...err }
error.message = err.message
console.log(err.stack.red)
//Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = `Resource not found${typeof(err.value) === 'string' ? ` with id of ${err.value}` : ''}`
error = new ErrorResponse(message, 404)
}
//Mongoose duplicate key
if (err.code === 11000) {
const message = `Duplicate field value entered`
error = new ErrorResponse(message, 400)
}
//Mongoose validation error
if(err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message)
error = new ErrorResponse(message, 400)
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
})
}
export default errorHandler

104
models/Game.js Normal file
View File

@ -0,0 +1,104 @@
import mongoose from 'mongoose'
import slugify from 'slugify'
const {Schema, model} = mongoose
const GameSchema = new Schema(
{
title: {type: String, trim: true, required: true},
series: {type: String, trim: true},
steamId: {
type: String,
index: {unique: true, partialFilterExpression: {steamId: {$exists: true, $gt: 0, $type: String}}}
},
slug: String,
frontImage: {type: String, required: true},
screenshots: Array,
genre: {type: Array, sparse: true},
os: {
windows: {type: Boolean},
mac: {type: Boolean},
linux: {type: Boolean},
android: {type: Boolean},
ios: {type: Boolean}
},
wine: {type: String, enum: ['Not Tested', 'Yes', 'No']},
controller: {
type: String,
enum: [
'No Controller Support',
'Partial Controller Support',
'Full Controller Support',
],
},
developer: {type: Array, sparse: true},
publisher: {type: Array, sparse: true},
releaseDate: {type: String},
shortDesc: {type: String},
reviews: String,
summary: String,
intel: {type: String, enum: ['Not Tested', 'Yes', 'No']},
systemRequirements: {
windows: {
minimum: String,
recommended: String,
},
mac: {
minimum: String,
recommended: String,
},
linux: {
minimum: String,
recommended: String,
},
},
steamRating: {type: Number, min: 0, max: 10, default: 0},
createdBy: {
type: Schema.ObjectId,
ref: 'User',
required: true,
},
accessedBy: [
{
user: {
type: Schema.ObjectId,
ref: 'User',
required: true,
},
store: {type: Array, required: true},
playStatus: {
type: String,
enum: ['Never Played', 'Up Next', 'Playing', 'Finished', 'Will Not Play'],
default: 'Never Played',
},
soundtrack: {type: String, enum: ['Yes', 'No'], default: 'No'},
rating: {type: Number, min: 0, max: 10, default: 0}
},
],
lastModifiedBy: {
type: Schema.ObjectId,
ref: 'User',
required: true,
},
},
{timestamps: {createdAt: 'createDate', updatedAt: 'lastUpdateDate'}}
)
GameSchema.pre('save', function (next) {
this.slug = slugify(this.title, {lower: true})
next()
})
GameSchema.index({accessedBy: {user: 1}}, {unique: true})
GameSchema.index({
title: 'text',
series: 'text',
steamId: 'text',
genre: 'text',
developer: 'text',
publisher: 'text',
slug: 'text',
})
export default model('Game', GameSchema)

95
models/User.js Normal file
View File

@ -0,0 +1,95 @@
import mongoose from 'mongoose'
const { Schema } = mongoose
import gravatar from 'gravatar'
// import crypt from 'argon2'
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
const UserSchema = new Schema({
name: {
type: String,
trim: true,
required: [true, 'Please add a name'],
},
email: {
type: String,
required: [true, 'Please enter a valid email address'],
match: [
/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
'Please add a valid email',
],
unique: true,
},
password: {
type: String,
required: [true, 'Please enter a password with a minimum of 6 characters'],
minlength: 6,
select: false
},
avatar: {
type: String,
},
displayName: {
type: String,
required: [true, 'Please add a name to show with your icon'],
unique: true,
},
createDate: {
type: Date,
default: Date.now,
},
updateDate: Date,
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
resetPasswordToken: String,
resetPasswordExpire: Date,
})
UserSchema.pre('save', async function (next) {
if(!this.isModified('password')) next()
this.password = await Bun.password.hash(this.password)
})
UserSchema.pre('save', async function(next) {
if(!this.isModified('email')) next()
this.avatar = await gravatar.url(this.email, {
s: '200',
r: 'pg',
d: 'retro',
})
})
// Sign JWT and return
UserSchema.methods.getSignedJwtToken = function () {
return jwt.sign({ id: this._id }, Bun.env.ACCESS_TOKEN_SECRET, {
expiresIn: Bun.env.JWT_EXPIRE,
})
}
// Match user entered password to hashed password in database
UserSchema.methods.matchPassword = async function (enteredPassword) {
return await Bun.password.verify(enteredPassword, this.password)
}
// Generate and hash password token
UserSchema.methods.getResetPasswordToken = async function () {
// Generate token
const resetToken = crypto.randomBytes(20).toString('hex')
// Hash token and set to resetPasswordToken field
this.resetPasswordToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex')
// Set expire
this.resetPasswordExpire = Date.now() + 10 * 60 * 1000
return resetToken
}
export default mongoose.model('User', UserSchema)

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "games-database-api",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "bun run --hot ./bin/www.js",
"startProd": "bun ./bin/www.js"
},
"type": "module",
"dependencies": {
"@imranbarbhuiya/mongoose-fuzzy-searching": "^3.0.5",
"colors": "1.4.0",
"cookie-parser": "1.4.6",
"cors": "^2.8.5",
"debug": "^4.3.2",
"dotenv": "^16.3.1",
"express": "~4.18.2",
"express-jwt": "^8.4.1",
"express-mongo-sanitize": "2.2.0",
"express-rate-limit": "5.5.1",
"express-validator": "^7.0.1",
"gravatar": "^1.8.2",
"helmet": "^7.0.0",
"hpp": "0.2.3",
"http-errors": "^2.0.0",
"js-base64": "^3.7.5",
"jsonwebtoken": "^8.5.1",
"mongoose": "^7.5.1",
"mongoose-partial-search": "^1.0.6",
"morgan": "1.10.0",
"node-html-parser": "^6.1.9",
"nodemailer": "6.9.5",
"slugify": "1.6.6",
"xss-clean": "0.1.4",
"yn": "^5.0.0"
},
"devDependencies": {
"@types/node": "^20.6.0",
"prettier": "^3.0.3"
}
}

20
routes/adminGames.js Normal file
View File

@ -0,0 +1,20 @@
import express from 'express'
const router = express.Router()
import { list, show, create, update, remove } from '../controllers/adminGames.js'
import { protect, authorize } from '../middleware/auth.js'
import advancedResults from '../middleware/advancedResults.js'
import Game from '../models/Game.js'
router.use(protect)
router.use(authorize('admin'))
router.route('/')
.get(advancedResults(Game), list)
.post(create)
router.route('/:id')
.get(show)
.put(update)
.delete(remove)
export default router

17
routes/auth.js Normal file
View File

@ -0,0 +1,17 @@
// noinspection SpellCheckingInspection
import express from 'express'
const router = express.Router()
import { register, login, getMe, forgotPassword, resetPassword, updateDetails, updatePassword, logout } from '../controllers/auth.js'
import { protect } from '../middleware/auth.js'
router.post('/register', register)
router.post('/login', login)
router.get('/logout', logout)
router.get('/me', protect, getMe)
router.put('/updatedetails', protect, updateDetails)
router.post('/forgotpassword', forgotPassword)
router.put('/resetpassword/:resettoken', resetPassword)
router.put('/updatepassword', protect, updatePassword)
export default router

20
routes/games.js Normal file
View File

@ -0,0 +1,20 @@
import express from 'express'
const router = express.Router()
import { list, show, create, update, remove } from '../controllers/games.js'
import { protect, authorize } from '../middleware/auth.js'
import advancedResults from '../middleware/advancedResults.js'
import Game from '../models/Game.js'
router.use(protect)
router.use(authorize('user', 'admin'))
router.route('/')
.get(advancedResults(Game), list)
.post(create)
router.route('/:id')
.get(show)
.put(update)
.delete(remove)
export default router

35
routes/tags.js Normal file
View File

@ -0,0 +1,35 @@
import express from 'express'
const router = express.Router()
import { genreList, seriesList, storeList, developerList, publisherList, tags } from '../controllers/tags.js'
/*
* GET
*/
router.get('/genres', genreList)
/*
* GET
*/
router.get('/series', seriesList)
/*
* GET
*/
router.get('/store', storeList)
/*
* GET
*/
router.get('/developer', developerList)
/*
* GET
*/
router.get('/publisher', publisherList)
/*
* GET
*/
router.get('/', tags)
export default router

28
routes/users.js Normal file
View File

@ -0,0 +1,28 @@
import express from 'express'
const router = express.Router({ mergeParams: true })
import User from '../models/User.js'
import {
getUsers,
getUser,
createUser,
updateUser,
deleteUser,
} from '../controllers/users.js'
import advancedResults from '../middleware/advancedResults.js'
import { protect, authorize } from '../middleware/auth.js'
router.use(protect)
router.use(authorize('admin'))
router
.route('/')
.get(advancedResults(User), getUsers)
.post(createUser)
router
.route('/:id')
.get(getUser)
.put(updateUser)
.delete(deleteUser)
export default router

49
scripts/adminUser.js Normal file
View File

@ -0,0 +1,49 @@
// noinspection SpellCheckingInspection
import UserModel from '../models/User.js'
// import Bun.password from 'argon2'
import gravatar from 'gravatar'
const createAdmin = async () => {
try {
const hash = await Bun.password.hash('game$databaseadmin1')
const oldUser = await UserModel.findOne({
email: 'admin@gamesdatabase.com',
})
if (oldUser) {
return
}
const avatar = gravatar.url('admin@gamesdatabase.com', {
s: '200',
r: 'pg',
d: 'retro',
})
const user = new UserModel({
name: 'Admin',
email: 'admin@gamesdatabase.com',
password: hash,
displayName: 'Administrator',
avatar,
role: 'admin',
gamesCreated: [],
gamesToAccess: [],
})
await user.save((err) => {
console.log('Admin user successfully created')
if (err) {
return console.error('Could not save admin user')
}
})
} catch (err) {
return console.error(
'Server Error: Could not create admin user' + err.message
)
}
}
export default createAdmin

175
scripts/scraper.js Normal file
View File

@ -0,0 +1,175 @@
import {parse} from "node-html-parser";
const steamScraper = async (resData) => {
const {_id, steamId, series, wine, intel} = resData
const {playStatus, soundtrack} = resData.accessedBy[0]
if (steamId === null || steamId === '') {
return 'Please enter a valid Steam Id'
}
const steamApiUrl = `https://store.steampowered.com/api/appdetails/?appids=${steamId}`
const steamRatingApiUrl = `https://store.steampowered.com/appreviews/${steamId}?json=1`
const steamWebUrl = `https://store.steampowered.com/app/${steamId}`
try {
const apiResponse = await fetch(steamApiUrl, {
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US',
Connection: 'keep-alive',
Host:
'store.steampowered.com',
'Upgrade-Insecure-Requests': '1',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
},
}),
steamRatingApiResponse = await fetch(steamRatingApiUrl, {
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US',
Connection: 'keep-alive',
Cookie: 'birthtime=470682001;path=/;domain=store.steampowered.com',
Host:
'store.steampowered.com',
'Upgrade-Insecure-Requests': '1',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
},
}),
webResponse = await fetch(steamWebUrl, {
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US',
Connection: 'keep-alive',
Cookie: 'birthtime=470682001;path=/;domain=store.steampowered.com',
Host:
'store.steampowered.com',
'Upgrade-Insecure-Requests': '1',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
},
}),
apiAnswer = await apiResponse.json(),
steamRatingApiAnswer = await steamRatingApiResponse.json(),
webAnswer = await webResponse.text(),
success = apiAnswer[steamId]['success']
if (success === true) {
const data = apiAnswer[steamId]['data']
const steamRating = steamRatingApiAnswer['success'] === 1 ?
steamRatingApiAnswer['query_summary']['review_score'] :
0
const webData = parse(webAnswer)
const screenshots = data.screenshots?.map((screenshot) => ({
id: screenshot.id,
full: screenshot.path_full,
thumbnail: screenshot.path_thumbnail,
})) || [],
steamGenres = data.genres?.map((genre) => genre.description) || [],
tags = webData.querySelectorAll('.app_tag').map(x => {
let tag = x.rawText.trim() !== '+' && x.rawText.trim()
if (typeof tag == 'string' && tag.includes("&amp;")) tag = tag.replace("&amp;", "&")
return tag
}),
manualGenres = resData.genre || [],
genre = [...new Set([...steamGenres, ...manualGenres, ...tags])].filter(x => x !== "" & x !== false),
filterController = data.categories
? data.categories.filter((x) => {
if (x.id === 28 || x.id === 18) {
return x
}
})
: [],
controller =
filterController.length === 1
? filterController[0].description
.split(' ')
.map(word => word[0].toUpperCase() + word.substring(1).toLowerCase())
.join(' ')
: 'No Controller Support',
store = resData.accessedBy[0].store.length > 0 ? resData.accessedBy[0].store : ['Steam'],
createdBy = resData.createdBy || '',
lastModifiedBy = resData.lastModifiedBy || '',
user = resData.accessedBy[0].user || '',
reviews = resData.reviews || '',
rating = resData.rating || 0,
json = {
_id,
steamId,
title: data.name,
series,
frontImage: data.header_image,
screenshots,
genre,
os: data.platforms,
wine,
controller,
developer: data.developers,
publisher: data.publishers,
releaseDate: data.release_date.date,
shortDesc: data.short_description,
reviews,
summary: data.about_the_game,
steamRating,
intel,
systemRequirements: {
windows: {
minimum: '',
recommended: '',
},
mac: {
minimum: '',
recommended: '',
},
linux: {
minimum: '',
recommended: '',
},
},
createdBy,
lastModifiedBy,
accessedBy: [
{
user,
store,
playStatus,
soundtrack,
},
],
rating,
}
if (data.platforms.windows) {
json.systemRequirements.windows.minimum =
data.pc_requirements.minimum || ''
json.systemRequirements.windows.recommended =
data.pc_requirements.recommended || ''
}
if (data.platforms.mac) {
json.systemRequirements.mac.minimum =
data.mac_requirements.minimum || ''
json.systemRequirements.mac.recommended =
data.mac_requirements.recommended || ''
}
if (data.platforms.linux) {
json.systemRequirements.linux.minimum =
data.linux_requirements.minimum || ''
json.systemRequirements.linux.recommended =
data.linux_requirements.recommended || ''
}
return json
} else {
return success
}
} catch (error) {
console.error(error)
return error
}
}
export default steamScraper

53
seeder.js Normal file
View File

@ -0,0 +1,53 @@
import fs from 'fs'
import mongoose from 'mongoose'
import 'colors'
import dotenv from 'dotenv'
dotenv.config()
import Game from './models/Game.js'
import User from './models/User.js'
mongoose.connect(Bun.env.MONGO_URI)
const games = JSON.parse(
fs.readFileSync(`${__dirname}/_data/games.json`, 'utf-8')
)
const users = JSON.parse(
fs.readFileSync(`${__dirname}/_data/users.json`, 'utf-8')
)
const importData = async () => {
try {
await Game.create(games)
await User.create(users)
console.log('Data Imported...'.green.inverse)
process.exit()
} catch (err) {
console.error(err)
}
}
const deleteData = async () => {
try {
await Game.deleteMany()
await User.deleteMany()
console.log('Data Destroyed...'.red.inverse)
process.exit()
} catch (err) {
console.error(err)
}
}
if (process.argv[2] === '-i') {
importData()
} else if (process.argv[2] === '-d') {
deleteData()
} else {
console.log(
'Please use the "-i" flag for importing or the "-d" flag for deleting'.red
.bold
)
process.exit()
}

8
utils/errorResponse.js Normal file
View File

@ -0,0 +1,8 @@
class ErrorResponse extends Error {
constructor(message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
export default ErrorResponse

12
utils/getTag.js Normal file
View File

@ -0,0 +1,12 @@
import Game from '../models/Game.js'
const getTag = async (tag) => {
const fullTags = await Game.find().distinct(tag)
let tags = []
for (let i = 0; i < fullTags.length; i++) {
if (fullTags[i] !== '') tags.push(fullTags[i])
}
return tags
}
export default getTag

30
utils/sendEmail.js Normal file
View File

@ -0,0 +1,30 @@
import nodemailer from 'nodemailer'
import yn from 'yn'
const sendEmail = async options => {
const transporter = nodemailer.createTransport({
host: Bun.env.SMTP_HOST,
port: Bun.env.SMTP_PORT,
secure: yn(Bun.env.SECURE), // true for 465, false for other ports
auth: {
user: Bun.env.SMTP_USER, // generated ethereal user
pass: Bun.env.SMTP_PASSWORD, // generated ethereal password
},
})
// send mail with defined transport object
const message = {
from: `${Bun.env.FROM_NAME} <${Bun.env.FROM_EMAIL}>`, // sender address
to: options.email, // list of receivers
subject: options.subject, // Subject line
html: options.message.html, // HTML body
text: options.message.text, // plain text body
}
const info = await transporter.sendMail(message).catch((err) => console.error(err))
console.log('Message sent: %s', info.messageId)
}
export default sendEmail