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