Skip to content

Commit 7638b17

Browse files
authored
Add filtering endpoints (#8)
* Begin to add filtering * Placeholder for filtering * Secure filtering endpoints * Fix: OnConfigUpdate * Fix multi and world filter * Add new field for customdb * Update readme * Add multi and world filter * Working filtering country * Move everything to CustomDBEntry * Rename CustomDBEntry to TitleDBEntry * Add tests for IsValidFilter Co-authored-by: DblK <admin@dblk.org>
1 parent eb28ea5 commit 7638b17

File tree

10 files changed

+279
-50
lines changed

10 files changed

+279
-50
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ Here is the list of all main features so far:
4747
- [X] Add the possibility to ban theme
4848
- [X] You can specify custom titledb to be merged with official one
4949
- [X] Auto-watch for mounted directories
50+
- [X] Add filters path for shop
51+
52+
## Filtering
53+
54+
When you setup your shop inside `tinfoil` you can now add the following path:
55+
- `multi` : Filter only multiplayer games
56+
- `fr`, `en`, ... : Filter by languages
57+
- `world` : All games without any filter (equivalent without path)
5058

5159
# Dev or build from source
5260

config/config.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ type security struct {
3232
// File holds all config information
3333
type File struct {
3434
rootShop string
35-
ShopHost string `mapstructure:"host"`
36-
ShopProtocol string `mapstructure:"protocol"`
37-
ShopPort int `mapstructure:"port"`
38-
Debug debug `mapstructure:"debug"`
39-
AllSources repository.Sources `mapstructure:"sources"`
40-
Name string `mapstructure:"name"`
41-
Security security `mapstructure:"security"`
42-
CustomTitleDB map[string]repository.CustomDBEntry `mapstructure:"customTitledb"`
35+
ShopHost string `mapstructure:"host"`
36+
ShopProtocol string `mapstructure:"protocol"`
37+
ShopPort int `mapstructure:"port"`
38+
Debug debug `mapstructure:"debug"`
39+
AllSources repository.Sources `mapstructure:"sources"`
40+
Name string `mapstructure:"name"`
41+
Security security `mapstructure:"security"`
42+
CustomTitleDB map[string]repository.TitleDBEntry `mapstructure:"customTitledb"`
4343
shopTemplateData repository.ShopTemplate
4444
}
4545

@@ -197,7 +197,7 @@ func (cfg *File) Directories() []string {
197197
}
198198

199199
// CustomDB returns the list of custom title db
200-
func (cfg *File) CustomDB() map[string]repository.CustomDBEntry {
200+
func (cfg *File) CustomDB() map[string]repository.TitleDBEntry {
201201
return cfg.CustomTitleDB
202202
}
203203

gamescollection/collection.go

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ import (
1010
"io"
1111
"log"
1212
"os"
13-
"reflect"
1413
"strings"
1514

1615
"github.com/DblK/tinshop/config"
1716
"github.com/DblK/tinshop/repository"
1817
"github.com/DblK/tinshop/utils"
1918
)
2019

21-
var library map[string]map[string]interface{}
22-
var mergedLibrary map[string]interface{}
20+
var library map[string]repository.TitleDBEntry
21+
var mergedLibrary map[string]repository.TitleDBEntry
2322
var games repository.GameType
2423

2524
// Load ensure that necessary data is loaded
@@ -67,7 +66,7 @@ func loadTitlesLibrary() {
6766
func ResetGamesCollection() {
6867
// Build games object
6968
games.Success = "Welcome to your own shop!"
70-
games.Titledb = make(map[string]interface{})
69+
games.Titledb = make(map[string]repository.TitleDBEntry)
7170
games.Files = make([]repository.GameFileType, 0)
7271
games.ThemeBlackList = nil
7372
}
@@ -77,7 +76,7 @@ func OnConfigUpdate(cfg repository.Config) {
7776
ResetGamesCollection()
7877

7978
// Create merged library
80-
mergedLibrary = make(map[string]interface{})
79+
mergedLibrary = make(map[string]repository.TitleDBEntry)
8180

8281
// Copy library
8382
for key, entry := range library {
@@ -87,43 +86,88 @@ func OnConfigUpdate(cfg repository.Config) {
8786
}
8887

8988
// Copy CustomDB
90-
for key, entry := range config.GetConfig().CustomDB() {
89+
for key, entry := range cfg.CustomDB() {
9190
gameID := strings.ToUpper(key)
92-
if mergedLibrary[gameID] != nil {
91+
if _, ok := mergedLibrary[gameID]; ok {
9392
log.Println("Duplicate customDB entry from official titledb (consider removing from configuration)", gameID)
9493
} else {
9594
mergedLibrary[gameID] = entry
9695
}
9796
}
9897

9998
// Check if blacklist entries
100-
if len(config.GetConfig().BannedTheme()) != 0 {
101-
games.ThemeBlackList = config.GetConfig().BannedTheme()
99+
if len(cfg.BannedTheme()) != 0 {
100+
games.ThemeBlackList = cfg.BannedTheme()
102101
} else {
103102
games.ThemeBlackList = nil
104103
}
105104
}
106105

107106
// Library returns the titledb library
108-
func Library() map[string]interface{} {
107+
func Library() map[string]repository.TitleDBEntry {
109108
return mergedLibrary
110109
}
111110

112111
// HasGameIDInLibrary tells if we have gameID information in library
113112
func HasGameIDInLibrary(gameID string) bool {
114-
return Library()[gameID] != nil
113+
_, ok := Library()[gameID]
114+
return ok
115115
}
116116

117117
// IsBaseGame tells if the gameID is a base game or not
118118
func IsBaseGame(gameID string) bool {
119-
return Library()[gameID].(map[string]interface{})["iconUrl"] != nil
119+
return Library()[gameID].IconURL != ""
120120
}
121121

122122
// Games returns the games inside the library
123123
func Games() repository.GameType {
124124
return games
125125
}
126126

127+
// Filter returns the games inside the library after filtering
128+
func Filter(filter string) repository.GameType {
129+
var filteredGames repository.GameType
130+
filteredGames.Success = games.Success
131+
filteredGames.ThemeBlackList = games.ThemeBlackList
132+
upperFilter := strings.ToUpper(filter)
133+
134+
newTitleDB := make(map[string]repository.TitleDBEntry)
135+
newFiles := make([]repository.GameFileType, 0)
136+
for ID, entry := range games.Titledb {
137+
entryFiltered := false
138+
if upperFilter == "WORLD" {
139+
entryFiltered = true
140+
} else if upperFilter == "MULTI" {
141+
numberPlayers := entry.NumberOfPlayers
142+
143+
if numberPlayers > 1 {
144+
entryFiltered = true
145+
}
146+
} else if utils.IsValidFilter(upperFilter) {
147+
languages := entry.Languages
148+
149+
if utils.Contains(languages, upperFilter) || utils.Contains(languages, strings.ToLower(upperFilter)) {
150+
entryFiltered = true
151+
}
152+
}
153+
154+
if entryFiltered {
155+
newTitleDB[ID] = entry
156+
idx := utils.Search(len(games.Files), func(index int) bool {
157+
return strings.Contains(games.Files[index].URL, ID)
158+
})
159+
160+
if idx != -1 {
161+
newFiles = append(newFiles, games.Files[idx])
162+
}
163+
}
164+
}
165+
filteredGames.Titledb = newTitleDB
166+
filteredGames.Files = newFiles
167+
168+
return filteredGames
169+
}
170+
127171
// RemoveGame remove ID from the collection
128172
func RemoveGame(ID string) {
129173
gameID := strings.ToUpper(ID)
@@ -146,14 +190,8 @@ func RemoveGame(ID string) {
146190
func CountGames() int {
147191
var uniqueGames int
148192
for _, entry := range games.Titledb {
149-
if reflect.TypeOf(entry).String() == "repository.CustomDBEntry" {
150-
if entry.(repository.CustomDBEntry).IconURL != "" {
151-
uniqueGames++
152-
}
153-
} else {
154-
if entry.(map[string]interface{})["iconUrl"] != nil {
155-
uniqueGames++
156-
}
193+
if entry.IconURL != "" {
194+
uniqueGames++
157195
}
158196
}
159197
return uniqueGames
@@ -173,7 +211,7 @@ func AddNewGames(newGames []repository.FileDesc) {
173211

174212
if HasGameIDInLibrary(file.GameID) {
175213
// Verify already present and not update nor dlc
176-
if games.Titledb[file.GameID] != nil && IsBaseGame(file.GameID) {
214+
if _, ok := games.Titledb[file.GameID]; ok && IsBaseGame(file.GameID) {
177215
log.Println("Already added id!", file.GameID, file.Path)
178216
} else {
179217
games.Titledb[file.GameID] = Library()[file.GameID]

gamescollection/collection_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package gamescollection_test
22

33
import (
4+
"github.com/golang/mock/gomock"
45
. "github.com/onsi/ginkgo"
56
. "github.com/onsi/gomega"
67

78
collection "github.com/DblK/tinshop/gamescollection"
9+
"github.com/DblK/tinshop/mock_repository"
810
"github.com/DblK/tinshop/repository"
911
)
1012

@@ -78,4 +80,92 @@ var _ = Describe("Collection", func() {
7880
Expect(collection.Games().Files).To(HaveLen(1))
7981
})
8082
})
83+
Describe("Filter", func() {
84+
var (
85+
myMockConfig *mock_repository.MockConfig
86+
ctrl *gomock.Controller
87+
)
88+
BeforeEach(func() {
89+
ctrl = gomock.NewController(GinkgoT())
90+
})
91+
JustBeforeEach(func() {
92+
myMockConfig = mock_repository.NewMockConfig(ctrl)
93+
customDB := make(map[string]repository.TitleDBEntry)
94+
custom1 := repository.TitleDBEntry{
95+
ID: "0000000000000001",
96+
Languages: []string{"FR", "EN", "US"},
97+
NumberOfPlayers: 1,
98+
}
99+
customDB["0000000000000001"] = custom1
100+
custom2 := repository.TitleDBEntry{
101+
ID: "0000000000000002",
102+
Languages: []string{"JP"},
103+
NumberOfPlayers: 2,
104+
}
105+
customDB["0000000000000001"] = custom1
106+
customDB["0000000000000002"] = custom2
107+
108+
myMockConfig.EXPECT().
109+
Host().
110+
Return("tinshop.example.com").
111+
AnyTimes()
112+
myMockConfig.EXPECT().
113+
CustomDB().
114+
Return(customDB).
115+
AnyTimes()
116+
myMockConfig.EXPECT().
117+
BannedTheme().
118+
Return(nil).
119+
AnyTimes()
120+
121+
collection.OnConfigUpdate(myMockConfig)
122+
123+
newGames := make([]repository.FileDesc, 0)
124+
newFile1 := repository.FileDesc{
125+
Size: 1,
126+
Path: "here",
127+
GameID: "0000000000000001",
128+
GameInfo: "[0000000000000001][v0].nsp",
129+
HostType: repository.LocalFile,
130+
}
131+
newFile2 := repository.FileDesc{
132+
Size: 22,
133+
Path: "here",
134+
GameID: "0000000000000002",
135+
GameInfo: "[0000000000000002][v0].nsp",
136+
HostType: repository.LocalFile,
137+
}
138+
newGames = append(newGames, newFile1)
139+
newGames = append(newGames, newFile2)
140+
collection.AddNewGames(newGames)
141+
})
142+
It("Filtering world", func() {
143+
filteredGames := collection.Filter("WORLD")
144+
Expect(len(filteredGames.Titledb)).To(Equal(2))
145+
Expect(filteredGames.Titledb["0000000000000001"]).To(Not(BeNil()))
146+
Expect(filteredGames.Titledb["0000000000000002"]).To(Not(BeNil()))
147+
Expect(len(filteredGames.Files)).To(Equal(2))
148+
})
149+
It("Filtering US", func() {
150+
filteredGames := collection.Filter("US")
151+
Expect(len(filteredGames.Titledb)).To(Equal(1))
152+
Expect(filteredGames.Titledb["0000000000000001"]).To(Not(BeNil()))
153+
_, ok := filteredGames.Titledb["0000000000000002"]
154+
Expect(ok).To(BeFalse())
155+
Expect(len(filteredGames.Files)).To(Equal(1))
156+
})
157+
It("Filtering non existing language entry (HK)", func() {
158+
filteredGames := collection.Filter("HK")
159+
Expect(len(filteredGames.Titledb)).To(Equal(0))
160+
Expect(len(filteredGames.Files)).To(Equal(0))
161+
})
162+
It("Filtering multi", func() {
163+
filteredGames := collection.Filter("MULTI")
164+
Expect(len(filteredGames.Titledb)).To(Equal(1))
165+
_, ok := filteredGames.Titledb["0000000000000001"]
166+
Expect(ok).To(BeFalse())
167+
Expect(filteredGames.Titledb["0000000000000002"]).To(Not(BeNil()))
168+
Expect(len(filteredGames.Files)).To(Equal(1))
169+
})
170+
})
81171
})

main.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/DblK/tinshop/config"
1515
collection "github.com/DblK/tinshop/gamescollection"
1616
"github.com/DblK/tinshop/sources"
17+
"github.com/DblK/tinshop/utils"
1718
"github.com/gorilla/mux"
1819
)
1920

@@ -26,6 +27,7 @@ func main() {
2627
r := mux.NewRouter()
2728
r.HandleFunc("/", HomeHandler)
2829
r.HandleFunc("/games/{game}", GamesHandler)
30+
r.HandleFunc("/{filter}", FilteringHandler)
2931
r.NotFoundHandler = http.HandlerFunc(notFound)
3032
r.MethodNotAllowedHandler = http.HandlerFunc(notAllowed)
3133
r.Use(tinfoilMiddleware)
@@ -98,9 +100,8 @@ func notAllowed(w http.ResponseWriter, r *http.Request) {
98100
w.WriteHeader(http.StatusMethodNotAllowed)
99101
}
100102

101-
// HomeHandler handles list of games
102-
func HomeHandler(w http.ResponseWriter, r *http.Request) {
103-
jsonResponse, jsonError := json.Marshal(collection.Games())
103+
func serveCollection(w http.ResponseWriter, tinfoilCollection interface{}) {
104+
jsonResponse, jsonError := json.Marshal(tinfoilCollection)
104105

105106
if jsonError != nil {
106107
log.Println("Unable to encode JSON")
@@ -112,10 +113,27 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) {
112113
_, _ = w.Write(jsonResponse)
113114
}
114115

116+
// HomeHandler handles list of games
117+
func HomeHandler(w http.ResponseWriter, r *http.Request) {
118+
serveCollection(w, collection.Games())
119+
}
120+
115121
// GamesHandler handles downloading games
116122
func GamesHandler(w http.ResponseWriter, r *http.Request) {
117123
vars := mux.Vars(r)
118124
log.Println("Requesting game", vars["game"])
119125

120126
sources.DownloadGame(vars["game"], w, r)
121127
}
128+
129+
// FilteringHandler handles filtering games collection
130+
func FilteringHandler(w http.ResponseWriter, r *http.Request) {
131+
vars := mux.Vars(r)
132+
133+
if !utils.IsValidFilter(vars["filter"]) {
134+
w.WriteHeader(http.StatusNotAcceptable)
135+
return
136+
}
137+
138+
serveCollection(w, collection.Filter(vars["filter"]))
139+
}

mock_repository/mock_config.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)