From 5e5dca40c4d74ada4291a012b9d8c9e7cc8197de Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 26 Sep 2025 11:27:46 +0800 Subject: [PATCH] feat(media meta): reverse geocoding from mapbox (#2922) --- application/dependency/dependency.go | 2 +- assets | 2 +- inventory/setting.go | 2 + pkg/cluster/routes/routes.go | 4 +- pkg/filemanager/driver/cos/cos.go | 2 +- pkg/filemanager/driver/handler.go | 3 +- pkg/filemanager/driver/ks3/ks3.go | 12 +- pkg/filemanager/driver/local/local.go | 2 +- pkg/filemanager/driver/obs/media.go | 2 +- pkg/filemanager/driver/onedrive/onedrive.go | 2 +- pkg/filemanager/driver/oss/oss.go | 2 +- pkg/filemanager/driver/qiniu/qiniu.go | 2 +- pkg/filemanager/driver/remote/client.go | 6 +- pkg/filemanager/driver/remote/remote.go | 4 +- pkg/filemanager/driver/s3/s3.go | 2 +- pkg/filemanager/driver/upyun/upyun.go | 2 +- pkg/filemanager/manager/mediameta.go | 10 +- pkg/mediameta/exif.go | 7 +- pkg/mediameta/extractor.go | 40 +++- pkg/mediameta/ffprobe.go | 7 +- pkg/mediameta/geocoding.go | 236 ++++++++++++++++++++ pkg/mediameta/music.go | 7 +- pkg/setting/provider.go | 12 + service/explorer/slave.go | 4 +- 24 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 pkg/mediameta/geocoding.go diff --git a/application/dependency/dependency.go b/application/dependency/dependency.go index 70d8514b..308ea10a 100644 --- a/application/dependency/dependency.go +++ b/application/dependency/dependency.go @@ -467,7 +467,7 @@ func (d *dependency) MediaMetaExtractor(ctx context.Context) mediameta.Extractor return d.mediaMeta } - d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger()) + d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger(), d.RequestClient()) return d.mediaMeta } diff --git a/assets b/assets index dece1c70..fc7791cd 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit dece1c7098de2efe38aaa25d6cafc41a2de568ff +Subproject commit fc7791cde1444e1be0935f1fbc32d956fa6eb756 diff --git a/inventory/setting.go b/inventory/setting.go index 19b4aa68..2c3422f1 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -636,6 +636,8 @@ var DefaultSettings = map[string]string{ "media_meta_ffprobe_path": "ffprobe", "media_meta_ffprobe_size_local": "0", "media_meta_ffprobe_size_remote": "0", + "media_meta_geocoding": "0", + "media_meta_geocoding_mapbox_ak": "", "site_logo": "/static/img/logo.svg", "site_logo_light": "/static/img/logo_light.svg", "tos_url": "https://cloudreve.org/privacy-policy", diff --git a/pkg/cluster/routes/routes.go b/pkg/cluster/routes/routes.go index 535db9ae..3e315978 100644 --- a/pkg/cluster/routes/routes.go +++ b/pkg/cluster/routes/routes.go @@ -180,9 +180,9 @@ func SlaveFileContentUrl(base *url.URL, srcPath, name string, download bool, spe return base } -func SlaveMediaMetaRoute(src, ext string) string { +func SlaveMediaMetaRoute(src, ext, language string) string { src = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(src))) - return fmt.Sprintf("file/meta/%s/%s", src, url.PathEscape(ext)) + return fmt.Sprintf("file/meta/%s/%s?language=%s", src, url.PathEscape(ext), language) } func SlaveFileListRoute(srcPath string, recursive bool) string { diff --git a/pkg/filemanager/driver/cos/cos.go b/pkg/filemanager/driver/cos/cos.go index ddec60d8..c9ad912d 100644 --- a/pkg/filemanager/driver/cos/cos.go +++ b/pkg/filemanager/driver/cos/cos.go @@ -594,7 +594,7 @@ func (handler Driver) Meta(ctx context.Context, path string) (*MetaData, error) }, nil } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { if util.ContainsString(supportedImageExt, ext) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/driver/handler.go b/pkg/filemanager/driver/handler.go index 21b0ec5f..1593ee0d 100644 --- a/pkg/filemanager/driver/handler.go +++ b/pkg/filemanager/driver/handler.go @@ -83,7 +83,7 @@ type ( Capabilities() *Capabilities // MediaMeta extracts media metadata from the given file. - MediaMeta(ctx context.Context, path, ext string) ([]MediaMeta, error) + MediaMeta(ctx context.Context, path, ext, language string) ([]MediaMeta, error) } Capabilities struct { @@ -117,6 +117,7 @@ const ( MetaTypeExif MetaType = "exif" MediaTypeMusic MetaType = "music" MetaTypeStreamMedia MetaType = "stream" + MetaTypeGeocoding MetaType = "geocoding" ) type ForceUsePublicEndpointCtx struct{} diff --git a/pkg/filemanager/driver/ks3/ks3.go b/pkg/filemanager/driver/ks3/ks3.go index ddc1c975..74a4ecc7 100644 --- a/pkg/filemanager/driver/ks3/ks3.go +++ b/pkg/filemanager/driver/ks3/ks3.go @@ -306,7 +306,7 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, case "jpg", "webp": thumbParam += fmt.Sprintf("&q=%d&F=%s", enco.Quality, enco.Format) case "png": - thumbParam += fmt.Sprintf("&F=%s", enco.Format) + thumbParam += fmt.Sprintf("&F=%s", enco.Format) } // 确保过期时间不小于 0 ,如果小于则设置为 7 天 @@ -318,10 +318,10 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, } thumbUrl, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{ - HTTPMethod: s3.GET, // 请求方法 - Bucket: &handler.policy.BucketName, // 存储空间名称 - Key: aws.String(e.Source()+thumbParam), // 对象的key - Expires: ttl, // 过期时间,转换为秒数 + HTTPMethod: s3.GET, // 请求方法 + Bucket: &handler.policy.BucketName, // 存储空间名称 + Key: aws.String(e.Source() + thumbParam), // 对象的key + Expires: ttl, // 过期时间,转换为秒数 }) if err != nil { @@ -505,7 +505,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } // MediaMeta 获取媒体元信息 -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/local/local.go b/pkg/filemanager/driver/local/local.go index 28e2b555..172effb8 100644 --- a/pkg/filemanager/driver/local/local.go +++ b/pkg/filemanager/driver/local/local.go @@ -298,6 +298,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities { return capabilities } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/obs/media.go b/pkg/filemanager/driver/obs/media.go index b7d75e41..fec894fd 100644 --- a/pkg/filemanager/driver/obs/media.go +++ b/pkg/filemanager/driver/obs/media.go @@ -17,7 +17,7 @@ import ( "github.com/samber/lo" ) -func (d *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (d *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { thumbURL, err := d.signSourceURL(&obs.CreateSignedUrlInput{ Method: obs.HttpMethodGet, Bucket: d.policy.BucketName, diff --git a/pkg/filemanager/driver/onedrive/onedrive.go b/pkg/filemanager/driver/onedrive/onedrive.go index 4eeef076..4a3170d1 100644 --- a/pkg/filemanager/driver/onedrive/onedrive.go +++ b/pkg/filemanager/driver/onedrive/onedrive.go @@ -241,7 +241,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index 737820ec..9d468078 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -535,7 +535,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { if util.ContainsString(supportedImageExt, ext) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/driver/qiniu/qiniu.go b/pkg/filemanager/driver/qiniu/qiniu.go index 06b37ba3..40ec09d7 100644 --- a/pkg/filemanager/driver/qiniu/qiniu.go +++ b/pkg/filemanager/driver/qiniu/qiniu.go @@ -433,7 +433,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { if util.ContainsString(supportedImageExt, ext) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/driver/remote/client.go b/pkg/filemanager/driver/remote/client.go index 2c8e6b4f..fe7e095b 100644 --- a/pkg/filemanager/driver/remote/client.go +++ b/pkg/filemanager/driver/remote/client.go @@ -43,7 +43,7 @@ type Client interface { // DeleteUploadSession deletes remote upload session DeleteUploadSession(ctx context.Context, sessionID string) error // MediaMeta gets media meta from remote server - MediaMeta(ctx context.Context, src, ext string) ([]driver.MediaMeta, error) + MediaMeta(ctx context.Context, src, ext, language string) ([]driver.MediaMeta, error) // DeleteFiles deletes files from remote server DeleteFiles(ctx context.Context, files ...string) ([]string, error) // List lists files from remote server @@ -183,10 +183,10 @@ func (c *remoteClient) DeleteFiles(ctx context.Context, files ...string) ([]stri return nil, nil } -func (c *remoteClient) MediaMeta(ctx context.Context, src, ext string) ([]driver.MediaMeta, error) { +func (c *remoteClient) MediaMeta(ctx context.Context, src, ext, language string) ([]driver.MediaMeta, error) { resp, err := c.httpClient.Request( http.MethodGet, - routes.SlaveMediaMetaRoute(src, ext), + routes.SlaveMediaMetaRoute(src, ext, language), nil, request.WithContext(ctx), request.WithLogger(c.l), diff --git a/pkg/filemanager/driver/remote/remote.go b/pkg/filemanager/driver/remote/remote.go index eb2b5efb..42b2b1af 100644 --- a/pkg/filemanager/driver/remote/remote.go +++ b/pkg/filemanager/driver/remote/remote.go @@ -179,6 +179,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { - return handler.uploadClient.MediaMeta(ctx, path, ext) +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { + return handler.uploadClient.MediaMeta(ctx, path, ext, language) } diff --git a/pkg/filemanager/driver/s3/s3.go b/pkg/filemanager/driver/s3/s3.go index 05c41c6e..04c8e8e9 100644 --- a/pkg/filemanager/driver/s3/s3.go +++ b/pkg/filemanager/driver/s3/s3.go @@ -482,7 +482,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/upyun/upyun.go b/pkg/filemanager/driver/upyun/upyun.go index 895548bf..bc1be1fb 100644 --- a/pkg/filemanager/driver/upyun/upyun.go +++ b/pkg/filemanager/driver/upyun/upyun.go @@ -345,7 +345,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/manager/mediameta.go b/pkg/filemanager/manager/mediameta.go index 4ed3d252..82bf8ed6 100644 --- a/pkg/filemanager/manager/mediameta.go +++ b/pkg/filemanager/manager/mediameta.go @@ -14,6 +14,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/mediameta" "github.com/cloudreve/Cloudreve/v4/pkg/queue" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/samber/lo" @@ -106,6 +107,11 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti return nil } + language := "" + if file.Owner().Settings != nil { + language = file.Owner().Settings.Language + } + var ( metas []driver.MediaMeta ) @@ -117,7 +123,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti driverCaps := d.Capabilities() if util.IsInExtensionList(driverCaps.MediaMetaSupportedExts, file.Name()) { m.l.Debug("Using native driver to generate media meta.") - metas, err = d.MediaMeta(ctx, targetVersion.Source(), file.Ext()) + metas, err = d.MediaMeta(ctx, targetVersion.Source(), file.Ext(), language) if err != nil { return fmt.Errorf("failed to get media meta using native driver: %w", err) } @@ -130,7 +136,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti return fmt.Errorf("failed to get entity source: %w", err) } - metas, err = extractor.Extract(ctx, file.Ext(), source) + metas, err = extractor.Extract(ctx, file.Ext(), source, mediameta.WithLanguage(language)) if err != nil { return fmt.Errorf("failed to extract media meta using local extractor: %w", err) } diff --git a/pkg/mediameta/exif.go b/pkg/mediameta/exif.go index 27e0d213..df55c885 100644 --- a/pkg/mediameta/exif.go +++ b/pkg/mediameta/exif.go @@ -145,7 +145,12 @@ func (e *exifExtractor) Exts() []string { } // Reference: https://github.com/photoprism/photoprism/blob/602097635f1c84d91f2d919f7aedaef7a07fc458/internal/meta/exif.go -func (e *exifExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (e *exifExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + localLimit, remoteLimit := e.settings.MediaMetaExifSizeLimit(ctx) if err := checkFileSize(localLimit, remoteLimit, source); err != nil { return nil, err diff --git a/pkg/mediameta/extractor.go b/pkg/mediameta/extractor.go index e871189e..90345947 100644 --- a/pkg/mediameta/extractor.go +++ b/pkg/mediameta/extractor.go @@ -4,12 +4,14 @@ import ( "context" "encoding/gob" "errors" + "io" + "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" "github.com/samber/lo" - "io" ) type ( @@ -17,7 +19,7 @@ type ( // Exts returns the supported file extensions. Exts() []string // Extract extracts the media meta from the given source. - Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) + Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) } ) @@ -29,7 +31,7 @@ func init() { gob.Register([]driver.MediaMeta{}) } -func NewExtractorManager(ctx context.Context, settings setting.Provider, l logging.Logger) Extractor { +func NewExtractorManager(ctx context.Context, settings setting.Provider, l logging.Logger, client request.Client) Extractor { e := &extractorManager{ settings: settings, extMap: make(map[string][]Extractor), @@ -52,6 +54,11 @@ func NewExtractorManager(ctx context.Context, settings setting.Provider, l loggi extractors = append(extractors, ffprobeE) } + if e.settings.MediaMetaGeocodingEnabled(ctx) { + geocodingE := newGeocodingExtractor(settings, l, client) + extractors = append(extractors, geocodingE) + } + for _, extractor := range extractors { for _, ext := range extractor.Exts() { if e.extMap[ext] == nil { @@ -73,12 +80,12 @@ func (e *extractorManager) Exts() []string { return lo.Keys(e.extMap) } -func (e *extractorManager) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (e *extractorManager) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { if extractor, ok := e.extMap[ext]; ok { res := []driver.MediaMeta{} for _, e := range extractor { _, _ = source.Seek(0, io.SeekStart) - data, err := e.Extract(ctx, ext, source) + data, err := e.Extract(ctx, ext, source, append(opts, WithExtracted(res))...) if err != nil { return nil, err } @@ -92,6 +99,29 @@ func (e *extractorManager) Extract(ctx context.Context, ext string, source entit } } +type option struct { + extracted []driver.MediaMeta + language string +} + +type optionFunc func(*option) + +func (f optionFunc) apply(o *option) { + f(o) +} + +func WithExtracted(extracted []driver.MediaMeta) optionFunc { + return optionFunc(func(o *option) { + o.extracted = extracted + }) +} + +func WithLanguage(language string) optionFunc { + return optionFunc(func(o *option) { + o.language = language + }) +} + // checkFileSize checks if the file size exceeds the limit. func checkFileSize(localLimit, remoteLimit int64, source entitysource.EntitySource) error { if source.IsLocal() && localLimit > 0 && source.Entity().Size() > localLimit { diff --git a/pkg/mediameta/ffprobe.go b/pkg/mediameta/ffprobe.go index 3cc1117f..369985e3 100644 --- a/pkg/mediameta/ffprobe.go +++ b/pkg/mediameta/ffprobe.go @@ -88,7 +88,12 @@ func (f *ffprobeExtractor) Exts() []string { return ffprobeExts } -func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + localLimit, remoteLimit := f.settings.MediaMetaFFProbeSizeLimit(ctx) if err := checkFileSize(localLimit, remoteLimit, source); err != nil { return nil, err diff --git a/pkg/mediameta/geocoding.go b/pkg/mediameta/geocoding.go new file mode 100644 index 00000000..e2b96423 --- /dev/null +++ b/pkg/mediameta/geocoding.go @@ -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 +} diff --git a/pkg/mediameta/music.go b/pkg/mediameta/music.go index ac2ec78c..6d4075c5 100644 --- a/pkg/mediameta/music.go +++ b/pkg/mediameta/music.go @@ -48,7 +48,12 @@ func (a *musicExtractor) Exts() []string { return audioExts } -func (a *musicExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (a *musicExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + localLimit, remoteLimit := a.settings.MediaMetaMusicSizeLimit(ctx) if err := checkFileSize(localLimit, remoteLimit, source); err != nil { return nil, err diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 1959433a..017ff2fb 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -102,6 +102,10 @@ type ( MediaMetaFFProbeSizeLimit(ctx context.Context) (int64, int64) // MediaMetaFFProbePath returns the path of ffprobe executable. MediaMetaFFProbePath(ctx context.Context) string + // MediaMetaGeocodingEnabled returns true if media meta geocoding is enabled. + MediaMetaGeocodingEnabled(ctx context.Context) bool + // MediaMetaGeocodingMapboxAK returns the Mapbox access token. + MediaMetaGeocodingMapboxAK(ctx context.Context) string // ThumbSize returns the size limit of thumbnails. ThumbSize(ctx context.Context) (int, int) // ThumbEncode returns the thumbnail encoding settings. @@ -527,6 +531,14 @@ func (s *settingProvider) MediaMetaEnabled(ctx context.Context) bool { return s.getBoolean(ctx, "media_meta", true) } +func (s *settingProvider) MediaMetaGeocodingEnabled(ctx context.Context) bool { + return s.getBoolean(ctx, "media_meta_geocoding", false) +} + +func (s *settingProvider) MediaMetaGeocodingMapboxAK(ctx context.Context) string { + return s.getString(ctx, "media_meta_geocoding_mapbox_ak", "") +} + func (s *settingProvider) PublicResourceMaxAge(ctx context.Context) int { return s.getInt(ctx, "public_resource_maxage", 0) } diff --git a/service/explorer/slave.go b/service/explorer/slave.go index dc356836..c4335aff 100644 --- a/service/explorer/slave.go +++ b/service/explorer/slave.go @@ -13,6 +13,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" + "github.com/cloudreve/Cloudreve/v4/pkg/mediameta" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/gin-gonic/gin" "github.com/samber/lo" @@ -135,8 +136,9 @@ func (s *SlaveMetaService) MediaMeta(c *gin.Context) ([]driver.MediaMeta, error) } defer entitySource.Close() + language := c.Query("language") extractor := dep.MediaMetaExtractor(c) - res, err := extractor.Extract(c, s.Ext, entitySource) + res, err := extractor.Extract(c, s.Ext, entitySource, mediameta.WithLanguage(language)) if err != nil { return nil, fmt.Errorf("failed to extract media meta: %w", err) }