Add package for retrieving information from GOG (the changelog)
This commit is contained in:
parent
9442d5993d
commit
32e0180f91
1
go.mod
1
go.mod
@ -10,6 +10,7 @@ require (
|
||||
github.com/google/uuid v1.1.4
|
||||
github.com/kr/pretty v0.2.1
|
||||
github.com/pierrec/lz4/v4 v4.1.3
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363
|
||||
gonum.org/v1/gonum v0.8.2
|
||||
)
|
||||
|
224
gog/changelog.go
Normal file
224
gog/changelog.go
Normal file
@ -0,0 +1,224 @@
|
||||
package gog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
type Change struct {
|
||||
Title string
|
||||
Changes []string
|
||||
Sub []Change
|
||||
recurse bool
|
||||
}
|
||||
|
||||
func (c Change) str(indent int) string {
|
||||
var (
|
||||
s = new(strings.Builder)
|
||||
ind = strings.Repeat("\t", indent)
|
||||
)
|
||||
fmt.Fprintln(s, ind+c.Title)
|
||||
for _, v := range c.Changes {
|
||||
fmt.Fprintln(s, ind+"\t"+v)
|
||||
}
|
||||
for _, v := range c.Sub {
|
||||
s.WriteString(v.str(indent + 1))
|
||||
}
|
||||
// s.WriteRune('\n')
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (c Change) String() string {
|
||||
return c.str(0)
|
||||
}
|
||||
|
||||
func debug(f ...interface{}) {
|
||||
if len(os.Args) > 2 && os.Args[2] == "debug" {
|
||||
fmt.Println(f...)
|
||||
}
|
||||
}
|
||||
|
||||
// Stringify returns the text from a node and all its children
|
||||
func stringify(h *html.Node) string {
|
||||
var (
|
||||
f func(*html.Node)
|
||||
str string
|
||||
def = h
|
||||
)
|
||||
f = func(n *html.Node) {
|
||||
if n.Type == html.TextNode {
|
||||
str += strings.TrimSpace(n.Data) + " "
|
||||
}
|
||||
if n.DataAtom == atom.Br {
|
||||
str += "\n"
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
f(def)
|
||||
return str
|
||||
}
|
||||
|
||||
// ParseChange returns the parsed change and the next node to start parsing the next change
|
||||
func ParseChange(h *html.Node, title string) (Change, *html.Node) {
|
||||
var c Change
|
||||
c.Title = title
|
||||
debug("change", strings.TrimSpace(h.Data))
|
||||
l:
|
||||
for h.Data != "hr" {
|
||||
switch h.Type {
|
||||
case html.ErrorNode:
|
||||
panic("An error happened!?!?")
|
||||
case html.TextNode:
|
||||
debug("text", strings.TrimSpace(h.Data))
|
||||
if strings.TrimSpace(h.Data) != "" {
|
||||
c.Changes = append(c.Changes, strings.TrimSpace(h.Data))
|
||||
}
|
||||
case html.ElementNode:
|
||||
switch h.DataAtom {
|
||||
case atom.H1, atom.H2, atom.H3, atom.H4, atom.H5, atom.H6, atom.P:
|
||||
debug(h.DataAtom.String())
|
||||
if c.Title == "" {
|
||||
c.Title = strings.TrimSpace(stringify(h))
|
||||
} else {
|
||||
tmp := drain(h.NextSibling)
|
||||
switch tmp.DataAtom {
|
||||
case atom.H1, atom.H2, atom.H3, atom.H4, atom.H5, atom.H6:
|
||||
h = tmp
|
||||
|
||||
c.Changes = append(c.Changes, strings.TrimSpace(stringify(h)))
|
||||
break
|
||||
}
|
||||
var cc Change
|
||||
debug("h", strings.TrimSpace(h.Data))
|
||||
debug("h", strings.TrimSpace(h.NextSibling.Data))
|
||||
cc, h = ParseChange(h.NextSibling, stringify(h))
|
||||
debug("h2", h.Data, h.PrevSibling.Data)
|
||||
// h = h.Parent
|
||||
c.Sub = append(c.Sub, cc)
|
||||
continue
|
||||
}
|
||||
case atom.Ul:
|
||||
debug(h.DataAtom.String())
|
||||
h = h.FirstChild
|
||||
continue
|
||||
case atom.Li:
|
||||
debug("li", h.DataAtom.String())
|
||||
if h.FirstChild != h.LastChild && h.FirstChild.NextSibling.DataAtom != atom.P && h.FirstChild.NextSibling.DataAtom != atom.A {
|
||||
var cc Change
|
||||
cc, h = ParseChange(h.FirstChild.NextSibling, strings.TrimSpace(h.FirstChild.Data))
|
||||
h = h.Parent
|
||||
if h.NextSibling != nil {
|
||||
h = h.NextSibling
|
||||
}
|
||||
// pretty.Println(cc)
|
||||
c.Sub = append(c.Sub, cc)
|
||||
break l
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
var (
|
||||
f func(*html.Node)
|
||||
str string
|
||||
def = h
|
||||
)
|
||||
f = func(n *html.Node) {
|
||||
if n.Type == html.TextNode {
|
||||
str += strings.TrimSpace(n.Data) + " "
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
f(def)
|
||||
c.Changes = append(c.Changes, strings.TrimSpace(str))
|
||||
}
|
||||
}
|
||||
if h.DataAtom == atom.Ul {
|
||||
h = h.Parent
|
||||
}
|
||||
if h.NextSibling != nil { // Move to the next node, h should never be nil
|
||||
debug("next", h.Type, h.NextSibling.Type)
|
||||
h = h.NextSibling
|
||||
} else if h.DataAtom == atom.Li || h.PrevSibling.DataAtom == atom.Li && h.Parent.DataAtom != atom.Body { // If we are currently in a list then go up one unless that would take us to body
|
||||
debug("ul", strings.TrimSpace(h.Data), strings.TrimSpace(h.Parent.Data))
|
||||
if h.Parent.NextSibling == nil {
|
||||
h = h.Parent
|
||||
} else { // go to parents next sibling so we don't parse the same node again
|
||||
h = h.Parent.NextSibling
|
||||
}
|
||||
debug("break2", strings.TrimSpace(h.Data))
|
||||
break
|
||||
} else {
|
||||
debug("I don't believe this should ever happen")
|
||||
break
|
||||
}
|
||||
}
|
||||
h = drain(h)
|
||||
return c, h
|
||||
}
|
||||
|
||||
// drain skips over non-Element Nodes
|
||||
func drain(h *html.Node) *html.Node {
|
||||
for h.NextSibling != nil {
|
||||
if h.Type == html.ElementNode {
|
||||
break
|
||||
}
|
||||
h = h.NextSibling
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func ParseChangelog(ch, title string) (Change, error) {
|
||||
var (
|
||||
p *html.Node
|
||||
v Change
|
||||
err error
|
||||
)
|
||||
v.Title = title
|
||||
p, err = html.Parse(strings.NewReader(ch))
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
p = p.FirstChild.FirstChild.NextSibling.FirstChild
|
||||
for p != nil && p.NextSibling != nil {
|
||||
var tmp Change
|
||||
tmp, p = ParseChange(p, "")
|
||||
if p.DataAtom == atom.Hr {
|
||||
p = p.NextSibling
|
||||
}
|
||||
v.Sub = append(v.Sub, tmp)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func getGOGInfo(id string) (GOGalaxy, error) {
|
||||
var (
|
||||
r *http.Response
|
||||
err error
|
||||
b []byte
|
||||
info GOGalaxy
|
||||
)
|
||||
r, err = http.Get("https://api.gog.com/products/" + id + "?expand=downloads,expanded_dlcs,description,screenshots,videos,related_products,changelog")
|
||||
if err != nil {
|
||||
return GOGalaxy{}, fmt.Errorf("failed to retrieve GOG info: %w", err)
|
||||
}
|
||||
b, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return GOGalaxy{}, fmt.Errorf("failed to retrieve GOG info: %w", err)
|
||||
}
|
||||
r.Body.Close()
|
||||
err = json.Unmarshal(b, &info)
|
||||
if err != nil {
|
||||
return GOGalaxy{}, fmt.Errorf("failed to retrieve GOG info: %w", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
202
gog/gog.go
Normal file
202
gog/gog.go
Normal file
@ -0,0 +1,202 @@
|
||||
package gog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GOGalaxy struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
PurchaseLink string `json:"purchase_link"`
|
||||
Slug string `json:"slug"`
|
||||
ContentSystemCompatibility struct {
|
||||
Windows bool `json:"windows"`
|
||||
Osx bool `json:"osx"`
|
||||
Linux bool `json:"linux"`
|
||||
} `json:"content_system_compatibility"`
|
||||
Languages map[string]string `json:"languages"`
|
||||
Links struct {
|
||||
PurchaseLink string `json:"purchase_link"`
|
||||
ProductCard string `json:"product_card"`
|
||||
Support string `json:"support"`
|
||||
Forum string `json:"forum"`
|
||||
} `json:"links"`
|
||||
InDevelopment struct {
|
||||
Active bool `json:"active"`
|
||||
Until interface{} `json:"until"`
|
||||
} `json:"in_development"`
|
||||
IsSecret bool `json:"is_secret"`
|
||||
IsInstallable bool `json:"is_installable"`
|
||||
GameType string `json:"game_type"`
|
||||
IsPreOrder bool `json:"is_pre_order"`
|
||||
ReleaseDate time.Time `json:"release_date"`
|
||||
Images Images `json:"images"`
|
||||
Dlcs DLCs `json:"dlcs"`
|
||||
Downloads struct {
|
||||
Installers []Download `json:"installers"`
|
||||
Patches []Download `json:"patches"`
|
||||
LanguagePacks []Download `json:"language_packs"`
|
||||
BonusContent []Download `json:"bonus_content"`
|
||||
} `json:"downloads"`
|
||||
ExpandedDlcs []Product `json:"expanded_dlcs"`
|
||||
Description struct {
|
||||
Lead string `json:"lead"`
|
||||
Full string `json:"full"`
|
||||
WhatsCoolAboutIt string `json:"whats_cool_about_it"`
|
||||
} `json:"description"`
|
||||
Screenshots []struct {
|
||||
ImageID string `json:"image_id"`
|
||||
FormatterTemplateURL string `json:"formatter_template_url"`
|
||||
FormattedImages []struct {
|
||||
FormatterName string `json:"formatter_name"`
|
||||
ImageURL string `json:"image_url"`
|
||||
} `json:"formatted_images"`
|
||||
} `json:"screenshots"`
|
||||
Videos []struct {
|
||||
VideoURL string `json:"video_url"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"videos"`
|
||||
RelatedProducts []Product `json:"related_products"`
|
||||
Changelog string `json:"changelog"`
|
||||
}
|
||||
|
||||
func (gog *GOGalaxy) UnmarshalJSON(d []byte) error {
|
||||
var err error
|
||||
type T2 GOGalaxy // create new type with same structure as T but without its method set to avoid infinite `UnmarshalJSON` call stack
|
||||
x := struct {
|
||||
T2
|
||||
ReleaseDate string `json:"release_date"`
|
||||
}{} // don't forget this, if you do and 't' already has some fields set you would lose them; see second example
|
||||
|
||||
if err = json.Unmarshal(d, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
*gog = GOGalaxy(x.T2)
|
||||
|
||||
if gog.ReleaseDate, err = time.Parse("2006-01-02T15:04:05-0700", x.ReleaseDate); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DLCs struct {
|
||||
Products []struct {
|
||||
ID int `json:"id"`
|
||||
Link string `json:"link"`
|
||||
ExpandedLink string `json:"expanded_link"`
|
||||
} `json:"products"`
|
||||
AllProductsURL string `json:"all_products_url"`
|
||||
ExpandedAllProductsURL string `json:"expanded_all_products_url"`
|
||||
}
|
||||
|
||||
func (DLC *DLCs) UnmarshalJSON(d []byte) error {
|
||||
type T2 GOGalaxy
|
||||
var x T2
|
||||
dec := json.NewDecoder(bytes.NewBuffer(d))
|
||||
t, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delim, ok := t.(json.Delim); ok {
|
||||
if delim == '[' {
|
||||
return nil
|
||||
} else {
|
||||
return json.Unmarshal(d, &x)
|
||||
}
|
||||
} else {
|
||||
return &json.UnmarshalTypeError{
|
||||
Value: reflect.TypeOf(t).String(),
|
||||
Type: reflect.TypeOf(*DLC),
|
||||
Offset: 0,
|
||||
Struct: "DLCs",
|
||||
Field: ".",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Languages map[string]string
|
||||
|
||||
type Download struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Os string `json:"os"`
|
||||
Language string `json:"language"`
|
||||
LanguageFull string `json:"language_full"`
|
||||
Version string `json:"version"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
Files []File `json:"files"`
|
||||
}
|
||||
|
||||
func (d *Download) UnmarshalJSON(data []byte) error {
|
||||
var i json.Number
|
||||
type T2 Download // create new type with same structure as T but without its method set to avoid infinite `UnmarshalJSON` call stack
|
||||
x := struct {
|
||||
T2
|
||||
ID json.RawMessage `json:"id"`
|
||||
}{} // don't forget this, if you do and 't' already has some fields set you would lose them; see second example
|
||||
|
||||
if err := json.Unmarshal(data, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
*d = Download(x.T2)
|
||||
if err := json.Unmarshal(x.ID, &i); err != nil {
|
||||
if err := json.Unmarshal(x.ID, &d.ID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
d.ID = i.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
Size int `json:"size"`
|
||||
Downlink string `json:"downlink"`
|
||||
}
|
||||
|
||||
type Product struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
PurchaseLink string `json:"purchase_link"`
|
||||
Slug string `json:"slug"`
|
||||
ContentSystemCompatibility struct {
|
||||
Windows bool `json:"windows"`
|
||||
Osx bool `json:"osx"`
|
||||
Linux bool `json:"linux"`
|
||||
} `json:"content_system_compatibility"`
|
||||
Links struct {
|
||||
PurchaseLink string `json:"purchase_link"`
|
||||
ProductCard string `json:"product_card"`
|
||||
Support string `json:"support"`
|
||||
Forum string `json:"forum"`
|
||||
} `json:"links"`
|
||||
InDevelopment struct {
|
||||
Active bool `json:"active"`
|
||||
Until time.Time `json:"until"`
|
||||
} `json:"in_development"`
|
||||
IsSecret bool `json:"is_secret"`
|
||||
IsInstallable bool `json:"is_installable"`
|
||||
GameType string `json:"game_type"`
|
||||
IsPreOrder bool `json:"is_pre_order"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Images Images `json:"images"`
|
||||
Languages Languages `json:"languages,omitempty"`
|
||||
}
|
||||
|
||||
type Images struct {
|
||||
Background string `json:"background"`
|
||||
Logo string `json:"logo"`
|
||||
Logo2X string `json:"logo2x"`
|
||||
Icon string `json:"icon"`
|
||||
SidebarIcon string `json:"sidebarIcon"`
|
||||
SidebarIcon2X string `json:"sidebarIcon2x"`
|
||||
MenuNotificationAv string `json:"menuNotificationAv"`
|
||||
MenuNotificationAv2 string `json:"menuNotificationAv2"`
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user