diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..880614b --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module switch/nsp/diff + +go 1.12 + +require ( + github.com/briandowns/spinner v1.6.1 + github.com/go-openapi/strfmt v0.19.2 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible + github.com/mattn/go-runewidth v0.0.2 // indirect + github.com/stretchr/testify v1.4.0 // indirect + golang.org/x/sys v0.0.0-20190904154756-749cb33beabd // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fcfc3a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/briandowns/spinner v1.6.1 h1:LBxHu5WLyVuVEtTD72xegiC7QJGx598LBpo3ywKTapA= +github.com/briandowns/spinner v1.6.1/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/strfmt v0.19.2 h1:clPGfBnJohokno0e+d7hs6Yocrzjlgz6EsQSDncCRnE= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..cc45150 --- /dev/null +++ b/main.go @@ -0,0 +1,213 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "github.com/briandowns/spinner" + "github.com/jedib0t/go-pretty/table" + "io" + "io/ioutil" + "net/http" + "os" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +const ( + titles_json_uri = "https://raw.githubusercontent.com/blawar/titledb/master/titles.US.en.json" + versions_json_url = "https://raw.githubusercontent.com/blawar/titledb/master/versions.json" +) + +var ( + nspFolder = flag.String("f", "", "path to NSP folder") + s = spinner.New(spinner.CharSets[26], 100*time.Millisecond) // Build our new spinner +) + +type title struct { + Id string `json:"id"` + Name string `json:"name,omitempty"` + Version json.Number `json:"version,omitempty"` + Region string `json:"region,omitempty"` + ReleaseDate int `json:"releaseDate,omitempty"` + Publisher string `json:"publisher,omitempty"` + IconUrl string `json:"iconUrl,omitempty"` + Screenshots []string `json:"screenshots,omitempty"` + BannerUrl string `json:"bannerUrl,omitempty"` + Description string `json:"description,omitempty"` + Size int `json:"size,omitempty"` +} + +func main() { + flag.Parse() + + if *nspFolder == "" { + flag.Usage() + os.Exit(1) + } + + var titlesDb = map[string]title{} + err := loadOrDownloadFileFromUrl(titles_json_uri, "titlesDb.json", &titlesDb) + if err != nil { + fmt.Printf("unable to download file - %v\n%v", titles_json_uri, err) + return + } + + var versionsDb = map[string]map[int]string{} + err = loadOrDownloadFileFromUrl(versions_json_url, "versionsDb.json", &versionsDb) + if err != nil { + fmt.Printf("unable to download file - %v\n%v", versions_json_url, err) + return + } + + s.Restart() + fmt.Printf("\nScanning nsp folder ") + files, err := ioutil.ReadDir(*nspFolder) + if err != nil { + fmt.Printf("\nfailed scanning NSP folder\n %v", err) + return + } + s.Stop() + var localVersionsDb = map[string][]int{} + var skippedFiles = map[string]string{} + + versionR := regexp.MustCompile(`\[[vV]?(?P[0-9]{1,10})\]`) + titleIdR := regexp.MustCompile(`\[(?P[A-Z,a-z,0-9]{16})\]`) + for _, file := range files { + if file.Name()[0:1] == "." || file.IsDir() { + continue + } + + if !strings.HasSuffix(file.Name(), "nsp") { + skippedFiles[file.Name()] = "non NSP file" + continue + } + + res := versionR.FindStringSubmatch(file.Name()) + if len(res) != 2 { + skippedFiles[file.Name()] = "failed to parse name" + continue + } + verStr := res[1] + res = titleIdR.FindStringSubmatch(file.Name()) + if len(res) != 2 { + skippedFiles[file.Name()] = "failed to parse name" + continue + } + if len(res) != 2 { + skippedFiles[file.Name()] = "failed to parse name" + continue + } + titleId := strings.ToLower(res[1]) + + if strings.HasSuffix(titleId, "800") { + titleId = titleId[0:len(titleId)-3] + "000" + } + + ver, err := strconv.Atoi(verStr) + if err != nil { + skippedFiles[file.Name()] = "failed to parse version" + continue + } + + localVersionsDb[titleId] = append(localVersionsDb[titleId], ver) + } + + var numTobeUpdated int = 0 + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleColoredBright) + t.AppendHeader(table.Row{"#", "Title","TitleId" ,"Current version", "Available Version", "Release date"}) + //iterate over local files, and compare to remote versions + for titleId, _ := range localVersionsDb { + + localVersions := localVersionsDb[titleId] + sort.Ints(localVersions) + + var remoteVersions []int + for k, _ := range versionsDb[titleId] { + remoteVersions = append(remoteVersions, k) + } + + if title, ok := titlesDb[strings.ToUpper(titleId[0:len(titleId)-3]+"800")]; ok { + ver, err := strconv.Atoi(title.Version.String()) + if err == nil { + remoteVersions = append(remoteVersions, ver) + } + } + if len(remoteVersions) == 0 { + continue + } + + var nspName string + if title, ok := titlesDb[strings.ToUpper(titleId)]; ok { + nspName = title.Name + ver, err := strconv.Atoi(title.Version.String()) + if err == nil { + remoteVersions = append(remoteVersions, ver) + } + } + + localVer := localVersions[len(localVersions)-1] + if localVersions[0] != 0 && !strings.Contains(strings.ToLower(nspName), "pack") { + //fmt.Printf("** game [%v][%v] missing base version\n",titleId,nspName) + continue + } + sort.Ints(remoteVersions) + remoteVer := remoteVersions[len(remoteVersions)-1] + + if remoteVer > localVer { + var nspName string + if title, ok := titlesDb[strings.ToUpper(titleId)]; ok { + nspName = title.Name + } + numTobeUpdated++ + t.AppendRow([]interface{}{numTobeUpdated, nspName,titleId, localVer, remoteVer, versionsDb[titleId][remoteVer]}) + } + } + t.AppendFooter(table.Row{"", "", "", "","Total", numTobeUpdated}) + if numTobeUpdated != 0{ + fmt.Printf("\nFound available updates:\n\n") + t.Render() + }else{ + fmt.Printf("\nAll NSP's are up to date!\n\n") + } + + +} + +func loadOrDownloadFileFromUrl(url string, fileName string, target interface{}) error { + + if _, err := os.Stat("./" + fileName); os.IsNotExist(err) { + file, err := os.Create("./" + fileName) + fmt.Printf("\nDownloading from - %v", url) + s.Start() + resp, err := http.Get(url) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return errors.New("got a non 200 response - " + resp.Status) + } + defer resp.Body.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + } + + fmt.Printf("\nLoading file - %v", fileName) + s.Start() + file, err := os.Open("./" + fileName) + err = json.NewDecoder(file).Decode(target) + if err != nil { + return err + } + return nil +}