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