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