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