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