Update cleaning script, needed for dynamic purchases
[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'          => 'entitlement',
384         'identifier'    => $product->identifier,
385         'name'          => $product->name,
386         'description'   => $product->description ?? '',
387         'localPrice'    => $product->localPrice,
388         'originalPrice' => $product->originalPrice,
389         'percentOff'    => 0,
390         'currency'      => $product->currency,
391     ];
392 }
393
394 /**
395  * Build /app/v1/details?app=org.example.game
396  */
397 function buildDetails($game)
398 {
399     $latestRelease = $game->latestRelease;
400
401     $mediaTiles = [];
402     if ($game->discover) {
403         $mediaTiles[] = [
404             'type' => 'image',
405             'urls' => [
406                 'thumbnail' => $game->discover,
407                 'full'      => $game->discover,
408             ],
409         ];
410     }
411     foreach ($game->media as $medium) {
412         if ($medium->type == 'image')  {
413             $mediaTiles[] = [
414                 'type' => 'image',
415                 'urls' => [
416                     'thumbnail' => $medium->thumb,
417                     'full'      => $medium->url,
418                 ],
419             ];
420         } else {
421             $mediaTiles[] = [
422                 'type' => 'video',
423                 'url'  => $medium->url,
424             ];
425         }
426     }
427
428     $buttons = [];
429     if (isset($game->links->unlocked)) {
430         $buttons[] = [
431             'text' => 'Show unlocked',
432             'url'  => 'ouya://launcher/details?app=' . $game->links->unlocked,
433             'bold' => true,
434         ];
435     }
436
437     $product      = null;
438     $gamePromoted = getPromotedProduct($game);
439     if ($gamePromoted) {
440         $product = buildProduct($gamePromoted);
441     }
442
443     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
444     return [
445         'type'             => 'Game',
446         'title'            => $game->title,
447         'description'      => $game->description,
448         'gamerNumbers'     => $game->players,
449         'genres'           => $game->genres,
450
451         'suggestedAge'     => $game->contentRating,
452         'premium'          => $game->premium,
453         'inAppPurchases'   => $game->inAppPurchases,
454         'firstPublishedAt' => strtotime($game->firstPublishedAt),
455         'ccUrl'            => null,
456
457         'rating' => [
458             'count'   => $game->rating->count,
459             'average' => $game->rating->average,
460         ],
461
462         'apk' => [
463             'fileSize'    => $latestRelease->size,
464             'nativeSize'  => $latestRelease->nativeSize,
465             'publicSize'  => $latestRelease->publicSize,
466             'md5sum'      => $latestRelease->md5sum,
467             'filename'    => 'FIXME',
468             'errors'      => '',
469             'package'     => $game->packageName,
470             'versionCode' => $latestRelease->versionCode,
471             'state'       => 'complete',
472         ],
473
474         'version' => [
475             'number'      => $latestRelease->name,
476             'publishedAt' => strtotime($latestRelease->date),
477             'uuid'        => $latestRelease->uuid,
478         ],
479
480         'developer' => [
481             'name'    => $game->developer->name,
482             'founder' => $game->developer->founder,
483         ],
484
485         'metaData' => [
486             'key:rating.average',
487             'key:developer.name',
488             'key:suggestedAge',
489             number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB',
490         ],
491
492         'tileImage'     => $game->discover,
493         'mediaTiles'    => $mediaTiles,
494         'mobileAppIcon' => null,
495         'heroImage'     => [
496             'url' => null,
497         ],
498
499         'promotedProduct' => $product,
500         'buttons'         => $buttons,
501     ];
502 }
503
504 function buildDeveloperCurrentGamer()
505 {
506     return [
507         'gamer' => [
508             'uuid'     => '00702342-0000-1111-2222-c3e1500cafe2',
509             'username' => 'stouyapi',
510         ],
511     ];
512 }
513
514 /**
515  * For /api/v1/developers/xxx/products/?only=yyy
516  */
517 function buildDeveloperProductOnly($product, $developer)
518 {
519     return [
520         'developerName' => $developer->name,
521         'currency'      => $product->currency,
522         'products'      => [
523             buildProduct($product),
524         ],
525     ];
526 }
527
528 /**
529  * For /api/v1/developers/xxx/products/
530  */
531 function buildDeveloperProducts($products, $developer)
532 {
533     $jsonProducts = [];
534     foreach ($products as $product) {
535         $jsonProducts[] = buildProduct($product);
536     }
537     return [
538         'developerName' => $developer->name,
539         'currency'      => $products[0]->currency ?? 'EUR',
540         'products'      => $jsonProducts,
541     ];
542 }
543
544 function buildPurchases($game)
545 {
546     $purchasesData = [
547         'purchases' => [],
548     ];
549     $promotedProduct = getPromotedProduct($game);
550     if ($promotedProduct) {
551         $purchasesData['purchases'][] = [
552             'purchaseDate' => time() * 1000,
553             'generateDate' => time() * 1000,
554             'identifier'   => $promotedProduct->identifier,
555             'gamer'        => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid
556             'uuid'         => '00702342-0000-1111-2222-c3e1500beef3',//transaction ID
557             'priceInCents' => $promotedProduct->originalPrice * 100,
558             'localPrice'   => $promotedProduct->localPrice,
559             'currency'     => $promotedProduct->currency,
560         ];
561     }
562
563     $encryptedOnce  = dummyEncrypt($purchasesData);
564     $encryptedTwice = dummyEncrypt($encryptedOnce);
565     return $encryptedTwice;
566 }
567
568 function buildSearch($games)
569 {
570     $games = sortByTitle($games);
571     $results = [];
572     foreach ($games as $game) {
573         $results[] = [
574             'title' => $game->title,
575             'url'   => 'ouya://launcher/details?app=' . $game->packageName,
576             'contentRating' => $game->contentRating,
577         ];
578     }
579     return [
580         'count'   => count($results),
581         'results' => $results,
582     ];
583 }
584
585 function dummyEncrypt($data)
586 {
587     return [
588         'key'  => base64_encode('0123456789abcdef') . "\n",
589         'iv'   => 't3jir1LHpICunvhlM76edQ==' . "\n",//random bytes
590         'blob' => base64_encode(
591             json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
592         ) . "\n",
593     ];
594 }
595
596 function addChunkedDiscoverRows(&$data, $games, $title)
597 {
598     $chunks = array_chunk($games, 4);
599     $first = true;
600     foreach ($chunks as $chunk) {
601         addDiscoverRow(
602             $data, $first ? $title : '',
603             $chunk
604         );
605         $first = false;
606     }
607 }
608
609 function addDiscoverRow(&$data, $title, $games)
610 {
611     $row = [
612         'title'     => $title,
613         'showPrice' => true,
614         'ranked'    => false,
615         'tiles'     => [],
616     ];
617     foreach ($games as $game) {
618         if (is_string($game)) {
619             //category link
620             $tilePos = count($data['tiles']);
621             $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
622
623         } else {
624             //game
625             if (isset($game->links->original)) {
626                 //do not link unlocked games.
627                 // people an access them via the original games
628                 continue;
629             }
630             $tilePos = findTile($data['tiles'], $game->packageName);
631             if ($tilePos === null) {
632                 $tilePos = count($data['tiles']);
633                 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
634             }
635         }
636         $row['tiles'][] = $tilePos;
637     }
638     $data['rows'][] = $row;
639 }
640
641 function findTile($tiles, $packageName)
642 {
643     foreach ($tiles as $pos => $tile) {
644         if ($tile['package'] == $packageName) {
645             return $pos;
646         }
647     }
648     return null;
649 }
650
651 function buildDiscoverCategoryTile($title)
652 {
653     return [
654         'url'   => 'ouya://launcher/discover/' . categoryPath($title),
655         'image' => '',
656         'title' => $title,
657         'type'  => 'discover'
658     ];
659 }
660
661 function buildDiscoverGameTile($game)
662 {
663     $latestRelease = $game->latestRelease;
664     return [
665         'gamerNumbers' => $game->players,
666         'genres' => $game->genres,
667         'url' => 'ouya://launcher/details?app=' . $game->packageName,
668         'latestVersion' => [
669             'apk' => [
670                 'md5sum' => $latestRelease->md5sum,
671             ],
672             'versionNumber' => $latestRelease->name,
673             'uuid' => $latestRelease->uuid,
674         ],
675         'inAppPurchases' => $game->inAppPurchases,
676         'promotedProduct' => null,
677         'premium' => $game->premium,
678         'type' => 'app',
679         'package' => $game->packageName,
680         'updated_at' => strtotime($latestRelease->date),
681         'updatedAt' => $latestRelease->date,
682         'title' => $game->title,
683         'image' => $game->discover,
684         'contentRating' => $game->contentRating,
685         'rating' => [
686             'count' => $game->rating->count,
687             'average' => $game->rating->average,
688         ],
689         'promotedProduct' => buildProduct(getPromotedProduct($game)),
690     ];
691 }
692
693 function categoryPath($title)
694 {
695     return str_replace(['/', '\\', ' ', '+', '?'], '_', $title);
696 }
697
698 function getAllAges($games)
699 {
700     $ages = [];
701     foreach ($games as $game) {
702         $ages[] = $game->contentRating;
703     }
704     return array_unique($ages);
705 }
706
707 function getAllGenres($games)
708 {
709     $genres = [];
710     foreach ($games as $game) {
711         $genres = array_merge($genres, $game->genres);
712     }
713     return array_unique($genres);
714 }
715
716 function addMissingGameProperties($game)
717 {
718     if (!isset($game->overview)) {
719         $game->overview = null;
720     }
721     if (!isset($game->description)) {
722         $game->description = '';
723     }
724     if (!isset($game->players)) {
725         $game->players = [1];
726     }
727     if (!isset($game->genres)) {
728         $game->genres = ['Unsorted'];
729     }
730     if (!isset($game->website)) {
731         $game->website = null;
732     }
733     if (!isset($game->contentRating)) {
734         $game->contentRating = 'Everyone';
735     }
736     if (!isset($game->premium)) {
737         $game->premium = false;
738     }
739     if (!isset($game->firstPublishedAt)) {
740         $game->firstPublishedAt = gmdate('c');
741     }
742
743     if (!isset($game->rating)) {
744         $game->rating = new stdClass();
745     }
746     if (!isset($game->rating->likeCount)) {
747         $game->rating->likeCount = 0;
748     }
749     if (!isset($game->rating->average)) {
750         $game->rating->average = 0;
751     }
752     if (!isset($game->rating->count)) {
753         $game->rating->count = 0;
754     }
755
756     $game->latestRelease = null;
757     $latestReleaseTimestamp = 0;
758     foreach ($game->releases as $release) {
759         if (!isset($release->publicSize)) {
760             $release->publicSize = 0;
761         }
762         if (!isset($release->nativeSize)) {
763             $release->nativeSize = 0;
764         }
765
766         $releaseTimestamp = strtotime($release->date);
767         if ($releaseTimestamp > $latestReleaseTimestamp) {
768             $game->latestRelease    = $release;
769             $latestReleaseTimestamp = $releaseTimestamp;
770         }
771     }
772     if ($game->latestRelease === null) {
773         error('No latest release for ' . $game->packageName);
774     }
775
776     if (!isset($game->media)) {
777         $game->media = [];
778     }
779
780     if (!isset($game->developer->uuid)) {
781         $game->developer->uuid = null;
782     }
783     if (!isset($game->developer->name)) {
784         $game->developer->name = 'unknown';
785     }
786     if (!isset($game->developer->supportEmail)) {
787         $game->developer->supportEmail = null;
788     }
789     if (!isset($game->developer->supportPhone)) {
790         $game->developer->supportPhone = null;
791     }
792     if (!isset($game->developer->founder)) {
793         $game->developer->founder = false;
794     }
795 }
796
797 /**
798  * Implements a sensible ranking system described in
799  * https://stackoverflow.com/a/1411268/2826013
800  */
801 function calculateRank(array $games)
802 {
803     $averageRatings = array_map(
804         function ($game) {
805             return $game->rating->average;
806         },
807         $games
808     );
809     $average = array_sum($averageRatings) / count($averageRatings);
810     $C = $average;
811     $m = 500;
812
813     foreach ($games as $game) {
814         $R = $game->rating->average;
815         $v = $game->rating->count;
816         $game->rating->rank = ($R * $v + $C * $m) / ($v + $m);
817     }
818 }
819
820 function getFirstVideoUrl($media)
821 {
822     foreach ($media as $medium) {
823         if ($medium->type == 'video') {
824             return $medium->url;
825         }
826     }
827     return null;
828 }
829
830 function getAllImageUrls($media)
831 {
832     $imageUrls = [];
833     foreach ($media as $medium) {
834         if ($medium->type == 'image') {
835             $imageUrls[] = $medium->url;
836         }
837     }
838     return $imageUrls;
839 }
840
841 function getPromotedProduct($game)
842 {
843     if (!isset($game->products) || !count($game->products)) {
844         return null;
845     }
846     foreach ($game->products as $gameProd) {
847         if ($gameProd->promoted) {
848             return $gameProd;
849         }
850     }
851     return null;
852 }
853
854 function removeMakeGames(array $games)
855 {
856     return filterByGenre($games, 'Tutorials', true);
857 }
858
859 function removeMakeGenres($genres)
860 {
861     $filtered = [];
862     foreach ($genres as $genre) {
863         if ($genre != 'Tutorials' && $genre != 'Builds') {
864             $filtered[] = $genre;
865         }
866     }
867     return $filtered;
868 }
869
870 function writeJson($path, $data)
871 {
872     global $wwwDir;
873     $fullPath = $wwwDir . $path;
874     $dir = dirname($fullPath);
875     if (!is_dir($dir)) {
876         mkdir($dir, 0777, true);
877     }
878     file_put_contents(
879         $fullPath,
880         json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
881     );
882 }
883
884 function error($msg)
885 {
886     fwrite(STDERR, $msg . "\n");
887     exit(1);
888 }
889 ?>