Skip to content

Commit

Permalink
Add filtering endpoints (#8)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
DblK and DblK authored Dec 23, 2021
1 parent eb28ea5 commit 7638b17
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 50 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ Here is the list of all main features so far:
- [X] Add the possibility to ban theme
- [X] You can specify custom titledb to be merged with official one
- [X] Auto-watch for mounted directories
- [X] Add filters path for shop

## Filtering

When you setup your shop inside `tinfoil` you can now add the following path:
- `multi` : Filter only multiplayer games
- `fr`, `en`, ... : Filter by languages
- `world` : All games without any filter (equivalent without path)

# Dev or build from source

Expand Down
18 changes: 9 additions & 9 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ type security struct {
// File holds all config information
type File struct {
rootShop string
ShopHost string `mapstructure:"host"`
ShopProtocol string `mapstructure:"protocol"`
ShopPort int `mapstructure:"port"`
Debug debug `mapstructure:"debug"`
AllSources repository.Sources `mapstructure:"sources"`
Name string `mapstructure:"name"`
Security security `mapstructure:"security"`
CustomTitleDB map[string]repository.CustomDBEntry `mapstructure:"customTitledb"`
ShopHost string `mapstructure:"host"`
ShopProtocol string `mapstructure:"protocol"`
ShopPort int `mapstructure:"port"`
Debug debug `mapstructure:"debug"`
AllSources repository.Sources `mapstructure:"sources"`
Name string `mapstructure:"name"`
Security security `mapstructure:"security"`
CustomTitleDB map[string]repository.TitleDBEntry `mapstructure:"customTitledb"`
shopTemplateData repository.ShopTemplate
}

Expand Down Expand Up @@ -197,7 +197,7 @@ func (cfg *File) Directories() []string {
}

// CustomDB returns the list of custom title db
func (cfg *File) CustomDB() map[string]repository.CustomDBEntry {
func (cfg *File) CustomDB() map[string]repository.TitleDBEntry {
return cfg.CustomTitleDB
}

Expand Down
80 changes: 59 additions & 21 deletions gamescollection/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ import (
"io"
"log"
"os"
"reflect"
"strings"

"github.com/DblK/tinshop/config"
"github.com/DblK/tinshop/repository"
"github.com/DblK/tinshop/utils"
)

var library map[string]map[string]interface{}
var mergedLibrary map[string]interface{}
var library map[string]repository.TitleDBEntry
var mergedLibrary map[string]repository.TitleDBEntry
var games repository.GameType

// Load ensure that necessary data is loaded
Expand Down Expand Up @@ -67,7 +66,7 @@ func loadTitlesLibrary() {
func ResetGamesCollection() {
// Build games object
games.Success = "Welcome to your own shop!"
games.Titledb = make(map[string]interface{})
games.Titledb = make(map[string]repository.TitleDBEntry)
games.Files = make([]repository.GameFileType, 0)
games.ThemeBlackList = nil
}
Expand All @@ -77,7 +76,7 @@ func OnConfigUpdate(cfg repository.Config) {
ResetGamesCollection()

// Create merged library
mergedLibrary = make(map[string]interface{})
mergedLibrary = make(map[string]repository.TitleDBEntry)

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

// Copy CustomDB
for key, entry := range config.GetConfig().CustomDB() {
for key, entry := range cfg.CustomDB() {
gameID := strings.ToUpper(key)
if mergedLibrary[gameID] != nil {
if _, ok := mergedLibrary[gameID]; ok {
log.Println("Duplicate customDB entry from official titledb (consider removing from configuration)", gameID)
} else {
mergedLibrary[gameID] = entry
}
}

// Check if blacklist entries
if len(config.GetConfig().BannedTheme()) != 0 {
games.ThemeBlackList = config.GetConfig().BannedTheme()
if len(cfg.BannedTheme()) != 0 {
games.ThemeBlackList = cfg.BannedTheme()
} else {
games.ThemeBlackList = nil
}
}

// Library returns the titledb library
func Library() map[string]interface{} {
func Library() map[string]repository.TitleDBEntry {
return mergedLibrary
}

// HasGameIDInLibrary tells if we have gameID information in library
func HasGameIDInLibrary(gameID string) bool {
return Library()[gameID] != nil
_, ok := Library()[gameID]
return ok
}

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

// Games returns the games inside the library
func Games() repository.GameType {
return games
}

// Filter returns the games inside the library after filtering
func Filter(filter string) repository.GameType {
var filteredGames repository.GameType
filteredGames.Success = games.Success
filteredGames.ThemeBlackList = games.ThemeBlackList
upperFilter := strings.ToUpper(filter)

newTitleDB := make(map[string]repository.TitleDBEntry)
newFiles := make([]repository.GameFileType, 0)
for ID, entry := range games.Titledb {
entryFiltered := false
if upperFilter == "WORLD" {
entryFiltered = true
} else if upperFilter == "MULTI" {
numberPlayers := entry.NumberOfPlayers

if numberPlayers > 1 {
entryFiltered = true
}
} else if utils.IsValidFilter(upperFilter) {
languages := entry.Languages

if utils.Contains(languages, upperFilter) || utils.Contains(languages, strings.ToLower(upperFilter)) {
entryFiltered = true
}
}

if entryFiltered {
newTitleDB[ID] = entry
idx := utils.Search(len(games.Files), func(index int) bool {
return strings.Contains(games.Files[index].URL, ID)
})

if idx != -1 {
newFiles = append(newFiles, games.Files[idx])
}
}
}
filteredGames.Titledb = newTitleDB
filteredGames.Files = newFiles

return filteredGames
}

// RemoveGame remove ID from the collection
func RemoveGame(ID string) {
gameID := strings.ToUpper(ID)
Expand All @@ -146,14 +190,8 @@ func RemoveGame(ID string) {
func CountGames() int {
var uniqueGames int
for _, entry := range games.Titledb {
if reflect.TypeOf(entry).String() == "repository.CustomDBEntry" {
if entry.(repository.CustomDBEntry).IconURL != "" {
uniqueGames++
}
} else {
if entry.(map[string]interface{})["iconUrl"] != nil {
uniqueGames++
}
if entry.IconURL != "" {
uniqueGames++
}
}
return uniqueGames
Expand All @@ -173,7 +211,7 @@ func AddNewGames(newGames []repository.FileDesc) {

if HasGameIDInLibrary(file.GameID) {
// Verify already present and not update nor dlc
if games.Titledb[file.GameID] != nil && IsBaseGame(file.GameID) {
if _, ok := games.Titledb[file.GameID]; ok && IsBaseGame(file.GameID) {
log.Println("Already added id!", file.GameID, file.Path)
} else {
games.Titledb[file.GameID] = Library()[file.GameID]
Expand Down
90 changes: 90 additions & 0 deletions gamescollection/collection_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package gamescollection_test

import (
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

collection "github.com/DblK/tinshop/gamescollection"
"github.com/DblK/tinshop/mock_repository"
"github.com/DblK/tinshop/repository"
)

Expand Down Expand Up @@ -78,4 +80,92 @@ var _ = Describe("Collection", func() {
Expect(collection.Games().Files).To(HaveLen(1))
})
})
Describe("Filter", func() {
var (
myMockConfig *mock_repository.MockConfig
ctrl *gomock.Controller
)
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
})
JustBeforeEach(func() {
myMockConfig = mock_repository.NewMockConfig(ctrl)
customDB := make(map[string]repository.TitleDBEntry)
custom1 := repository.TitleDBEntry{
ID: "0000000000000001",
Languages: []string{"FR", "EN", "US"},
NumberOfPlayers: 1,
}
customDB["0000000000000001"] = custom1
custom2 := repository.TitleDBEntry{
ID: "0000000000000002",
Languages: []string{"JP"},
NumberOfPlayers: 2,
}
customDB["0000000000000001"] = custom1
customDB["0000000000000002"] = custom2

myMockConfig.EXPECT().
Host().
Return("tinshop.example.com").
AnyTimes()
myMockConfig.EXPECT().
CustomDB().
Return(customDB).
AnyTimes()
myMockConfig.EXPECT().
BannedTheme().
Return(nil).
AnyTimes()

collection.OnConfigUpdate(myMockConfig)

newGames := make([]repository.FileDesc, 0)
newFile1 := repository.FileDesc{
Size: 1,
Path: "here",
GameID: "0000000000000001",
GameInfo: "[0000000000000001][v0].nsp",
HostType: repository.LocalFile,
}
newFile2 := repository.FileDesc{
Size: 22,
Path: "here",
GameID: "0000000000000002",
GameInfo: "[0000000000000002][v0].nsp",
HostType: repository.LocalFile,
}
newGames = append(newGames, newFile1)
newGames = append(newGames, newFile2)
collection.AddNewGames(newGames)
})
It("Filtering world", func() {
filteredGames := collection.Filter("WORLD")
Expect(len(filteredGames.Titledb)).To(Equal(2))
Expect(filteredGames.Titledb["0000000000000001"]).To(Not(BeNil()))
Expect(filteredGames.Titledb["0000000000000002"]).To(Not(BeNil()))
Expect(len(filteredGames.Files)).To(Equal(2))
})
It("Filtering US", func() {
filteredGames := collection.Filter("US")
Expect(len(filteredGames.Titledb)).To(Equal(1))
Expect(filteredGames.Titledb["0000000000000001"]).To(Not(BeNil()))
_, ok := filteredGames.Titledb["0000000000000002"]
Expect(ok).To(BeFalse())
Expect(len(filteredGames.Files)).To(Equal(1))
})
It("Filtering non existing language entry (HK)", func() {
filteredGames := collection.Filter("HK")
Expect(len(filteredGames.Titledb)).To(Equal(0))
Expect(len(filteredGames.Files)).To(Equal(0))
})
It("Filtering multi", func() {
filteredGames := collection.Filter("MULTI")
Expect(len(filteredGames.Titledb)).To(Equal(1))
_, ok := filteredGames.Titledb["0000000000000001"]
Expect(ok).To(BeFalse())
Expect(filteredGames.Titledb["0000000000000002"]).To(Not(BeNil()))
Expect(len(filteredGames.Files)).To(Equal(1))
})
})
})
24 changes: 21 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/DblK/tinshop/config"
collection "github.com/DblK/tinshop/gamescollection"
"github.com/DblK/tinshop/sources"
"github.com/DblK/tinshop/utils"
"github.com/gorilla/mux"
)

Expand All @@ -26,6 +27,7 @@ func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/games/{game}", GamesHandler)
r.HandleFunc("/{filter}", FilteringHandler)
r.NotFoundHandler = http.HandlerFunc(notFound)
r.MethodNotAllowedHandler = http.HandlerFunc(notAllowed)
r.Use(tinfoilMiddleware)
Expand Down Expand Up @@ -98,9 +100,8 @@ func notAllowed(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}

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

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

// HomeHandler handles list of games
func HomeHandler(w http.ResponseWriter, r *http.Request) {
serveCollection(w, collection.Games())
}

// GamesHandler handles downloading games
func GamesHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
log.Println("Requesting game", vars["game"])

sources.DownloadGame(vars["game"], w, r)
}

// FilteringHandler handles filtering games collection
func FilteringHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

if !utils.IsValidFilter(vars["filter"]) {
w.WriteHeader(http.StatusNotAcceptable)
return
}

serveCollection(w, collection.Filter(vars["filter"]))
}
4 changes: 2 additions & 2 deletions mock_repository/mock_config.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 7638b17

Please sign in to comment.