Initial commit

This commit is contained in:
Jean-André Santoni 2019-03-11 23:12:01 +07:00
commit d30f449bd1
14 changed files with 601 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
webdb

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "database"]
path = database
url = https://github.com/libretro/libretro-database.git

1
database Submodule

@ -0,0 +1 @@
Subproject commit 297a6b2719c8156d4de0abbfb323aee3538243f1

10
go.mod Normal file
View File

@ -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
)

27
go.sum Normal file
View File

@ -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=

BIN
img-broken.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

315
main.go Normal file
View File

@ -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()
}
}

64
templates/game.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
{{template "head"}}
</head>
<body>
{{template "navbar"}}
<div class="container">
<h1 class="py-4">
{{.Game.Name | WithoutTags}}
{{range $_, $tag := (Tags .Game.Name) }}
<span class="badge badge-secondary">{{$tag}}</span>
{{end}}
</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/{{.System}}/index-0.html">{{.System}}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{.Game.Name}}</li>
</ol>
</nav>
<div class="row">
<div class="col-sm">
<p>{{.Game.Description}}</p>
<table class="table table-sm table-striped">
<tbody>
{{if .Game.Genre}}
<tr><th scope="row" style="width: 25%">Genre:</th><td><a href="/genre/{{.Game.Genre | Clean}}.html">{{.Game.Genre}}</a></td></tr>
{{end}}
{{if .Game.ReleaseMonth}}
<tr><th scope="row">Release Month:</th><td>{{.Game.ReleaseMonth}}</td></tr>
{{end}}
{{if .Game.ReleaseYear}}
<tr><th scope="row">Release Year:</th><td>{{.Game.ReleaseYear}}</td></tr>
{{end}}
{{if .Game.Developer}}
<tr><th scope="row">Developer:</th><td><a href="/developer/{{.Game.Developer | Clean}}.html">{{.Game.Developer}}</a></td></tr>
{{end}}
{{if .Game.Developer}}
<tr><th scope="row">Publisher:</th><td><a href="/publisher/{{.Game.Publisher | Clean}}.html">{{.Game.Publisher}}</a></td></tr>
{{end}}
{{if .Game.Franchise}}
<tr><th scope="row">Franchise:</th><td><a href="/franchise/{{.Game.Franchise | Clean}}.html">{{.Game.Franchise}}</a></td></tr>
{{end}}
{{if .Game.Origin}}
<tr><th scope="row">Origin:</th><td><a href="/origin/{{.Game.Origin | Clean}}.html">{{.Game.Origin}}</a></td></tr>
{{end}}
<tr><th scope="row">Serial:</th><td>{{.Game.Serial}}</td></tr>
<tr><th scope="row">Size:</th><td>{{.Game.Size}}</td></tr>
<tr><th scope="row">CRC32:</th><td>{{.Game.CRC32}}</td></tr>
</tbody>
</table>
</div>
<div class="col-sm">
<div class="row">
<div class="col-12 mb-4"><img class="w-100" src="http://thumbnails.libretro.com/{{.System}}/Named_Boxarts/{{.Game.Name | Clean}}.png" onerror="this.src='/img-broken.png'"></div>
<div class="col-6 mb-4"><img class="w-100" src="http://thumbnails.libretro.com/{{.System}}/Named_Titles/{{.Game.Name | Clean}}.png" onerror="this.src='/img-broken.png'"></div>
<div class="col-6 mb-4"><img class="w-100" src="http://thumbnails.libretro.com/{{.System}}/Named_Snaps/{{.Game.Name | Clean}}.png" onerror="this.src='/img-broken.png'"></div>
</div>
</div>
</div>
</div>
</body>
</html>

9
templates/head.html Normal file
View File

@ -0,0 +1,9 @@
{{define "head"}}
<meta charset="utf-8">
<title>Libretro Database</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
{{end}}

