Buying games #1: output promoted products
[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 foreach ($gameFiles as $gameFile) {
45     $game = json_decode(file_get_contents($gameFile));
46     if ($game === null) {
47         error('JSON invalid at ' . $gameFile);
48     }
49     addMissingGameProperties($game);
50     $games[$game->packageName] = $game;
51
52     writeJson(
53         'api/v1/details-data/' . $game->packageName . '.json',
54         buildDetails($game)
55     );
56     /* this crashes babylonian twins
57     writeJson(
58         'api/v1/games/' . $game->packageName . '/purchases',
59         "{}\n"
60     );
61     */
62
63     writeJson(
64         'api/v1/apps/' . $game->packageName . '.json',
65         buildApps($game)
66     );
67     $latestRelease = $game->latestRelease;
68     writeJson(
69         'api/v1/apps/' . $latestRelease->uuid . '.json',
70         buildApps($game)
71     );
72
73     writeJson(
74         'api/v1/apps/' . $latestRelease->uuid . '-download.json',
75         buildAppDownload($game, $latestRelease)
76     );
77
78     if ($count++ > 20) {
79         //break;
80     }
81 }
82
83 writeJson('api/v1/discover-data/discover.json', buildDiscover($games));
84 writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games));
85
86
87 function buildDiscover(array $games)
88 {
89     $data = [
90         'title' => 'DISCOVER',
91         'rows'  => [],
92         'tiles' => [],
93     ];
94
95     addDiscoverRow(
96         $data, 'Last Updated',
97         filterLastUpdated($games, 10)
98     );
99     addDiscoverRow(
100         $data, 'Best rated',
101         filterBestRated($games, 10)
102     );
103     addDiscoverRow(
104         $data, "cweiske's picks",
105         filterByPackageNames($games, $GLOBALS['packagelists']['cweiskepicks'])
106     );
107
108     $players = [
109         //1 => '1 player',
110         2 => '2 players',
111         3 => '3 players',
112         4 => '4 players',
113     ];
114     addDiscoverRow($data, '# of players', $players);
115     foreach ($players as $num => $title) {
116         writeJson(
117             'api/v1/discover-data/' . categoryPath($title) . '.json',
118             buildDiscoverCategory($title, filterByPlayers($games, $num))
119         );
120     }
121
122     $ages = getAllAges($games);
123     natsort($ages);
124     addDiscoverRow($data, 'Content rating', $ages);
125     foreach ($ages as $num => $title) {
126         writeJson(
127             'api/v1/discover-data/' . categoryPath($title) . '.json',
128             buildDiscoverCategory($title, filterByAge($games, $title))
129         );
130     }
131
132     $genres = getAllGenres($games);
133     sort($genres);
134     addChunkedDiscoverRows($data, $genres, 'Genres');
135
136     foreach ($genres as $genre) {
137         writeJson(
138             'api/v1/discover-data/' . categoryPath($genre) . '.json',
139             buildDiscoverCategory($genre, filterByGenre($games, $genre))
140         );
141     }
142
143     $abc = array_merge(range('A', 'Z'), ['Other']);
144     addChunkedDiscoverRows($data, $abc, 'Alphabetical');
145     foreach ($abc as $letter) {
146         writeJson(
147             'api/v1/discover-data/' . categoryPath($letter) . '.json',
148             buildDiscoverCategory($letter, filterByLetter($games, $letter))
149         );
150     }
151
152     return $data;
153 }
154
155 /**
156  * A genre category page
157  */
158 function buildDiscoverCategory($name, $games)
159 {
160     $data = [
161         'title' => $name,
162         'rows'  => [],
163         'tiles' => [],
164     ];
165     addDiscoverRow(
166         $data, 'Last Updated',
167         filterLastUpdated($games, 10)
168     );
169     addDiscoverRow(
170         $data, 'Best rated',
171         filterBestRated($games, 10)
172     );
173
174     usort(
175         $games,
176         function ($gameA, $gameB) {
177             return strcmp($gameA->title, $gameB->title);
178         }
179     );
180     $chunks = array_chunk($games, 4);
181     foreach ($chunks as $chunkGames) {
182         addDiscoverRow($data, '', $chunkGames);
183     }
184
185     return $data;
186 }
187
188 function buildDiscoverHome(array $games)
189 {
190     //we do not want anything here for now
191     $data = [
192         'title' => 'home',
193         'rows'  => [
194             [
195                 'title' => 'FEATURED',
196                 'showPrice' => false,
197                 'ranked'    => false,
198                 'tiles'     => [],
199             ]
200         ],
201         'tiles' => [],
202     ];
203     return $data;
204 }
205
206 /**
207  * Build api/v1/apps/$packageName
208  */
209 function buildApps($game)
210 {
211     $latestRelease = $game->latestRelease;
212
213     $product      = null;
214     $gamePromoted = getPromotedProduct($game);
215     if ($gamePromoted) {
216         $product = [
217             'type'          => 'entitlement',
218             'identifier'    => $gamePromoted->identifier,
219             'name'          => $gamePromoted->name,
220             'description'   => $gamePromoted->description ?? '',
221             'localPrice'    => $gamePromoted->localPrice,
222             'originalPrice' => $gamePromoted->originalPrice,
223             'percentOff'    => 0,
224             'currency'      => $gamePromoted->currency,
225         ];
226     }
227
228     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx
229     return [
230         'app' => [
231             'uuid'          => $latestRelease->uuid,
232             'title'         => $game->title,
233             'overview'      => $game->overview,
234             'description'   => $game->description,
235             'gamerNumbers'  => $game->players,
236             'genres'        => $game->genres,
237
238             'website'       => $game->website,
239             'contentRating' => $game->contentRating,
240             'premium'       => $game->premium,
241             'firstPublishedAt' => $game->firstPublishedAt,
242
243             'likeCount'     => $game->rating->likeCount,
244             'ratingAverage' => $game->rating->average,
245             'ratingCount'   => $game->rating->count,
246
247             'versionNumber' => $latestRelease->name,
248             'latestVersion' => $latestRelease->uuid,
249             'md5sum'        => $latestRelease->md5sum,
250             'apkFileSize'   => $latestRelease->size,
251             'publishedAt'   => $latestRelease->date,
252             'publicSize'    => $latestRelease->publicSize,
253             'nativeSize'    => $latestRelease->nativeSize,
254
255             'mainImageFullUrl' => $game->discover,
256             'videoUrl'         => getFirstVideoUrl($game->media),
257             'filepickerScreenshots' => getAllImageUrls($game->media),
258             'mobileAppIcon'    => null,
259
260             'developer'           => $game->developer->name,
261             'supportEmailAddress' => $game->developer->supportEmail,
262             'supportPhone'        => $game->developer->supportPhone,
263             'founder'             => $game->developer->founder,
264
265             'promotedProduct' => $product,
266         ],
267     ];
268 }
269
270 function buildAppDownload($game, $release)
271 {
272     return [
273         'app' => [
274             'fileSize'      => $release->size,
275             'version'       => $release->uuid,
276             'contentRating' => $game->contentRating,
277             'downloadLink'  => $release->url,
278         ]
279     ];
280 }
281
282 /**
283  * Build /app/v1/details?app=org.example.game
284  */
285 function buildDetails($game)
286 {
287     $latestRelease = $game->latestRelease;
288
289     $mediaTiles = [];
290     if ($game->discover) {
291         $mediaTiles[] = [
292             'type' => 'image',
293             'urls' => [
294                 'thumbnail' => $game->discover,
295                 'full'      => $game->discover,
296             ],
297         ];
298     }
299     foreach ($game->media as $medium) {
300         if ($medium->type == 'image')  {
301             $mediaTiles[] = [
302                 'type' => 'image',
303                 'urls' => [
304                     'thumbnail' => $medium->thumb,
305                     'full'      => $medium->url,
306                 ],
307             ];
308         } else {
309             $mediaTiles[] = [
310                 'type' => 'video',
311                 'url'  => $medium->url,
312             ];
313         }
314     }
315
316     $buttons = [];
317     if (isset($game->links->unlocked)) {
318         $buttons[] = [
319             'text' => 'Show unlocked',
320             'url'  => 'ouya://launcher/details?app=' . $game->links->unlocked,
321             'bold' => true,
322         ];
323     }
324
325     $product      = null;
326     $gamePromoted = getPromotedProduct($game);
327     if ($gamePromoted) {
328         $product = [
329             'type'          => 'entitlement',
330             'identifier'    => $gamePromoted->identifier,
331             'name'          => $gamePromoted->name,
332             'description'   => $gamePromoted->description ?? '',
333             'localPrice'    => $gamePromoted->localPrice,
334             'originalPrice' => $gamePromoted->originalPrice,
335             'percentOff'    => 0,
336             'currency'      => $gamePromoted->currency,
337         ];
338     }
339
340     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
341     return [
342         'type'             => 'Game',
343         'title'            => $game->title,
344         'description'      => $game->description,
345         'gamerNumbers'     => $game->players,
346         'genres'           => $game->genres,
347
348         'suggestedAge'     => $game->contentRating,
349         'premium'          => $game->premium,
350         'inAppPurchases'   => $game->inAppPurchases,
351         'firstPublishedAt' => strtotime($game->firstPublishedAt),
352         'ccUrl'            => null,
353
354         'rating' => [
355             'count'   => $game->rating->count,
356             'average' => $game->rating->average,
357         ],
358
359         'apk' => [
360             'fileSize'    => $latestRelease->size,
361             'nativeSize'  => $latestRelease->nativeSize,
362             'publicSize'  => $latestRelease->publicSize,
363             'md5sum'      => $latestRelease->md5sum,
364             'filename'    => 'FIXME',
365             'errors'      => '',
366             'package'     => $game->packageName,
367             'versionCode' => $latestRelease->versionCode,
368             'state'       => 'complete',
369         ],
370
371         'version' => [
372             'number'      => $latestRelease->name,
373             'publishedAt' => strtotime($latestRelease->date),
374             'uuid'        => $latestRelease->uuid,
375         ],
376
377         'developer' => [
378             'name'    => $game->developer->name,
379             'founder' => $game->developer->founder,
380         ],
381
382         'metaData' => [
383             'key:rating.average',
384             'key:developer.name',
385             'key:suggestedAge',
386             number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB',
387         ],
388
389         'tileImage'     => $game->discover,
390         'mediaTiles'    => $mediaTiles,
391         'mobileAppIcon' => null,
392         'heroImage'     => [
393             'url' => null,
394         ],
395
396         'promotedProduct' => $product,
397         'buttons'         => $buttons,
398     ];
399 }
400
401 function addChunkedDiscoverRows(&$data, $games, $title)
402 {
403     $chunks = array_chunk($games, 4);
404     $first = true;
405     foreach ($chunks as $chunk) {
406         addDiscoverRow(
407             $data, $first ? $title : '',
408             $chunk
409         );
410         $first = false;
411     }
412 }
413
414 function addDiscoverRow(&$data, $title, $games)
415 {
416     $row = [
417         'title'     => $title,
418         'showPrice' => false,
419         'ranked'    => false,
420         'tiles'     => [],
421     ];
422     foreach ($games as $game) {
423         if (is_string($game)) {
424             //category link
425             $tilePos = count($data['tiles']);
426             $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
427
428         } else {
429             //game
430             if (isset($game->links->original)) {
431                 //do not link unlocked games.
432                 // people an access them via the original games
433                 continue;
434             }
435             $tilePos = findTile($data['tiles'], $game->packageName);
436             if ($tilePos === null) {
437                 $tilePos = count($data['tiles']);
438                 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
439             }
440         }
441         $row['tiles'][] = $tilePos;
442     }
443     $data['rows'][] = $row;
444 }
445
446 function findTile($tiles, $packageName)
447 {
448     foreach ($tiles as $pos => $tile) {
449         if ($tile['package'] == $packageName) {
450             return $pos;
451         }
452     }
453     return null;
454 }
455
456 function buildDiscoverCategoryTile($title)
457 {
458     return [
459         'url'   => 'ouya://launcher/discover/' . categoryPath($title),
460         'image' => '',
461         'title' => $title,
462         'type'  => 'discover'
463     ];
464 }
465
466 function buildDiscoverGameTile($game)
467 {
468     $latestRelease = $game->latestRelease;
469     return [
470         'gamerNumbers' => $game->players,
471         'genres' => $game->genres,
472         'url' => 'ouya://launcher/details?app=' . $game->packageName,
473         'latestVersion' => [
474             'apk' => [
475                 'md5sum' => $latestRelease->md5sum,
476             ],
477             'versionNumber' => $latestRelease->name,
478             'uuid' => $latestRelease->uuid,
479         ],
480         'inAppPurchases' => $game->inAppPurchases,
481         'promotedProduct' => null,
482         'premium' => $game->premium,
483         'type' => 'app',
484         'package' => $game->packageName,
485         'updated_at' => strtotime($latestRelease->date),
486         'updatedAt' => $latestRelease->date,
487         'title' => $game->title,
488         'image' => $game->discover,
489         'contentRating' => $game->contentRating,
490         'rating' => [
491             'count' => $game->rating->count,
492             'average' => $game->rating->average,
493         ],
494     ];
495 }
496
497 function categoryPath($title)
498 {
499     return str_replace(['/', '\\', ' ', '+'], '_', $title);
500 }
501
502 function getAllAges($games)
503 {
504     $ages = [];
505     foreach ($games as $game) {
506         $ages[] = $game->contentRating;
507     }
508     return array_unique($ages);
509 }
510
511 function getAllGenres($games)
512 {
513     $genres = [];
514     foreach ($games as $game) {
515         $genres = array_merge($genres, $game->genres);
516     }
517     return array_unique($genres);
518 }
519
520 function addMissingGameProperties($game)
521 {
522     if (!isset($game->overview)) {
523         $game->overview = null;
524     }
525     if (!isset($game->description)) {
526         $game->description = '';
527     }
528     if (!isset($game->players)) {
529         $game->players = [1];
530     }
531     if (!isset($game->genres)) {
532         $game->genres = ['Unsorted'];
533     }
534     if (!isset($game->website)) {
535         $game->website = null;
536     }
537     if (!isset($game->contentRating)) {
538         $game->contentRating = 'Everyone';
539     }
540     if (!isset($game->premium)) {
541         $game->premium = false;
542     }
543     if (!isset($game->firstPublishedAt)) {
544         $game->firstPublishedAt = gmdate('c');
545     }
546
547     if (!isset($game->rating)) {
548         $game->rating = new stdClass();
549     }
550     if (!isset($game->rating->likeCount)) {
551         $game->rating->likeCount = 0;
552     }
553     if (!isset($game->rating->average)) {
554         $game->rating->average = 0;
555     }
556     if (!isset($game->rating->count)) {
557         $game->rating->count = 0;
558     }
559
560     $game->latestRelease = null;
561     $latestReleaseTimestamp = 0;
562     foreach ($game->releases as $release) {
563         if (!isset($release->publicSize)) {
564             $release->publicSize = 0;
565         }
566         if (!isset($release->nativeSize)) {
567             $release->nativeSize = 0;
568         }
569
570         $releaseTimestamp = strtotime($release->date);
571         if ($releaseTimestamp > $latestReleaseTimestamp) {
572             $game->latestRelease    = $release;
573             $latestReleaseTimestamp = $releaseTimestamp;
574         }
575     }
576     if ($game->latestRelease === null) {
577         error('No latest release for ' . $game->packageName);
578     }
579
580     if (!isset($game->media)) {
581         $game->media = [];
582     }
583
584     if (!isset($game->developer->uuid)) {
585         $game->developer->uuid = null;
586     }
587     if (!isset($game->developer->name)) {
588         $game->developer->name = 'unknown';
589     }
590     if (!isset($game->developer->supportEmail)) {
591         $game->developer->supportEmail = null;
592     }
593     if (!isset($game->developer->supportPhone)) {
594         $game->developer->supportPhone = null;
595     }
596     if (!isset($game->developer->founder)) {
597         $game->developer->founder = false;
598     }
599 }
600
601 function getFirstVideoUrl($media)
602 {
603     foreach ($media as $medium) {
604         if ($medium->type == 'video') {
605             return $medium->url;
606         }
607     }
608     return null;
609 }
610
611 function getAllImageUrls($media)
612 {
613     $imageUrls = [];
614     foreach ($media as $medium) {
615         if ($medium->type == 'image') {
616             $imageUrls[] = $medium->url;
617         }
618     }
619     return $imageUrls;
620 }
621
622 function getPromotedProduct($game)
623 {
624     if (!isset($game->products) || !count($game->products)) {
625         return null;
626     }
627     foreach ($game->products as $gameProd) {
628         if ($gameProd->promoted) {
629             return $gameProd;
630         }
631     }
632     return null;
633 }
634
635 function writeJson($path, $data)
636 {
637     global $wwwDir;
638     $fullPath = $wwwDir . $path;
639     $dir = dirname($fullPath);
640     if (!is_dir($dir)) {
641         mkdir($dir, 0777, true);
642     }
643     file_put_contents(
644         $fullPath,
645         json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"
646     );
647 }
648
649 function error($msg)
650 {
651     fwrite(STDERR, $msg . "\n");
652     exit(1);
653 }
654 ?>