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