commit d30f449bd1d980b6a6993a7e4b4c07a12f9f7bde Author: Jean-André Santoni Date: Mon Mar 11 23:12:01 2019 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fb92d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +webdb diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5398a75 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "database"] + path = database + url = https://github.com/libretro/libretro-database.git diff --git a/database b/database new file mode 160000 index 0000000..297a6b2 --- /dev/null +++ b/database @@ -0,0 +1 @@ +Subproject commit 297a6b2719c8156d4de0abbfb323aee3538243f1 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9c35dfa --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/kivutar/webdb + +require ( + github.com/disintegration/imaging v1.6.0 // indirect + github.com/go-gl/glfw v0.0.0-20190217072633-93b30450e032 // indirect + github.com/libretro/ludo v0.5.4-0.20190309060234-c7f145152c90 + golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect + golang.org/x/mobile v0.0.0-20190307202846-d2e1c1c4a691 // indirect + golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..11e7e80 --- /dev/null +++ b/go.sum @@ -0,0 +1,27 @@ +github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU= +github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/go-gl/gl v0.0.0-20181026044259-55b76b7df9d2/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw v0.0.0-20190217072633-93b30450e032/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/kivutar/glfont v0.0.0-20190119064645-3f4c07235fb3/go.mod h1:yvRxgIrpErStul4gzj88N+5yL15ze5JUVqk6Ze4mU+I= +github.com/libretro/ludo v0.5.2 h1:cPFIl4mrlskiWMYLcGT/e0btFsZX5DWUlipx0sbj678= +github.com/libretro/ludo v0.5.2/go.mod h1:VzVK8b71TYyUHQc5UQJj9ULC06O6HPCQD+QOe4YhqAU= +github.com/libretro/ludo v0.5.4-0.20190304150752-d547d4a5b25f h1:gcvXAXmCm90feQLn73EOhLmzHFjnaksbVJiPo06oywM= +github.com/libretro/ludo v0.5.4-0.20190304150752-d547d4a5b25f/go.mod h1:VzVK8b71TYyUHQc5UQJj9ULC06O6HPCQD+QOe4YhqAU= +github.com/libretro/ludo v0.5.4-0.20190309054306-2241406a64b0 h1:WCPRk5Gb63wnqDs7seb7JfI/FmRcYf1EK5qjXheF7dE= +github.com/libretro/ludo v0.5.4-0.20190309054306-2241406a64b0/go.mod h1:VzVK8b71TYyUHQc5UQJj9ULC06O6HPCQD+QOe4YhqAU= +github.com/libretro/ludo v0.5.4-0.20190309060234-c7f145152c90 h1:Q8Q0sOAqNINUKQclux4ADohpZRWBfNBSihCyrmDYoDg= +github.com/libretro/ludo v0.5.4-0.20190309060234-c7f145152c90/go.mod h1:VzVK8b71TYyUHQc5UQJj9ULC06O6HPCQD+QOe4YhqAU= +github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/tanema/gween v0.0.0-20171018143308-f05d97ee644d/go.mod h1:q58RIyFkCWKEi86xWwyOYLA9YZCY6myIgdN1Os5HECU= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190118043309-183bebdce1b2/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/mobile v0.0.0-20190127143845-a42111704963/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190307202846-d2e1c1c4a691/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/img-broken.png b/img-broken.png new file mode 100644 index 0000000..9c8411b Binary files /dev/null and b/img-broken.png differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..b5282bf --- /dev/null +++ b/main.go @@ -0,0 +1,315 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "math" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "text/template" + + "github.com/libretro/ludo/rdb" +) + +var target = "build" +var perPage = 24 +var tmpl *template.Template + +// Scrub characters that are not cross-platform and/or violate the +// No-Intro filename standard. +func scrubIllegalChars(str string) string { + str = strings.Replace(str, "&", "_", -1) + str = strings.Replace(str, "*", "_", -1) + str = strings.Replace(str, "/", "_", -1) + str = strings.Replace(str, ":", "_", -1) + str = strings.Replace(str, "`", "_", -1) + str = strings.Replace(str, "<", "_", -1) + str = strings.Replace(str, ">", "_", -1) + str = strings.Replace(str, "?", "_", -1) + str = strings.Replace(str, "|", "_", -1) + str = strings.Replace(str, "\"", "_", -1) + return str +} + +func reverse(s rdb.RDB) rdb.RDB { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } + return s +} + +func loadDB(dir string) (rdb.DB, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return rdb.DB{}, err + } + db := make(rdb.DB) + for _, f := range files { + name := f.Name() + if !strings.Contains(name, ".rdb") { + continue + } + system := name[0 : len(name)-4] + bytes, _ := ioutil.ReadFile(filepath.Join(dir, name)) + db[system] = reverse(rdb.Parse(bytes)) + } + return db, nil +} + +func buildHome(db rdb.DB) { + os.MkdirAll(target, os.ModePerm) + os.Link("img-broken.png", filepath.Join(target, "img-broken.png")) + + f, err := os.OpenFile(filepath.Join(target, "index.html"), os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + tmpl.ExecuteTemplate(f, "home.html", struct { + DB rdb.DB + }{ + db, + }) +} + +func buildSystemPages(system string, games rdb.RDB) { + os.MkdirAll(filepath.Join(target, system), os.ModePerm) + numPages := int(math.Ceil(float64(len(games)) / float64(perPage))) + for p := 0; p < numPages; p++ { + page := fmt.Sprintf("%d", p) + f, err := os.OpenFile(filepath.Join(target, system, "index-"+page+".html"), os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + log.Fatal(err) + } + + tmpl.ExecuteTemplate(f, "systempage.html", struct { + System string + Games rdb.RDB + Page int + LastPage int + }{ + system, + games[p*perPage : min(p*perPage+perPage, len(games))], + p, + numPages - 1, + }) + + f.Close() + } +} + +func buildGame(system string, game rdb.Game) { + path := filepath.Join(target, system, scrubIllegalChars(game.Name)+".html") + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + tmpl.ExecuteTemplate(f, "game.html", struct { + System string + Game rdb.Game + }{ + system, + game, + }) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +var funcMap = template.FuncMap{ + "mkslice": func(a []rdb.Game, start, end int) []rdb.Game { + e := min(end, len(a)) + return a[start:e] + }, + "Clean": scrubIllegalChars, + "Tags": func(name string) []string { + _, tags := extractTags(name) + return tags + }, + "WithoutTags": func(name string) string { + sname, _ := extractTags(name) + return sname + }, + "add": func(a, b int) int { + return a + b + }, + "title": func(name string) string { + return strings.Title(name) + }, +} + +func extractTags(name string) (string, []string) { + re := regexp.MustCompile(`\(.*?\)`) + pars := re.FindAllString(name, -1) + var tags []string + for _, par := range pars { + name = strings.Replace(name, par, "", -1) + par = strings.Replace(par, "(", "", -1) + par = strings.Replace(par, ")", "", -1) + results := strings.Split(par, ",") + for _, result := range results { + tags = append(tags, strings.TrimSpace(result)) + } + } + name = strings.TrimSpace(name) + return name, tags +} + +func build() { + tmpl = template.Must( + template.New("main").Funcs(funcMap).ParseGlob("templates/*.html"), + ) + + db, err := loadDB("./database/rdb") + if err != nil { + log.Fatal(err) + } + + buildHome(db) + + wg := sync.WaitGroup{} + for system, games := range db { + buildSystemPages(system, games) + system := system + games := games + wg.Add(1) + go func() { + for _, game := range games { + buildGame(system, game) + } + wg.Done() + }() + } + + buildTags(db, "franchise") + buildTags(db, "developer") + buildTags(db, "publisher") + buildTags(db, "genre") + buildTags(db, "origin") + + wg.Wait() +} + +// Given a metatag like franchise or genre or developer, will create a hierarchy +// of tag files and indexes allowing to browse the database based on this +// property. +func buildTags(db rdb.DB, tagType string) { + perTag := map[string][]rdb.Game{} + for system, games := range db { + for _, game := range games { + game.System = system + switch tagType { + case "franchise": + if game.Franchise == "" { + continue + } + perTag[game.Franchise] = append(perTag[game.Franchise], game) + case "developer": + if game.Developer == "" { + continue + } + perTag[game.Developer] = append(perTag[game.Developer], game) + case "publisher": + if game.Publisher == "" { + continue + } + perTag[game.Publisher] = append(perTag[game.Publisher], game) + case "genre": + if game.Genre == "" { + continue + } + perTag[game.Genre] = append(perTag[game.Genre], game) + case "origin": + if game.Origin == "" { + continue + } + perTag[game.Origin] = append(perTag[game.Origin], game) + } + } + } + buildTagIndex(perTag, tagType) + for tag, games := range perTag { + buildTagPage(tagType, tag, games) + } +} + +// Will build a page like /franchise.html that will list all the franchises in +// the database. +func buildTagIndex(perTag map[string][]rdb.Game, tagType string) { + os.MkdirAll(filepath.Join(target, tagType), os.ModePerm) + path := filepath.Join(target, tagType, "index.html") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + log.Fatal(err) + } + defer f.Close() + err = tmpl.ExecuteTemplate(f, "tags.html", struct { + TagType string + PerTag map[string][]rdb.Game + }{ + tagType, + perTag, + }) + if err != nil { + log.Fatal(err) + } +} + +// Will build a page like /franchise/Bomberman.html that will list all the +// games of the bomberman franchise. +func buildTagPage(tagType, tag string, games []rdb.Game) { + path := filepath.Join(target, tagType, scrubIllegalChars(tag)+".html") + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + err = tmpl.ExecuteTemplate(f, "tag.html", struct { + TagType string + Tag string + Games []rdb.Game + }{ + tagType, + tag, + games, + }) + if err != nil { + log.Fatal(err) + } +} + +func serve() { + fs := http.FileServer(http.Dir(target)) + http.Handle("/", fs) + + log.Println("Listening on http://0.0.0.0:3003") + http.ListenAndServe(":3003", nil) +} + +func main() { + flag.Parse() + args := flag.Args() + switch args[0] { + case "serve": + serve() + case "build": + fallthrough + default: + build() + } +} diff --git a/templates/game.html b/templates/game.html new file mode 100644 index 0000000..96bd63b --- /dev/null +++ b/templates/game.html @@ -0,0 +1,64 @@ + + + + {{template "head"}} + + + {{template "navbar"}} +
+

