4 * Import games from a OUYA game data repository
6 * @link https://github.com/cweiske/ouya-game-data/
7 * @author Christian Weiske <cweiske@cweiske.de>
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';
13 //command line option parsing
15 $opts = getopt('h', ['help', 'mini', 'noqr'], $optind);
16 $args = array_slice($argv, $optind);
18 if (isset($opts['help']) || isset($opts['h'])) {
19 echo "Import games from a OUYA game data repository\n";
21 echo "Usage: import-game-data.php [--mini] [--noqr] [--help|-h]\n";
22 echo " --mini Generate small but ugly JSON files\n";
23 echo " --noqr Do not generate and link QR code images\n";
27 if (!isset($args[0])) {
28 error('Pass the path to a "folders" file with game data json files folder names');
30 $foldersFile = $args[0];
31 if (!is_file($foldersFile)) {
32 error('Given path is not a file: ' . $foldersFile);
35 $cfgMini = isset($opts['mini']);
36 $cfgEnableQr = !isset($opts['noqr']);
39 //default configuration values
40 $GLOBALS['baseUrl'] = 'http://ouya.cweiske.de/';
41 $GLOBALS['categorySubtitles'] = [];
42 $GLOBALS['packagelists'] = [];
43 $GLOBALS['urlRewrites'] = [];
44 $cfgFile = __DIR__ . '/../config.php';
45 if (file_exists($cfgFile)) {
49 $wwwDir = __DIR__ . '/../www/';
52 $qrDir = $wwwDir . 'gen-qr/';
53 if (!is_dir($qrDir)) {
58 $baseDir = dirname($foldersFile);
60 foreach (file($foldersFile) as $line) {
63 if (strpos($line, '..') !== false) {
64 error('Path attack in ' . $folder);
66 $folder = $baseDir . '/' . $line;
67 if (!is_dir($folder)) {
68 error('Folder does not exist: ' . $folder);
70 $gameFiles = array_merge($gameFiles, glob($folder . '/*.json'));
74 //store git repository version of last folder
77 $gitDate = `git log --max-count=1 --format="%h %cI"`;
79 file_put_contents($wwwDir . '/game-data-version', $gitDate);
85 //load game data. doing early to collect a developer's games
86 foreach ($gameFiles as $gameFile) {
87 $game = json_decode(file_get_contents($gameFile));
89 error('JSON invalid at ' . $gameFile);
91 addMissingGameProperties($game);
92 $games[$game->packageName] = $game;
94 if (!isset($developers[$game->developer->uuid])) {
95 $developers[$game->developer->uuid] = [
96 'info' => $game->developer,
101 $developers[$game->developer->uuid]['gameNames'][] = $game->packageName;
104 //write json api files
105 foreach ($games as $game) {
106 $products = $game->products ?? [];
107 foreach ($products as $product) {
109 'api/v1/developers/' . $game->developer->uuid
110 . '/products/' . $product->identifier . '.json',
111 buildDeveloperProductOnly($product, $game->developer)
113 $developers[$game->developer->uuid]['products'][] = $product;
117 'api/v1/details-data/' . $game->packageName . '.json',
120 count($developers[$game->developer->uuid]['gameNames']) > 1
125 'api/v1/games/' . $game->packageName . '/purchases',
126 buildPurchases($game)
130 'api/v1/apps/' . $game->packageName . '.json',
133 $latestRelease = $game->latestRelease;
135 'api/v1/apps/' . $latestRelease->uuid . '.json',
140 'api/v1/apps/' . $latestRelease->uuid . '-download.json',
141 buildAppDownload($game, $latestRelease)
149 calculateRank($games);
151 foreach ($developers as $developer) {
153 //index.htm does not need a rewrite rule
154 'api/v1/developers/' . $developer['info']->uuid
155 . '/products/index.htm',
156 buildDeveloperProducts($developer['products'], $developer['info'])
159 'api/v1/developers/' . $developer['info']->uuid
161 buildDeveloperCurrentGamer()
164 if (count($developer['gameNames']) > 1) {
166 'api/v1/discover-data/dev--' . $developer['info']->uuid . '.json',
167 buildSpecialCategory(
168 'Developer: ' . $developer['info']->name,
169 filterByPackageNames($games, $developer['gameNames'])
175 $data = buildDiscover($games);
176 writeJson('api/v1/discover-data/discover.json', $data);
177 writeJson('api/v1/discover-data/discover.forge.json', convertCategoryToForge($data));
178 writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games));
182 'api/v1/discover-data/tutorials.json',
183 buildMakeCategory('Tutorials', filterByGenre($games, 'Tutorials'))
186 $searchLetters = 'abcdefghijklmnopqrstuvwxyz0123456789., ';
187 foreach (str_split($searchLetters) as $letter) {
188 $letterGames = filterBySearchWord($games, $letter);
190 'api/v1/search-data/' . $letter . '.json',
191 buildSearch($letterGames)
196 function buildDiscover(array $games)
198 $games = removeMakeGames($games);
200 'title' => 'DISCOVER',
207 filterLastAdded($games, 10)
210 $data, 'Best rated games',
211 filterBestRatedGames($games, 10),
215 foreach ($GLOBALS['packagelists'] as $listTitle => $listPackageNames) {
218 filterByPackageNames($games, $listPackageNames)
233 'api/v1/discover-data/' . categoryPath('Best rated') . '.json',
234 buildSpecialCategory('Best rated', filterBestRated($games, 99))
237 'api/v1/discover-data/' . categoryPath('Best rated games') . '.json',
238 buildSpecialCategory('Best rated games', filterBestRatedGames($games, 99))
241 'api/v1/discover-data/' . categoryPath('Most rated') . '.json',
242 buildSpecialCategory('Most rated', filterMostDownloaded($games, 99))
245 'api/v1/discover-data/' . categoryPath('Random') . '.json',
246 buildSpecialCategory(
247 'Random ' . date('Y-m-d H:i'),
248 filterRandom($games, 99)
252 'api/v1/discover-data/' . categoryPath('Last updated') . '.json',
253 buildSpecialCategory('Last updated', filterLastUpdated($games, 99))
262 addDiscoverRow($data, 'Multiplayer', $players);
263 foreach ($players as $num => $title) {
265 'api/v1/discover-data/' . categoryPath($title) . '.json',
266 buildDiscoverCategory(
268 //I do not want emulators here,
269 // and neither Streaming apps
272 filterByPlayers($games, $num),
281 $ages = getAllAges($games);
283 addDiscoverRow($data, 'Content rating', $ages);
284 foreach ($ages as $num => $title) {
286 'api/v1/discover-data/' . categoryPath($title) . '.json',
287 buildDiscoverCategory($title, filterByAge($games, $title))
291 $genres = removeMakeGenres(getAllGenres($games));
293 addChunkedDiscoverRows($data, $genres, 'Genres');
295 foreach ($genres as $genre) {
297 'api/v1/discover-data/' . categoryPath($genre) . '.json',
298 buildDiscoverCategory($genre, filterByGenre($games, $genre))
302 $abc = array_merge(range('A', 'Z'), ['Other']);
303 addChunkedDiscoverRows($data, $abc, 'Alphabetical');
304 foreach ($abc as $letter) {
306 'api/v1/discover-data/' . categoryPath($letter) . '.json',
307 buildDiscoverCategory($letter, filterByLetter($games, $letter))
315 * A genre category page
317 function buildDiscoverCategory($name, $games)
324 if (isset($GLOBALS['categorySubtitles'][$name])) {
325 $data['stouyapi']['subtitle'] = $GLOBALS['categorySubtitles'][$name];
328 if (count($games) >= 20) {
330 $data, 'Last updated',
331 filterLastUpdated($games, 10)
335 filterBestRated($games, 10),
340 $games = sortByTitle($games);
341 $chunks = array_chunk($games, 4);
343 foreach ($chunks as $chunkGames) {
344 addDiscoverRow($data, $title, $chunkGames);
352 * Modify a category to make it suitable for the Razer Forge TV
354 * - Fold rows without title into the previous row
355 * - Remove automatically generated categories ("Last updated", "Best rated")
357 * @see buildDiscoverCategory()
359 function convertCategoryToForge($data, $removeAutoCategories = false)
361 //merge tiles from rows without title into the previous row
362 $lastTitleRowId = null;
363 foreach ($data['rows'] as $rowId => $row) {
364 if ($row['title'] !== '') {
365 $lastTitleRowId = $rowId;
366 } else if ($lastTitleRowId !== null) {
367 $data['rows'][$lastTitleRowId]['tiles'] = array_merge(
368 $data['rows'][$lastTitleRowId]['tiles'],
371 unset($data['rows'][$rowId]);
375 if ($removeAutoCategories) {
376 foreach ($data['rows'] as $rowId => $row) {
377 if ($row['title'] === 'Last updated'
378 || $row['title'] === 'Best rated'
380 unset($data['rows'][$rowId]);
385 $data['rows'] = array_values($data['rows']);
390 function buildMakeCategory($name, $games)
398 $games = sortByTitle($games);
399 addDiscoverRow($data, '', $games);
405 * Category without the "Last updated" or "Best rated" top rows
407 * Used for "Best rated", "Most rated", "Random"
409 function buildSpecialCategory($name, $games)
417 $first3 = array_slice($games, 0, 3);
418 $chunks = array_chunk(array_slice($games, 3), 4);
419 array_unshift($chunks, $first3);
421 foreach ($chunks as $chunkGames) {
422 addDiscoverRow($data, '', $chunkGames);
428 function buildDiscoverHome(array $games)
437 if (isset($GLOBALS['home'])) {
438 reset($GLOBALS['home']);
439 $title = key($GLOBALS['home']);
442 filterByPackageNames($games, $GLOBALS['home'][$title])
446 'title' => 'FEATURED',
447 'showPrice' => false,
457 * Build api/v1/apps/$packageName
459 function buildApps($game)
461 $latestRelease = $game->latestRelease;
464 $gamePromoted = getPromotedProduct($game);
466 $product = buildProduct($gamePromoted);
469 // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx
472 'uuid' => $latestRelease->uuid,
473 'title' => $game->title,
474 'overview' => $game->overview,
475 'description' => $game->description,
476 'gamerNumbers' => $game->players,
477 'genres' => $game->genres,
479 'website' => $game->website,
480 'contentRating' => $game->contentRating,
481 'premium' => $game->premium,
482 'firstPublishedAt' => $game->firstPublishedAt,
484 'likeCount' => $game->rating->likeCount,
485 'ratingAverage' => $game->rating->average,
486 'ratingCount' => $game->rating->count,
488 'versionNumber' => $latestRelease->name,
489 'latestVersion' => $latestRelease->uuid,
490 'md5sum' => $latestRelease->md5sum,
491 'apkFileSize' => $latestRelease->size,
492 'publishedAt' => $latestRelease->date,
493 'publicSize' => $latestRelease->publicSize,
494 'nativeSize' => $latestRelease->nativeSize,
496 'mainImageFullUrl' => $game->discover,
497 'videoUrl' => getFirstVideoUrl($game->media),
498 'filepickerScreenshots' => getAllImageUrls($game->media),
499 'mobileAppIcon' => null,
501 'developer' => $game->developer->name,
502 'supportEmailAddress' => $game->developer->supportEmail,
503 'supportPhone' => $game->developer->supportPhone,
504 'founder' => $game->developer->founder,
506 'promotedProduct' => $product,
511 function buildAppDownload($game, $release)
515 'fileSize' => $release->size,
516 'version' => $release->uuid,
517 'contentRating' => $game->contentRating,
518 'downloadLink' => $release->url,
523 function buildProduct($product)
525 if ($product === null) {
529 'type' => $product->type ?? 'entitlement',
530 'identifier' => $product->identifier,
531 'name' => $product->name,
532 'description' => $product->description ?? '',
533 'localPrice' => $product->localPrice,
534 'originalPrice' => $product->originalPrice,
535 'priceInCents' => $product->originalPrice * 100,
537 'currency' => $product->currency,
542 * Build /app/v1/details?app=org.example.game
544 function buildDetails($game, $linkDeveloperPage = false)
546 $latestRelease = $game->latestRelease;
549 if ($game->discover) {
553 'thumbnail' => $game->discover,
554 'full' => $game->discover,
558 foreach ($game->media as $medium) {
559 if ($medium->type == 'image') {
563 'thumbnail' => $medium->thumb ?? $medium->url,
564 'full' => $medium->url,
568 if (!isUnsupportedVideoUrl($medium->url)) {
571 'url' => $medium->url,
578 if (isset($game->links->unlocked)) {
580 'text' => 'Show unlocked',
581 'url' => 'ouya://launcher/details?app=' . $game->links->unlocked,
587 $gamePromoted = getPromotedProduct($game);
589 $product = buildProduct($gamePromoted);
593 if (isset($game->latestRelease->url)
594 && substr($game->latestRelease->url, 0, 29) == 'https://archive.org/download/'
596 $iaUrl = dirname($game->latestRelease->url) . '/';
599 $description = $game->description;
600 if (isset($game->notes) && trim($game->notes)) {
601 $description = "Technical notes:\r\n" . $game->notes
606 // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details
609 'title' => $game->title,
610 'description' => $description,
611 'gamerNumbers' => $game->players,
612 'genres' => $game->genres,
614 'suggestedAge' => $game->contentRating,
615 'premium' => $game->premium,
616 'inAppPurchases' => $game->inAppPurchases,
617 'firstPublishedAt' => strtotime($game->firstPublishedAt),
621 'count' => $game->rating->count,
622 'average' => $game->rating->average,
626 'fileSize' => $latestRelease->size,
627 'nativeSize' => $latestRelease->nativeSize,
628 'publicSize' => $latestRelease->publicSize,
629 'md5sum' => $latestRelease->md5sum,
630 'filename' => 'FIXME',
632 'package' => $game->packageName,
633 'versionCode' => $latestRelease->versionCode,
634 'state' => 'complete',
638 'number' => $latestRelease->name,
639 'publishedAt' => strtotime($latestRelease->date),
640 'uuid' => $latestRelease->uuid,
644 'name' => $game->developer->name,
645 'founder' => $game->developer->founder,
649 'key:rating.average',
650 'key:developer.name',
652 number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB',
655 'tileImage' => $game->discover,
656 'mediaTiles' => $mediaTiles,
657 'mobileAppIcon' => null,
662 'promotedProduct' => $product,
663 'buttons' => $buttons,
666 'internet-archive' => $iaUrl,
667 'developer-url' => $game->developer->website ?? null,
671 if ($linkDeveloperPage) {
672 $data['developer']['url'] = 'ouya://launcher/discover/dev--'
673 . categoryPath($game->developer->uuid);
679 function buildDeveloperCurrentGamer()
683 'uuid' => '00702342-0000-1111-2222-c3e1500cafe2',
684 'username' => 'stouyapi',
690 * For /api/v1/developers/xxx/products/?only=yyy
692 function buildDeveloperProductOnly($product, $developer)
695 'developerName' => $developer->name,
696 'currency' => $product->currency,
698 buildProduct($product),
704 * For /api/v1/developers/xxx/products/
706 function buildDeveloperProducts($products, $developer)
709 $products = array_values(array_column($products, null, 'identifier'));
712 foreach ($products as $product) {
713 $jsonProducts[] = buildProduct($product);
716 'developerName' => $developer->name,
717 'currency' => $products[0]->currency ?? 'EUR',
718 'products' => $jsonProducts,
722 function buildPurchases($game)
727 $promotedProduct = getPromotedProduct($game);
728 if ($promotedProduct) {
729 $purchasesData['purchases'][] = [
730 'purchaseDate' => time() * 1000,
731 'generateDate' => time() * 1000,
732 'identifier' => $promotedProduct->identifier,
733 'gamer' => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid
734 'uuid' => '00702342-0000-1111-2222-c3e1500beef3',//transaction ID
735 'priceInCents' => $promotedProduct->originalPrice * 100,
736 'localPrice' => $promotedProduct->localPrice,
737 'currency' => $promotedProduct->currency,
741 $encryptedOnce = dummyEncrypt($purchasesData);
742 $encryptedTwice = dummyEncrypt($encryptedOnce);
743 return $encryptedTwice;
746 function buildSearch($games)
748 $games = sortByTitle($games);
750 foreach ($games as $game) {
752 'title' => $game->title,
753 'url' => 'ouya://launcher/details?app=' . $game->packageName,
754 'contentRating' => $game->contentRating,
758 'count' => count($results),
759 'results' => $results,
763 function dummyEncrypt($data)
766 'key' => base64_encode('0123456789abcdef'),
767 'iv' => 't3jir1LHpICunvhlM76edQ==',//random bytes
768 'blob' => base64_encode(
769 json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
774 function addChunkedDiscoverRows(&$data, $games, $title)
776 $chunks = array_chunk($games, 4);
778 foreach ($chunks as $chunk) {
780 $data, $first ? $title : '',
787 function addDiscoverRow(&$data, $title, $games, $ranked = false)
795 foreach ($games as $game) {
796 if (is_string($game)) {
798 $tilePos = count($data['tiles']);
799 $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game);
803 if (isset($game->links->original)) {
804 //do not link unlocked games.
805 // people an access them via the original games
808 $tilePos = findTile($data['tiles'], $game->packageName);
809 if ($tilePos === null) {
810 $tilePos = count($data['tiles']);
811 $data['tiles'][$tilePos] = buildDiscoverGameTile($game);
814 $row['tiles'][] = $tilePos;
816 $data['rows'][] = $row;
819 function findTile($tiles, $packageName)
821 foreach ($tiles as $pos => $tile) {
822 if ($tile['package'] == $packageName) {
829 function buildDiscoverCategoryTile($title)
832 'url' => 'ouya://launcher/discover/' . categoryPath($title),
839 function buildDiscoverGameTile($game)
841 $latestRelease = $game->latestRelease;
843 'gamerNumbers' => $game->players,
844 'genres' => $game->genres,
845 'url' => 'ouya://launcher/details?app=' . $game->packageName,
848 'md5sum' => $latestRelease->md5sum,
850 'versionNumber' => $latestRelease->name,
851 'uuid' => $latestRelease->uuid,
853 'inAppPurchases' => $game->inAppPurchases,
854 'promotedProduct' => null,
855 'premium' => $game->premium,
857 'package' => $game->packageName,
858 'updated_at' => strtotime($latestRelease->date),
859 'updatedAt' => $latestRelease->date,
860 'title' => $game->title,
861 'image' => $game->discover,
862 'contentRating' => $game->contentRating,
864 'count' => $game->rating->count,
865 'average' => $game->rating->average,
867 'promotedProduct' => buildProduct(getPromotedProduct($game)),
871 function getAllAges($games)
874 foreach ($games as $game) {
875 $ages[] = $game->contentRating;
877 return array_unique($ages);
880 function getAllGenres($games)
883 foreach ($games as $game) {
884 $genres = array_merge($genres, $game->genres);
886 return array_unique($genres);
889 function addMissingGameProperties($game)
893 if (!isset($game->overview)) {
894 $game->overview = null;
896 if (!isset($game->description)) {
897 $game->description = '';
899 if (!isset($game->players)) {
900 $game->players = [1];
902 if (!isset($game->genres)) {
903 $game->genres = ['Unsorted'];
905 if (!isset($game->website)) {
906 $game->website = null;
908 if (!isset($game->contentRating)) {
909 $game->contentRating = 'Everyone';
911 if (!isset($game->premium)) {
912 $game->premium = false;
914 if (!isset($game->firstPublishedAt)) {
915 $game->firstPublishedAt = gmdate('c');
918 if (!isset($game->rating)) {
919 $game->rating = new stdClass();
921 if (!isset($game->rating->likeCount)) {
922 $game->rating->likeCount = 0;
924 if (!isset($game->rating->average)) {
925 $game->rating->average = 0;
927 if (!isset($game->rating->count)) {
928 $game->rating->count = 0;
931 $game->firstRelease = null;
932 $game->latestRelease = null;
933 $firstReleaseTimestamp = null;
934 $latestReleaseTimestamp = 0;
935 foreach ($game->releases as $release) {
936 if (isset($release->broken) && $release->broken) {
939 if (!isset($release->publicSize)) {
940 $release->publicSize = 0;
942 if (!isset($release->nativeSize)) {
943 $release->nativeSize = 0;
946 $releaseTimestamp = strtotime($release->date);
947 if ($releaseTimestamp > $latestReleaseTimestamp) {
948 $game->latestRelease = $release;
949 $latestReleaseTimestamp = $releaseTimestamp;
951 if ($firstReleaseTimestamp === null
952 || $releaseTimestamp < $firstReleaseTimestamp
954 $game->firstRelease = $release;
955 $firstReleaseTimestamp = $releaseTimestamp;
958 if ($game->firstRelease === null) {
959 error('No first release for ' . $game->packageName);
961 if ($game->latestRelease === null) {
962 error('No latest release for ' . $game->packageName);
965 if (!isset($game->media)) {
969 if (!isset($game->developer->uuid)) {
970 $game->developer->uuid = null;
972 if (!isset($game->developer->name)) {
973 $game->developer->name = 'unknown';
975 if (!isset($game->developer->supportEmail)) {
976 $game->developer->supportEmail = null;
978 if (!isset($game->developer->supportPhone)) {
979 $game->developer->supportPhone = null;
981 if (!isset($game->developer->founder)) {
982 $game->developer->founder = false;
985 if ($cfgEnableQr && $game->website) {
986 $qrfileName = preg_replace('#[^\\w\\d._-]#', '_', $game->website) . '.png';
987 $qrfilePath = $GLOBALS['qrDir'] . $qrfileName;
988 if (!file_exists($qrfilePath)) {
989 $cmd = __DIR__ . '/create-qr.sh'
990 . ' ' . escapeshellarg($game->website)
991 . ' ' . escapeshellarg($qrfilePath);
992 passthru($cmd, $retval);
997 $qrUrlPath = $GLOBALS['baseUrl'] . 'gen-qr/' . $qrfileName;
998 $game->media[] = (object) [
1000 'url' => $qrUrlPath,
1004 //rewrite urls from Internet Archive to our servers
1005 $game->discover = rewriteUrl($game->discover);
1006 foreach ($game->media as $medium) {
1007 $medium->url = rewriteUrl($medium->url);
1009 foreach ($game->releases as $release) {
1010 $release->url = rewriteUrl($release->url);
1015 * Implements a sensible ranking system described in
1016 * https://stackoverflow.com/a/1411268/2826013
1018 function calculateRank(array $games)
1020 $averageRatings = array_map(
1022 return $game->rating->average;
1026 $average = array_sum($averageRatings) / count($averageRatings);
1030 foreach ($games as $game) {
1031 $R = $game->rating->average;
1032 $v = $game->rating->count;
1033 $game->rating->rank = ($R * $v + $C * $m) / ($v + $m);
1037 function getFirstVideoUrl($media)
1039 foreach ($media as $medium) {
1040 if ($medium->type == 'video') {
1041 return $medium->url;
1047 function getAllImageUrls($media)
1050 foreach ($media as $medium) {
1051 if ($medium->type == 'image') {
1052 $imageUrls[] = $medium->url;
1058 function getPromotedProduct($game)
1060 if (!isset($game->products) || !count($game->products)) {
1063 foreach ($game->products as $gameProd) {
1064 if ($gameProd->promoted) {
1072 * vimeo only work with HTTPS now,
1073 * and the OUYA does not support SNI.
1074 * We get SSL errors and no video for them :/
1076 function isUnsupportedVideoUrl($url)
1078 return strpos($url, '://vimeo.com/') !== false;
1081 function removeMakeGames(array $games)
1083 return filterByGenre($games, 'Tutorials', true);
1086 function removeMakeGenres($genres)
1089 foreach ($genres as $genre) {
1090 if ($genre != 'Tutorials' && $genre != 'Builds') {
1091 $filtered[] = $genre;
1097 function rewriteUrl($url)
1099 foreach ($GLOBALS['urlRewrites'] as $pattern => $replacement) {
1100 $url = preg_replace($pattern, $replacement, $url);
1105 function writeJson($path, $data)
1107 global $cfgMini, $wwwDir;
1108 $fullPath = $wwwDir . $path;
1109 $dir = dirname($fullPath);
1110 if (!is_dir($dir)) {
1111 mkdir($dir, 0777, true);
1113 $opts = JSON_UNESCAPED_SLASHES;
1115 $opts |= JSON_PRETTY_PRINT;
1119 json_encode($data, $opts) . "\n"
1123 function writeCategoryJson($path, $data)
1125 writeJson($path, $data);
1127 $forgePath = str_replace('.json', '.forge.json', $path);
1128 $forgeData = convertCategoryToForge($data, true);
1129 writeJson($forgePath, $forgeData);
1132 function error($msg)
1134 fwrite(STDERR, $msg . "\n");