97486ff201ff2a90573fdced45d41329236a7aa6
[stouyapi.git] / bin / import-game-data.php
1 #!/usr/bin/env php
2 <?php
3 /**
4  * Import games from a OUYA game data repository
5  *
6  * @link https://github.com/cweiske/ouya-game-data/
7  * @author Christian Weiske <cweiske@cweiske.de>
8  */
9 ini_set('xdebug.halt_level', E_WARNING|E_NOTICE|E_USER_WARNING|E_USER_NOTICE);
10 require_once __DIR__ . '/filters.php';
11 if (!isset($argv[1])) {
12     error('Pass the path to a "folders" file with game data json files folder names');
13 }
14 $foldersFile = $argv[1];
15 if (!is_file($foldersFile)) {
16     error('Given path is not a file: ' . $foldersFile);
17 }
18
19 $GLOBALS['packagelists']['cweiskepicks'] = [
20     'de.eiswuxe.blookid2',
21     'com.cosmos.babyloniantwins',
22     'com.inverseblue.skyriders',
23 ];
24
25 $wwwDir = __DIR__ . '/../www/';
26
27 $baseDir   = dirname($foldersFile);
28 $gameFiles = [];
29 foreach (file($foldersFile) as $line) {
30     $line = trim($line);
31     if (strlen($line)) {
32         if (strpos($line, '..') !== false) {
33             error('Path attack in ' . $folder);
34         }
35         $folder = $baseDir . '/' . $line;
36         if (!is_dir($folder)) {
37             error('Folder does not exist: ' . $folder);
38         }
39         $gameFiles = array_merge($gameFiles, glob($folder . '/*.json'));
40     }
41 }
42
43 $games = [];
44 $count = 0;
45 $developers = [];
46 foreach ($gameFiles as $gameFile) {
47     $game = json_decode(file_get_contents($gameFile));
48     if ($game === null) {
49         error('JSON invalid at ' . $gameFile);
50     }
51     addMissingGameProperties($game);
52     $games[$game->packageName] = $game;
53
54     writeJson(
55         'api/v1/details-data/' . $game->packageName . '.json',
56         buildDetails($game)
57     );
58
59     if (!isset($developers[$game->developer->uuid])) {
60         $developers[$game->developer->uuid] = [
61             'info'     => $game->developer,
62             'products' => [],
63         ];
64     }
65
66     $products = $game->products ?? [];
67     foreach ($products as $product) {
68         writeJson(
69             'api/v1/developers/' . $game->developer->uuid
70             . '/products/' . $product->identifier . '.json',
71             buildDeveloperProductOnly($product, $game->developer)
72         );
73         $developers[$game->developer->uuid]['products'][] = $product;
74     }
75
76     /**/
77     writeJson(
78         'api/v1/games/' . $game->packageName . '/purchases',
79         buildPurchases($game)
80     );
81
82     writeJson(
83         'api/v1/apps/' . $game->packageName . '.json',
84         buildApps($game)
85     );
86     $latestRelease = $game->latestRelease;
87     writeJson(
88         'api/v1/apps/' . $latestRelease->uuid . '.json',
89         buildApps($game)
90     );
91
92     writeJson(
93         'api/v1/apps/' . $latestRelease->uuid . '-download.json',
94         buildAppDownload($game, $latestRelease)
95     );
96
97     if ($count++ > 20) {
98         //break;
99     }
100 }
101
102 calculateRank($games);
103
104 foreach ($developers as $developer) {
105     writeJson(
106         //index.htm does not need a rewrite rule
107         'api/v1/developers/' . $developer['info']->uuid
108         . '/products/index.htm',
109         buildDeveloperProducts($developer['products'], $developer['info'])
110     );
111     writeJson(
112         //index.htm does not need a rewrite rule
113         'api/v1/developers/' . $developer['info']->uuid
114         . '/current_gamer',
115         buildDeveloperCurrentGamer()
116     );
117 }
118
119 writeJson('api/v1/discover-data/discover.json', buildDiscover($games));
120 writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games));
121
122 //make
123 writeJson(
124     'api/v1/discover-data/tutorials.json',
125     buildMakeCategory('Tutorials', filterByGenre($games, 'Tutorials'))
126 );
127
128 $searchLetters = 'abcdefghijklmnopqrstuvwxyz0123456789., ';
129 foreach (str_split($searchLetters) as $letter) {
130     $letterGames = filterBySearchWord($games, $letter);
131     writeJson(
132         'api/v1/search-data/' . $letter . '.json',
133         buildSearch($letterGames)
134     );
135 }
136
137
138 function buildDiscover(array $games)
139 {
140     $games = removeMakeGames($games);
141     $data = [
142         'title' => 'DISCOVER',
143         'rows'  => [],
144         'tiles' => [],
145     ];
146
147     addDiscoverRow(
148         $data, 'Last Updated',
149         filterLastUpdated($games, 10)
150     );
151     addDiscoverRow(
152         $data, 'Best rated',
153         filterBestRated($games, 10)
154     );
155     addDiscoverRow(
156         $data, "cweiske's picks",
157         filterByPackageNames($games, $GLOBALS['packagelists']['cweiskepicks'])
158     );
159
160     addDiscoverRow(
161         $data, 'Special',
162         [
163             'Best rated',
164             'Most rated',
165             'Random',
166         ]
167     );
168     writeJson(
169         'api/v1/discover-data/' . categoryPath('Best rated') . '.json',
170         buildSpecialCategory('Best rated', filterBestRated($games, 99))
171     );
172     writeJson(
173         'api/v1/discover-data/' . categoryPath('Most rated') . '.json',
174         buildSpecialCategory('Most rated', filterMostDownloaded($games, 99))
175     );
176     writeJson(
177         'api/v1/discover-data/' . categoryPath('Random') . '.json',
178         buildSpecialCategory(
179             'Random ' . date('Y-m-d H:i'),
180             filterRandom($games, 99)
181         )
182     );
183
184     $players = [
185         //1 => '1 player',
186         2 => '2 players',
187         3 => '3 players',
188         4 => '4 players',
189     ];
190     addDiscoverRow($data, 'Multiplayer', $players);
191     foreach ($players as $num => $title) {
192         writeJson(
193             'api/v1/discover-data/' . categoryPath($title) . '.json',
194             buildDiscoverCategory($title, filterByPlayers($games, $num))
195         );
196     }
197
198     $ages = getAllAges($games);
199     natsort($ages);
200     addDiscoverRow($data, 'Content rating', $ages);
201     foreach ($ages as $num => $title) {
202         writeJson(
203             'api/v1/discover-data/' . categoryPath($title) . '.json',
204             buildDiscoverCategory($title, filterByAge($games, $title))
205         );
206     }
207
208     $genres = removeMakeGenres(getAllGenres($games));
209     sort($genres);
210     addChunkedDiscoverRows($data, $genres, 'Genres');
211
212     foreach ($genres as $genre) {
213         writeJson(
214             'api/v1/discover-data/' . categoryPath($genre) . '.json',
215             buildDiscoverCategory($genre, filterByGenre($games, $genre))
216         );
217     }
218
219     $abc = array_merge(range('A', 'Z'), ['Other']);
220     addChunkedDiscoverRows($data, $abc, 'Alphabetical');
221     foreach ($abc as $letter) {
222         writeJson(
223             'api/v1/discover-data/' . categoryPath($letter) . '.json',
224             buildDiscoverCategory($letter, filterByLetter($games, $letter))
225         );
226     }
227
228     return $data;
229 }
230
231 /**
232  * A genre category page
233  */
234 function buildDiscoverCategory($name, $games)
235 {
236     $data = [
237         'title' => $name,
238         'rows'  => [],
239         'tiles' => [],
240     ];
241     addDiscoverRow(
242         $data, 'Last Updated',
243         filterLastUpdated($games, 10)
244     );
245     addDiscoverRow(
246         $data, 'Best rated',
247         filterBestRated($games, 10)
248     );
249
250     $games = sortByTitle($games);
251     $chunks = array_chunk($games, 4);
252     foreach ($chunks as $chunkGames) {
253         addDiscoverRow($data, '', $chunkGames);
254     }
255
256     return $data;
257 }
258
259 function buildMakeCategory($name, $games)
260 {
261     $data = [
262         'title' => $name,
263         'rows'  => [],
264         'tiles' => [],
265     ];
266
267     $games = sortByTitle($games);
268     addDiscoverRow($data, '', $games);
269
270     return $data;
271 }
272
273 function buildSpecialCategory($name, $games)
274 {
275     $data = [
276         'title' => $name,
277         'rows'  => [],
278         'tiles' => [],
279     ];
280
281     $first3 = array_slice($games, 0, 3);
282     $chunks = array_chunk(array_slice($games, 3), 4);
283     array_unshift($chunks, $first3);
284
285     foreach ($chunks as $chunkGames) {
286         addDiscoverRow($data, '', $chunkGames);
287     }
288
289     return $data;
290 }
291
292 function buildDiscoverHome(array $games)
293 {
294     //we do not want anything here for now
295     $data = [
296         'title' => 'home',
297         'rows'  => [
298             [
299                 'title' => 'FEATURED',
300                 'showPrice' => false,
301                 'ranked'    => false,
302                 'tiles'     => [],
303             ]
304         ],
305         'tiles' => [],
306     ];
307     return $data;
308 }
309
310 /**
311  * Build api/v1/apps/$packageName
312  */
313 function buildApps($game)
314 {
315     $latestRelease = $game->latestRelease;
316
317     $product      = null;
318     $gamePromoted = getPromotedProduct($game);
319     if ($gamePromoted) {
320         $product = buildProduct($gamePromoted);
321     }
322
323     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx
324     return [
325         'app' => [
326             'uuid'          => $latestRelease->uuid,
327             'title'         => $game->title,
328             'overview'      => $game->overview,
329             'description'   => $game->description,
330             'gamerNumbers'  => $game->players,
331             'genres'        => $game->genres,
332
333             'website'       => $game->website,
334             'contentRating' => $game->contentRating,
335             'premium'       => $game->premium,
336             'firstPublishedAt' => $game->firstPublishedAt,
337
338             'likeCount'     => $game->rating->likeCount,
339             'ratingAverage' => $game->rating->average,
340             'ratingCount'   => $game->rating->count,
341
342             'versionNumber' => $latestRelease->name,
343             'latestVersion' => $latestRelease->uuid,
344             'md5sum'        => $latestRelease->md5sum,
345             'apkFileSize'   => $latestRelease->size,
346             'publishedAt'   => $latestRelease->date,
347             'publicSize'    => $latestRelease->publicSize,
348             'nativeSize'    => $latestRelease->nativeSize,
349
350             'mainImageFullUrl' => $game->discover,
351             'videoUrl'         => getFirstVideoUrl($game->media),
352             'filepickerScreenshots' => getAllImageUrls($game->media),
353             'mobileAppIcon'    => null,
354
355             'developer'           => $game->developer->name,
356             'supportEmailAddress' => $game->developer->supportEmail,
357             'supportPhone'        => $game->developer->supportPhone,
358             'founder'             => $game->developer->founder,
359
360             'promotedProduct' => $product,
361         ],
362     ];
363 }
364
365 function buildAppDownload($game, $release)
366 {
367     return [
368         'app' => [
369             'fileSize'      => $release->size,
370             'version'       => $release->uuid,
371             'contentRating' => $game->contentRating,
372             'downloadLink'  => $release->url,
373         ]
374     ];
375 }
376
377 function buildProduct($product)
378 {
379     if ($product === null) {
380         return null;
381     }
382     return [
383         'type'          => $product->type ?? 'entitlement',
384         'identifier'    => $product->identifier,
385         'name'          => $product->name,
386         'description'   => $product->description ?? '',
387         'localPrice'    => $product->localPrice,
388         'originalPrice' => $product->originalPrice,
389         'priceInCents'  => $product->originalPrice * 100,
390         'percentOff'    => 0,
391         'currency'      => $product->currency,
392     ];
393 }
394
395 /**
396  * Build /app/v1/details?app=org.example.game
397  */
398 function buildDetails($game)
399 {
400     $latestRelease = $game->latestRelease;
401
402     $mediaTiles = [];
403     if ($game->discover) {
404         $mediaTiles[] = [
405             'type' => 'image',
406             'urls' => [
407                 'thumbnail' => $game->discover,
408                 'full'      => $game->discover,
409             ],
410         ];
411     }
412     foreach ($game->media as $medium) {
413         if ($medium->type == 'image')  {
414             $mediaTiles[] = [
415                 'type' => 'image',
416                 'urls' => [
417                     'thumbnail' => $medium->thumb,
418                     'full'      => $medium->url,
419                 ],
420             ];
421         } else {
422             $mediaTiles[] = [
423                 'type' => 'video',
424                 'url'  => $medium->url,
425             ];
426         }
427     }
428
429     $buttons = [];
430     if (isset($game->links->unlocked)) {
431         $buttons[] = [
432             'text' => 'Show unlocked',
433             'url'  => 'ouya://launcher/details?app=' . $game->links->unlocked,
434             'bold' => true,
435         ];
436     }
437
438     $product      = null;
439     $gamePromoted = getPromotedProduct($game);
440     if ($gamePromoted) {
441         $product = buildProduct($gamePromoted);
442     }
443
444     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
445     return [
446         'type'             => 'Game',
447         'title'            => $game->title,
448         'description'      => $game->description,
449         'gamerNumbers'     => $game->players,
450         'genres'           => $game->genres,
451
452         'suggestedAge'     => $game->contentRating,
453         'premium'          => $game->premium,
454         'inAppPurchases'   => $game->inAppPurchases,
455         'firstPublishedAt' => strtotime($game->firstPublishedAt),
456         'ccUrl'            => null,
457
458         'rating' => [
459             'count'   => $game->rating->count,
460             'average' => $game->rating->average,
461         ],
462
463         'apk' => [
464             'fileSize'    => $latestRelease->size,
465             'nativeSize'  => $latestRelease->nativeSize,
466             'publicSize'  => $latestRelease->publicSize,
467             'md5sum'      => $latestRelease->md5sum,
468             'filename'    => 'FIXME',
469             'errors'      => '',
470             'package'     => $game->packageName,
471             'versionCode' => $latestRelease->versionCode,
472             'state'       => 'complete',
473         ],
474
475         'version' => [
476             'number'      => $latestRelease->name,
477             'publishedAt' => strtotime($latestRelease->date),
478             'uuid'        => $latestRelease->uuid,
479         ],
480
481         'developer' => [
482             'name'    => $game->developer->name,
483             'founder' => $game->developer->founder,
484         ],
485
486         'metaData' => [
487             'key:rating.average',
488             'key:developer.name',
489             'key:suggestedAge',
490             number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB',
491         ],
492
493         'tileImage'     => $game->discover,
494         'mediaTiles'    => $mediaTiles,
495         'mobileAppIcon' => null,
496         'heroImage'     => [
497             'url' => null,
498         ],
499
500         'promotedProduct' => $product,
501         'buttons'         => $buttons,
502     ];
503 }
504
505 function buildDeveloperCurrentGamer()
506 {
507     return [
508         'gamer' => [
509             'uuid'     => '00702342-0000-1111-2222-c3e1500cafe2',
510             'username' => 'stouyapi',
511         ],
512     ];
513 }
514
515 /**
516  * For /api/v1/developers/xxx/products/?only=yyy
517  */
518 function buildDeveloperProductOnly($product, $developer)
519 {
520     return [
521         'developerName' => $developer->name,
522         'currency'      => $product->currency,
523         'products'      => [
524             buildProduct($product),
525         ],
526     ];
527 }
528
529 /**
530  * For /api/v1/developers/xxx/products/
531  */
532 function buildDeveloperProducts($products, $developer)
533 {
534     $jsonProducts = [];
535     foreach ($products as $product) {
536         $jsonProducts[] = buildProduct($product);
537     }
538     return [
539         'developerName' => $developer->name,
540         'currency'      => $products[0]->currency ?? 'EUR',
541         'products'      => $jsonProducts,
542     ];
543 }
544
545 function buildPurchases($game)
546 {
547     $purchasesData = [
548         'purchases' => [],
549     ];
550     $promotedProduct = getPromotedProduct($game);
551     if ($promotedProduct) {
552         $purchasesData['purchases'][] = [
553             'purchaseDate' => time() * 1000,
554             'generateDate' => time() * 1000,
555             'identifier'   => $promotedProduct->identifier,
556             'gamer'        => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid
557             'uuid'         => '00702342-0000-1111-2222-c3e1500beef3',//transaction ID
558             'priceInCents' => $promotedProduct->originalPrice * 100,
559             'localPrice'   => $promotedProduct->localPrice,
560             'currency'     => $promotedProduct->currency,
561         ];
562     }
563
564     $encryptedOnce  = dummyEncrypt($purchasesData);
565     $encryptedTwice = dummyEncrypt($encryptedOnce);
566     return $encryptedTwice;
567 }
568
569 function buildSearch($games)
570 {
571     $games = sortByTitle($games);
572     $results = [];
573     foreach ($games as $game) {
574         $results[] = [
575             'title' => $game->title,
576             'url'   => 'ouya://launcher/details?app=' . $game->packageName,
577             'contentRating' => $game->contentRating,
578         ];
579     }
580     return [
581         'count'   => count($results),
582         'results' => $results,
583     ];
584 }
585
586 function dummyEncrypt($data)
587 {
588     return [
589         'key'  => base64_encode('0123456789abcdef'),
590         'iv'   => 't3jir1LHpICunvhlM76edQ==',//random bytes
591         'blob' => base64_encode(
592             json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
593         ),
594     ];
595 }
596
597 function addChunkedDiscoverRows(&$data, $games, $title)
598 {
599     $chunks = array_chunk($games, 4);
600     $first = true;
601     foreach ($chunks as $chunk) {
602         addDiscoverRow(
603             $data, $first ? $title : '',
604             $chunk
605         );
606         $first = false;
607     }
608 }
609
610 function addDiscoverRow(&$data, $title, $games)
611 {
612     $row = [
613         'title'     => $title,
614         'showPrice' => true,
615         'ranked'    => false,
616         'tiles'     => [],
617     ];
618     foreach ($games as $game) {
619         if (is_string($game)) {
620             //category link
621             $tilePos = count($data['tiles']);
622             $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
623
624         } else {
625             //game
626             if (isset($game->links->original)) {
627                 //do not link unlocked games.
628                 // people an access them via the original games
629                 continue;
630             }
631             $tilePos = findTile($data['tiles'], $game->packageName);
632             if ($tilePos === null) {
633                 $tilePos = count($data['tiles']);
634                 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
635             }
636         }
637         $row['tiles'][] = $tilePos;
638     }
639     $data['rows'][] = $row;
640 }
641
642 function findTile($tiles, $packageName)
643 {
644     foreach ($tiles as $pos => $tile) {
645         if ($tile['package'] == $packageName) {
646             return $pos;
647         }
648     }
649     return null;
650 }
651
652 function buildDiscoverCategoryTile($title)
653 {
654     return [
655         'url'   => 'ouya://launcher/discover/' . categoryPath($title),
656         'image' => '',
657         'title' => $title,
658         'type'  => 'discover'
659     ];
660 }
661
662 function buildDiscoverGameTile($game)
663 {
664     $latestRelease = $game->latestRelease;
665     return [
666         'gamerNumbers' => $game->players,
667         'genres' => $game->genres,
668         'url' => 'ouya://launcher/details?app=' . $game->packageName,
669         'latestVersion' => [
670             'apk' => [
671                 'md5sum' => $latestRelease->md5sum,
672             ],
673             'versionNumber' => $latestRelease->name,
674             'uuid' => $latestRelease->uuid,
675         ],
676         'inAppPurchases' => $game->inAppPurchases,
677         'promotedProduct' => null,
678         'premium' => $game->premium,
679         'type' => 'app',
680         'package' => $game->packageName,
681         'updated_at' => strtotime($latestRelease->date),
682         'updatedAt' => $latestRelease->date,
683         'title' => $game->title,
684         'image' => $game->discover,
685         'contentRating' => $game->contentRating,
686         'rating' => [
687             'count' => $game->rating->count,
688             'average' => $game->rating->average,
689         ],
690         'promotedProduct' => buildProduct(getPromotedProduct($game)),
691     ];
692 }
693
694 function categoryPath($title)
695 {
696     return str_replace(['/', '\\', ' ', '+', '?'], '_', $title);
697 }
698
699 function getAllAges($games)
700 {
701     $ages = [];
702     foreach ($games as $game) {
703         $ages[] = $game->contentRating;
704     }
705     return array_unique($ages);
706 }
707
708 function getAllGenres($games)
709 {
710     $genres = [];
711     foreach ($games as $game) {
712         $genres = array_merge($genres, $game->genres);
713     }
714     return array_unique($genres);
715 }
716
717 function addMissingGameProperties($game)
718 {
719     if (!isset($game->overview)) {
720         $game->overview = null;
721     }
722     if (!isset($game->description)) {
723         $game->description = '';
724     }
725     if (!isset($game->players)) {
726         $game->players = [1];
727     }
728     if (!isset($game->genres)) {
729         $game->genres = ['Unsorted'];
730     }
731     if (!isset($game->website)) {
732         $game->website = null;
733     }
734     if (!isset($game->contentRating)) {
735         $game->contentRating = 'Everyone';
736     }
737     if (!isset($game->premium)) {
738         $game->premium = false;
739     }
740     if (!isset($game->firstPublishedAt)) {
741         $game->firstPublishedAt = gmdate('c');
742     }
743
744     if (!isset($game->rating)) {
745         $game->rating = new stdClass();
746     }
747     if (!isset($game->rating->likeCount)) {
748         $game->rating->likeCount = 0;
749     }
750     if (!isset($game->rating->average)) {
751         $game->rating->average = 0;
752     }
753     if (!isset($game->rating->count)) {
754         $game->rating->count = 0;
755     }
756
757     $game->latestRelease = null;
758     $latestReleaseTimestamp = 0;
759     foreach ($game->releases as $release) {
760         if (!isset($release->publicSize)) {
761             $release->publicSize = 0;
762         }
763         if (!isset($release->nativeSize)) {
764             $release->nativeSize = 0;
765         }
766
767         $releaseTimestamp = strtotime($release->date);
768         if ($releaseTimestamp > $latestReleaseTimestamp) {
769             $game->latestRelease    = $release;
770             $latestReleaseTimestamp = $releaseTimestamp;
771         }
772     }
773     if ($game->latestRelease === null) {
774         error('No latest release for ' . $game->packageName);
775     }
776
777     if (!isset($game->media)) {
778         $game->media = [];
779     }
780
781     if (!isset($game->developer->uuid)) {
782         $game->developer->uuid = null;
783     }
784     if (!isset($game->developer->name)) {
785         $game->developer->name = 'unknown';
786     }
787     if (!isset($game->developer->supportEmail)) {
788         $game->developer->supportEmail = null;
789     }
790     if (!isset($game->developer->supportPhone)) {
791         $game->developer->supportPhone = null;
792     }
793     if (!isset($game->developer->founder)) {
794         $game->developer->founder = false;
795     }
796 }
797
798 /**
799  * Implements a sensible ranking system described in
800  * https://stackoverflow.com/a/1411268/2826013
801  */
802 function calculateRank(array $games)
803 {
804     $averageRatings = array_map(
805         function ($game) {
806             return $game->rating->average;
807         },
808         $games
809     );
810     $average = array_sum($averageRatings) / count($averageRatings);
811     $C = $average;
812     $m = 500;
813
814     foreach ($games as $game) {
815         $R = $game->rating->average;
816         $v = $game->rating->count;
817         $game->rating->rank = ($R * $v + $C * $m) / ($v + $m);
818     }
819 }
820
821 function getFirstVideoUrl($media)
822 {
823     foreach ($media as $medium) {
824         if ($medium->type == 'video') {
825             return $medium->url;
826         }
827     }
828     return null;
829 }
830
831 function getAllImageUrls($media)
832 {
833     $imageUrls = [];
834     foreach ($media as $medium) {
835         if ($medium->type == 'image') {
836             $imageUrls[] = $medium->url;
837         }
838     }
839     return $imageUrls;
840 }
841
842 function getPromotedProduct($game)
843 {
844     if (!isset($game->products) || !count($game->products)) {
845         return null;
846     }
847     foreach ($game->products as $gameProd) {
848         if ($gameProd->promoted) {
849             return $gameProd;
850         }
851     }
852     return null;
853 }
854
855 function removeMakeGames(array $games)
856 {
857     return filterByGenre($games, 'Tutorials', true);
858 }
859
860 function removeMakeGenres($genres)
861 {
862     $filtered = [];
863     foreach ($genres as $genre) {
864         if ($genre != 'Tutorials' && $genre != 'Builds') {
865             $filtered[] = $genre;
866         }
867     }
868     return $filtered;
869 }
870
871 function writeJson($path, $data)
872 {
873     global $wwwDir;
874     $fullPath = $wwwDir . $path;
875     $dir = dirname($fullPath);
876     if (!is_dir($dir)) {
877         mkdir($dir, 0777, true);
878     }
879     file_put_contents(
880         $fullPath,
881         json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
882     );
883 }
884
885 function error($msg)
886 {
887     fwrite(STDERR, $msg . "\n");
888     exit(1);
889 }
890 ?>