mirror of https://github.com/cloudreve/Cloudreve
feat(media meta): reverse geocoding from mapbox (#2922)
parent
668b542c59
commit
5e5dca40c4
|
@ -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
|
||||
}
|
||||
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit dece1c7098de2efe38aaa25d6cafc41a2de568ff
|
||||
Subproject commit fc7791cde1444e1be0935f1fbc32d956fa6eb756
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue