X-Git-Url: https://git.cweiske.de/stouyapi.git/blobdiff_plain/78ed9b8b83fa0592501c6333fd31ec02a5335d4d..f19759be619ef2263f7724572ddfde14da47ab9a:/bin/import-game-data.php diff --git a/bin/import-game-data.php b/bin/import-game-data.php index b1ec629..0227b45 100755 --- a/bin/import-game-data.php +++ b/bin/import-game-data.php @@ -6,36 +6,84 @@ * @link https://github.com/cweiske/ouya-game-data/ * @author Christian Weiske */ +ini_set('xdebug.halt_level', E_WARNING|E_NOTICE|E_USER_WARNING|E_USER_NOTICE); +require_once __DIR__ . '/filters.php'; if (!isset($argv[1])) { - error('Pass the path to a directory with game data json files'); + error('Pass the path to a "folders" file with game data json files folder names'); } -$gameDataDir = $argv[1]; -if (!is_dir($gameDataDir)) { - error('Given path is not a directory: ' . $gameDataDir); +$foldersFile = $argv[1]; +if (!is_file($foldersFile)) { + error('Given path is not a file: ' . $foldersFile); } +$GLOBALS['packagelists']['cweiskepicks'] = [ + 'de.eiswuxe.blookid2', + 'com.cosmos.babyloniantwins', + 'com.inverseblue.skyriders', +]; + $wwwDir = __DIR__ . '/../www/'; -$gameFiles = glob($gameDataDir . '/*.json'); +$baseDir = dirname($foldersFile); +$gameFiles = []; +foreach (file($foldersFile) as $line) { + $line = trim($line); + if (strlen($line)) { + if (strpos($line, '..') !== false) { + error('Path attack in ' . $folder); + } + $folder = $baseDir . '/' . $line; + if (!is_dir($folder)) { + error('Folder does not exist: ' . $folder); + } + $gameFiles = array_merge($gameFiles, glob($folder . '/*.json')); + } +} + $games = []; +$count = 0; +$developers = []; foreach ($gameFiles as $gameFile) { $game = json_decode(file_get_contents($gameFile)); if ($game === null) { error('JSON invalid at ' . $gameFile); } addMissingGameProperties($game); - $games[$game->package] = $game; + $games[$game->packageName] = $game; writeJson( - 'api/v1/details-data/' . $game->package . '.json', + 'api/v1/details-data/' . $game->packageName . '.json', buildDetails($game) ); - + + if (!isset($developers[$game->developer->uuid])) { + $developers[$game->developer->uuid] = [ + 'info' => $game->developer, + 'products' => [], + ]; + } + + $products = $game->products ?? []; + foreach ($products as $product) { + writeJson( + 'api/v1/developers/' . $game->developer->uuid + . '/products/' . $product->identifier . '.json', + buildDeveloperProductOnly($product, $game->developer) + ); + $developers[$game->developer->uuid]['products'][] = $product; + } + + /**/ + writeJson( + 'api/v1/games/' . $game->packageName . '/purchases', + buildPurchases($game) + ); + writeJson( - 'api/v1/apps/' . $game->package . '.json', + 'api/v1/apps/' . $game->packageName . '.json', buildApps($game) ); - $latestRelease = getLatestRelease($game); + $latestRelease = $game->latestRelease; writeJson( 'api/v1/apps/' . $latestRelease->uuid . '.json', buildApps($game) @@ -45,24 +93,237 @@ foreach ($gameFiles as $gameFile) { 'api/v1/apps/' . $latestRelease->uuid . '-download.json', buildAppDownload($game, $latestRelease) ); - //exit(2); + if ($count++ > 20) { + //break; + } +} + +calculateRank($games); + +foreach ($developers as $developer) { + writeJson( + //index.htm does not need a rewrite rule + 'api/v1/developers/' . $developer['info']->uuid + . '/products/index.htm', + buildDeveloperProducts($developer['products'], $developer['info']) + ); + writeJson( + //index.htm does not need a rewrite rule + 'api/v1/developers/' . $developer['info']->uuid + . '/current_gamer', + buildDeveloperCurrentGamer() + ); } -writeJson('api/v1/discover.json', buildDiscover($games)); +writeJson('api/v1/discover-data/discover.json', buildDiscover($games)); writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games)); +//make +writeJson( + 'api/v1/discover-data/tutorials.json', + buildMakeCategory('Tutorials', filterByGenre($games, 'Tutorials')) +); + +$searchLetters = 'abcdefghijklmnopqrstuvwxyz0123456789., '; +foreach (str_split($searchLetters) as $letter) { + $letterGames = filterBySearchWord($games, $letter); + writeJson( + 'api/v1/search-data/' . $letter . '.json', + buildSearch($letterGames) + ); +} + + +function buildDiscover(array $games) +{ + $games = removeMakeGames($games); + $data = [ + 'title' => 'DISCOVER', + 'rows' => [], + 'tiles' => [], + ]; + + addDiscoverRow( + $data, 'Last Updated', + filterLastUpdated($games, 10) + ); + addDiscoverRow( + $data, 'Best rated', + filterBestRated($games, 10) + ); + addDiscoverRow( + $data, "cweiske's picks", + filterByPackageNames($games, $GLOBALS['packagelists']['cweiskepicks']) + ); + + addDiscoverRow( + $data, 'Special', + [ + 'Best rated', + 'Most rated', + 'Random', + ] + ); + writeJson( + 'api/v1/discover-data/' . categoryPath('Best rated') . '.json', + buildSpecialCategory('Best rated', filterBestRated($games, 99)) + ); + writeJson( + 'api/v1/discover-data/' . categoryPath('Most rated') . '.json', + buildSpecialCategory('Most rated', filterMostDownloaded($games, 99)) + ); + writeJson( + 'api/v1/discover-data/' . categoryPath('Random') . '.json', + buildSpecialCategory( + 'Random ' . date('Y-m-d H:i'), + filterRandom($games, 99) + ) + ); + + $players = [ + //1 => '1 player', + 2 => '2 players', + 3 => '3 players', + 4 => '4 players', + ]; + addDiscoverRow($data, 'Multiplayer', $players); + foreach ($players as $num => $title) { + writeJson( + 'api/v1/discover-data/' . categoryPath($title) . '.json', + buildDiscoverCategory($title, filterByPlayers($games, $num)) + ); + } + + $ages = getAllAges($games); + natsort($ages); + addDiscoverRow($data, 'Content rating', $ages); + foreach ($ages as $num => $title) { + writeJson( + 'api/v1/discover-data/' . categoryPath($title) . '.json', + buildDiscoverCategory($title, filterByAge($games, $title)) + ); + } + + $genres = removeMakeGenres(getAllGenres($games)); + sort($genres); + addChunkedDiscoverRows($data, $genres, 'Genres'); + + foreach ($genres as $genre) { + writeJson( + 'api/v1/discover-data/' . categoryPath($genre) . '.json', + buildDiscoverCategory($genre, filterByGenre($games, $genre)) + ); + } + + $abc = array_merge(range('A', 'Z'), ['Other']); + addChunkedDiscoverRows($data, $abc, 'Alphabetical'); + foreach ($abc as $letter) { + writeJson( + 'api/v1/discover-data/' . categoryPath($letter) . '.json', + buildDiscoverCategory($letter, filterByLetter($games, $letter)) + ); + } + + return $data; +} + +/** + * A genre category page + */ +function buildDiscoverCategory($name, $games) +{ + $data = [ + 'title' => $name, + 'rows' => [], + 'tiles' => [], + ]; + addDiscoverRow( + $data, 'Last Updated', + filterLastUpdated($games, 10) + ); + addDiscoverRow( + $data, 'Best rated', + filterBestRated($games, 10) + ); + + $games = sortByTitle($games); + $chunks = array_chunk($games, 4); + foreach ($chunks as $chunkGames) { + addDiscoverRow($data, '', $chunkGames); + } + + return $data; +} + +function buildMakeCategory($name, $games) +{ + $data = [ + 'title' => $name, + 'rows' => [], + 'tiles' => [], + ]; + + $games = sortByTitle($games); + addDiscoverRow($data, '', $games); + + return $data; +} + +function buildSpecialCategory($name, $games) +{ + $data = [ + 'title' => $name, + 'rows' => [], + 'tiles' => [], + ]; + + $first3 = array_slice($games, 0, 3); + $chunks = array_chunk(array_slice($games, 3), 4); + array_unshift($chunks, $first3); + + foreach ($chunks as $chunkGames) { + addDiscoverRow($data, '', $chunkGames); + } + + return $data; +} + +function buildDiscoverHome(array $games) +{ + //we do not want anything here for now + $data = [ + 'title' => 'home', + 'rows' => [ + [ + 'title' => 'FEATURED', + 'showPrice' => false, + 'ranked' => false, + 'tiles' => [], + ] + ], + 'tiles' => [], + ]; + return $data; +} + /** - * Build api/v1/apps/$package + * Build api/v1/apps/$packageName */ function buildApps($game) { - $latestRelease = getLatestRelease($game); + $latestRelease = $game->latestRelease; + + $product = null; + $gamePromoted = getPromotedProduct($game); + if ($gamePromoted) { + $product = buildProduct($gamePromoted); + } // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx return [ 'app' => [ - 'uuid' => $game->uuid, + 'uuid' => $latestRelease->uuid, 'title' => $game->title, 'overview' => $game->overview, 'description' => $game->description, @@ -86,9 +347,9 @@ function buildApps($game) 'publicSize' => $latestRelease->publicSize, 'nativeSize' => $latestRelease->nativeSize, - 'mainImageFullUrl' => $game->media->large, - 'videoUrl' => $game->media->video, - 'filepickerScreenshots' => $game->media->screenshots, + 'mainImageFullUrl' => $game->discover, + 'videoUrl' => getFirstVideoUrl($game->media), + 'filepickerScreenshots' => getAllImageUrls($game->media), 'mobileAppIcon' => null, 'developer' => $game->developer->name, @@ -96,7 +357,7 @@ function buildApps($game) 'supportPhone' => $game->developer->supportPhone, 'founder' => $game->developer->founder, - 'promotedProduct' => null, + 'promotedProduct' => $product, ], ]; } @@ -113,41 +374,73 @@ function buildAppDownload($game, $release) ]; } +function buildProduct($product) +{ + if ($product === null) { + return null; + } + return [ + 'type' => $product->type ?? 'entitlement', + 'identifier' => $product->identifier, + 'name' => $product->name, + 'description' => $product->description ?? '', + 'localPrice' => $product->localPrice, + 'originalPrice' => $product->originalPrice, + 'priceInCents' => $product->originalPrice * 100, + 'percentOff' => 0, + 'currency' => $product->currency, + ]; +} + /** * Build /app/v1/details?app=org.example.game */ function buildDetails($game) { - $latestRelease = getLatestRelease($game); + $latestRelease = $game->latestRelease; $mediaTiles = []; - if ($game->media->large) { + if ($game->discover) { $mediaTiles[] = [ 'type' => 'image', 'urls' => [ - 'thumbnail' => $game->media->large, - 'full' => $game->media->large, + 'thumbnail' => $game->discover, + 'full' => $game->discover, ], - 'fp_url' => $game->media->large, ]; } - if ($game->media->video) { - $mediaTiles[] = [ - 'type' => 'video', - 'url' => $game->media->video, - ]; + foreach ($game->media as $medium) { + if ($medium->type == 'image') { + $mediaTiles[] = [ + 'type' => 'image', + 'urls' => [ + 'thumbnail' => $medium->thumb, + 'full' => $medium->url, + ], + ]; + } else { + $mediaTiles[] = [ + 'type' => 'video', + 'url' => $medium->url, + ]; + } } - foreach ($game->media->screenshots as $screenshot) { - $mediaTiles[] = [ - 'type' => 'image', - 'urls' => [ - 'thumbnail' => $screenshot, - 'full' => $screenshot, - ], - 'fp_url' => $screenshot, + + $buttons = []; + if (isset($game->links->unlocked)) { + $buttons[] = [ + 'text' => 'Show unlocked', + 'url' => 'ouya://launcher/details?app=' . $game->links->unlocked, + 'bold' => true, ]; } + $product = null; + $gamePromoted = getPromotedProduct($game); + if ($gamePromoted) { + $product = buildProduct($gamePromoted); + } + // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details return [ 'type' => 'Game', @@ -174,7 +467,7 @@ function buildDetails($game) 'md5sum' => $latestRelease->md5sum, 'filename' => 'FIXME', 'errors' => '', - 'package' => $game->package, + 'package' => $game->packageName, 'versionCode' => $latestRelease->versionCode, 'state' => 'complete', ], @@ -197,69 +490,182 @@ function buildDetails($game) number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB', ], - 'tileImage' => $game->media->discover, + 'tileImage' => $game->discover, 'mediaTiles' => $mediaTiles, 'mobileAppIcon' => null, 'heroImage' => [ 'url' => null, ], - 'promotedProduct' => null, + 'promotedProduct' => $product, + 'buttons' => $buttons, ]; } -function buildDiscover(array $games) +function buildDeveloperCurrentGamer() { - $data = [ - 'title' => 'DISCOVER', - 'rows' => [], - 'tiles' => [], + return [ + 'gamer' => [ + 'uuid' => '00702342-0000-1111-2222-c3e1500cafe2', + 'username' => 'stouyapi', + ], + ]; +} + +/** + * For /api/v1/developers/xxx/products/?only=yyy + */ +function buildDeveloperProductOnly($product, $developer) +{ + return [ + 'developerName' => $developer->name, + 'currency' => $product->currency, + 'products' => [ + buildProduct($product), + ], + ]; +} + +/** + * For /api/v1/developers/xxx/products/ + */ +function buildDeveloperProducts($products, $developer) +{ + $jsonProducts = []; + foreach ($products as $product) { + $jsonProducts[] = buildProduct($product); + } + return [ + 'developerName' => $developer->name, + 'currency' => $products[0]->currency ?? 'EUR', + 'products' => $jsonProducts, ]; - $tileMap = []; +} + +function buildPurchases($game) +{ + $purchasesData = [ + 'purchases' => [], + ]; + $promotedProduct = getPromotedProduct($game); + if ($promotedProduct) { + $purchasesData['purchases'][] = [ + 'purchaseDate' => time() * 1000, + 'generateDate' => time() * 1000, + 'identifier' => $promotedProduct->identifier, + 'gamer' => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid + 'uuid' => '00702342-0000-1111-2222-c3e1500beef3',//transaction ID + 'priceInCents' => $promotedProduct->originalPrice * 100, + 'localPrice' => $promotedProduct->localPrice, + 'currency' => $promotedProduct->currency, + ]; + } + + $encryptedOnce = dummyEncrypt($purchasesData); + $encryptedTwice = dummyEncrypt($encryptedOnce); + return $encryptedTwice; +} + +function buildSearch($games) +{ + $games = sortByTitle($games); + $results = []; + foreach ($games as $game) { + $results[] = [ + 'title' => $game->title, + 'url' => 'ouya://launcher/details?app=' . $game->packageName, + 'contentRating' => $game->contentRating, + ]; + } + return [ + 'count' => count($results), + 'results' => $results, + ]; +} + +function dummyEncrypt($data) +{ + return [ + 'key' => base64_encode('0123456789abcdef') . "\n", + 'iv' => 't3jir1LHpICunvhlM76edQ==' . "\n",//random bytes + 'blob' => base64_encode( + json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ) . "\n", + ]; +} + +function addChunkedDiscoverRows(&$data, $games, $title) +{ + $chunks = array_chunk($games, 4); + $first = true; + foreach ($chunks as $chunk) { + addDiscoverRow( + $data, $first ? $title : '', + $chunk + ); + $first = false; + } +} - $rowAll = [ - 'title' => 'ALL GAMES', - 'showPrice' => false, +function addDiscoverRow(&$data, $title, $games) +{ + $row = [ + 'title' => $title, + 'showPrice' => true, 'ranked' => false, 'tiles' => [], ]; foreach ($games as $game) { - $tilePos = count($tileMap); - $data['tiles'][$tilePos] = buildDiscoverGameTile($game); - $tileMap[$game->package] = $tilePos; - - $rowAll['tiles'][] = $tilePos; + if (is_string($game)) { + //category link + $tilePos = count($data['tiles']); + $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game); + + } else { + //game + if (isset($game->links->original)) { + //do not link unlocked games. + // people an access them via the original games + continue; + } + $tilePos = findTile($data['tiles'], $game->packageName); + if ($tilePos === null) { + $tilePos = count($data['tiles']); + $data['tiles'][$tilePos] = buildDiscoverGameTile($game); + } + } + $row['tiles'][] = $tilePos; } - $data['rows'][] = $rowAll; + $data['rows'][] = $row; +} - return $data; +function findTile($tiles, $packageName) +{ + foreach ($tiles as $pos => $tile) { + if ($tile['package'] == $packageName) { + return $pos; + } + } + return null; } -function buildDiscoverHome(array $games) +function buildDiscoverCategoryTile($title) { - //we do not want anything here for now - $data = [ - 'title' => 'home', - 'rows' => [ - [ - 'title' => 'FEATURED', - 'showPrice' => false, - 'ranked' => false, - 'tiles' => [], - ] - ], - 'tiles' => [], + return [ + 'url' => 'ouya://launcher/discover/' . categoryPath($title), + 'image' => '', + 'title' => $title, + 'type' => 'discover' ]; - return $data; } function buildDiscoverGameTile($game) { - $latestRelease = getLatestRelease($game); + $latestRelease = $game->latestRelease; return [ 'gamerNumbers' => $game->players, 'genres' => $game->genres, - 'url' => 'ouya://launcher/details?app=' . $game->package, + 'url' => 'ouya://launcher/details?app=' . $game->packageName, 'latestVersion' => [ 'apk' => [ 'md5sum' => $latestRelease->md5sum, @@ -271,19 +677,43 @@ function buildDiscoverGameTile($game) 'promotedProduct' => null, 'premium' => $game->premium, 'type' => 'app', - 'package' => $game->package, + 'package' => $game->packageName, 'updated_at' => strtotime($latestRelease->date), 'updatedAt' => $latestRelease->date, 'title' => $game->title, - 'image' => $game->media->discover, + 'image' => $game->discover, 'contentRating' => $game->contentRating, 'rating' => [ 'count' => $game->rating->count, 'average' => $game->rating->average, ], + 'promotedProduct' => buildProduct(getPromotedProduct($game)), ]; } +function categoryPath($title) +{ + return str_replace(['/', '\\', ' ', '+', '?'], '_', $title); +} + +function getAllAges($games) +{ + $ages = []; + foreach ($games as $game) { + $ages[] = $game->contentRating; + } + return array_unique($ages); +} + +function getAllGenres($games) +{ + $genres = []; + foreach ($games as $game) { + $genres = array_merge($genres, $game->genres); + } + return array_unique($genres); +} + function addMissingGameProperties($game) { if (!isset($game->overview)) { @@ -324,6 +754,8 @@ function addMissingGameProperties($game) $game->rating->count = 0; } + $game->latestRelease = null; + $latestReleaseTimestamp = 0; foreach ($game->releases as $release) { if (!isset($release->publicSize)) { $release->publicSize = 0; @@ -331,14 +763,21 @@ function addMissingGameProperties($game) if (!isset($release->nativeSize)) { $release->nativeSize = 0; } - } - if (!isset($game->media->video)) { - $game->media->video = null; + $releaseTimestamp = strtotime($release->date); + if ($releaseTimestamp > $latestReleaseTimestamp) { + $game->latestRelease = $release; + $latestReleaseTimestamp = $releaseTimestamp; + } } - if (!isset($game->media->screenshots)) { - $game->media->screenshots = []; + if ($game->latestRelease === null) { + error('No latest release for ' . $game->packageName); } + + if (!isset($game->media)) { + $game->media = []; + } + if (!isset($game->developer->uuid)) { $game->developer->uuid = null; } @@ -356,19 +795,77 @@ function addMissingGameProperties($game) } } -function getLatestRelease($game) +/** + * Implements a sensible ranking system described in + * https://stackoverflow.com/a/1411268/2826013 + */ +function calculateRank(array $games) { - $latestRelease = null; - foreach ($game->releases as $release) { - if ($release->latest ?? false) { - $latestRelease = $release; - break; + $averageRatings = array_map( + function ($game) { + return $game->rating->average; + }, + $games + ); + $average = array_sum($averageRatings) / count($averageRatings); + $C = $average; + $m = 500; + + foreach ($games as $game) { + $R = $game->rating->average; + $v = $game->rating->count; + $game->rating->rank = ($R * $v + $C * $m) / ($v + $m); + } +} + +function getFirstVideoUrl($media) +{ + foreach ($media as $medium) { + if ($medium->type == 'video') { + return $medium->url; } } - if ($latestRelease === null) { - error('No latest release for ' . $game->package); + return null; +} + +function getAllImageUrls($media) +{ + $imageUrls = []; + foreach ($media as $medium) { + if ($medium->type == 'image') { + $imageUrls[] = $medium->url; + } + } + return $imageUrls; +} + +function getPromotedProduct($game) +{ + if (!isset($game->products) || !count($game->products)) { + return null; + } + foreach ($game->products as $gameProd) { + if ($gameProd->promoted) { + return $gameProd; + } + } + return null; +} + +function removeMakeGames(array $games) +{ + return filterByGenre($games, 'Tutorials', true); +} + +function removeMakeGenres($genres) +{ + $filtered = []; + foreach ($genres as $genre) { + if ($genre != 'Tutorials' && $genre != 'Builds') { + $filtered[] = $genre; + } } - return $latestRelease; + return $filtered; } function writeJson($path, $data) @@ -381,7 +878,7 @@ function writeJson($path, $data) } file_put_contents( $fullPath, - json_encode($data, JSON_PRETTY_PRINT) . "\n" + json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n" ); }