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