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