diff --git a/README.md b/README.md index ca35936..1a083c7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Currently used for: https://angie44.github.io/theOffice ## GET -* Get quotes or nodes/links by season number ⚠️ **Under Maintenance (only `quotes` supported at this time)** ⚠️ +* Get quotes or nodes/links by season number * **URL:** _/season/:season/format/:format_ * **Method:** `GET` * **URL Params:** @@ -26,7 +26,7 @@ Currently used for: https://angie44.github.io/theOffice * **Success Response:** * **Code:** 200 * **Content [Quotes]:** `[{ "season": seasonNumber, "episode" : episodeNumber, "scene": sceneNumber, "episode_name": episodeName, "character": character, "quote" : quote}]` - * **Content [Connections]:** `{ "data" : [{ "episode": episodeNumber, "name": episodeName, "links" : [{ "source" : characterName, "target": characterName, "value" : numberOfCoOccurencesInEpisode }], "nodes" : [{ "id" : characterName }]}` + * **Content [Connections]:** `[{ "episode": episodeNumber, "name": episodeName, "links" : [{ "source" : characterName, "target": characterName, "value" : numberOfCoOccurencesInEpisode }], "nodes" : [{ "id" : characterName }]` * Get quotes for a specific season and episode diff --git a/data/connections.go b/data/connections.go new file mode 100644 index 0000000..4f64b45 --- /dev/null +++ b/data/connections.go @@ -0,0 +1,116 @@ +package data + +import ( + combinations "github.com/mxschmitt/golang-combinations" +) + +type Connection struct { + Episode int `bson:"episode" json:"episode"` + EpisodeName string `bson:"episode_name" json:"episode_name"` + Links []*Link `bson:"links" json:"links"` + Nodes []*Node `bson:"nodes" json:"nodes"` +} + +type Link struct { + Source string `bson:"source" json:"source"` + Target string `bson:"target" json:"target"` + Value int `bson:"value" json:"value"` +} + +type Node struct { + Id string `bson:"id" json:"id"` +} + +type Connections []*Connection + +type episode struct { + name string + scenes map[int]Quotes +} + +func GetConnectionsPerEpisode(seasonQuotes Quotes) Connections { + // Map of quotes per episode + quotesByEpisode := make(map[int]*episode) + + for _, quote := range seasonQuotes { + _, ok := quotesByEpisode[quote.Episode] + if ok { + _, sceneOk := quotesByEpisode[quote.Episode].scenes[quote.Scene] + if sceneOk { + quotesByEpisode[quote.Episode].scenes[quote.Scene] = append(quotesByEpisode[quote.Episode].scenes[quote.Scene], quote) + } else { + quotesByEpisode[quote.Episode].scenes[quote.Scene] = Quotes{quote} + } + } else { + quotesByEpisode[quote.Episode] = &episode{ + name: quote.EpisodeName, + scenes: map[int]Quotes{ + quote.Scene: []*Quote{quote}, + }, + } + } + } + + var connections Connections + + for episodeNum, episode := range quotesByEpisode { + // Loop through scenes in an episode + linksMap := make(map[Link]int) + nodesMap := make(map[string]struct{}) + + for _, quotes := range episode.scenes { + charactersInScene := make(map[string]struct{}) + var exists = struct{}{} + + for _, quote := range quotes { + charactersInScene[quote.Character] = exists + } + + var chars []string + for c := range charactersInScene { + chars = append(chars, c) + nodesMap[c] = exists + } + + if len(chars) < 2 { + continue + } + + combinations := combinations.Combinations(chars, 2) + + for _, combo := range combinations { + link := Link{ + Source: combo[0], + Target: combo[1], + } + + linksMap[link]++ + } + } + + var nodes []*Node + for c := range nodesMap { + nodes = append(nodes, &Node{ + Id: c, + }) + } + + var links []*Link + for l, count := range linksMap { + links = append(links, &Link{ + Source: l.Source, + Target: l.Target, + Value: count, + }) + } + + connections = append(connections, &Connection{ + Episode: episodeNum, + EpisodeName: episode.name, + Links: links, + Nodes: nodes, + }) + } + + return connections +} diff --git a/data/quotes.go b/data/quotes.go index ac05dfa..8cb374e 100644 --- a/data/quotes.go +++ b/data/quotes.go @@ -54,7 +54,7 @@ func (q *QuotesDB) GetQuotes() (Quotes, error) { } // GetQuotesBySeason returns all quotes for the given season from the database -func (q *QuotesDB) GetQuotesBySeason(season int, format string) (Quotes, error) { +func (q *QuotesDB) GetQuotesBySeason(season int) (Quotes, error) { serverAPIOptions := options.ServerAPI(options.ServerAPIVersion1) clientOptions := options.Client(). ApplyURI(fmt.Sprintf("mongodb+srv://%s:%s@%s", q.opts.username, q.opts.password, q.opts.hostname)). diff --git a/go.mod b/go.mod index e3a2507..b7004e8 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,16 @@ module github.com/anGie44/theOffice-api go 1.18 require ( - github.com/go-playground/validator v9.31.0+incompatible github.com/gorilla/mux v1.8.0 + github.com/mxschmitt/golang-combinations v1.1.0 github.com/nicholasjackson/env v0.6.0 go.mongodb.org/mongo-driver v1.9.1 ) require ( - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/klauspost/compress v1.13.6 // indirect - github.com/leodido/go-urn v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.7.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -25,6 +22,5 @@ require ( golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/text v0.3.7 // indirect - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 668be6f..2d096a9 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,6 @@ 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/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= -github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= 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/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= @@ -20,9 +14,9 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mxschmitt/golang-combinations v1.1.0 h1:WlIZCnDm+Xlb2pRPf+R/qPKlGOU1w8lpN69/uy5z+Zg= +github.com/mxschmitt/golang-combinations v1.1.0/go.mod h1:RbMhWvfCelHR6WROvT2bVfxJvZHoEvBj71SKe+H0MYU= github.com/nicholasjackson/env v0.6.0 h1:6xdio52m7cKRtgZPER6NFeBZxicR88rx5a+5Jl4/qus= github.com/nicholasjackson/env v0.6.0/go.mod h1:/GtSb9a/BDUCLpcnpauN0d/Bw5ekSI1vLC1b9Lw0Vyk= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -60,7 +54,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -69,8 +62,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/get.go b/handlers/get.go index 08efba4..4f4d5be 100644 --- a/handlers/get.go +++ b/handlers/get.go @@ -1,7 +1,6 @@ package handlers import ( - "fmt" "net/http" "github.com/anGie44/theOffice-api/data" @@ -26,7 +25,7 @@ func (q *Quotes) GetQuotes(rw http.ResponseWriter, r *http.Request) { // GetQuotesBySeason handles GET requests and returns quotes for the specified season and format // GET /season/{season}/format/{format} -func (q *Quotes) GetQuotesBySeason(rw http.ResponseWriter, r *http.Request) { +func (q *Quotes) GetQuotesBySeasonWithFormat(rw http.ResponseWriter, r *http.Request) { season, err := getSeason(r) if err != nil { http.Error(rw, "Unable to convert season", http.StatusBadRequest) @@ -37,23 +36,32 @@ func (q *Quotes) GetQuotesBySeason(rw http.ResponseWriter, r *http.Request) { if format == "" { http.Error(rw, "Must specify a format", http.StatusBadRequest) return - } else if format != "quotes" { - http.Error(rw, fmt.Sprintf("%s format not implemented", format), http.StatusNotImplemented) - return } q.l.Printf("Handle GET Quotes for Season (%d) in Format (%s)\n", season, format) - quotes, err := q.quotesDB.GetQuotesBySeason(season, format) + quotes, err := q.quotesDB.GetQuotesBySeason(season) if err != nil { rw.WriteHeader(http.StatusInternalServerError) data.ToJSON(&GenericError{Message: err.Error()}, rw) return } - err = data.ToJSON(quotes, rw) + if format == "quotes" { + err = data.ToJSON(quotes, rw) + if err != nil { + http.Error(rw, "Unable to marshal quotes json", http.StatusInternalServerError) + } + return + } + + // Connections + + connections := data.GetConnectionsPerEpisode(quotes) + + err = data.ToJSON(connections, rw) if err != nil { - http.Error(rw, "Unable to marshal json", http.StatusInternalServerError) + http.Error(rw, "Unable to marshal connections json", http.StatusInternalServerError) } } diff --git a/main.go b/main.go index 2216fb7..b3684a1 100644 --- a/main.go +++ b/main.go @@ -23,9 +23,12 @@ var dbPassword = env.String("MONGODB_PASSWORD", false, "password", "mongodb data var dbCollection = env.String("MONGODB_COLLECTION", false, "quotes", "mongodb database collection") func main() { - bindAddress := fmt.Sprintf(":%s", os.Getenv("PORT")) // value provided by Heroku - if bindAddress == "" { + var bindAddress string + port := os.Getenv("PORT") // value provided by Heroku + if port == "" { bindAddress = ":8080" + } else { + bindAddress = fmt.Sprintf(":%s", port) } env.Parse() @@ -44,7 +47,7 @@ func main() { getRouter := sm.Methods(http.MethodGet).Subrouter() getRouter.HandleFunc("/", wh.Welcome) // Deprecated handlers - getRouter.HandleFunc("/season/{season:[1-9]}/format/{format:quotes|connections}", qh.GetQuotesBySeason) + getRouter.HandleFunc("/season/{season:[1-9]}/format/{format:quotes|connections}", qh.GetQuotesBySeasonWithFormat) getRouter.HandleFunc("/season/{season:[1-9]}/episode/{episode:[1-9]|[1][0-9]|2[0-3]}", qh.GetQuotesBySeasonAndEpisode) // V2 handlers to use request body for filtering data getRouter.HandleFunc("/v2/quotes", qh.GetQuotes) @@ -53,7 +56,7 @@ func main() { Addr: bindAddress, Handler: sm, ErrorLog: l, - ReadTimeout: 5 * time.Second, + ReadTimeout: 1 * time.Minute, IdleTimeout: 2 * time.Minute, }