2017-06-03 01:36:17 +00:00
const fs = require ( 'fs' ) ;
const fsextra = require ( 'fs-extra' ) ;
const path = require ( 'path' ) ;
const util = require ( 'util' ) ;
const logger = require ( 'winston' ) ;
2017-06-01 22:00:37 +00:00
2017-06-03 01:36:17 +00:00
const sanitizeHtml = require ( 'sanitize-html' ) ;
2017-06-01 22:00:37 +00:00
2017-06-07 07:32:12 +00:00
const request = require ( 'request-promise' ) ;
2017-06-06 00:46:21 +00:00
const toml = require ( 'toml' ) ;
2017-06-07 04:48:39 +00:00
const tomlify = require ( 'tomlify-j0.4' ) ;
2017-06-03 01:36:17 +00:00
const blackfriday = require ( './blackfriday.js' ) ;
2017-06-01 22:00:37 +00:00
2017-06-03 01:36:17 +00:00
const del = require ( 'delete' ) ;
const exec = require ( 'sync-exec' ) ;
2017-07-29 02:23:07 +00:00
const fsPathCode = './citra-games-wiki/games' ;
2017-06-07 04:48:39 +00:00
const fsPathWiki = './citra-games-wiki.wiki' ;
const fsPathHugoContent = '../../site/content/game' ;
const fsPathHugoBoxart = '../../site/static/images/game/boxart' ;
const fsPathHugoIcon = '../../site/static/images/game/icons' ;
const fsPathHugoScreenshots = '../../site/static/images/screenshots0' ;
const fsPathHugoSavefiles = '../../site/static/savefiles/' ;
2017-06-01 22:00:37 +00:00
2017-06-03 01:36:17 +00:00
function gitPull ( directory , repository ) {
if ( fs . existsSync ( directory ) ) {
logger . info ( ` Fetching latest from Github : ${ directory } ` ) ;
2017-06-06 00:46:21 +00:00
logger . info ( ` git --git-dir= ${ directory } pull ` ) ;
exec ( ` git --git-dir= ${ directory } pull ` ) ;
2017-06-03 01:36:17 +00:00
} else {
logger . info ( ` Cloning repository from Github : ${ directory } ` ) ;
2017-06-06 00:46:21 +00:00
logger . info ( ` git clone ${ repository } ` ) ;
2017-06-03 01:36:17 +00:00
exec ( ` git clone ${ repository } ` ) ;
}
2017-06-01 22:00:37 +00:00
}
2017-06-03 01:36:17 +00:00
function getDirectories ( srcpath ) {
return fs . readdirSync ( srcpath )
. filter ( file => fs . lstatSync ( path . join ( srcpath , file ) ) . isDirectory ( ) )
2017-06-01 22:00:37 +00:00
}
2017-06-07 07:32:12 +00:00
async function getGithubIssues ( ) {
var results = [ ] ;
2017-07-29 02:23:07 +00:00
// Only loop through the first x pages to prevent API limiting.
for ( var page = 0 ; page <= 15 ; page ++ ) {
2017-06-07 07:32:12 +00:00
let options = {
2017-06-10 17:03:55 +00:00
url : ` https://api.github.com/repos/citra-emu/citra/issues?per_page=99&page= ${ page } ` ,
2017-06-07 07:32:12 +00:00
headers : { 'User-Agent' : 'citrabot' }
} ;
logger . verbose ( ` Requesting Github Issue Page ${ page } . ` ) ;
var body = await request . get ( options ) ;
var obj = JSON . parse ( body ) ;
if ( obj == null || obj . length == 0 ) {
logger . verbose ( ` No more Github issues found -- on page ${ page } . ` ) ;
break ;
} else {
results = results . concat ( obj ) ;
}
}
return results ;
}
2017-12-23 02:38:32 +00:00
async function getTestcases ( ) {
let options = {
url : 'https://api.citra-emu.org/telemetry/testcase/' ,
headers : { 'User-Agent' : 'citrabot' } ,
} ;
var body = await request . get ( options ) ;
return JSON . parse ( body ) . map ( x => {
return {
2018-01-21 02:11:56 +00:00
id : x . id ,
2017-12-23 02:38:32 +00:00
title : x . title ,
compatibility : x . compatibility . toString ( ) ,
date : x . date ,
version : x . version ,
buildName : x . buildName ,
buildDate : x . buildDate ,
cpu : x . cpu ,
gpu : x . gpu ,
os : x . os ,
author : x . author
}
} ) ;
}
2017-06-03 01:36:17 +00:00
// Fetch game information stored in Github repository.
2017-07-29 02:23:07 +00:00
gitPull ( './citra-games-wiki' , 'https://github.com/citra-emu/citra-games-wiki.git' ) ;
2017-06-03 01:36:17 +00:00
// Fetch game articles stored in Github wiki.
2017-07-29 02:23:07 +00:00
gitPull ( './citra-games-wiki.wiki' , 'https://github.com/citra-emu/citra-games-wiki.wiki.git' ) ;
2017-06-01 22:00:37 +00:00
2017-06-07 07:32:12 +00:00
// Fetch all issues from Github.
2017-12-23 02:38:32 +00:00
var githubIssues = [ ] ;
2017-07-27 09:02:02 +00:00
var wikiEntries = { } ;
2017-12-23 02:38:32 +00:00
var testcases = [ ] ;
2017-06-07 07:32:12 +00:00
2017-12-23 02:38:32 +00:00
async function setup ( ) {
2017-12-23 02:38:59 +00:00
githubIssues = await getGithubIssues ( ) ;
logger . info ( ` Imported ${ githubIssues . length } issues from Github. ` ) ;
2017-12-23 02:38:32 +00:00
testcases = await getTestcases ( ) ;
logger . info ( ` Obtained ${ testcases . length } testcases from Telemetry API. ` ) ;
} ;
setup ( ) . then ( function ( ) {
2017-06-07 07:32:12 +00:00
// Make sure the output directories in Hugo exist.
[ fsPathHugoContent , fsPathHugoBoxart , fsPathHugoIcon , fsPathHugoSavefiles , fsPathHugoScreenshots ] . forEach ( function ( path ) {
if ( fs . existsSync ( path ) == false ) {
logger . info ( ` Creating Hugo output directory: ${ path } ` ) ;
fs . mkdirSync ( path ) ;
}
} ) ;
} )
. then ( function ( ) {
2017-07-27 09:02:02 +00:00
// Transform wiki entries to lowercase
const files = fs . readdirSync ( fsPathWiki ) ;
logger . info ( ` Generating wiki database... ` ) ;
files . forEach ( ( file ) => {
wikiEntries [ file . toLowerCase ( ) ] = file ;
} ) ;
2017-06-07 07:32:12 +00:00
// Loop through each game and process it.
getDirectories ( fsPathCode ) . forEach ( function ( game ) {
processGame ( game ) ;
} ) ;
} )
. catch ( function ( err ) {
logger . error ( err ) ;
2017-06-07 04:48:39 +00:00
} ) ;
2017-06-03 20:10:29 +00:00
2017-06-07 07:32:12 +00:00
function processGame ( game ) {
try {
if ( game == '.git' || game == '_validation' ) { return ; }
2017-06-05 01:27:57 +00:00
2017-06-07 07:32:12 +00:00
logger . info ( ` Processing game: ${ game } ` ) ;
2017-06-03 20:10:29 +00:00
2017-06-07 07:32:12 +00:00
// Copy the boxart for the game.
fsextra . copySync ( ` ${ fsPathCode } / ${ game } /boxart.png ` , ` ${ fsPathHugoBoxart } / ${ game } .png ` ) ;
// Copy the icon for the game.
fsextra . copySync ( ` ${ fsPathCode } / ${ game } /icon.png ` , ` ${ fsPathHugoIcon } / ${ game } .png ` ) ;
var model = toml . parse ( fs . readFileSync ( ` ${ fsPathCode } / ${ game } /game.dat ` , 'utf8' ) ) ;
let currentDate = new Date ( ) ;
model . date = ` ${ currentDate . toISOString ( ) } ` ;
2017-12-23 02:38:32 +00:00
// Look up all testcases associated with the Title IDs.
let releases = model . releases . map ( y => y . title ) ;
let foundTestcases = testcases . filter ( x => {
return releases . includes ( x . title ) ;
} ) ;
2018-02-10 16:40:47 +00:00
// If no testcases exist in the toml file, create a blank array.
if ( ! model . testcases ) {
model . testcases = [ ] ;
}
2017-12-23 02:38:32 +00:00
logger . info ( ` Found ${ foundTestcases . length } testcases from telemetry, found ${ model . testcases . length } in toml file. ` ) ;
model . testcases = model . testcases . concat ( foundTestcases ) ;
// Sort the testcases from most recent to least recent.
model . testcases . sort ( function ( a , b ) {
// Turn your strings into dates, and then subtract them
// to get a value that is either negative, positive, or zero.
return new Date ( b . date ) - new Date ( a . date ) ;
} ) ;
2017-06-07 07:32:12 +00:00
// SHORTCUTS BLOCK
// Parse testcase information out of the dat to reinject as shortcut values.
if ( model . testcases == null || model . testcases . length == 0 ) {
model . compatibility = "99" ;
model . testcase _date = "2000-01-01" ;
} else {
let recent = model . testcases [ 0 ] ;
2018-03-19 01:22:51 +00:00
// The displayed compatibility rating is an weighted arithmetic mean of the submitted ratings.
let numerator = 0 ;
let denominator = 0 ;
model . testcases . forEach ( testcase => {
// Build date is a better metric but testcases from the TOML files only have a submission date.
let weigh _date = new Date ( testcase . buildDate == undefined ? testcase . date : testcase . buildDate ) ;
// The test case is weighted on an exponential decay curve such that a test case is half as relevant as each month (30 days) passes.
// The exponent is obtained by dividing the time in millisecond between the current date and date of testing by the number of milliseconds in 30 days (but negative because it's a decay).
let weight = Math . pow ( . 5 , ( currentDate - weigh _date ) / - ( 30 * 1000 * 3600 * 24 ) ) ;
numerator += testcase . compatibility * weight ;
denominator += weight ;
} ) ;
model . compatibility = Math . round ( numerator / denominator ) . toString ( ) ;
2017-06-07 07:32:12 +00:00
model . testcase _date = recent . date ;
}
2017-06-11 12:38:58 +00:00
2017-06-11 13:20:14 +00:00
const toTrim = [ "the" , "a" , "an" ] ;
let trimmedTitle = model . title . toLowerCase ( ) ;
toTrim . forEach ( trim => {
2017-06-19 04:15:31 +00:00
if ( trimmedTitle . startsWith ( trim + " " ) ) {
2017-06-19 07:05:33 +00:00
trimmedTitle = trimmedTitle . substr ( trim . length + 1 ) ;
2017-06-11 13:20:14 +00:00
}
} ) ;
let section _id = ` ${ trimmedTitle [ 0 ] } ` ;
2017-06-11 12:38:58 +00:00
if ( ! section _id . match ( /[a-z]+/ ) ) {
section _id = "#" ;
}
model . section _id = section _id ;
2017-06-07 07:32:12 +00:00
// END SHORTCUTS BLOCK
// SAVEFILE BLOCK
var fsPathCodeSavefilesGame = ` ${ fsPathCode } / ${ game } /savefiles/ ` ;
var fsPathHugoSavefilesGame = ` ${ fsPathHugoSavefiles } / ${ game } / ` ;
if ( fs . existsSync ( fsPathCodeSavefilesGame ) ) {
// Create the savefile directory for the game.
if ( fs . existsSync ( fsPathHugoSavefilesGame ) == false ) {
fs . mkdirSync ( fsPathHugoSavefilesGame ) ;
2017-06-03 20:10:29 +00:00
}
2017-06-07 07:32:12 +00:00
// Copy all savefiles into the output folder, and read their data.
model . savefiles = [ ] ;
fs . readdirSync ( fsPathCodeSavefilesGame ) . forEach ( file => {
if ( path . extname ( file ) == '.zip' ) {
fsextra . copySync ( ` ${ fsPathCodeSavefilesGame } / ${ file } ` , ` ${ fsPathHugoSavefilesGame } / ${ file } ` ) ;
} else if ( path . extname ( file ) == '.dat' ) {
// Read the data file into an object.
let savefile = toml . parse ( fs . readFileSync ( ` ${ fsPathCodeSavefilesGame } / ${ file } ` , 'utf8' ) ) ;
let stats = fs . statSync ( ` ${ fsPathCodeSavefilesGame } / ${ file } ` ) ;
// Store the contents of the file in memory for adding it into the markdown later.
model . savefiles . push ( {
date : new Date ( util . inspect ( stats . mtime ) ) ,
filename : file . replace ( '.dat' , '.zip' ) ,
title : savefile . title ,
description : savefile . description ,
author : savefile . author ,
title _id : savefile . title _id
} ) ;
}
} ) ;
// Finished copying all savefiles into the output folder, and reading their data.
}
// END SAVEFILE BLOCK
// GITHUB ISSUES BLOCK
2017-06-24 15:19:33 +00:00
model . issues = [ ] ;
model . closed _issues = [ ] ;
2017-07-29 02:23:07 +00:00
2017-06-07 07:32:12 +00:00
if ( model . github _issues != null && model . github _issues . length > 0 ) {
model . github _issues . forEach ( function ( number ) {
let issue = githubIssues . find ( x => x . number == number ) ;
if ( issue == null ) {
model . closed _issues . push ( number ) ;
} else {
model . issues . push ( {
number : issue . number . toString ( ) ,
title : issue . title ,
state : issue . state ,
created _at : issue . created _at ,
updated _at : issue . updated _at ,
labels : issue . labels . map ( function ( x ) { return { name : x . name , color : x . color } } )
} ) ;
}
} ) ;
github _issues = null ;
}
// END GITHUB ISSUES BLOCK
// Copy the screenshots for the game.
let fsPathScreenshotInputGame = ` ${ fsPathCode } / ${ game } /screenshots/ ` ;
let fsPathScreenshotOutputGame = ` ${ fsPathHugoScreenshots } / ${ game } / ` ;
if ( fs . existsSync ( fsPathScreenshotInputGame ) ) {
// Create the savefile directory for each game.
if ( fs . existsSync ( fsPathScreenshotOutputGame ) == false ) {
fs . mkdirSync ( fsPathScreenshotOutputGame ) ;
2017-06-05 01:27:57 +00:00
}
2017-06-03 01:36:17 +00:00
2017-06-07 07:32:12 +00:00
// Copy all screenshots into the output folder.
fs . readdirSync ( fsPathScreenshotInputGame ) . forEach ( file => {
if ( path . extname ( file ) == '.png' ) {
fsextra . copySync ( ` ${ fsPathScreenshotInputGame } / ${ file } ` , ` ${ fsPathScreenshotOutputGame } / ${ file } ` ) ;
}
} ) ;
}
2017-06-03 20:10:29 +00:00
2017-06-07 07:32:12 +00:00
// WIKI BLOCK
var wikiText = "" ;
2017-07-27 09:02:02 +00:00
let fsPathWikiGame = ` ${ fsPathWiki } / ${ wikiEntries [ game . toLowerCase ( ) + ".md" ] } ` ;
2017-06-07 07:32:12 +00:00
if ( fs . existsSync ( fsPathWikiGame ) ) {
wikiText = fs . readFileSync ( fsPathWikiGame , 'utf8' ) ;
// Fix Blackfriday markdown rendering differences.
wikiText = blackfriday . fixLists ( wikiText ) ;
wikiText = blackfriday . fixLinks ( wikiText ) ;
} else {
wikiText = "## No wiki exists yet for this game." ;
2017-06-05 01:27:57 +00:00
}
2017-06-07 07:32:12 +00:00
// END WIKI BLOCK
let modelText = tomlify ( model , null , 2 ) ;
let contentOutput = ` +++ \r \n ${ modelText } \r \n +++ \r \n \r \n ${ wikiText } \r \n ` ;
fs . writeFileSync ( ` ${ fsPathHugoContent } / ${ game } .md ` , contentOutput ) ;
} catch ( ex ) {
logger . warn ( ` ${ game } failed to generate: ${ ex } ` ) ;
logger . error ( ex ) ;
}
2017-06-03 01:36:17 +00:00
}