added frontend and made it usable
This commit is contained in:
1 parent
96408f00d8
commit
b9b15152bc
50 files changed
+7974
-1
No files matched your search
@@ -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")))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user