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