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