working categories
[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.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     $genres = getAllGenres($games);
123     sort($genres);
124     $genreChunks = array_chunk($genres, 4);
125     $first = true;
126     foreach ($genreChunks as $chunk) {
127         addDiscoverRow(
128             $data, $first ? 'Genres' : '',
129             $chunk
130         );
131         $first = false;
132     }
133
134     foreach ($genres as $genre) {
135         writeJson(
136             'api/v1/discover-data/' . categoryPath($genre) . '.json',
137             buildDiscoverCategory($genre, filterByGenre($games, $genre))
138         );
139     }
140
141     return $data;
142 }
143
144 /**
145  * A genre category page
146  */
147 function buildDiscoverCategory($name, $games)
148 {
149     $data = [
150         'title' => $name,
151         'rows'  => [],
152         'tiles' => [],
153     ];
154     addDiscoverRow(
155         $data, 'Last Updated',
156         filterLastUpdated($games, 10)
157     );
158     addDiscoverRow(
159         $data, 'Best rated',
160         filterBestRated($games, 10)
161     );
162
163     usort(
164         $games,
165         function ($gameA, $gameB) {
166             return strcmp($gameB->title, $gameA->title);
167         }
168     );
169     $chunks = array_chunk($games, 4);
170     foreach ($chunks as $chunkGames) {
171         addDiscoverRow($data, '', $chunkGames);
172     }
173
174     return $data;
175 }
176
177 function buildDiscoverHome(array $games)
178 {
179     //we do not want anything here for now
180     $data = [
181         'title' => 'home',
182         'rows'  => [
183             [
184                 'title' => 'FEATURED',
185                 'showPrice' => false,
186                 'ranked'    => false,
187                 'tiles'     => [],
188             ]
189         ],
190         'tiles' => [],
191     ];
192     return $data;
193 }
194
195 /**
196  * Build api/v1/apps/$packageName
197  */
198 function buildApps($game)
199 {
200     $latestRelease = $game->latestRelease;
201
202     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx
203     return [
204         'app' => [
205             'uuid'          => $latestRelease->uuid,
206             'title'         => $game->title,
207             'overview'      => $game->overview,
208             'description'   => $game->description,
209             'gamerNumbers'  => $game->players,
210             'genres'        => $game->genres,
211
212             'website'       => $game->website,
213             'contentRating' => $game->contentRating,
214             'premium'       => $game->premium,
215             'firstPublishedAt' => $game->firstPublishedAt,
216
217             'likeCount'     => $game->rating->likeCount,
218             'ratingAverage' => $game->rating->average,
219             'ratingCount'   => $game->rating->count,
220
221             'versionNumber' => $latestRelease->name,
222             'latestVersion' => $latestRelease->uuid,
223             'md5sum'        => $latestRelease->md5sum,
224             'apkFileSize'   => $latestRelease->size,
225             'publishedAt'   => $latestRelease->date,
226             'publicSize'    => $latestRelease->publicSize,
227             'nativeSize'    => $latestRelease->nativeSize,
228
229             'mainImageFullUrl' => $game->media->discover,
230             'videoUrl'         => $game->media->video,
231             'filepickerScreenshots' => $game->media->screenshots,
232             'mobileAppIcon'    => null,
233
234             'developer'           => $game->developer->name,
235             'supportEmailAddress' => $game->developer->supportEmail,
236             'supportPhone'        => $game->developer->supportPhone,
237             'founder'             => $game->developer->founder,
238
239             'promotedProduct' => null,
240         ],
241     ];
242 }
243
244 function buildAppDownload($game, $release)
245 {
246     return [
247         'app' => [
248             'fileSize'      => $release->size,
249             'version'       => $release->uuid,
250             'contentRating' => $game->contentRating,
251             'downloadLink'  => $release->url,
252         ]
253     ];
254 }
255
256 /**
257  * Build /app/v1/details?app=org.example.game
258  */
259 function buildDetails($game)
260 {
261     $latestRelease = $game->latestRelease;
262
263     $mediaTiles = [];
264     if ($game->media->discover) {
265         $mediaTiles[] = [
266             'type' => 'image',
267             'urls' => [
268                 'thumbnail' => $game->media->discover,
269                 'full'      => $game->media->discover,
270             ],
271             'fp_url' => $game->media->discover,
272         ];
273     }
274     if ($game->media->video) {
275         $mediaTiles[] = [
276             'type' => 'video',
277             'url'  => $game->media->video,
278         ];
279     }
280     foreach ($game->media->screenshots as $screenshot) {
281         $mediaTiles[] = [
282             'type' => 'image',
283             'urls' => [
284                 'thumbnail' => $screenshot,
285                 'full'      => $screenshot,
286             ],
287             'fp_url' => $screenshot,
288         ];
289     }
290
291     // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
292     return [
293         'type'             => 'Game',
294         'title'            => $game->title,
295         'description'      => $game->description,
296         'gamerNumbers'     => $game->players,
297         'genres'           => $game->genres,
298
299         'suggestedAge'     => $game->contentRating,
300         'premium'          => $game->premium,
301         'inAppPurchases'   => $game->inAppPurchases,
302         'firstPublishedAt' => strtotime($game->firstPublishedAt),
303         'ccUrl'            => null,
304
305         'rating' => [
306             'count'   => $game->rating->count,
307             'average' => $game->rating->average,
308         ],
309
310         'apk' => [
311             'fileSize'    => $latestRelease->size,
312             'nativeSize'  => $latestRelease->nativeSize,
313             'publicSize'  => $latestRelease->publicSize,
314             'md5sum'      => $latestRelease->md5sum,
315             'filename'    => 'FIXME',
316             'errors'      => '',
317             'package'     => $game->packageName,
318             'versionCode' => $latestRelease->versionCode,
319             'state'       => 'complete',
320         ],
321
322         'version' => [
323             'number'      => $latestRelease->name,
324             'publishedAt' => strtotime($latestRelease->date),
325             'uuid'        => $latestRelease->uuid,
326         ],
327
328         'developer' => [
329             'name'    => $game->developer->name,
330             'founder' => $game->developer->founder,
331         ],
332
333         'metaData' => [
334             'key:rating.average',
335             'key:developer.name',
336             'key:suggestedAge',
337             number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB',
338         ],
339
340         'tileImage'     => $game->media->discover,
341         'mediaTiles'    => $mediaTiles,
342         'mobileAppIcon' => null,
343         'heroImage'     => [
344             'url' => null,
345         ],
346
347         'promotedProduct' => null,
348     ];
349 }
350
351 function addDiscoverRow(&$data, $title, $games)
352 {
353     $row = [
354         'title'     => $title,
355         'showPrice' => false,
356         'ranked'    => false,
357         'tiles'     => [],
358     ];
359     foreach ($games as $game) {
360         if (is_string($game)) {
361             //category link
362             $tilePos = count($data['tiles']);
363             $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
364
365         } else {
366             //game
367             $tilePos = findTile($data['tiles'], $game->packageName);
368             if ($tilePos === null) {
369                 $tilePos = count($data['tiles']);
370                 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
371             }
372         }
373         $row['tiles'][] = $tilePos;
374     }
375     $data['rows'][] = $row;
376 }
377
378 function findTile($tiles, $packageName)
379 {
380     foreach ($tiles as $pos => $tile) {
381         if ($tile['package'] == $packageName) {
382             return $pos;
383         }
384     }
385     return null;
386 }
387
388 function buildDiscoverCategoryTile($title)
389 {
390     return [
391         'url'   => 'ouya://launcher/discover/' . categoryPath($title),
392         'image' => '',
393         'title' => $title,
394         'type'  => 'discover'
395     ];
396 }
397
398 function buildDiscoverGameTile($game)
399 {
400     $latestRelease = $game->latestRelease;
401     return [
402         'gamerNumbers' => $game->players,
403         'genres' => $game->genres,
404         'url' => 'ouya://launcher/details?app=' . $game->packageName,
405         'latestVersion' => [
406             'apk' => [
407                 'md5sum' => $latestRelease->md5sum,
408             ],
409             'versionNumber' => $latestRelease->name,
410             'uuid' => $latestRelease->uuid,
411         ],
412         'inAppPurchases' => $game->inAppPurchases,
413         'promotedProduct' => null,
414         'premium' => $game->premium,
415         'type' => 'app',
416         'package' => $game->packageName,
417         'updated_at' => strtotime($latestRelease->date),
418         'updatedAt' => $latestRelease->date,
419         'title' => $game->title,
420         'image' => $game->media->discover,
421         'contentRating' => $game->contentRating,
422         'rating' => [
423             'count' => $game->rating->count,
424             'average' => $game->rating->average,
425         ],
426     ];
427 }
428
429 function categoryPath($title)
430 {
431     return str_replace(['/', '\\', ' '], '_', $title);
432 }
433
434 function getAllGenres($games)
435 {
436     $genres = [];
437     foreach ($games as $game) {
438         $genres = array_merge($genres, $game->genres);
439     }
440     return array_unique($genres);
441 }
442
443 function addMissingGameProperties($game)
444 {
445     if (!isset($game->overview)) {
446         $game->overview = null;
447     }
448     if (!isset($game->description)) {
449         $game->description = '';
450     }
451     if (!isset($game->players)) {
452         $game->players = [1];
453     }
454     if (!isset($game->genres)) {
455         $game->genres = ['Unsorted'];
456     }
457     if (!isset($game->website)) {
458         $game->website = null;
459     }
460     if (!isset($game->contentRating)) {
461         $game->contentRating = 'Everyone';
462     }
463     if (!isset($game->premium)) {
464         $game->premium = false;
465     }
466     if (!isset($game->firstPublishedAt)) {
467         $game->firstPublishedAt = gmdate('c');
468     }
469
470     if (!isset($game->rating)) {
471         $game->rating = new stdClass();
472     }
473     if (!isset($game->rating->likeCount)) {
474         $game->rating->likeCount = 0;
475     }
476     if (!isset($game->rating->average)) {
477         $game->rating->average = 0;
478     }
479     if (!isset($game->rating->count)) {
480         $game->rating->count = 0;
481     }
482
483     $game->latestRelease = null;
484     $latestReleaseTimestamp = 0;
485     foreach ($game->releases as $release) {
486         if (!isset($release->publicSize)) {
487             $release->publicSize = 0;
488         }
489         if (!isset($release->nativeSize)) {
490             $release->nativeSize = 0;
491         }
492
493         $releaseTimestamp = strtotime($release->date);
494         if ($releaseTimestamp > $latestReleaseTimestamp) {
495             $game->latestRelease    = $release;
496             $latestReleaseTimestamp = $releaseTimestamp;
497         }
498     }
499     if ($game->latestRelease === null) {
500         error('No latest release for ' . $game->packageName);
501     }
502
503     if (!isset($game->media->video)) {
504         $game->media->video = null;
505     }
506     if (!isset($game->media->screenshots)) {
507         $game->media->screenshots = [];
508     }
509     if (!isset($game->developer->uuid)) {
510         $game->developer->uuid = null;
511     }
512     if (!isset($game->developer->name)) {
513         $game->developer->name = 'unknown';
514     }
515     if (!isset($game->developer->supportEmail)) {
516         $game->developer->supportEmail = null;
517     }
518     if (!isset($game->developer->supportPhone)) {
519         $game->developer->supportPhone = null;
520     }
521     if (!isset($game->developer->founder)) {
522         $game->developer->founder = false;
523     }
524 }
525
526 function writeJson($path, $data)
527 {
528     global $wwwDir;
529     $fullPath = $wwwDir . $path;
530     $dir = dirname($fullPath);
531     if (!is_dir($dir)) {
532         mkdir($dir, 0777, true);
533     }
534     file_put_contents(
535         $fullPath,
536         json_encode($data, JSON_PRETTY_PRINT) . "\n"
537     );
538 }
539
540 function error($msg)
541 {
542     fwrite(STDERR, $msg . "\n");
543     exit(1);
544 }
545 ?>