forked from sselph/scraper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ds.go
224 lines (206 loc) · 5.01 KB
/
ds.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
package ds
import (
"compress/gzip"
"encoding/csv"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/mitchellh/go-homedir"
)
const (
hashURL = "https://storage.googleapis.com/stevenselph.appspot.com/hash.csv.gz"
hashName = "hash.csv"
hashMeta = "hash.meta"
)
// NotFoundErr is the error returned when a rom can't be found in the soruce.
var NotFoundErr = errors.New("hash not found")
type ImgType string
// Image types for Datasource options. Not all types are valid for all sources.
const (
IMG_BOXART ImgType = "b"
IMG_SCREEN ImgType = "s"
IMG_FANART ImgType = "f"
IMG_BANNER ImgType = "a"
IMG_LOGO ImgType = "l"
IMG_TITLE ImgType = "t"
IMG_MARQUEE ImgType = "m"
IMG_CABINET ImgType = "c"
)
// Game is the standard Game that all sources will return.
// They don't have to populate all values.
type Game struct {
ID string
Source string
GameTitle string
Overview string
Images map[ImgType]string
Thumbs map[ImgType]string
Rating float64
ReleaseDate string
Developer string
Publisher string
Genre string
Players int64
}
// NewGame returns a new Game.
func NewGame() *Game {
g := &Game{}
g.Images = make(map[ImgType]string)
g.Thumbs = make(map[ImgType]string)
return g
}
// DS is the interface all DataSoures should implement.
type DS interface {
// GetGame takes the path of a ROM and returns the Pretty name if it differs from the Sources normal name.
GetName(string) string
// GetGame takes an id and returns the Game.
GetGame(string) (*Game, error)
// GetID takes the path of a ROM and returns the id to use in GetGame.
GetID(string) (string, error)
}
// mkDir checks if directory exists and if it doesn't create it.
func mkDir(d string) error {
fi, err := os.Stat(d)
switch {
case os.IsNotExist(err):
return os.MkdirAll(d, 0775)
case err != nil:
return err
case fi.IsDir():
return nil
}
return fmt.Errorf("%s is a file not a directory.", d)
}
// HashMap a mapping of hash to names and GDB IDs.
type HashMap struct {
data map[string][]string
}
// GetID returns the id for the given hash.
func (hm *HashMap) GetID(s string) (string, bool) {
d, ok := hm.data[s]
if !ok || d[0] == "" {
return "", false
} else {
return d[0], true
}
}
// GetName returns the no-intro name for the given hash.
func (hm *HashMap) GetName(s string) (string, bool) {
d, ok := hm.data[s]
if !ok || d[1] == "" {
return "", false
} else {
return d[1], true
}
}
// DefaultCachePath returns the path used for all cached data.
// Current <HOME>/.sselph-scraper or <HOMEDIR>\Application Data\sselph-scraper
func DefaultCachePath() (string, error) {
hd, err := homedir.Dir()
if err != nil {
return "", err
}
var p string
if runtime.GOOS == "windows" {
p = filepath.Join(hd, "Application Data", "sselph-scraper")
} else {
p = filepath.Join(hd, ".sselph-scraper")
}
err = mkDir(p)
if err != nil {
return "", err
}
return p, nil
}
// updateHash downloads the latest hash file.
func updateHash(version, p string) error {
log.Print("INFO: Checking for new hash.csv.")
req, err := http.NewRequest("GET", hashURL, nil)
if err != nil {
return err
}
req.Header.Set("if-none-match", version)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotModified {
log.Printf("INFO: hash.csv %s up to date.", version)
return nil
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("got %v response", resp.Status)
}
newVersion := resp.Header.Get("etag")
log.Printf("INFO: Upgrading hash.csv: %s -> %s.", version, newVersion)
bz, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
defer bz.Close()
b, err := ioutil.ReadAll(bz)
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(p, hashName), b, 0664)
if err != nil {
return err
}
ioutil.WriteFile(filepath.Join(p, hashMeta), []byte(newVersion), 0664)
return nil
}
// exists checks if a file exists and contains data.
func exists(s string) bool {
fi, err := os.Stat(s)
return !os.IsNotExist(err) && fi.Size() > 0
}
// CachedHashMap gets the mapping of hashes to IDs.
func CachedHashMap(p string) (*HashMap, error) {
var err error
if p == "" {
p, err = DefaultCachePath()
if err != nil {
return nil, err
}
}
fp := filepath.Join(p, hashName)
mp := filepath.Join(p, hashMeta)
var version string
if exists(fp) && exists(mp) {
b, err := ioutil.ReadFile(mp)
if err != nil {
return nil, err
}
version = strings.Trim(string(b[:]), "\n\r")
}
err = updateHash(version, p)
if err != nil {
return nil, err
}
return FileHashMap(fp)
}
// FileHashMap creates a hash map from a csv file.
func FileHashMap(p string) (*HashMap, error) {
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
c := csv.NewReader(f)
r, err := c.ReadAll()
if err != nil {
return nil, err
}
ret := &HashMap{data: make(map[string][]string)}
for _, v := range r {
ret.data[strings.ToLower(v[0])] = []string{v[1], v[3]}
}
return ret, nil
}