+ {{.Game.Name | WithoutTags}} + {{range $_, $tag := (Tags .Game.Name) }} + {{$tag}} + {{end}} +

+ +
+
+

{{.Game.Description}}

+ + + {{if .Game.Genre}} + + {{end}} + {{if .Game.ReleaseMonth}} + + {{end}} + {{if .Game.ReleaseYear}} + + {{end}} + {{if .Game.Developer}} + + {{end}} + {{if .Game.Developer}} + + {{end}} + {{if .Game.Franchise}} + + {{end}} + {{if .Game.Origin}} + + {{end}} + + + + +
Genre:{{.Game.Genre}}
Release Month:{{.Game.ReleaseMonth}}
Release Year:{{.Game.ReleaseYear}}
Developer:{{.Game.Developer}}
Publisher:{{.Game.Publisher}}
Franchise:{{.Game.Franchise}}
Origin:{{.Game.Origin}}
Serial:{{.Game.Serial}}
Size:{{.Game.Size}}
CRC32:{{.Game.CRC32}}
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 0000000..6cf15e3 --- /dev/null +++ b/templates/head.html @@ -0,0 +1,9 @@ +{{define "head"}} + +Libretro Database + + + + + +{{end}} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..e7cbfa5 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,23 @@ + + + + {{template "head"}} + + + {{template "navbar"}} +
+
+ {{range $system, $games := .DB}} + {{if $games}} +
+
+

