e90ccc948043cad9b6c13df017c14689bebe4984
[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     $jsonProducts = [];
555     foreach ($products as $product) {
556         $jsonProducts[] = buildProduct($product);
557     }
558     return [
559         'developerName' => $developer->name,
560         'currency'      => $products[0]->currency ?? 'EUR',
561         'products'      => $jsonProducts,
562     ];
563 }
564
565 function buildPurchases($game)
566 {
567     $purchasesData = [
568         'purchases' => [],
569     ];
570     $promotedProduct = getPromotedProduct($game);
571     if ($promotedProduct) {
572         $purchasesData['purchases'][] = [
573             'purchaseDate' => time() * 1000,
574             'generateDate' => time() * 1000,
575             'identifier'   => $promotedProduct->identifier,
576             'gamer'        => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid
577             'uuid'         => '00702342-0000-1111-2222-c3e1500beef3',//transaction ID
578             'priceInCents' => $promotedProduct->originalPrice * 100,
579             'localPrice'   => $promotedProduct->localPrice,
580             'currency'     => $promotedProduct->currency,
581         ];
582     }
583
584     $encryptedOnce  = dummyEncrypt($purchasesData);
585     $encryptedTwice = dummyEncrypt($encryptedOnce);
586     return $encryptedTwice;
587 }
588
589 function buildSearch($games)
590 {
591     $games = sortByTitle($games);
592     $results = [];
593     foreach ($games as $game) {
594         $results[] = [
595             'title' => $game->title,
596             'url'   => 'ouya://launcher/details?app=' . $game->packageName,
597             'contentRating' => $game->contentRating,
598         ];
599     }
600     return [
601         'count'   => count($results),
602         'results' => $results,
603     ];
604 }
605
606 function dummyEncrypt($data)
607 {
608     return [
609         'key'  => base64_encode('0123456789abcdef'),
610         'iv'   => 't3jir1LHpICunvhlM76edQ==',//random bytes
611         'blob' => base64_encode(
612             json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
613         ),
614     ];
615 }
616
617 function addChunkedDiscoverRows(&$data, $games, $title)
618 {
619     $chunks = array_chunk($games, 4);
620     $first = true;
621     foreach ($chunks as $chunk) {
622         addDiscoverRow(
623             $data, $first ? $title : '',
624             $chunk
625         );
626         $first = false;
627     }
628 }
629
630 function addDiscoverRow(&$data, $title, $games)
631 {
632     $row = [
633         'title'     => $title,
634         'showPrice' => true,
635         'ranked'    => false,
636         'tiles'     => [],
637     ];
638     foreach ($games as $game) {
639         if (is_string($game)) {
640             //category link
641             $tilePos = count($data['tiles']);
642             $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
643
644         } else {
645             //game
646             if (isset($game->links->original)) {
647                 //do not link unlocked games.
648                 // people an access them via the original games
649                 continue;
650             }
651             $tilePos = findTile($data['tiles'], $game->packageName);
652             if ($tilePos === null) {
653                 $tilePos = count($data['tiles']);
654                 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
655             }
656         }
657         $row['tiles'][] = $tilePos;
658     }
659     $data['rows'][] = $row;
660 }
661
662 function findTile($tiles, $packageName)
663 {
664     foreach ($tiles as $pos => $tile) {
665         if ($tile['package'] == $packageName) {
666             return $pos;
667         }
668     }
669     return null;
670 }
671
672 function buildDiscoverCategoryTile($title)
673 {
674     return [
675         'url'   => 'ouya://launcher/discover/' . categoryPath($title),
676         'image' => '',
677         'title' => $title,
678         'type'  => 'discover'
679     ];
680 }
681
682 function buildDiscoverGameTile($game)
683 {
684     $latestRelease = $game->latestRelease;
685     return [
686         'gamerNumbers' => $game->players,
687         'genres' => $game->genres,
688         'url' => 'ouya://launcher/details?app=' . $game->packageName,
689         'latestVersion' => [
690             'apk' => [
691                 'md5sum' => $latestRelease->md5sum,
692             ],
693             'versionNumber' => $latestRelease->name,
694             'uuid' => $latestRelease->uuid,
695         ],
696         'inAppPurchases' => $game->inAppPurchases,
697         'promotedProduct' => null,
698         'premium' => $game->premium,
699         'type' => 'app',
700         'package' => $game->packageName,
701         'updated_at' => strtotime($latestRelease->date),
702         'updatedAt' => $latestRelease->date,
703         'title' => $game->title,
704         'image' => $game->discover,
705         'contentRating' => $game->contentRating,
706         'rating' => [
707             'count' => $game->rating->count,
708             'average' => $game->rating->average,
709         ],
710         'promotedProduct' => buildProduct(getPromotedProduct($game)),
711     ];
712 }
713
714 function getAllAges($games)
715 {
716     $ages = [];
717     foreach ($games as $game) {
718         $ages[] = $game->contentRating;
719     }
720     return array_unique($ages);
721 }
722
723 function getAllGenres($games)
724 {
725     $genres = [];
726     foreach ($games as $game) {
727         $genres = array_merge($genres, $game->genres);
728     }
729     return array_unique($genres);
730 }
731
732 function addMissingGameProperties($game)
733 {
734     if (!isset($game->overview)) {
735         $game->overview = null;
736     }
737     if (!isset($game->description)) {
738         $game->description = '';
739     }
740     if (!isset($game->players)) {
741         $game->players = [1];
742     }
743     if (!isset($game->genres)) {
744         $game->genres = ['Unsorted'];
745     }
746     if (!isset($game->website)) {
747         $game->website = null;
748     }
749     if (!isset($game->contentRating)) {
750         $game->contentRating = 'Everyone';
751     }
752     if (!isset($game->premium)) {
753         $game->premium = false;
754     }
755     if (!isset($game->firstPublishedAt)) {
756         $game->firstPublishedAt = gmdate('c');
757     }
758
759     if (!isset($game->rating)) {
760         $game->rating = new stdClass();
761     }
762     if (!isset($game->rating->likeCount)) {
763         $game->rating->likeCount = 0;
764     }
765     if (!isset($game->rating->average)) {
766         $game->rating->average = 0;
767     }
768     if (!isset($game->rating->count)) {
769         $game->rating->count = 0;
770     }
771
772     $game->latestRelease = null;
773     $latestReleaseTimestamp = 0;
774     foreach ($game->releases as $release) {
775         if (!isset($release->publicSize)) {
776             $release->publicSize = 0;
777         }
778         if (!isset($release->nativeSize)) {
779             $release->nativeSize = 0;
780         }
781
782         $releaseTimestamp = strtotime($release->date);
783         if ($releaseTimestamp > $latestReleaseTimestamp) {
784             $game->latestRelease    = $release;
785             $latestReleaseTimestamp = $releaseTimestamp;
786         }
787     }
788     if ($game->latestRelease === null) {
789         error('No latest release for ' . $game->packageName);
790     }
791
792     if (!isset($game->media)) {
793         $game->media = [];
794     }
795
796     if (!isset($game->developer->uuid)) {
797         $game->developer->uuid = null;
798     }
799     if (!isset($game->developer->name)) {
800         $game->developer->name = 'unknown';
801     }
802     if (!isset($game->developer->supportEmail)) {
803         $game->developer->supportEmail = null;
804     }
805     if (!isset($game->developer->supportPhone)) {
806         $game->developer->supportPhone = null;
807     }
808     if (!isset($game->developer->founder)) {
809         $game->developer->founder = false;
810     }
811 }
812
813 /**
814  * Implements a sensible ranking system described in
815  * https://stackoverflow.com/a/1411268/2826013
816  */
817 function calculateRank(array $games)
818 {
819     $averageRatings = array_map(
820         function ($game) {
821             return $game->rating->average;
822         },
823         $games
824     );
825     $average = array_sum($averageRatings) / count($averageRatings);
826     $C = $average;
827     $m = 500;
828
829     foreach ($games as $game) {
830         $R = $game->rating->average;
831         $v = $game->rating->count;
832         $game->rating->rank = ($R * $v + $C * $m) / ($v + $m);
833     }
834 }
835
836 function getFirstVideoUrl($media)
837 {
838     foreach ($media as $medium) {
839         if ($medium->type == 'video') {
840             return $medium->url;
841         }
842     }
843     return null;
844 }
845
846 function getAllImageUrls($media)
847 {
848     $imageUrls = [];
849     foreach ($media as $medium) {
850         if ($medium->type == 'image') {
851             $imageUrls[] = $medium->url;
852         }
853     }
854     return $imageUrls;
855 }
856
857 function getPromotedProduct($game)
858 {
859     if (!isset($game->products) || !count($game->products)) {
860         return null;
861     }
862     foreach ($game->products as $gameProd) {
863         if ($gameProd->promoted) {
864             return $gameProd;
865         }
866     }
867     return null;
868 }
869
870 /**
871  * vimeo only work with HTTPS now,
872  * and the OUYA does not support SNI.
873  * We get SSL errors and no video for them :/
874  */
875 function isUnsupportedVideoUrl($url)
876 {
877     return strpos($url, '://vimeo.com/') !== false;
878 }
879
880 function removeMakeGames(array $games)
881 {
882     return filterByGenre($games, 'Tutorials', true);
883 }
884
885 function removeMakeGenres($genres)
886 {
887     $filtered = [];
888     foreach ($genres as $genre) {
889         if ($genre != 'Tutorials' && $genre != 'Builds') {
890             $filtered[] = $genre;
891         }
892     }
893     return $filtered;
894 }
895
896 function rewriteUrl($url)
897 {
898     foreach ($GLOBALS['urlRewrites'] as $pattern => $replacement) {
899         $url = preg_replace($pattern, $replacement, $url);
900     }
901     return $url;
902 }
903
904 function writeJson($path, $data)
905 {
906     global $wwwDir;
907     $fullPath = $wwwDir . $path;
908     $dir = dirname($fullPath);
909     if (!is_dir($dir)) {
910         mkdir($dir, 0777, true);
911     }
912     file_put_contents(
913         $fullPath,
914         json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
915     );
916 }
917
918 function error($msg)
919 {
920     fwrite(STDERR, $msg . "\n");
921     exit(1);
922 }
923 ?>