added frontend and made it usable

This commit is contained in:
Hendrik Behme committed 2026-08-15 14:50:42 +02:00
1 parent 96408f00d8
commit b9b15152bc
50 files changed
+7974 -1

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
package main
import (
"log"
"whimsoc/vindn-backend/config"
"whimsoc/vindn-backend/models"
"whimsoc/vindn-backend/repositories"
"whimsoc/vindn-backend/services"
"whimsoc/vindn-backend/tasks"
"github.com/gofiber/fiber/v2"
)
func main() {
// Lade .env
config.LoadEnv()
// Verbinde DB
db := config.ConnectDatabase()
models.Migrate(db)
// Repositories & Services
eventRepo := repositories.NewEventRepository(db)
eventService := services.NewEventService(eventRepo)
// Starte Cronjob
tasks.StartCronJobs(eventService)
// Direkt beim Start einmal holen
go func() {
if err := eventService.ImportEvents(); err != nil {
log.Printf("Fehler beim initialen Import: %v", err)
}
}()
app := fiber.New()
app.Get("/health", func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
log.Printf("Start server")
// Starte Fiber
log.Fatal(app.Listen(":" + config.GetEnv("PORT", "8080")))
}
+32
View File
@@ -0,0 +1,32 @@
package config
import (
"log"
"os"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func LoadEnv() {
if err := godotenv.Load(); err != nil {
log.Println("Keine .env Datei gefunden")
}
}
func GetEnv(key string, fallback string) string {
if value := os.Getenv(key); len(value) != 0 {
return value
}
return fallback
}
func ConnectDatabase() *gorm.DB {
dsn := GetEnv("DATABASE_URL", "")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Fehler beim Verbinden der DB:", err)
}
return db
}
+38
View File
@@ -0,0 +1,38 @@
module whimsoc/vindn-backend
go 1.24.2
require (
github.com/joho/godotenv v1.5.1
github.com/robfig/cron/v3 v3.0.1
gorm.io/driver/postgres v1.5.11
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.49.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sync v0.1.0 // indirect
)
require (
github.com/gofiber/fiber/v2 v2.49.2
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
gorm.io/gorm v1.25.10
)
+67
View File
@@ -0,0 +1,67 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
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/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE=
github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+12
View File
@@ -0,0 +1,12 @@
package models
import "encoding/json"
type ApiResponseRaw struct {
Status string `json:"status"`
Count int `json:"count"`
OverallCount int `json:"overallcount"`
Channels []interface{} `json:"channels"`
FacetGroups []interface{} `json:"facetGroups"`
Items []json.RawMessage `json:"items"` // Raw JSON for items
}
+68
View File
@@ -0,0 +1,68 @@
package models
import "gorm.io/gorm"
type Event struct {
gorm.Model
ExternalID string `gorm:"uniqueIndex" json:"id"`
Title string `json:"title"`
City string `json:"city"`
Street string `json:"street"`
Country string
Categories []Category `gorm:"many2many:event_categories;" json:"-"`
CategoryNames []string `gorm:"-" json:"categories"` // used only for JSON decoding MediaObjects []MediaObject `json:"media_objects"`
Features []string `gorm:"-" json:"features"`
Keywords []string `gorm:"-" json:"keywords"`
Type string
Zip string
Phone string
Web string
Email string
Author string
Latitude float64 `gorm:"index"`
Longitude float64 `gorm:"index"`
Geo *GeoWrapper `gorm:"-" json:"geo"` // ignored by GORM, used for JSON
Description string
Teaser string
StartDate string
EndDate string
MediaObjects []MediaObject `gorm:"many2many:event_media_objects;" json:"-"`
RawMediaObjects []MediaObject `gorm:"-" json:"media_objects"`
}
type MediaObject struct {
gorm.Model
ID uint `gorm:"primaryKey"`
URL string `json:"url"`
Alt string `json:"alt"`
Rel string `json:"rel"`
Name string `json:"value" `
Type string `json:"type"`
RefID string `json:"id"`
Source string `json:"source"`
Prio int `json:"prio"`
Description string `json:"description"`
License string `json:"license"`
AiOptimized bool `json:"aiOptimized"`
Ext string
Hash string
}
type Category struct {
gorm.Model
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex"`
}
type GeoWrapper struct {
Main *GeoCoordinates `json:"main"`
}
type GeoCoordinates struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
func Migrate(db *gorm.DB) {
db.AutoMigrate(&Event{}, &Category{}, &MediaObject{})
}
+146
View File
@@ -0,0 +1,146 @@
package repositories
import (
"crypto/md5"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"whimsoc/vindn-backend/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type EventRepository interface {
Save(event *models.Event) error
FindAll() ([]models.Event, error)
}
type eventRepository struct {
db *gorm.DB
}
func NewEventRepository(db *gorm.DB) EventRepository {
return &eventRepository{db}
}
func (r *eventRepository) Save(event *models.Event) error {
if event.Geo != nil && event.Geo.Main != nil {
event.Latitude = event.Geo.Main.Latitude
event.Longitude = event.Geo.Main.Longitude
}
var categories []models.Category
for _, name := range event.CategoryNames {
if name == "" {
continue
}
var cat models.Category
if err := r.db.FirstOrCreate(&cat, models.Category{Name: name}).Error; err != nil {
return err
}
categories = append(categories, cat)
}
event.Categories = categories
var mediaObjects []models.MediaObject
for _, rawMediaObject := range event.RawMediaObjects {
if rawMediaObject.Name == "" {
continue
}
var mediaObject models.MediaObject
h := md5.New()
io.WriteString(h, rawMediaObject.Rel)
io.WriteString(h, rawMediaObject.Name)
io.WriteString(h, rawMediaObject.URL)
if err := r.db.FirstOrCreate(&mediaObject, models.MediaObject{
Hash: fmt.Sprintf("%x", h.Sum(nil)),
RefID: rawMediaObject.RefID,
Name: rawMediaObject.Name,
URL: rawMediaObject.URL,
Type: rawMediaObject.Type,
Rel: rawMediaObject.Rel,
Alt: rawMediaObject.Alt,
Source: rawMediaObject.Source,
Description: rawMediaObject.Description,
Prio: rawMediaObject.Prio,
AiOptimized: rawMediaObject.AiOptimized,
License: rawMediaObject.License,
Ext: filepath.Ext(rawMediaObject.URL),
}).Error; err != nil {
return err
}
if mediaObject.Type != "" {
mediaType, _, mediaTypeFound := strings.Cut(mediaObject.Type, "/")
if err := r.saveMediaFile(mediaObject.Hash, mediaObject.URL, mediaType, mediaObject.Ext); mediaTypeFound && err != nil {
log.Fatal(err)
}
}
mediaObjects = append(mediaObjects, mediaObject)
}
event.MediaObjects = mediaObjects
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "external_id"}},
UpdateAll: true,
}).Create(event).Error
}
func (r *eventRepository) FindAll() ([]models.Event, error) {
var events []models.Event
result := r.db.Find(&events)
return events, result.Error
}
func (r *eventRepository) saveMediaFile(filename string, url string, mediaType string, extension string) error {
mediaFilePath := fmt.Sprintf("./public/storage/%s/%s%s", strings.ToLower(mediaType), filename, extension)
log.Printf("Get file %s and save it to %s", url, mediaFilePath)
// Check if the directory exists
if _, err := os.Stat(filepath.Dir(mediaFilePath)); os.IsNotExist(err) {
// Create the directory if it doesn't exist
err := os.MkdirAll(filepath.Dir(mediaFilePath), 0755) // 0755 is a common permission for directories
if err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
}
fileInfo, err := os.Stat(mediaFilePath)
if errors.Is(err, os.ErrNotExist) || fileInfo.Size() == 0 {
// Create the file
out, err := os.Create(mediaFilePath)
if err != nil {
return err
}
defer out.Close()
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
} else {
log.Printf("File already exists, do not download again. Stats: Name: %s, Size: %d", fileInfo.Name(), fileInfo.Size())
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package services
import (
"log"
"whimsoc/vindn-backend/models"
"whimsoc/vindn-backend/repositories"
"whimsoc/vindn-backend/utils"
)
func FetchEvents() ([]models.Event, error) {
return utils.FetchDataFromAPI()
}
type EventService struct {
repo repositories.EventRepository
}
func NewEventService(repo repositories.EventRepository) *EventService {
return &EventService{repo}
}
func (s *EventService) ImportEvents() error {
events, err := utils.FetchDataFromAPI()
if err != nil {
return err
}
log.Println("Data will be saved.")
for _, event := range events {
if err := s.repo.Save(&event); err != nil {
log.Println("Fehler beim Speichern:", err)
}
}
log.Println("Data is saved.")
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package tasks
import (
"log"
"whimsoc/vindn-backend/services"
"github.com/robfig/cron/v3"
)
func StartCronJobs(eventService *services.EventService) {
c := cron.New()
// Zweimal täglich, um 06:00 und 18:00 Uhr
c.AddFunc("0 6,18 * * *", func() {
log.Println("Starte geplanten Event Import...")
if err := eventService.ImportEvents(); err != nil {
log.Printf("Fehler beim geplanten Import: %v", err)
}
})
c.Start()
}
+173
View File
@@ -0,0 +1,173 @@
package utils
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"whimsoc/vindn-backend/models"
)
// odhText mirrors one entry of an ODH item's "texts" array (rel/type/value
// triples holding description, teaser, price info, etc. in several
// languages/formats).
type odhText struct {
Rel string `json:"rel"`
Type string `json:"type"`
Value string `json:"value"`
}
// odhExtras captures the ODH fields that don't map onto flat Event columns
// by name (description/teaser live inside "texts", dates inside
// "timeIntervals") and need extraction after the plain json.Unmarshal.
type odhExtras struct {
Texts []odhText `json:"texts"`
TimeIntervals []struct {
Start string `json:"start"`
End string `json:"end"`
} `json:"timeIntervals"`
}
var htmlTagPattern = regexp.MustCompile(`<[^>]*>`)
// extractText returns the plain-text value of the first "texts" entry whose
// rel matches one of the given candidates (checked in order), preferring a
// text/plain variant over text/html and stripping tags if only html exists.
func extractText(texts []odhText, rels ...string) string {
var htmlFallback string
for _, rel := range rels {
for _, t := range texts {
if t.Rel != rel || t.Value == "" {
continue
}
if t.Type == "text/plain" {
return t.Value
}
if htmlFallback == "" && t.Type == "text/html" {
htmlFallback = t.Value
}
}
}
return strings.TrimSpace(htmlTagPattern.ReplaceAllString(htmlFallback, ""))
}
func FetchDataFromAPI2(url string) ([]models.Event, error) {
resp, err := http.Get(url)
if err != nil {
log.Printf("Failed to fetch data: %v", err)
return nil, err
}
defer resp.Body.Close()
var events []models.Event
if err := json.NewDecoder(resp.Body).Decode(&events); err != nil {
log.Printf("Failed to decode JSON: %v", err)
return nil, err
}
return events, nil
}
func FetchDataFromAPI() ([]models.Event, error) {
fmt.Println("Starting to get odh files")
//dataHubTypes := [5]string{"Tour", "POI", "Gastro", "Hotel", "Event"}
dataHubTypes := [1]string{"Event"}
licenseKey := os.Getenv("ODH_LICENCE_KEY")
var events []models.Event
for _, dataHubType := range dataHubTypes {
queryOffset := 0
limit := 20
for {
rawItems, err := getItemsRaw(dataHubType, licenseKey, queryOffset, limit)
if err != nil {
log.Fatalf("Failed to fetch items: %v", err)
}
if len(rawItems) == 0 {
break
}
for _, rawItem := range rawItems {
var event models.Event
if err := json.Unmarshal(rawItem, &event); err != nil {
log.Printf("Failed to decode JSON: %v", err)
return nil, err
}
var extras odhExtras
if err := json.Unmarshal(rawItem, &extras); err != nil {
log.Printf("Failed to decode text/date extras for %s: %v", event.ExternalID, err)
} else {
event.Description = extractText(extras.Texts, "details", "open-data-nrw-detail")
event.Teaser = extractText(extras.Texts, "teaser", "open-data-nrw-teaser")
if len(extras.TimeIntervals) > 0 {
event.StartDate = extras.TimeIntervals[0].Start
event.EndDate = extras.TimeIntervals[0].End
}
}
fmt.Printf("Successfully got event %s \"%s\"\n", event.ExternalID, event.Title)
events = append(events, event)
}
queryOffset += limit
}
}
return events, nil
}
// get all items of a single type
func getItemsRaw(dataHubType string, licenseKey string, queryOffset int, limit int) ([]json.RawMessage, error) {
req, err := http.NewRequest("GET", "https://meta.et4.de/rest.ashx/search/", nil)
if err != nil {
log.Print(err)
os.Exit(1)
}
q := req.URL.Query()
q.Add("experience", "open-data-nrw-tourismus")
q.Add("type", dataHubType)
q.Add("licenseKey", licenseKey)
q.Add("q", "area:\"Nordrhein-Westfalen\"")
q.Add("template", "ET2014A.json")
q.Add("limit", strconv.Itoa(limit))
q.Add("offset", strconv.Itoa(queryOffset))
q.Add("sort", "id asc")
req.URL.RawQuery = q.Encode()
fmt.Println("Query: " + req.URL.String())
client := http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
os.Exit(1)
}
defer resp.Body.Close()
var apiResp models.ApiResponseRaw
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, err
}
return apiResp.Items, nil
}