{{$system}}

+ +
+
+ {{end}} + {{end}} +
+
+ + \ No newline at end of file diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..a5fe850 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,32 @@ +{{define "navbar"}} +
+
+ +
+
+{{end}} \ No newline at end of file diff --git a/templates/systempage.html b/templates/systempage.html new file mode 100644 index 0000000..5d0bcc8 --- /dev/null +++ b/templates/systempage.html @@ -0,0 +1,56 @@ + + + + {{template "head"}} + + + {{template "navbar"}} +
+

{{.System}} (Page {{add .Page 1}})

+ + + +
+ {{range .Games}} +
+
+ Game screenshot +
+
+ {{.Name | WithoutTags}} + {{range $_, $tag := (Tags .Name) }} + {{$tag}} + {{end}} +
+

{{.Genre}}

+
+ +
+
+ {{end}} +
+ + + +
+ + \ No newline at end of file diff --git a/templates/tag.html b/templates/tag.html new file mode 100644 index 0000000..93d633f --- /dev/null +++ b/templates/tag.html @@ -0,0 +1,33 @@ + + + + {{template "head"}} + + + {{template "navbar"}} +
+

{{.Tag}}

+ + + {{range .Games}} + + + + + + {{end}} +
+ {{.Name | WithoutTags}} + {{range $_, $tag := (Tags .Name) }} + {{$tag}} + {{end}} + {{.System}}{{.ReleaseYear}}
+
+ + \ No newline at end of file diff --git a/templates/tags.html b/templates/tags.html new file mode 100644 index 0000000..ee39547 --- /dev/null +++ b/templates/tags.html @@ -0,0 +1,26 @@ + + + + {{template "head"}} + + + {{template "navbar"}} +
+

{{.TagType | title}}

+ + + {{range $tag, $games := .PerTag}} + + + + + {{end}} +
{{$tag}}{{len $games}} games
+
+ + \ No newline at end of file