From 8d5cee292d737bd19914664196f700071e6dad73 Mon Sep 17 00:00:00 2001 From: Guntram Date: Wed, 27 Mar 2024 18:59:42 +0100 Subject: [PATCH] Movie search (#7) #6 add movie search --------- Co-authored-by: kubegu --- .github/workflows/go.yml | 2 +- cmd/cli/main.go | 8 +- domain/movie.go | 7 + go.mod | 13 +- go.sum | 50 +++-- library-le/client.go | 94 ++++----- library-le/client_test.go | 38 +++- ..._example.html => game_search_example.html} | 0 library-le/movie_copies_example.html | 178 ++++++++++++++++++ library-le/parse_results.go | 86 +++++++++ library-le/parse_results_test.go | 53 ++++++ library-le/request.go | 71 +++++++ library-le/searchresult.go | 37 ---- library-le/searchresult_test.go | 33 ---- web/server.go | 75 +++++++- web/server_test.go | 26 +++ web/templates/games.html | 52 ++++- web/templates/index.html | 49 ----- web/templates/item-list-by-branch.html | 18 ++ web/templates/item-list.html | 3 + web/templates/movies.html | 25 +++ 21 files changed, 700 insertions(+), 218 deletions(-) create mode 100644 domain/movie.go rename library-le/{searchresult_example.html => game_search_example.html} (100%) create mode 100644 library-le/movie_copies_example.html create mode 100644 library-le/parse_results.go create mode 100644 library-le/parse_results_test.go create mode 100644 library-le/request.go delete mode 100644 library-le/searchresult.go delete mode 100644 library-le/searchresult_test.go create mode 100644 web/server_test.go delete mode 100644 web/templates/index.html create mode 100644 web/templates/item-list-by-branch.html create mode 100644 web/templates/item-list.html create mode 100644 web/templates/movies.html diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 159b748..b322c7b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '1.22' - name: Go Mod run: go mod download diff --git a/cmd/cli/main.go b/cmd/cli/main.go index f9868d9..fbb96c1 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -11,19 +11,13 @@ import ( func main() { branchPtr := flag.Int("branch", 20, "Branch code of the library") platformPtr := flag.String("platform", "Nintendo Switch", "Console platform to list games") - allBranchesPtr := flag.Bool("all", false, "Search in all Branches") flag.Parse() client := libClient.Client{} var games []domain.Game - if *allBranchesPtr { - fmt.Printf("Searching all games for %s \n", *platformPtr) - games = client.GetAllAvailableGamesPlatform(*platformPtr) - } else { - games = client.FindAvailabelGames(*branchPtr, *platformPtr) - } + games = client.FindAvailabelGames(*branchPtr, *platformPtr) for _, game := range games { fmt.Printf("%s (%s)\n", game.Title, game.Branch) diff --git a/domain/movie.go b/domain/movie.go new file mode 100644 index 0000000..88bd33f --- /dev/null +++ b/domain/movie.go @@ -0,0 +1,7 @@ +package domain + +type Movie struct { + Title string `json:"title"` + Branch string `json:"branch"` + IsAvailable bool `json:"isAvailable"` +} diff --git a/go.mod b/go.mod index 83d3d3d..25e02ca 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,16 @@ module github.com/gunni1/leipzig-library-game-stock-api -go 1.21 +go 1.22 -require github.com/stretchr/testify v1.8.3 - -require github.com/PuerkitoBio/goquery v1.8.0 +require ( + github.com/PuerkitoBio/goquery v1.9.1 + github.com/stretchr/testify v1.9.0 +) require ( - github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 88e9984..124d901 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,49 @@ -github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= -github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= -github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/library-le/client.go b/library-le/client.go index 3714df1..dfc2702 100644 --- a/library-le/client.go +++ b/library-le/client.go @@ -5,12 +5,15 @@ import ( "fmt" "log" "net/http" - "strconv" - "sync" "github.com/gunni1/leipzig-library-game-stock-api/domain" ) +const ( + LIB_BASE_URL = "https://webopac.stadtbibliothek-leipzig.de" +) + +// Deprecated? var BranchCodes = map[int]string{ 0: "Stadtbibliothek", 20: "Bibliothek Plagwitz", @@ -31,6 +34,7 @@ var BranchCodes = map[int]string{ 90: "Fahrbibliothek", } +// Deprecated? func BranchCodeKeys() []int { keys := make([]int, 0, len(BranchCodes)) for key := range BranchCodes { @@ -54,17 +58,16 @@ func (libClient Client) FindAvailabelGames(branchCode int, platform string) []do fmt.Println(sessionErr) return nil } - request := createSearchRequest(branchCode, platform, libClient.session.jSessionId, libClient.session.userSessionId) + request := createGameSearchRequest(branchCode, platform, libClient.session) httpClient := http.Client{} response, err := httpClient.Do(request) if err != nil { - log.Fatal("error during search") + log.Println("error during search") return nil } defer response.Body.Close() - games, parseResultErr := parseSearchResult(response.Body) - //Add branchCode to games? + games, parseResultErr := parseGameSearchResult(response.Body) if parseResultErr != nil { log.Fatalln(parseResultErr) return nil @@ -72,66 +75,45 @@ func (libClient Client) FindAvailabelGames(branchCode int, platform string) []do return games } -func (libClient Client) GetAllAvailableGamesPlatform(platform string) []domain.Game { - searchResults := make(chan domain.Game) - - wg := &sync.WaitGroup{} - for _, code := range BranchCodeKeys() { - wg.Add(1) - go getAvailableGames(code, platform, searchResults, wg, libClient) +// Search for a specific movie title in all library branches +func (libClient Client) FindMovies(title string) []domain.Movie { + sessionErr := libClient.openSession() + if sessionErr != nil { + fmt.Println(sessionErr) + return nil } - go func() { - wg.Wait() - close(searchResults) - }() - games := make([]domain.Game, 0) - for game := range searchResults { - games = append(games, game) + searchRequest := createMovieSearchRequest(title, libClient.session) + httpClient := http.Client{} + searchResponse, err := httpClient.Do(searchRequest) + if err != nil { + log.Println("error during search") + return nil } - return games -} + resultTitles := parseMovieSearch(searchResponse.Body) -func getAvailableGames(branchCode int, platform string, results chan domain.Game, wg *sync.WaitGroup, client Client) { - defer wg.Done() - games := client.FindAvailabelGames(branchCode, platform) - for _, game := range games { - results <- game + movies := make([]domain.Movie, 0) + for _, resultTitle := range resultTitles { + movies = append(movies, resultTitle.loadMovieCopies(libClient.session)...) } + //Parallel Ergebnislinks folgen und Details über Zweigstelle und Verfpgbarkeit sammeln + return movies } -func createSearchRequest(branchCode int, searchString string, jSessionId string, userSessionId string) *http.Request { - request, _ := http.NewRequest("GET", "https://webopac.stadtbibliothek-leipzig.de/webOPACClient/search.do", nil) - jSessionCookie := &http.Cookie{ - Name: "JSESSIONID", - Value: jSessionId, - } - userSessionCookie := &http.Cookie{ - Name: "USERSESSIONID", - Value: userSessionId, - } - request.AddCookie(jSessionCookie) - request.AddCookie(userSessionCookie) +// Load all existing copys of a result title over all library branches +func (result searchResult) loadMovieCopies(libSession webOpacSession) []domain.Movie { + request := createRequest(libSession, result.resultUrl) - query := request.URL.Query() - //Fix Query Params to make the search working - query.Add("methodToCall", "submit") - query.Add("methodToCallParameter", "submitSearch") - query.Add("searchCategories[0]", "902") - query.Add("submitSearch", "Suchen") - query.Add("callingPage", "searchPreferences") - query.Add("numberOfHits", "500") - query.Add("timeOut", "20") - //Query Params dependend on user input / session - query.Add("CSId", userSessionId) - query.Add("searchString[0]", searchString) - query.Add("selectedSearchBranchlib", strconv.FormatInt(int64(branchCode), 10)) - query.Add("selectedViewBranchlib", strconv.FormatInt(int64(branchCode), 10)) - request.URL.RawQuery = query.Encode() - return request + httpClient := http.Client{} + movieResponse, err := httpClient.Do(request) + if err != nil { + log.Println("error during search") + return nil + } + return parseMovieCopiesPage(result.title, movieResponse.Body) } func (client *Client) openSession() error { - resp, err := http.Get("https://webopac.stadtbibliothek-leipzig.de/webOPACClient") + resp, err := http.Get(LIB_BASE_URL + "/webOPACClient") if err != nil { return err } diff --git a/library-le/client_test.go b/library-le/client_test.go index 8f97fe6..c8ac1bc 100644 --- a/library-le/client_test.go +++ b/library-le/client_test.go @@ -1,6 +1,7 @@ package libraryle import ( + "net/http" "testing" . "github.com/stretchr/testify/assert" @@ -11,10 +12,10 @@ const ( userSessionId string = "2267N112S85e7645be446dd6c4e2e4bc558a206f3c4a88788" ) -func TestRequestHasSearchParameters(t *testing.T) { - +func TestGameRequestHasSearchParameters(t *testing.T) { + session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} searchString := "Nintendo Switch" - result := createSearchRequest(40, searchString, jSessionId, userSessionId) + result := createGameSearchRequest(40, searchString, session) Equal(t, "submit", result.URL.Query().Get("methodToCall")) Equal(t, "submitSearch", result.URL.Query().Get("methodToCallParameter")) @@ -29,14 +30,17 @@ func TestRequestHasSearchParameters(t *testing.T) { Equal(t, "40", result.URL.Query().Get("selectedSearchBranchlib")) } -func TestRequestHasCookiesSet(t *testing.T) { - result := createSearchRequest(40, "Nintendo Switch", jSessionId, userSessionId) - - Equal(t, 2, len(result.Cookies())) +func TestGameRequestHasCookiesSet(t *testing.T) { + session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} + request := createGameSearchRequest(40, "Nintendo Switch", session) + assertSessionCookiesExists(request, t) +} +func assertSessionCookiesExists(request *http.Request, t *testing.T) { + Equal(t, 2, len(request.Cookies())) foundJSessionId := false foundUserSessionId := false - for _, cookie := range result.Cookies() { + for _, cookie := range request.Cookies() { switch cookie.Name { case "JSESSIONID": foundJSessionId = true @@ -47,3 +51,21 @@ func TestRequestHasCookiesSet(t *testing.T) { True(t, foundJSessionId) True(t, foundUserSessionId) } + +func TestMovieSearchRequestHasCookiesSet(t *testing.T) { + session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} + request := createMovieSearchRequest("Terminator", session) + assertSessionCookiesExists(request, t) +} + +func TestMovieSearchRequestHasQueryParamsSet(t *testing.T) { + session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} + request := createMovieSearchRequest("Terminator", session) + Equal(t, "submit", request.URL.Query().Get("methodToCall")) + Equal(t, "331", request.URL.Query().Get("searchCategories[0]")) + Equal(t, "500", request.URL.Query().Get("numberOfHits")) + Equal(t, "3", request.URL.Query().Get("searchRestrictionID[2]")) + Equal(t, "29", request.URL.Query().Get("searchRestrictionValue1[2]")) + Equal(t, "0", request.URL.Query().Get("selectedViewBranchlib")) + Empty(t, request.URL.Query().Get("selectedSearchBranchlib")) +} diff --git a/library-le/searchresult_example.html b/library-le/game_search_example.html similarity index 100% rename from library-le/searchresult_example.html rename to library-le/game_search_example.html diff --git a/library-le/movie_copies_example.html b/library-le/movie_copies_example.html new file mode 100644 index 0000000..b5dbce0 --- /dev/null +++ b/library-le/movie_copies_example.html @@ -0,0 +1,178 @@ +
+
+
+
+
+ +
+ +
+ Mediennummer +
+
+ +
+ Zweigstelle - Nutzung im Lesesaal? +
+
+ +
+ Standort +
+
+
+ Status/Vormerkung (1,-€) +
+
+
+ +
+
+ +
+ +
+ 001652278 +
+
+ +
+ Stadtbibliothek / Multimedia - 2.OG +
+
+ +
+ SciFi/Fantasy Ter +
+
+ +
+ +
+
+ +
+ +
+ 002045449 +
+
+ +
+ Bibliothek Plagwitz / Phonothek - 1. OG +
+
+ +
+ R 11 Ter +
+
+
+ andere Zweigstelle (entliehen) +
+
+ +
+
+ +
+ +
+ 002045277 +
+
+ +
+ Bibliothek Wiederitzsch / Erwachsenenbibliothek +
+
+ +
+ Fantasy & SciFi Ter +
+
+
+ andere Zweigstelle (ausleihbar) +
+
+ +
+
+ +
+ +
+ 002045445 +
+
+ +
+ Bibliothek Südvorstadt / Erwachsenenbibliothek - EG +
+
+ +
+ R 11 Ter +
+
+
+ andere Zweigstelle (ausleihbar) +
+
+ +
+
+ +
+ +
+ 002037536 +
+
+ +
+ Bibliothek Gohlis / Erwachsenenbibliothek +
+
+ +
+ Fantasy & SciFi Ter +
+
+
+ andere Zweigstelle (entliehen) +
+
+ +
+
+ +
+ +
+ 002045447 +
+
+ +
+ Bibliothek Grünau-Nord / Erwachsenenbibliothek +
+
+ +
+ R 11 Ter +
+
+
+ andere Zweigstelle (entliehen) +
+
+
+
+ \ No newline at end of file diff --git a/library-le/parse_results.go b/library-le/parse_results.go new file mode 100644 index 0000000..e7c0bf6 --- /dev/null +++ b/library-le/parse_results.go @@ -0,0 +1,86 @@ +package libraryle + +import ( + "io" + "log" + "strings" + + "github.com/PuerkitoBio/goquery" + "github.com/gunni1/leipzig-library-game-stock-api/domain" +) + +const ( + resultItemSelector string = "h2[class^=recordtitle]" + titleSelector string = "a[href^='/webOPACClient/singleHit']" + availabilitySelector string = "span[class^=textgruen]" + copiesSelector string = "#tab-content > div > div:nth-child(n+2)" +) + +type searchResult struct { + title string + resultUrl string +} + +// Takes a html as reader from a webopac search and +// try to parse it to an array of games that are listed as available. +func parseGameSearchResult(searchResult io.Reader) ([]domain.Game, error) { + doc, docErr := goquery.NewDocumentFromReader(searchResult) + if docErr != nil { + log.Println("Could not create document from response.") + return nil, docErr + } + games := make([]domain.Game, 0) + doc.Find(resultItemSelector).Each(func(i int, resultItem *goquery.Selection) { + title := resultItem.Find(titleSelector).Text() + if isGameAvailable(resultItem.Parent()) { + games = append(games, domain.Game{Title: title}) + } + }) + return games, nil +} + +func isGameAvailable(searchHitNode *goquery.Selection) bool { + return len(searchHitNode.Find(availabilitySelector).Nodes) > 0 +} + +func parseMovieSearch(searchResponse io.Reader) []searchResult { + doc, docErr := goquery.NewDocumentFromReader(searchResponse) + if docErr != nil { + log.Println("Could not create document from response.") + return nil + } + titles := make([]searchResult, 0) + doc.Find(resultItemSelector).Each(func(i int, resultItem *goquery.Selection) { + title := resultItem.Find(titleSelector).Text() + resultUrl, _ := resultItem.Find(titleSelector).Attr("href") + titles = append(titles, searchResult{title: title, resultUrl: resultUrl}) + }) + return titles +} + +func parseMovieCopiesPage(title string, page io.Reader) []domain.Movie { + doc, docErr := goquery.NewDocumentFromReader(page) + if docErr != nil { + log.Println("Could not create document from response.") + return nil + } + movies := make([]domain.Movie, 0) + + doc.Find(copiesSelector).Each(func(i int, copy *goquery.Selection) { + branch := copy.Find("div.col-12.col-md-4.my-md-2 > b").Text() + status := isMovieAvailable(copy) + movies = append(movies, domain.Movie{Title: title, Branch: branch, IsAvailable: status}) + }) + + return movies +} + +func isMovieAvailable(copy *goquery.Selection) bool { + rentalStateLink := copy.Find("div:nth-child(5) > div > a") + //Link indicates a rented state (can reserve a copy) + if rentalStateLink.Length() != 0 { + return false + } + statusText := copy.Find("div:nth-child(5)").Text() + return strings.Contains(statusText, "ausleihbar") +} diff --git a/library-le/parse_results_test.go b/library-le/parse_results_test.go new file mode 100644 index 0000000..966481e --- /dev/null +++ b/library-le/parse_results_test.go @@ -0,0 +1,53 @@ +package libraryle + +import ( + "bufio" + "io" + "log" + "os" + "testing" + + "github.com/gunni1/leipzig-library-game-stock-api/domain" + . "github.com/stretchr/testify/assert" +) + +func TestAvailability(t *testing.T) { + fileReader := loadTestData("game_search_example.html") + + games, _ := parseGameSearchResult(fileReader) + + True(t, hasElement(games, "Spiel2")) + False(t, hasElement(games, "Spiel1")) + +} + +func TestParseMovieCopiesResult(t *testing.T) { + testResponse := loadTestData("movie_copies_example.html") + movies := parseMovieCopiesPage("Terminator - Genesis", testResponse) + Equal(t, 6, len(movies)) + + available := 0 + for _, movie := range movies { + if movie.IsAvailable { + available++ + } + } + Equal(t, 2, available) +} + +func loadTestData(filePath string) io.Reader { + file, fileErr := os.Open(filePath) + if fileErr != nil { + log.Fatal(fileErr) + } + return bufio.NewReader(file) +} + +func hasElement(games []domain.Game, title string) bool { + for _, game := range games { + if game.Title == title { + return true + } + } + return false +} diff --git a/library-le/request.go b/library-le/request.go new file mode 100644 index 0000000..8b46fe9 --- /dev/null +++ b/library-le/request.go @@ -0,0 +1,71 @@ +package libraryle + +import ( + "net/http" + "strconv" +) + +func createRequest(libSession webOpacSession, path string) *http.Request { + request, _ := http.NewRequest("GET", LIB_BASE_URL+path, nil) + jSessionCookie := &http.Cookie{ + Name: "JSESSIONID", + Value: libSession.jSessionId, + } + userSessionCookie := &http.Cookie{ + Name: "USERSESSIONID", + Value: libSession.userSessionId, + } + request.AddCookie(jSessionCookie) + request.AddCookie(userSessionCookie) + return request +} + +func createMovieSearchRequest(searchString string, libSession webOpacSession) *http.Request { + request := createRequest(libSession, "/webOPACClient/search.do") + request.URL.RawQuery = createMovieSearchQuery(*request, searchString, libSession.userSessionId) + return request +} + +func createGameSearchRequest(branchCode int, platform string, libSession webOpacSession) *http.Request { + request := createRequest(libSession, "/webOPACClient/search.do") + request.URL.RawQuery = createGameIndexQuery(*request, platform, libSession.userSessionId, branchCode) + return request +} + +func createMovieSearchQuery(request http.Request, searchString string, userSessionId string) string { + query := request.URL.Query() + query.Add("methodToCall", "submit") + query.Add("methodToCallParameter", "submitSearch") + + query.Add("submitSearch", "Suchen") + query.Add("callingPage", "searchPreferences") + query.Add("numberOfHits", "500") + query.Add("timeOut", "20") + query.Add("CSId", userSessionId) + query.Add("searchString[0]", searchString) + query.Add("selectedViewBranchlib", strconv.FormatInt(int64(0), 10)) + //Search for category title + query.Add("searchCategories[0]", "331") + //Restrict search to dvd/bluray + query.Add("searchRestrictionID[2]", "3") + query.Add("searchRestrictionValue1[2]", "29") + return query.Encode() +} + +func createGameIndexQuery(request http.Request, platform string, userSessionId string, branchCode int) string { + query := request.URL.Query() + query.Add("methodToCall", "submit") + query.Add("methodToCallParameter", "submitSearch") + query.Add("submitSearch", "Suchen") + query.Add("callingPage", "searchPreferences") + query.Add("numberOfHits", "500") + query.Add("timeOut", "20") + query.Add("CSId", userSessionId) + query.Add("selectedSearchBranchlib", strconv.FormatInt(int64(branchCode), 10)) + query.Add("selectedViewBranchlib", strconv.FormatInt(int64(branchCode), 10)) + //Search the platform as a catchword + query.Add("searchString[0]", platform) + query.Add("searchCategories[0]", "902") + + return query.Encode() +} diff --git a/library-le/searchresult.go b/library-le/searchresult.go deleted file mode 100644 index a6af116..0000000 --- a/library-le/searchresult.go +++ /dev/null @@ -1,37 +0,0 @@ -package libraryle - -import ( - "io" - "log" - - "github.com/PuerkitoBio/goquery" - "github.com/gunni1/leipzig-library-game-stock-api/domain" -) - -const ( - resultItemSelector string = "h2[class^=recordtitle]" - gameTitleSelector string = "a[href^='/webOPACClient/singleHit']" - availabilitySelector string = "span[class^=textgruen]" -) - -// Takes a html as reader from a webopac search and -// try to parse it to an array of games that are listed as available. -func parseSearchResult(searchResult io.Reader) ([]domain.Game, error) { - doc, docErr := goquery.NewDocumentFromReader(searchResult) - if docErr != nil { - log.Fatal("Could not create document from response.") - return nil, docErr - } - games := make([]domain.Game, 0) - doc.Find(resultItemSelector).Each(func(i int, resultItem *goquery.Selection) { - title := resultItem.Find(gameTitleSelector).Text() - if isAvailable(resultItem.Parent()) { - games = append(games, domain.Game{Title: title}) - } - }) - return games, nil -} - -func isAvailable(searchHitNode *goquery.Selection) bool { - return len(searchHitNode.Find(availabilitySelector).Nodes) > 0 -} diff --git a/library-le/searchresult_test.go b/library-le/searchresult_test.go deleted file mode 100644 index 3885474..0000000 --- a/library-le/searchresult_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package libraryle - -import ( - "bufio" - "log" - "os" - "testing" - - "github.com/gunni1/leipzig-library-game-stock-api/domain" - . "github.com/stretchr/testify/assert" -) - -func TestAvailability(t *testing.T) { - file, fileErr := os.Open("searchresult_example.html") - if fileErr != nil { - log.Fatal(fileErr) - } - fileReader := bufio.NewReader(file) - games, _ := parseSearchResult(fileReader) - - True(t, hasElement(games, "Spiel2")) - False(t, hasElement(games, "Spiel1")) - -} - -func hasElement(games []domain.Game, title string) bool { - for _, game := range games { - if game.Title == title { - return true - } - } - return false -} diff --git a/web/server.go b/web/server.go index 57bd4ba..3bc9e26 100644 --- a/web/server.go +++ b/web/server.go @@ -18,17 +18,78 @@ var htmlTemplates embed.FS // Create Mux and setup routes func InitMux() *http.ServeMux { mux := http.NewServeMux() - mux.HandleFunc("/", indexHandler) - mux.HandleFunc("/game-index/", gameIndexHandler) + mux.HandleFunc("GET /games/", gamesIndexHandler) + mux.HandleFunc("GET /movies/", movieHandler) + mux.HandleFunc("POST /games-search/", gameSearchHandler) + mux.HandleFunc("POST /movies-search/", movieSearchHandler) return mux } -func indexHandler(respWriter http.ResponseWriter, request *http.Request) { - templ := template.Must(template.ParseFS(htmlTemplates, "templates/index.html")) +type MoviesByBranch struct { + Branch string + Movies []domain.Movie +} + +func gamesIndexHandler(respWriter http.ResponseWriter, request *http.Request) { + templ := template.Must(template.ParseFS(htmlTemplates, "templates/games.html")) templ.Execute(respWriter, nil) } -func gameIndexHandler(respWriter http.ResponseWriter, request *http.Request) { +func movieHandler(respWriter http.ResponseWriter, request *http.Request) { + template := template.Must(template.ParseFS(htmlTemplates, "templates/movies.html")) + template.Execute(respWriter, nil) +} + +func movieSearchHandler(respWriter http.ResponseWriter, request *http.Request) { + title := strings.ToLower(request.PostFormValue("movie-title")) + client := libClient.Client{} + movies := client.FindMovies(title) + + if len(movies) == 0 { + fmt.Fprint(respWriter, "

Es wurden keine Titel gefunden.

") + return + } + + availableMovies := filterAvailable(movies) + + byBranch := arrangeByBranch(availableMovies) + + data := map[string][]MoviesByBranch{ + "Branches": byBranch, + } + templ := template.Must(template.ParseFS(htmlTemplates, "templates/item-list-by-branch.html")) + templ.Execute(respWriter, data) +} + +func filterAvailable(movies []domain.Movie) []domain.Movie { + available := make([]domain.Movie, 0) + for _, movie := range movies { + if movie.IsAvailable { + available = append(available, movie) + } + } + return available +} + +func arrangeByBranch(movies []domain.Movie) []MoviesByBranch { + result := make([]MoviesByBranch, 0) + + byBranch := make(map[string][]domain.Movie) + for _, movie := range movies { + if otherMovies, branchExists := byBranch[movie.Branch]; branchExists { + byBranch[movie.Branch] = append(otherMovies, movie) + } else { + byBranch[movie.Branch] = []domain.Movie{movie} + } + } + + for branch, mvs := range byBranch { + result = append(result, MoviesByBranch{Branch: branch, Movies: mvs}) + } + return result +} + +func gameSearchHandler(respWriter http.ResponseWriter, request *http.Request) { branch := strings.ToLower(request.PostFormValue("branch")) platform := strings.ToLower(request.PostFormValue("platform")) branchCode, exists := libClient.GetBranchCode(branch) @@ -45,8 +106,8 @@ func gameIndexHandler(respWriter http.ResponseWriter, request *http.Request) { } data := map[string][]domain.Game{ - "Games": games, + "Items": games, } - templ := template.Must(template.ParseFS(htmlTemplates, "templates/games.html")) + templ := template.Must(template.ParseFS(htmlTemplates, "templates/item-list.html")) templ.Execute(respWriter, data) } diff --git a/web/server_test.go b/web/server_test.go new file mode 100644 index 0000000..1fa7e3d --- /dev/null +++ b/web/server_test.go @@ -0,0 +1,26 @@ +package web + +import ( + "testing" + + "github.com/gunni1/leipzig-library-game-stock-api/domain" + . "github.com/stretchr/testify/assert" +) + +func TestArrangeByBranch(t *testing.T) { + + t1_stadt := domain.Movie{Title: "Terminator", Branch: "Stadt", IsAvailable: true} + t2_stadt := domain.Movie{Title: "Terminator 2", Branch: "Stadt", IsAvailable: false} + t1_gohlis := domain.Movie{Title: "Terminator", Branch: "Gohlis", IsAvailable: true} + movies := []domain.Movie{t1_stadt, t2_stadt, t1_gohlis} + + result := arrangeByBranch(movies) + + expected := []MoviesByBranch{ + {Branch: "Stadt", Movies: []domain.Movie{t1_stadt, t2_stadt}}, + {Branch: "Gohlis", Movies: []domain.Movie{t1_gohlis}}, + } + + Equal(t, 2, len(result)) + ElementsMatch(t, result, expected) +} diff --git a/web/templates/games.html b/web/templates/games.html index a536d01..fbe4ccf 100644 --- a/web/templates/games.html +++ b/web/templates/games.html @@ -1,3 +1,49 @@ -{{ range .Games }} -
  • {{ .Title }}
  • -{{ end }} \ No newline at end of file + + + + + + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
      +
    +
    + + + \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html deleted file mode 100644 index 5dbec63..0000000 --- a/web/templates/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
      -
    -
    - - - \ No newline at end of file diff --git a/web/templates/item-list-by-branch.html b/web/templates/item-list-by-branch.html new file mode 100644 index 0000000..cab4ff7 --- /dev/null +++ b/web/templates/item-list-by-branch.html @@ -0,0 +1,18 @@ +{{ range $i, $e := .Branches }} +
    +

    + + {{ .Branch }} + +

    +
    +
    +
      + {{ range .Movies}} +
    • {{ .Title }}
    • + {{ end }} +
    +
    +
    +
    +{{ end }} \ No newline at end of file diff --git a/web/templates/item-list.html b/web/templates/item-list.html new file mode 100644 index 0000000..d23a52b --- /dev/null +++ b/web/templates/item-list.html @@ -0,0 +1,3 @@ +{{ range .Items }} +
  • {{ .Title }}
  • +{{ end }} \ No newline at end of file diff --git a/web/templates/movies.html b/web/templates/movies.html new file mode 100644 index 0000000..b59516a --- /dev/null +++ b/web/templates/movies.html @@ -0,0 +1,25 @@ + + + + + + + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + + \ No newline at end of file