diff --git a/domain/game.go b/domain/game.go deleted file mode 100644 index d6dff24..0000000 --- a/domain/game.go +++ /dev/null @@ -1,6 +0,0 @@ -package domain - -type Game struct { - Title string `json:"title"` - Branch string `json:"branch"` -} diff --git a/domain/media.go b/domain/media.go index 01ec619..c8b65c6 100644 --- a/domain/media.go +++ b/domain/media.go @@ -1,7 +1,26 @@ package domain -type Media struct { +const MOVIE string = "movie" +const GAME string = "game" + +type Movie struct { + Title string `json:"title"` + Branch string `json:"branch"` + IsAvailable string `json:"isAvailable"` +} + +//Platform als DVD/Bluray verwenden? -> Gleich zu behandeln, ggf vorteile bei geneuer Suche + +type Game struct { Title string `json:"title"` Branch string `json:"branch"` - IsAvailable bool `json:"isAvailable"` + Platform string `json:"platform"` + IsAvailable string `json:"isAvailable"` +} + +type Media struct { + Title string + Branch string + Platform string + IsAvailable bool } diff --git a/domain/movie.go b/domain/movie.go deleted file mode 100644 index 88bd33f..0000000 --- a/domain/movie.go +++ /dev/null @@ -1,7 +0,0 @@ -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 25e02ca..f7cc908 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/PuerkitoBio/goquery v1.9.1 + github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 124d901..5632621 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/library-le/client.go b/library-le/client.go index 30c9509..189c457 100644 --- a/library-le/client.go +++ b/library-le/client.go @@ -18,6 +18,12 @@ type webOpacSession struct { userSessionId string } +func NewClientWithSession() Client { + client := Client{} + client.openSession() + return client +} + func (client *Client) openSession() error { resp, err := http.Get(LIB_BASE_URL + "/webOPACClient") if err != nil { diff --git a/library-le/request.go b/library-le/request.go index 040ffb9..915ccd5 100644 --- a/library-le/request.go +++ b/library-le/request.go @@ -20,9 +20,19 @@ func createRequest(libSession webOpacSession, path string) *http.Request { return request } -func NewMovieSearchRequest(searchString string, libSession webOpacSession) *http.Request { +func NewReturnDateRequest(title string, platform string, branchCode int, libSession webOpacSession) *http.Request { request := createRequest(libSession, "/webOPACClient/search.do") - request.URL.RawQuery = createMovieSearchQuery(*request, searchString, libSession.userSessionId) + if platform == "dvd" || platform == "bluray" { + request.URL.RawQuery = createSinglePlatformMovieSearchQuery(*request, title, platform, branchCode, libSession.userSessionId) + } else { + request.URL.RawQuery = createGameSearchQuery(*request, title, platform, branchCode, libSession.userSessionId) + } + return request +} + +func NewMovieSearchRequest(title string, branchCode int, libSession webOpacSession) *http.Request { + request := createRequest(libSession, "/webOPACClient/search.do") + request.URL.RawQuery = createMovieSearchQuery(*request, title, branchCode, libSession.userSessionId) return request } @@ -32,13 +42,13 @@ func NewGameIndexRequest(branchCode int, platform string, libSession webOpacSess return request } -func NewGameSearchRequest(title string, platform string, libSession webOpacSession) *http.Request { +func NewGameSearchRequest(title string, platform string, branchCode int, libSession webOpacSession) *http.Request { request := createRequest(libSession, "/webOPACClient/search.do") - request.URL.RawQuery = createGameSearchQuery(*request, title, platform, libSession.userSessionId) + request.URL.RawQuery = createGameSearchQuery(*request, title, platform, branchCode, libSession.userSessionId) return request } -func createGameSearchQuery(request http.Request, title string, platform string, userSessionId string) string { +func createGameSearchQuery(request http.Request, title string, platform string, branchCode int, userSessionId string) string { query := request.URL.Query() query.Add("methodToCall", "submit") query.Add("methodToCallParameter", "submitSearch") @@ -47,8 +57,8 @@ func createGameSearchQuery(request http.Request, title string, platform string, query.Add("numberOfHits", "500") query.Add("timeOut", "20") query.Add("CSId", userSessionId) - //Search for Stadtbibliothek, collect actual branch information un further requests - query.Add("selectedViewBranchlib", strconv.FormatInt(int64(0), 10)) + query.Add("selectedSearchBranchlib", strconv.FormatInt(int64(branchCode), 10)) + query.Add("selectedViewBranchlib", strconv.FormatInt(int64(branchCode), 10)) //Search for category title query.Add("searchString[0]", title) query.Add("searchCategories[0]", "331") @@ -61,7 +71,30 @@ func createGameSearchQuery(request http.Request, title string, platform string, return query.Encode() } -func createMovieSearchQuery(request http.Request, searchString string, userSessionId string) string { +func createSinglePlatformMovieSearchQuery(request http.Request, title string, platform string, branchCode int, 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("selectedSearchBranchlib", strconv.FormatInt(int64(branchCode), 10)) + query.Add("selectedViewBranchlib", strconv.FormatInt(int64(branchCode), 10)) + //Search for category title + query.Add("searchString[0]", title) + query.Add("searchCategories[0]", "331") + //Search for one specific mediatype dvd or bluray + query.Add("searchString[1]", platform) + query.Add("searchCategories[1]", "800") + //Restrict search to dvd/bluray + query.Add("searchRestrictionID[2]", "3") + query.Add("searchRestrictionValue1[2]", "29") + return query.Encode() +} + +func createMovieSearchQuery(request http.Request, title string, branchCode int, userSessionId string) string { query := request.URL.Query() query.Add("methodToCall", "submit") query.Add("methodToCallParameter", "submitSearch") @@ -71,8 +104,9 @@ func createMovieSearchQuery(request http.Request, searchString string, userSessi 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)) + query.Add("searchString[0]", title) + query.Add("selectedSearchBranchlib", strconv.FormatInt(int64(branchCode), 10)) + query.Add("selectedViewBranchlib", strconv.FormatInt(int64(branchCode), 10)) //Search for category title query.Add("searchCategories[0]", "331") //Restrict search to dvd/bluray diff --git a/library-le/request_test.go b/library-le/request_test.go index fa985d0..e524a7b 100644 --- a/library-le/request_test.go +++ b/library-le/request_test.go @@ -8,18 +8,36 @@ import ( func TestMovieSearchRequestHasCookiesSet(t *testing.T) { session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} - request := NewMovieSearchRequest("Terminator", session) + request := NewMovieSearchRequest("Terminator", 0, session) assertSessionCookiesExists(request, t) } func TestMovieSearchRequestHasQueryParamsSet(t *testing.T) { session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} - request := NewMovieSearchRequest("Terminator", session) + request := NewMovieSearchRequest("Terminator", 0, 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")) + Equal(t, "0", request.URL.Query().Get("selectedSearchBranchlib")) +} + +func TestMovieReturnDateRequest(t *testing.T) { + session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} + request := NewReturnDateRequest("Terminator", "dvd", 41, session) + //Expect results to be restricted to dvd/bluray + Equal(t, "29", request.URL.Query().Get("searchRestrictionValue1[2]")) + Equal(t, "dvd", request.URL.Query().Get("searchString[1]")) + Equal(t, "800", request.URL.Query().Get("searchCategories[1]")) +} + +func TestGameReturnDateRequest(t *testing.T) { + session := webOpacSession{jSessionId: jSessionId, userSessionId: userSessionId} + request := NewReturnDateRequest("Mario", "switch", 41, session) + //Expect results to be restricted to games + Equal(t, "27", request.URL.Query().Get("searchRestrictionValue1[2]")) + Equal(t, "switch", request.URL.Query().Get("searchString[1]")) + Equal(t, "902", request.URL.Query().Get("searchCategories[1]")) } diff --git a/library-le/search.go b/library-le/search.go index 3027420..b6eedc8 100644 --- a/library-le/search.go +++ b/library-le/search.go @@ -10,10 +10,12 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/gunni1/leipzig-library-game-stock-api/domain" + "github.com/pkg/errors" ) const ( - copiesSelector string = "#tab-content > div > div:nth-child(n+2)" + copiesSelector string = "#tab-content > div > div:nth-child(n+2)" + mediaTypeSelector string = "div.results-teaser > div > div > ul > li:nth-child(4)" ) type searchResult struct { @@ -28,14 +30,19 @@ func (libClient Client) FindMovies(title string) []domain.Media { fmt.Println(sessionErr) return nil } - searchRequest := NewMovieSearchRequest(title, libClient.session) + searchRequest := NewMovieSearchRequest(title, 0, libClient.session) httpClient := http.Client{} searchResponse, err := httpClient.Do(searchRequest) if err != nil { log.Println(err) return nil } - resultTitles := extractTitles(searchResponse.Body) + doc, docErr := goquery.NewDocumentFromReader(searchResponse.Body) + if docErr != nil { + log.Println("Could not create document from response.") + return nil + } + resultTitles := extractTitles(doc) movies := make([]domain.Media, 0) //TODO: Parallel Ergbnislinks folgen und Details sammeln @@ -52,14 +59,19 @@ func (libClient Client) FindGames(title string, platform string) []domain.Media fmt.Println(sessionErr) return nil } - searchRequest := NewGameSearchRequest(title, platform, libClient.session) + searchRequest := NewGameSearchRequest(title, platform, 0, libClient.session) httpClient := http.Client{} searchResponse, err := httpClient.Do(searchRequest) if err != nil { log.Println(err) return nil } - resultTitles := extractTitles(searchResponse.Body) + doc, docErr := goquery.NewDocumentFromReader(searchResponse.Body) + if docErr != nil { + log.Println("Could not create document from response.") + return nil + } + resultTitles := extractTitles(doc) games := make([]domain.Media, 0) for _, resultTitle := range resultTitles { games = append(games, resultTitle.loadMediaCopies(libClient.session)...) @@ -67,6 +79,34 @@ func (libClient Client) FindGames(title string, platform string) []domain.Media return games } +func (libClient Client) RetrieveReturnDate(branchCode int, platform string, title string) (string, error) { + request := NewReturnDateRequest(title, platform, branchCode, libClient.session) + httpClient := http.Client{} + searchResponse, err := httpClient.Do(request) + if err != nil { + log.Printf("Error during search: %s", err.Error()) + return "-", err + } + doc, docErr := goquery.NewDocumentFromReader(searchResponse.Body) + if docErr != nil { + log.Println("Could not create document from response.") + return "", docErr + } + + if isSingleResultPage(doc) { + return findReturnDateInCopiesPage(doc) + } else { + resultTitles := extractTitles(doc) + exactMatchTitles := filterExactTitle(title, resultTitles) + return loadMediaReturnDate(exactMatchTitles, libClient.session) + } +} + +func isSingleResultPage(doc *goquery.Document) bool { + pageTitle := doc.Find("title").Text() + return strings.TrimSpace(pageTitle) == "Einzeltreffer" +} + // Load all existing copys of a result title over all library branches func (result searchResult) loadMediaCopies(libSession webOpacSession) []domain.Media { request := createRequest(libSession, result.resultUrl) @@ -74,20 +114,82 @@ func (result searchResult) loadMediaCopies(libSession webOpacSession) []domain.M httpClient := http.Client{} mediaResponse, err := httpClient.Do(request) if err != nil { - log.Println("error during search") + log.Printf("Error during search: %s", err.Error()) return nil } return parseMediaCopiesPage(result.title, mediaResponse.Body) } -// Go through the search overview page and create a result object for each title found. -// The result contain details of each copie availabile of the media. -func extractTitles(searchResponse io.Reader) []searchResult { - doc, docErr := goquery.NewDocumentFromReader(searchResponse) +// load the return date for a searched title. Return the date of the first copy found. +func (result searchResult) loadReturnDate(libSession webOpacSession) (string, error) { + request := createRequest(libSession, result.resultUrl) + httpClient := http.Client{} + mediaResponse, err := httpClient.Do(request) + if err != nil { + log.Printf("Error during search: %s", err.Error()) + return "", nil + } + doc, docErr := goquery.NewDocumentFromReader(mediaResponse.Body) if docErr != nil { log.Println("Could not create document from response.") - return nil + return "", docErr + } + return findReturnDateInCopiesPage(doc) +} + +func loadMediaReturnDate(titles []searchResult, libSession webOpacSession) (string, error) { + //do a request for every searchresult + //TODO: find earliest date + for _, title := range titles { + returnDate, err := title.loadReturnDate(libSession) + if err == nil { + return returnDate, nil + } + log.Printf("No return date found for title %s ", title.title) + } + return "", errors.New("No return date found") +} + +// find a return date for a copy or return an error instead. +func findReturnDateInCopiesPage(doc *goquery.Document) (string, error) { + returnDate := "" + doc.Find(copiesSelector).Each(func(i int, copy *goquery.Selection) { + rentalStateLink := copy.Find("div:nth-child(5) > div > a") + dateStr, findErr := extractDate(rentalStateLink.Text()) + if findErr == nil { + returnDate = dateStr + } + }) + if returnDate != "" { + return returnDate, nil + } else { + return "", errors.New("found no copy with a return date") + } +} + +// find a date string inside a string. Format DD.MM.YYYY +func extractDate(text string) (string, error) { + dateForm := regexp.MustCompile(`\d{2}\.\d{2}\.\d{4}`) + date := dateForm.FindString(text) + if date == "" { + return "", fmt.Errorf("no date found in: %s", text) } + return date, nil +} + +func filterExactTitle(title string, results []searchResult) []searchResult { + filtered := make([]searchResult, 0) + for _, result := range results { + if result.title == title { + filtered = append(filtered, result) + } + } + return filtered +} + +// Go through the search overview page and create a result object for each title found. +// The result contain details of each copie availabile of the media. +func extractTitles(doc *goquery.Document) []searchResult { titles := make([]searchResult, 0) doc.Find(resultItemSelector).Each(func(i int, resultItem *goquery.Selection) { title := clearTitle(resultItem.Find(titleSelector).Text()) @@ -106,16 +208,33 @@ func parseMediaCopiesPage(title string, page io.Reader) []domain.Media { return nil } movies := make([]domain.Media, 0) + platformIndicator := doc.Find(mediaTypeSelector).Text() + platform := determinePlatform(platformIndicator) 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 := isMediaAvailable(copy) - movies = append(movies, domain.Media{Title: title, Branch: branch, IsAvailable: status}) + movies = append(movies, domain.Media{Title: title, Branch: removeBranchSuffix(branch), Platform: platform, IsAvailable: status}) }) - return movies } +// Look for DVD or Blu-Ray in a String to decide a movie platform +func determinePlatform(platformIndicator string) string { + platform := strings.ToLower(platformIndicator) + if strings.Contains(platform, "dvd") { + return "dvd" + } else if strings.Contains(platform, "blu-ray") { + return "bluray" + } + return "" +} + +// Remove location detail suffix from branch name +func removeBranchSuffix(branchName string) string { + return strings.TrimSpace(strings.Split(branchName, "/")[0]) +} + // Remove additional media information from titles in square brackets func clearTitle(title string) string { brackets := regexp.MustCompile(`\[.*\]`) @@ -129,5 +248,5 @@ func isMediaAvailable(copy *goquery.Selection) bool { return false } statusText := copy.Find("div:nth-child(5)").Text() - return strings.Contains(statusText, "ausleihbar") + return strings.Contains(statusText, "ausleihbar") || strings.Contains(statusText, "frei") } diff --git a/library-le/search_test.go b/library-le/search_test.go index 7333f84..402b3ce 100644 --- a/library-le/search_test.go +++ b/library-le/search_test.go @@ -1,8 +1,11 @@ package libraryle import ( + "io" + "strings" "testing" + "github.com/PuerkitoBio/goquery" "github.com/gunni1/leipzig-library-game-stock-api/domain" . "github.com/stretchr/testify/assert" ) @@ -12,10 +15,10 @@ func TestParseGameCopiesResult(t *testing.T) { games := parseMediaCopiesPage("Monster Hunter Rise", testResponse) Equal(t, 4, len(games)) - mediaEqualTo(t, games[0], "Monster Hunter Rise", "Stadtbibliothek / Jugendbereich - 2.OG", false) - mediaEqualTo(t, games[1], "Monster Hunter Rise", "Stadtbibliothek / Jugendbereich - 2.OG", false) - mediaEqualTo(t, games[2], "Monster Hunter Rise", "Bibliothek Südvorstadt / Erwachsenenbibliothek - EG", true) - mediaEqualTo(t, games[3], "Monster Hunter Rise", "Bibliothek Gohlis / Kinderbibliothek", false) + mediaEqualTo(t, games[0], "Monster Hunter Rise", "Stadtbibliothek", false) + mediaEqualTo(t, games[1], "Monster Hunter Rise", "Stadtbibliothek", false) + mediaEqualTo(t, games[2], "Monster Hunter Rise", "Bibliothek Südvorstadt", true) + mediaEqualTo(t, games[3], "Monster Hunter Rise", "Bibliothek Gohlis", false) } func mediaEqualTo(t *testing.T, media domain.Media, exptTitle string, exptBranch string, exptAvalia bool) { @@ -40,7 +43,7 @@ func TestParseMovieCopiesResult(t *testing.T) { func TestParseSearchResultMovies(t *testing.T) { testResponse := loadTestData("testdata/movie_search_result.html") - results := extractTitles(testResponse) + results := extractTitles(asDoc(testResponse)) Equal(t, 3, len(results)) Equal(t, "Der Clou", results[0].title) @@ -56,7 +59,7 @@ func TestParseSearchResultMovies(t *testing.T) { func TestParseSearchResultGames(t *testing.T) { testResponse := loadTestData("testdata/game_search_result.html") - results := extractTitles(testResponse) + results := extractTitles(asDoc(testResponse)) Equal(t, 3, len(results)) Equal(t, "Monster hunter generations ultimate", results[0].title) @@ -73,3 +76,52 @@ func TestClearTitle(t *testing.T) { Equal(t, "Terminator", clearTitle("Terminator [Bildtonträger]")) Equal(t, "Mad Max - Fury Road", clearTitle("Mad Max - Fury Road [blu-ray]")) } + +func TestRemoveBranchSuffix(t *testing.T) { + Equal(t, "Bibliothek Gohlis", removeBranchSuffix("Bibliothek Gohlis / Erwachsenenbibliothek")) + Equal(t, "Bibliothek Grünau-Nord", removeBranchSuffix("Bibliothek Grünau-Nord / Erwachsenenbibliothek")) + Equal(t, "Fahrbibliothek", removeBranchSuffix("Fahrbibliothek")) + Equal(t, "", removeBranchSuffix("")) +} + +func TestDetermPlatform(t *testing.T) { + Equal(t, "dvd", determinePlatform("Umfang:\n 1 DVD-Video (131 Min.)")) + Equal(t, "bluray", determinePlatform("Umfang:\n 1 Blu-ray Disc (138 min)")) + Equal(t, "", determinePlatform("nix")) +} + +func TestFilterSearchResult(t *testing.T) { + search := []searchResult{ + {title: "Terminator"}, + {title: "Terminator 2"}, + } + filtered := filterExactTitle("Terminator", search) + Equal(t, 1, len(filtered)) + Equal(t, "Terminator", filtered[0].title) +} + +func TestExtractDate(t *testing.T) { + date, emptyErr := extractDate("Today is the 20.08.2024.") + Equal(t, "20.08.2024", date) + Nil(t, emptyErr) + + _, err := extractDate("Whops, this date has a formatting issue: 11.11,2011") + NotNil(t, err) +} + +func TestIsSinglePageResultTRUE(t *testing.T) { + data := strings.NewReader("
Es wurden keine Titel gefunden.
") return } byBranch := arrangeByBranch(media) - data := map[string][]MediaByBranch{ - "Branches": byBranch, + data := MediaTemplateData{ + Branches: byBranch, + MediaType: mediaType, } - templ := template.Must(template.ParseFS(htmlTemplates, "templates/item-list-by-branch.html")) - templ.Execute(respWriter, data) + templ, _ := template.New("item-list-by-branch.html").Funcs(template.FuncMap{ + "encodeBranch": encodeBranch, + }).ParseFS(htmlTemplates, "templates/item-list-by-branch.html") + err := templ.Execute(respWriter, data) + log.Println(err) +} + +func encodeBranch(branchName string) int { + tokens := strings.Split(branchName, " ") + var branch string + if len(tokens) > 1 { + branch = tokens[1] + } else { + branch = tokens[0] + } + code, _ := libClient.GetBranchCode(branch) + return code } func filterAvailable(medias []domain.Media) []domain.Media { @@ -95,7 +135,6 @@ func arrangeByBranch(medias []domain.Media) []MediaByBranch { byBranch[media.Branch] = []domain.Media{media} } } - for branch, mds := range byBranch { result = append(result, MediaByBranch{Branch: branch, Media: mds}) } @@ -117,7 +156,6 @@ func gameIndexHandler(respWriter http.ResponseWriter, request *http.Request) { fmt.Fprint(respWriter, "Es wurden keine ausleihbaren Titel gefunden.
") return } - data := map[string][]domain.Game{ "Items": games, } diff --git a/web/server_test.go b/web/server_test.go index 4871334..3fab20d 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -24,3 +24,9 @@ func TestArrangeByBranch(t *testing.T) { Equal(t, 2, len(result)) ElementsMatch(t, result, expected) } + +func TestEncodeBranchName(t *testing.T) { + Equal(t, 20, encodeBranch("Bibliothek Plagwitz")) + Equal(t, 0, encodeBranch("Stadtbibliothek")) + Equal(t, 41, encodeBranch("Bibliothek Gohlis")) +} diff --git a/web/static/movies.html b/web/static/movies.html index 8e97b47..000823f 100644 --- a/web/static/movies.html +++ b/web/static/movies.html @@ -19,7 +19,6 @@ Nicht ausleihbare anzeigen -