Special handling for "Tutorials" genre
[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__ . '/filters.php';
11 if (!isset($argv[1])) {
12     error('Pass the path to a "folders" file with game data json files folder names');
13 }
14 $foldersFile = $argv[1];
15 if (!is_file($foldersFile)) {
16     error('Given path is not a file: ' . $foldersFile);
17 }
18
19 $GLOBALS['packagelists']['cweiskepicks'] = [
20     'de.eiswuxe.blookid2',
21     'com.cosmos.babyloniantwins'
22 ];
23
24 $wwwDir = __DIR__ . '/../www/';
25
26 $baseDir   = dirname($foldersFile);
27 $gameFiles = [];
28 foreach (file($foldersFile) as $line) {
29     $line = trim($line);
30     if (strlen($line)) {
31         if (strpos($line, '..') !== false) {
32             error('Path attack in ' . $folder);
33         }
34         $folder = $baseDir . '/' . $line;
35         if (!is_dir($folder)) {
36             error('Folder does not exist: ' . $folder);
37         }
38         $gameFiles = array_merge($gameFiles, glob($folder . '/*.json'));
39     }
40 }
41
42 $games = [];
43 $count = 0;
44 $developers = [];
45 foreach ($gameFiles as $gameFile) {
46     $game = json_decode(file_get_contents($gameFile));
47     if ($game === null) {
48         error('JSON invalid at ' . $gameFile);
49     }
50     addMissingGameProperties($game);
51     $games[$game->packageName] = $game;
52
53     writeJson(
54         'api/v1/details-data/' . $game->packageName . '.json',
55         buildDetails($game)
56     );
57
58     if (!isset($developers[$game->developer->uuid])) {
59         $developers[$game->developer->uuid] = [
60             'info'     => $game->developer,
61             'products' => [],
62         ];
63     }
64
65     $products = $game->products ?? [];
66     foreach ($products as $product) {
67         writeJson(
68             'api/v1/developers/' . $game->developer->uuid
69             . '/products/' . $product->identifier . '.json',
70             buildDeveloperProductOnly($product, $game->developer)
71         );
72         $developers[$game->developer->uuid]['products'][] = $product;
73     }
74
75     /**/
76     writeJson(
77         'api/v1/games/' . $game->packageName . '/purchases',
78         buildPurchases($game)
79     );
80     /**/
81
82     /* this crashes babylonian twins
83     writeJson(
84         'api/v1/games/' . $game->packageName . '/purchases',
85         "{}\n"
86     );
87     */
88
89     writeJson(
90         'api/v1/apps/' . $game->packageName . '.json',
91         buildApps($game)
92     );
93     $latestRelease = $game->latestRelease;
94     writeJson(
95         'api/v1/apps/' . $latestRelease->uuid . '.json',
96         buildApps($game)
97     );
98
99     writeJson(
100         'api/v1/apps/' . $latestRelease->uuid . '-download.json',
101         buildAppDownload($game, $latestRelease)
102     );
103
104     if ($count++ > 20) {
105         //break;
106     }
107 }
108
109 foreach ($developers as $developer) {
110     writeJson(
111         //index.htm does not need a rewrite rule
112         'api/v1/developers/' . $developer['info']->uuid
113         . '/products/index.htm',
114         buildDeveloperProducts($developer['products'], $developer['info'])
115     );
116     writeJson(
117         //index.htm does not need a rewrite rule
118         'api/v1/developers/' . $developer['info']->uuid
119         . '/current_gamer',
120         buildDeveloperCurrentGamer()
121     );
122 }
123
124 writeJson('api/v1/discover-data/discover.json', buildDiscover($games));
125 writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games));
126
127 //make
128 writeJson(
129     'api/v1/discover-data/tutorials.json',
130     buildMakeCategory('Tutorials', filterByGenre($games, 'Tutorials'))
131 );
132
133
134
135 function buildDiscover(array $games)
136 {
137     $data = [
138         'title' => 'DISCOVER',
139         'rows'  => [],
140         'tiles' => [],
141     ];
142
143     addDiscoverRow(
144         $data, 'Last Updated',
145         filterLastUpdated($games, 10)
146     );
147     addDiscoverRow(
148         $data, 'Best rated',
149         filterBestRated($games, 10)
150     );
151     addDiscoverRow(
152         $data, "cweiske's picks",
153         filterByPackageNames($games, $GLOBALS['packagelists']['cweiskepicks'])
154     );
155
156     $players = [
157         //1 => '1 player',
158         2 => '2 players',
159         3 => '3 players',
160         4 => '4 players',
161     ];
162     addDiscoverRow($data, '# of players', $players);
163     foreach ($players as $num => $title) {
164         writeJson(
165             'api/v1/discover-data/' . categoryPath($title) . '.json',
166             buildDiscoverCategory($title, filterByPlayers($games, $num))
167         );
168     }
169
170     $ages = getAllAges($games);
171     natsort($ages);
172     addDiscoverRow($data, 'Content rating', $ages);
173     foreach ($ages as $num => $title) {
174         writeJson(
175             'api/v1/discover-data/' . categoryPath($title) . '.json',
176             buildDiscoverCategory($title, filterByAge($games, $title))
177         );
178     }
179
180     $genres = removeMakeGenres(getAllGenres($games));
181     sort($genres);
182     addChunkedDiscoverRows($data, $genres, 'Genres');
183
184     foreach ($genres as $genre) {
185         writeJson(
186             'api/v1/discover-data/' . categoryPath($genre) . '.json',
187             buildDiscoverCategory($genre, filterByGenre($games, $genre))
188         );
189     }
190
191     $abc = array_merge(range('A', 'Z'), ['Other']);
192     addChunkedDiscoverRows($data, $abc, 'Alphabetical');
193     foreach ($abc as $letter) {
194         writeJson(
195             'api/v1/discover-data/' . categoryPath($letter) . '.json',
196             buildDiscoverCategory($letter, filterByLetter($games, $letter))
197         );
198     }
199
200     return $data;
201 }
202
203 /**
204  * A genre category page
205  */
206 function buildDiscoverCategory($name, $games)
207 {
208     $data = [
209         'title' => $name,
210         'rows'  => [],
211         'tiles' => [],
212     ];
213     addDiscoverRow(
214         $data, 'Last Updated',
215         filterLastUpdated($games, 10)
216     );
217     addDiscoverRow(
218         $data, 'Best rated',
219         filterBestRated($games, 10)
220     );
221
222     usort(
223         $games,
224         function ($gameA, $gameB) {
225             return strcasecmp($gameA->title, $gameB->title);
226         }
227     );
228     $chunks = array_chunk($games, 4);
229     foreach ($chunks as $chunkGames) {
230         addDiscoverRow($data, '', $chunkGames);
231     }
232
233     return $data;
234 }
235
236 function buildMakeCategory($name, $games)
237 {
238     $data = [
239         'title' => $name,
240         'rows'  => [],
241         'tiles' => [],
242     ];
243
244     usort(
245         $games,
246         function ($gameA, $gameB) {
247             return strcasecmp($gameA->title, $gameB->title);
248         }
249     );
250     addDiscoverRow($data, '', $games);
251
252     return $data;
253 }
254
255 function buildDiscoverHome(array $games)
256 {
257     //we do not want anything here for now
258     $data = [
259         'title' => 'home',
260         'rows'  => [
261             [
262                 'title' => 'FEATURED',
263                 'showPrice' => false,
264                 'ranked'    => false,
265                 'tiles'     => [],
266             ]
267         ],
268         'tiles' => [],
269     ];
270     return $data;
271 }
272
273 /**
274  * Build api/v1/apps/$packageName
275  */
276 function buildApps($game)
277 {
278     $latestRelease = $game->latestRelease;
279
280     $product      = null;
281     $gamePromoted = getPromotedProduct($game);
282     if ($gamePromoted) {
283         $product = buildProduct($gamePromoted);
284     }
285
286     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx
287     return [
288         'app' => [
289             'uuid'          => $latestRelease->uuid,
290             'title'         => $game->title,
291             'overview'      => $game->overview,
292             'description'   => $game->description,
293             'gamerNumbers'  => $game->players,
294             'genres'        => $game->genres,
295
296             'website'       => $game->website,
297             'contentRating' => $game->contentRating,
298             'premium'       => $game->premium,
299             'firstPublishedAt' => $game->firstPublishedAt,
300
301             'likeCount'     => $game->rating->likeCount,
302             'ratingAverage' => $game->rating->average,
303             'ratingCount'   => $game->rating->count,
304
305             'versionNumber' => $latestRelease->name,
306             'latestVersion' => $latestRelease->uuid,
307             'md5sum'        => $latestRelease->md5sum,
308             'apkFileSize'   => $latestRelease->size,
309             'publishedAt'   => $latestRelease->date,
310             'publicSize'    => $latestRelease->publicSize,
311             'nativeSize'    => $latestRelease->nativeSize,
312
313             'mainImageFullUrl' => $game->discover,
314             'videoUrl'         => getFirstVideoUrl($game->media),
315             'filepickerScreenshots' => getAllImageUrls($game->media),
316             'mobileAppIcon'    => null,
317
318             'developer'           => $game->developer->name,
319             'supportEmailAddress' => $game->developer->supportEmail,
320             'supportPhone'        => $game->developer->supportPhone,
321             'founder'             => $game->developer->founder,
322
323             'promotedProduct' => $product,
324         ],
325     ];
326 }
327
328 function buildAppDownload($game, $release)
329 {
330     return [
331         'app' => [
332             'fileSize'      => $release->size,
333             'version'       => $release->uuid,
334             'contentRating' => $game->contentRating,
335             'downloadLink'  => $release->url,
336         ]
337     ];
338 }
339
340 function buildProduct($product)
341 {
342     if ($product === null) {
343         return null;
344     }
345     return [
346         'type'          => 'entitlement',
347         'identifier'    => $product->identifier,
348         'name'          => $product->name,
349         'description'   => $product->description ?? '',
350         'localPrice'    => $product->localPrice,
351         'originalPrice' => $product->originalPrice,
352         'percentOff'    => 0,
353         'currency'      => $product->currency,
354     ];
355 }
356
357 /**
358  * Build /app/v1/details?app=org.example.game
359  */
360 function buildDetails($game)
361 {
362     $latestRelease = $game->latestRelease;
363
364     $mediaTiles = [];
365     if ($game->discover) {
366         $mediaTiles[] = [
367             'type' => 'image',
368             'urls' => [
369                 'thumbnail' => $game->discover,
370                 'full'      => $game->discover,
371             ],
372         ];
373     }
374     foreach ($game->media as $medium) {
375         if ($medium->type == 'image')  {
376             $mediaTiles[] = [
377                 'type' => 'image',
378                 'urls' => [
379                     'thumbnail' => $medium->thumb,
380                     'full'      => $medium->url,
381                 ],
382             ];
383         } else {
384             $mediaTiles[] = [
385                 'type' => 'video',
386                 'url'  => $medium->url,
387             ];
388         }
389     }
390
391     $buttons = [];
392     if (isset($game->links->unlocked)) {
393         $buttons[] = [
394             'text' => 'Show unlocked',
395             'url'  => 'ouya://launcher/details?app=' . $game->links->unlocked,
396             'bold' => true,
397         ];
398     }
399
400     $product      = null;
401     $gamePromoted = getPromotedProduct($game);
402     if ($gamePromoted) {
403         $product = buildProduct($gamePromoted);
404     }
405
406     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
407     return [
408         'type'             => 'Game',
409         'title'            => $game->title,
410         'description'      => $game->description,
411         'gamerNumbers'     => $game->players,
412         'genres'           => $game->genres,
413
414         'suggestedAge'     => $game->contentRating,
415         'premium'          => $game->premium,
416         'inAppPurchases'   => $game->inAppPurchases,
417         'firstPublishedAt' => strtotime($game->firstPublishedAt),
418         'ccUrl'            => null,
419
420         'rating' => [
421             'count'   => $game->rating->count,
422             'average' => $game->rating->average,
423         ],
424
425         'apk' => [
426             'fileSize'    => $latestRelease->size,
427             'nativeSize'  => $latestRelease->nativeSize,
428             'publicSize'  => $latestRelease->publicSize,
429             'md5sum'      => $latestRelease->md5sum,
430             'filename'    => 'FIXME',
431             'errors'      => '',
432             'package'     => $game->packageName,
433             'versionCode' => $latestRelease->versionCode,
434             'state'       => 'complete',
435         ],
436
437         'version' => [
438             'number'      => $latestRelease->name,
439             'publishedAt' => strtotime($latestRelease->date),
440             'uuid'        => $latestRelease->uuid,
441         ],
442
443         'developer' => [
444             'name'    => $game->developer->name,
445             'founder' => $game->developer->founder,
446         ],
447
448         'metaData' => [
449             'key:rating.average',
450             'key:developer.name',
451             'key:suggestedAge',
452             number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB',
453         ],
454
455         'tileImage'     => $game->discover,
456         'mediaTiles'    => $mediaTiles,
457         'mobileAppIcon' => null,
458         'heroImage'     => [
459             'url' => null,
460         ],
461
462         'promotedProduct' => $product,
463         'buttons'         => $buttons,
464     ];
465 }
466
467 function buildDeveloperCurrentGamer()
468 {
469     return [
470         'gamer' => [
471             'uuid'     => '00702342-0000-1111-2222-c3e1500cafe2',
472             'username' => 'stouyapi',
473         ],
474     ];
475 }
476
477 /**
478  * For /api/v1/developers/xxx/products/?only=yyy
479  */
480 function buildDeveloperProductOnly($product, $developer)
481 {
482     return [
483         'developerName' => $developer->name,
484         'currency'      => $product->currency,
485         'products'      => [
486             buildProduct($product),
487         ],
488     ];
489 }
490
491 /**
492  * For /api/v1/developers/xxx/products/
493  */
494 function buildDeveloperProducts($products, $developer)
495 {
496     $jsonProducts = [];
497     foreach ($products as $product) {
498         $jsonProducts[] = buildProduct($product);
499     }
500     return [
501         'developerName' => $developer->name,
502         'currency'      => $products[0]->currency ?? 'EUR',
503         'products'      => $jsonProducts,
504     ];
505 }
506
507 function buildPurchases($game)
508 {
509     $purchasesData = [
510         'purchases' => [],
511     ];
512     $promotedProduct = getPromotedProduct($game);
513     if ($promotedProduct) {
514         $purchasesData['purchases'][] = [
515             'purchaseDate' => time() * 1000,
516             'generateDate' => time() * 1000,
517             'identifier'   => $promotedProduct->identifier,
518             'gamer'        => 'stouyapi',
519             'uuid'         => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid
520             'priceInCents' => $promotedProduct->originalPrice * 100,
521             'localPrice'   => $promotedProduct->localPrice,
522             'currency'     => $promotedProduct->currency,
523         ];
524     }
525
526     $encryptedOnce  = dummyEncrypt($purchasesData);
527     $encryptedTwice = dummyEncrypt($encryptedOnce);
528     return $encryptedTwice;
529 }
530
531 function dummyEncrypt($data)
532 {
533     return [
534         'key'  => base64_encode('0123456789abcdef') . "\n",
535         'iv'   => 't3jir1LHpICunvhlM76edQ==' . "\n",//random bytes
536         'blob' => base64_encode(
537             json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
538         ) . "\n",
539     ];
540 }
541
542 function addChunkedDiscoverRows(&$data, $games, $title)
543 {
544     $chunks = array_chunk($games, 4);
545     $first = true;
546     foreach ($chunks as $chunk) {
547         addDiscoverRow(
548             $data, $first ? $title : '',
549             $chunk
550         );
551         $first = false;
552     }
553 }
554
555 function addDiscoverRow(&$data, $title, $games)
556 {
557     $row = [
558         'title'     => $title,
559         'showPrice' => true,
560         'ranked'    => false,
561         'tiles'     => [],
562     ];
563     foreach ($games as $game) {
564         if (is_string($game)) {
565             //category link
566             $tilePos = count($data['tiles']);
567             $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
568
569         } else {
570             //game
571             if (isset($game->links->original)) {
572                 //do not link unlocked games.
573                 // people an access them via the original games
574                 continue;
575             }
576             $tilePos = findTile($data['tiles'], $game->packageName);
577             if ($tilePos === null) {
578                 $tilePos = count($data['tiles']);
579                 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
580             }
581         }
582         $row['tiles'][] = $tilePos;
583     }
584     $data['rows'][] = $row;
585 }
586
587 function findTile($tiles, $packageName)
588 {
589     foreach ($tiles as $pos => $tile) {
590         if ($tile['package'] == $packageName) {
591             return $pos;
592         }
593     }
594     return null;
595 }
596
597 function buildDiscoverCategoryTile($title)
598 {
599     return [
600         'url'   => 'ouya://launcher/discover/' . categoryPath($title),
601         'image' => '',
602         'title' => $title,
603         'type'  => 'discover'
604     ];
605 }
606
607 function buildDiscoverGameTile($game)
608 {
609     $latestRelease = $game->latestRelease;
610     return [
611         'gamerNumbers' => $game->players,
612         'genres' => $game->genres,
613         'url' => 'ouya://launcher/details?app=' . $game->packageName,
614         'latestVersion' => [
615             'apk' => [
616                 'md5sum' => $latestRelease->md5sum,
617             ],
618             'versionNumber' => $latestRelease->name,
619             'uuid' => $latestRelease->uuid,
620         ],
621         'inAppPurchases' => $game->inAppPurchases,
622         'promotedProduct' => null,
623         'premium' => $game->premium,
624         'type' => 'app',
625         'package' => $game->packageName,
626         'updated_at' => strtotime($latestRelease->date),
627         'updatedAt' => $latestRelease->date,
628         'title' => $game->title,
629         'image' => $game->discover,
630         'contentRating' => $game->contentRating,
631         'rating' => [
632             'count' => $game->rating->count,
633             'average' => $game->rating->average,
634         ],
635         'promotedProduct' => buildProduct(getPromotedProduct($game)),
636     ];
637 }
638
639 function categoryPath($title)
640 {
641     return str_replace(['/', '\\', ' ', '+'], '_', $title);
642 }
643
644 function getAllAges($games)
645 {
646     $ages = [];
647     foreach ($games as $game) {
648         $ages[] = $game->contentRating;
649     }
650     return array_unique($ages);
651 }
652
653 function getAllGenres($games)
654 {
655     $genres = [];
656     foreach ($games as $game) {
657         $genres = array_merge($genres, $game->genres);
658     }
659     return array_unique($genres);
660 }
661
662 function addMissingGameProperties($game)
663 {
664     if (!isset($game->overview)) {
665         $game->overview = null;
666     }
667     if (!isset($game->description)) {
668         $game->description = '';
669     }
670     if (!isset($game->players)) {
671         $game->players = [1];
672     }
673     if (!isset($game->genres)) {
674         $game->genres = ['Unsorted'];
675     }
676     if (!isset($game->website)) {
677         $game->website = null;
678     }
679     if (!isset($game->contentRating)) {
680         $game->contentRating = 'Everyone';
681     }
682     if (!isset($game->premium)) {
683         $game->premium = false;
684     }
685     if (!isset($game->firstPublishedAt)) {
686         $game->firstPublishedAt = gmdate('c');
687     }
688
689     if (!isset($game->rating)) {
690         $game->rating = new stdClass();
691     }
692     if (!isset($game->rating->likeCount)) {
693         $game->rating->likeCount = 0;
694     }
695     if (!isset($game->rating->average)) {
696         $game->rating->average = 0;
697     }
698     if (!isset($game->rating->count)) {
699         $game->rating->count = 0;
700     }
701
702     $game->latestRelease = null;
703     $latestReleaseTimestamp = 0;
704     foreach ($game->releases as $release) {
705         if (!isset($release->publicSize)) {
706             $release->publicSize = 0;
707         }
708         if (!isset($release->nativeSize)) {
709             $release->nativeSize = 0;
710         }
711
712         $releaseTimestamp = strtotime($release->date);
713         if ($releaseTimestamp > $latestReleaseTimestamp) {
714             $game->latestRelease    = $release;
715             $latestReleaseTimestamp = $releaseTimestamp;
716         }
717     }
718     if ($game->latestRelease === null) {
719         error('No latest release for ' . $game->packageName);
720     }
721
722     if (!isset($game->media)) {
723         $game->media = [];
724     }
725
726     if (!isset($game->developer->uuid)) {
727         $game->developer->uuid = null;
728     }
729     if (!isset($game->developer->name)) {
730         $game->developer->name = 'unknown';
731     }
732     if (!isset($game->developer->supportEmail)) {
733         $game->developer->supportEmail = null;
734     }
735     if (!isset($game->developer->supportPhone)) {
736         $game->developer->supportPhone = null;
737     }
738     if (!isset($game->developer->founder)) {
739         $game->developer->founder = false;
740     }
741 }
742
743 function getFirstVideoUrl($media)
744 {
745     foreach ($media as $medium) {
746         if ($medium->type == 'video') {
747             return $medium->url;
748         }
749     }
750     return null;
751 }
752
753 function getAllImageUrls($media)
754 {
755     $imageUrls = [];
756     foreach ($media as $medium) {
757         if ($medium->type == 'image') {
758             $imageUrls[] = $medium->url;
759         }
760     }
761     return $imageUrls;
762 }
763
764 function getPromotedProduct($game)
765 {
766     if (!isset($game->products) || !count($game->products)) {
767         return null;
768     }
769     foreach ($game->products as $gameProd) {
770         if ($gameProd->promoted) {
771             return $gameProd;
772         }
773     }
774     return null;
775 }
776
777 function removeMakeGenres($genres)
778 {
779     $filtered = [];
780     foreach ($genres as $genre) {
781         if ($genre != 'Tutorials' && $genre != 'Builds') {
782             $filtered[] = $genre;
783         }
784     }
785     return $filtered;
786 }
787
788 function writeJson($path, $data)
789 {
790     global $wwwDir;
791     $fullPath = $wwwDir . $path;
792     $dir = dirname($fullPath);
793     if (!is_dir($dir)) {
794         mkdir($dir, 0777, true);
795     }
796     file_put_contents(
797         $fullPath,
798         json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
799     );
800 }
801
802 function error($msg)
803 {
804     fwrite(STDERR, $msg . "\n");
805     exit(1);
806 }
807 ?>