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