feat: add plugin system (#1034)
* feat: add plugin system * fix db docker build * fix hammerhead readme, add strava subscription news to docs * fixes and sdk improvements * fix: reduce Meilisearch load, debounce federation sync (#1012) * optimize meili trail index * several fixes --------- Co-authored-by: Flomp <Flomp@users.noreply.github.com> * Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032) Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0. - [Release notes](https://github.com/sveltejs/svelte/releases) - [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte) --- updated-dependencies: - dependency-name: svelte dependency-version: 5.56.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Flomp <Flomp@users.noreply.github.com> * Release v0.19.2 (#1035) * chore: release v0.19.2 * add changelog --------- Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com> Co-authored-by: Christian Beutel <> * speed up plugin sync and several small fixes * concepts for security improvements and process stability * improve concept * security concept implemented * remove insecure TLS * worker concept implemented * fixes and cleanup * fixes * docu * mermaid, namings * WASM plugin host improvements, plugin logging * fix db migration * Improve plugin config and category mapping UI * fixes * further fixes * remove manual test sync * fix db migration and strava mapping * type added, UI improvements * fix plugin card toggle clickable area * optimize synch status card layout * plugin type 'trails' instead of 'integration' * session auth validation in UI * fix komoot date and waypoints * improve category mapping * fix send to hammerhead: trail name * plugin setup error handling improved * fix review findings * re-mapping added * rename remote_category --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Flomp <Flomp@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
This commit is contained in:
226
plugins/komoot/mapper.go
Normal file
226
plugins/komoot/mapper.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdkgpx "github.com/open-wanderer/wanderer/plugins/sdk/gpx"
|
||||
)
|
||||
|
||||
func tourImport(tour *detailedTour, routeImages []imageItem) (trailImport, error) {
|
||||
gpxData, err := tourGPX(tour)
|
||||
if err != nil {
|
||||
return trailImport{}, err
|
||||
}
|
||||
|
||||
privacy := privacyFromStatus(tour.Status)
|
||||
return trailImport{
|
||||
Source: trailImportSource{
|
||||
Provider: "komoot",
|
||||
ExternalID: strconv.FormatInt(tour.ID, 10),
|
||||
},
|
||||
Kind: kindFromType(tour.Type),
|
||||
Name: tour.Name,
|
||||
Description: tour.Description,
|
||||
StartedAt: tour.Date,
|
||||
ActivityType: activityType(tour.Sport),
|
||||
Privacy: &privacy,
|
||||
Track: track{
|
||||
Format: "gpx",
|
||||
ContentBase64: base64.StdEncoding.EncodeToString(gpxData),
|
||||
},
|
||||
Waypoints: waypoints(tour),
|
||||
Photos: photos(tour, routeImages),
|
||||
Metadata: map[string]any{
|
||||
"distance": tour.Distance,
|
||||
"elevationGain": tour.ElevationUp,
|
||||
"elevationLoss": tour.ElevationDown,
|
||||
"duration": tour.Duration,
|
||||
"providerCategory": tour.Sport,
|
||||
"sourceSport": tour.Sport,
|
||||
"difficulty": tour.Difficulty.Grade,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tourGPX(tour *detailedTour) ([]byte, error) {
|
||||
items := tour.Embedded.Coordinates.Items
|
||||
points := make([]sdkgpx.Point, 0, len(items))
|
||||
startedAt, _ := time.Parse(time.RFC3339, tour.Date)
|
||||
for _, item := range items {
|
||||
elevation := item.Alt
|
||||
point := sdkgpx.Point{
|
||||
Lat: item.Lat,
|
||||
Lon: item.Lng,
|
||||
Elevation: &elevation,
|
||||
}
|
||||
if !startedAt.IsZero() {
|
||||
pointTime := startedAt.Add(time.Duration(item.T) * time.Millisecond).UTC()
|
||||
point.Time = &pointTime
|
||||
}
|
||||
points = append(points, point)
|
||||
}
|
||||
return sdkgpx.Track("wanderer Komoot plugin", tour.Name, points)
|
||||
}
|
||||
|
||||
func waypoints(tour *detailedTour) []waypoint {
|
||||
result := make([]waypoint, 0, len(tour.Embedded.WayPoints.Embedded.Items)+len(tour.Embedded.Timeline.Embedded.Items))
|
||||
seen := map[string]bool{}
|
||||
result = appendWaypoints(result, seen, tour.Embedded.WayPoints.Embedded.Items)
|
||||
result = appendWaypoints(result, seen, tour.Embedded.Timeline.Embedded.Items)
|
||||
return result
|
||||
}
|
||||
|
||||
func appendWaypoints(result []waypoint, seen map[string]bool, items []timelineItem) []waypoint {
|
||||
for _, item := range items {
|
||||
ref := item.Embedded.Reference
|
||||
point, ok := waypointPoint(ref)
|
||||
if ref.Name == "" || !ok {
|
||||
continue
|
||||
}
|
||||
key := ref.ID.String()
|
||||
if key == "" {
|
||||
key = fmt.Sprintf("%s:%0.7f:%0.7f", ref.Name, point.Lat, point.Lng)
|
||||
}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
description := ""
|
||||
if len(ref.Embedded.Tips.Embedded.Items) > 0 {
|
||||
description = ref.Embedded.Tips.Embedded.Items[0].Text
|
||||
}
|
||||
ele := point.Alt
|
||||
result = append(result, waypoint{
|
||||
ExternalID: ref.ID.String(),
|
||||
Name: ref.Name,
|
||||
Description: description,
|
||||
Lat: point.Lat,
|
||||
Lon: point.Lng,
|
||||
Ele: &ele,
|
||||
Icon: "circle",
|
||||
Photos: waypointPhotos(item),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func waypointPoint(ref waypointReference) (point, bool) {
|
||||
if ref.StartPoint.Lat != 0 || ref.StartPoint.Lng != 0 {
|
||||
return ref.StartPoint, true
|
||||
}
|
||||
if ref.Location.Lat != 0 || ref.Location.Lng != 0 {
|
||||
return ref.Location, true
|
||||
}
|
||||
return point{}, false
|
||||
}
|
||||
|
||||
func photos(tour *detailedTour, routeImages []imageItem) []photo {
|
||||
images := routeImages
|
||||
if len(images) == 0 {
|
||||
images = tour.Embedded.CoverImages.Embedded.Items
|
||||
}
|
||||
if len(images) == 0 && tour.MapImage.Src != "" {
|
||||
images = []imageItem{{Src: tour.MapImage.Src, Type: "image/jpeg"}}
|
||||
}
|
||||
return photosFromImages(images, "komoot-photo.jpg")
|
||||
}
|
||||
|
||||
func waypointPhotos(item timelineItem) []photo {
|
||||
ref := item.Embedded.Reference
|
||||
images := ref.Embedded.Images.Embedded.Items
|
||||
if ref.Embedded.FrontImage.Src != "" {
|
||||
images = append([]imageItem{ref.Embedded.FrontImage}, images...)
|
||||
}
|
||||
return photosFromImages(images, "komoot-waypoint-photo.jpg")
|
||||
}
|
||||
|
||||
func photosFromImages(images []imageItem, fallbackFilename string) []photo {
|
||||
result := make([]photo, 0, len(images))
|
||||
seen := map[string]bool{}
|
||||
for _, image := range images {
|
||||
source := expandImageURL(image.Src)
|
||||
if source == "" || strings.HasSuffix(strings.ToLower(source), ".gif") {
|
||||
continue
|
||||
}
|
||||
key := image.ID.String()
|
||||
if key == "" {
|
||||
key = source
|
||||
}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
result = append(result, photo{
|
||||
ExternalID: image.ID.String(),
|
||||
Filename: filenameForImage(image.ID, fallbackFilename),
|
||||
ContentType: contentType(image.Type),
|
||||
Lat: optionalCoordinate(image.Location.Lat),
|
||||
Lon: optionalCoordinate(image.Location.Lng),
|
||||
Source: mediaSource{
|
||||
Type: "url",
|
||||
URL: source,
|
||||
},
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func expandImageURL(source string) string {
|
||||
source = strings.ReplaceAll(source, "{crop}", "false")
|
||||
source = strings.ReplaceAll(source, "{width}", "")
|
||||
source = strings.ReplaceAll(source, "{height}", "")
|
||||
return source
|
||||
}
|
||||
|
||||
func filenameForImage(id flexibleID, fallback string) string {
|
||||
if id.String() == "" {
|
||||
return fallback
|
||||
}
|
||||
return fmt.Sprintf("komoot-%s.jpg", id.String())
|
||||
}
|
||||
|
||||
func contentType(value string) string {
|
||||
if strings.HasPrefix(value, "image/") {
|
||||
return value
|
||||
}
|
||||
return "image/jpeg"
|
||||
}
|
||||
|
||||
func optionalCoordinate(value float64) *float64 {
|
||||
if value == 0 {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func kindFromType(value string) string {
|
||||
if value == "tour_recorded" {
|
||||
return "completed"
|
||||
}
|
||||
return "planned"
|
||||
}
|
||||
|
||||
func privacyFromStatus(value string) string {
|
||||
if value == "public" {
|
||||
return "public"
|
||||
}
|
||||
return "private"
|
||||
}
|
||||
|
||||
func activityType(sport string) string {
|
||||
switch sport {
|
||||
case "hike", "mountaineering":
|
||||
return "hiking"
|
||||
case "jogging":
|
||||
return "running"
|
||||
case "touringbicycle", "mtb", "racebike", "mtb_easy", "mtb_advanced":
|
||||
return "biking"
|
||||
default:
|
||||
return sport
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user