23
templates/home.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
{{template "head"}}
</head>
<body>
{{template "navbar"}}
<div class="container">
<div class="row pt-5">
{{range $system, $games := .DB}}
{{if $games}}
<div class="col-md-6 col-sm-12">
<div class="jumbotron position-relative" style="background-color: #555;">
<h2 class="text-light pb-3">{{$system}}</h2>
<div class="clearfix"><a href="/{{$system}}/index-0.html" class="float-right btn btn-light stretched-link">{{len $games}} games</a></div>
</div>
</div>
{{end}}
{{end}}
</div>
</div>
</body>
</html>

32
templates/navbar.html Normal file
View File

@ -0,0 +1,32 @@
{{define "navbar"}}
<div class="bg-light">
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light">
<a class="navbar-brand" href="/">Libretro Database</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/franchise">Franchises</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/developer">Developers</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/publisher">Publishers</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/genre">Genres</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/origin">Origins</a>
</li>
</ul>
</div>
</nav>
</div>
</div>
{{end}}

56
templates/systempage.html Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
{{template "head"}}
</head>
<body>
{{template "navbar"}}
<div class="container">
<h1 class="py-4">{{.System}} <span class="text-secondary">(Page {{add .Page 1}})</span></h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">{{.System}}</li>
</ol>
</nav>
<div class="row">
{{range .Games}}
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 mb-3">
<div class="card shadow-sm">
<img src="http://thumbnails.libretro.com/{{$.System}}/Named_Snaps/{{.Name | Clean}}.png" class="card-img-top" alt="Game screenshot" onerror="this.src='/img-broken.png'">
<div class="card-body">
<h6 class="card-title mb-0">
{{.Name | WithoutTags}}
{{range $_, $tag := (Tags .Name) }}
<span class="badge badge-secondary">{{$tag}}</span>
{{end}}
</h6>
<p class="card-text">{{.Genre}}</p>
</div>
<div class="card-footer">
<a href="/{{$.System}}/{{.Name | Clean}}.html" class="float-right stretched-link">See details</a>
</div>
</div>
</div>
{{end}}
</div>
<nav>
<ul class="pagination justify-content-center">
<li class="page-item {{if eq .Page 0}}disabled{{end}}">
<a class="page-link" href="/{{$.System}}/index-{{add .Page -1}}.html" tabindex="-1">Previous</a>
</li>
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">{{.Page}} <span class="sr-only">(current)</span></a>
</li>
<li class="page-item {{if eq .Page .LastPage}}disabled{{end}}">
<a class="page-link" href="/{{$.System}}/index-{{add .Page 1}}.html">Next</a>
</li>
</ul>
</nav>
</div>
</body>
</html>

33
templates/tag.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
{{template "head"}}
</head>
<body>
{{template "navbar"}}
<div class="container">
<h1 class="py-4">{{.Tag}}</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/{{.TagType}}/index.html">{{.TagType | title}}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{.Tag}}</li>
</ol>
</nav>
<table class="table">
{{range .Games}}
<tr>
<td>
<a href="/{{.System}}/{{.Name | Clean}}.html">{{.Name | WithoutTags}}</a>
{{range $_, $tag := (Tags .Name) }}
<span class="badge badge-secondary">{{$tag}}</span>
{{end}}
</td>
<td><a href="/{{.System}}/index-0.html">{{.System}}</a></td>
<td>{{.ReleaseYear}}</td>
</tr>
{{end}}
</table>
</div>
</body>
</html>

26
templates/tags.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
{{template "head"}}
</head>
<body>
{{template "navbar"}}
<div class="container">
<h1 class="py-4">{{.TagType | title}}</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">{{.TagType | title}}</li>
</ol>
</nav>
<table class="table">
{{range $tag, $games := .PerTag}}
<tr>
<td><a href="/{{$.TagType}}/{{$tag | Clean}}.html">{{$tag}}</a></td>
<td>{{len $games}} games</td>
</tr>
{{end}}
</table>
</div>
</body>
</html>