feat(media meta): reverse geocoding from mapbox (#2922)
parent
668b542c59
commit
5e5dca40c4
@ -1 +1 @@
|
||||
Subproject commit dece1c7098de2efe38aaa25d6cafc41a2de568ff
|
||||
Subproject commit fc7791cde1444e1be0935f1fbc32d956fa6eb756
|
@ -0,0 +1,236 @@
|
||||
package mediameta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
)
|
||||
|
||||
const mapBoxURL = "https://api.mapbox.com/search/geocode/v6/reverse"
|
||||
|
||||
const (
|
||||
Street = "street"
|
||||
Locality = "locality"
|
||||
Place = "place"
|
||||
District = "district"
|
||||
Region = "region"
|
||||
Country = "country"
|
||||
)
|
||||
|
||||
type geocodingExtractor struct {
|
||||
settings setting.Provider
|
||||
l logging.Logger
|
||||
client request.Client
|
||||
}
|
||||
|
||||
func newGeocodingExtractor(settings setting.Provider, l logging.Logger, client request.Client) *geocodingExtractor {
|
||||
return &geocodingExtractor{
|
||||
settings: settings,
|
||||
l: l,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *geocodingExtractor) Exts() []string {
|
||||
return exifExts
|
||||
}
|
||||
|
||||
func (e *geocodingExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||
option := &option{}
|
||||
for _, opt := range opts {
|
||||
opt.apply(option)
|
||||
}
|
||||
|
||||
// Find GPS info from extracted
|
||||
var latStr, lngStr string
|
||||
for _, meta := range option.extracted {
|
||||
if meta.Key == GpsLat {
|
||||
latStr = meta.Value
|
||||
}
|
||||
if meta.Key == GpsLng {
|
||||
lngStr = meta.Value
|
||||
}
|
||||
}
|
||||
|
||||
if latStr == "" || lngStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
lat, err := strconv.ParseFloat(latStr, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding: failed to parse latitude: %w", err)
|
||||
}
|
||||
|
||||
lng, err := strconv.ParseFloat(lngStr, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding: failed to parse longitude: %w", err)
|
||||
}
|
||||
|
||||
metas, err := e.getGeocoding(ctx, lat, lng, option.language)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocoding: failed to get geocoding: %w", err)
|
||||
}
|
||||
|
||||
for i, _ := range metas {
|
||||
metas[i].Type = driver.MetaTypeGeocoding
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
func (e *geocodingExtractor) getGeocoding(ctx context.Context, lat, lng float64, language string) ([]driver.MediaMeta, error) {
|
||||
values := url.Values{}
|
||||
values.Add("longitude", fmt.Sprintf("%f", lng))
|
||||
values.Add("latitude", fmt.Sprintf("%f", lat))
|
||||
values.Add("limit", "1")
|
||||
values.Add("access_token", e.settings.MediaMetaGeocodingMapboxAK(ctx))
|
||||
if language != "" {
|
||||
values.Add("language", language)
|
||||
}
|
||||
|
||||
resp, err := e.client.Request(
|
||||
"GET",
|
||||
mapBoxURL+"?"+values.Encode(),
|
||||
nil,
|
||||
request.WithContext(ctx),
|
||||
request.WithLogger(e.l),
|
||||
).CheckHTTPResponse(http.StatusOK).GetResponse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get geocoding from mapbox: %w", err)
|
||||
}
|
||||
|
||||
var geocoding MapboxGeocodingResponse
|
||||
if err := json.Unmarshal([]byte(resp), &geocoding); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal geocoding from mapbox: %w", err)
|
||||
}
|
||||
|
||||
if len(geocoding.Features) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metas := make([]driver.MediaMeta, 0)
|
||||
contexts := geocoding.Features[0].Properties.Context
|
||||
if contexts.Street != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Street,
|
||||
Value: contexts.Street.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Locality != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Locality,
|
||||
Value: contexts.Locality.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Place != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Place,
|
||||
Value: contexts.Place.Name,
|
||||
})
|
||||
}
|
||||
if contexts.District != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: District,
|
||||
Value: contexts.District.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Region != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Region,
|
||||
Value: contexts.Region.Name,
|
||||
})
|
||||
}
|
||||
if contexts.Country != nil {
|
||||
metas = append(metas, driver.MediaMeta{
|
||||
Key: Country,
|
||||
Value: contexts.Country.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
|
||||
// MapboxGeocodingResponse represents the response from Mapbox Geocoding API
|
||||
type MapboxGeocodingResponse struct {
|
||||
Type string `json:"type"` // "FeatureCollection"
|
||||
Features []Feature `json:"features"` // Array of feature objects
|
||||
Attribution string `json:"attribution"` // Attribution to Mapbox
|
||||
}
|
||||
|
||||
// Feature represents a feature object in the geocoding response
|
||||
type Feature struct {
|
||||
ID string `json:"id"` // Feature ID (same as mapbox_id)
|
||||
Type string `json:"type"` // "Feature"
|
||||
Geometry Geometry `json:"geometry"` // Spatial geometry of the feature
|
||||
Properties Properties `json:"properties"` // Feature details
|
||||
}
|
||||
|
||||
// Geometry represents the spatial geometry of a feature
|
||||
type Geometry struct {
|
||||
Type string `json:"type"` // "Point"
|
||||
Coordinates []float64 `json:"coordinates"` // [longitude, latitude]
|
||||
}
|
||||
|
||||
// Properties contains the feature's detailed information
|
||||
type Properties struct {
|
||||
MapboxID string `json:"mapbox_id"` // Unique feature identifier
|
||||
FeatureType string `json:"feature_type"` // Type of feature (country, region, etc.)
|
||||
Name string `json:"name"` // Formatted address string
|
||||
NamePreferred string `json:"name_preferred"` // Canonical or common alias
|
||||
PlaceFormatted string `json:"place_formatted"` // Formatted context string
|
||||
FullAddress string `json:"full_address"` // Full formatted address
|
||||
Context Context `json:"context"` // Hierarchy of parent features
|
||||
Coordinates Coordinates `json:"coordinates"` // Geographic position and accuracy
|
||||
BBox []float64 `json:"bbox,omitempty"` // Bounding box [minLon,minLat,maxLon,maxLat]
|
||||
MatchCode MatchCode `json:"match_code"` // Metadata about result matching
|
||||
}
|
||||
|
||||
// Context represents the hierarchy of encompassing parent features
|
||||
type Context struct {
|
||||
Country *ContextFeature `json:"country,omitempty"`
|
||||
Region *ContextFeature `json:"region,omitempty"`
|
||||
Postcode *ContextFeature `json:"postcode,omitempty"`
|
||||
District *ContextFeature `json:"district,omitempty"`
|
||||
Place *ContextFeature `json:"place,omitempty"`
|
||||
Locality *ContextFeature `json:"locality,omitempty"`
|
||||
Neighborhood *ContextFeature `json:"neighborhood,omitempty"`
|
||||
Street *ContextFeature `json:"street,omitempty"`
|
||||
}
|
||||
|
||||
// ContextFeature represents a feature in the context hierarchy
|
||||
type ContextFeature struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
NamePreferred string `json:"name_preferred,omitempty"`
|
||||
MapboxID string `json:"mapbox_id"`
|
||||
}
|
||||
|
||||
// Coordinates represents geographical position and accuracy information
|
||||
type Coordinates struct {
|
||||
Longitude float64 `json:"longitude"` // Longitude of result
|
||||
Latitude float64 `json:"latitude"` // Latitude of result
|
||||
Accuracy string `json:"accuracy,omitempty"` // Accuracy metric for address results
|
||||
RoutablePoints []RoutablePoint `json:"routable_points,omitempty"` // Array of routable points
|
||||
}
|
||||
|
||||
// RoutablePoint represents a routable point for an address feature
|
||||
type RoutablePoint struct {
|
||||
Name string `json:"name"` // Name of the routable point
|
||||
Longitude float64 `json:"longitude"` // Longitude coordinate
|
||||
Latitude float64 `json:"latitude"` // Latitude coordinate
|
||||
}
|
||||
|
||||
// MatchCode contains metadata about how result components match the input query
|
||||
type MatchCode struct {
|
||||
// Add specific match code fields as needed based on Mapbox documentation
|
||||
// This structure may vary depending on the specific match codes returned
|
||||
}
|
Loading…
Reference in new issue