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
|
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
|
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_path": "ffprobe",
|
||||||
"media_meta_ffprobe_size_local": "0",
|
"media_meta_ffprobe_size_local": "0",
|
||||||
"media_meta_ffprobe_size_remote": "0",
|
"media_meta_ffprobe_size_remote": "0",
|
||||||
|
"media_meta_geocoding": "0",
|
||||||
|
"media_meta_geocoding_mapbox_ak": "",
|
||||||
"site_logo": "/static/img/logo.svg",
|
"site_logo": "/static/img/logo.svg",
|
||||||
"site_logo_light": "/static/img/logo_light.svg",
|
"site_logo_light": "/static/img/logo_light.svg",
|
||||||
"tos_url": "https://cloudreve.org/privacy-policy",
|
"tos_url": "https://cloudreve.org/privacy-policy",
|
||||||
|
|
|
@ -180,9 +180,9 @@ func SlaveFileContentUrl(base *url.URL, srcPath, name string, download bool, spe
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
|
|
||||||
func SlaveMediaMetaRoute(src, ext string) string {
|
func SlaveMediaMetaRoute(src, ext, language string) string {
|
||||||
src = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(src)))
|
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 {
|
func SlaveFileListRoute(srcPath string, recursive bool) string {
|
||||||
|
|
|
@ -594,7 +594,7 @@ func (handler Driver) Meta(ctx context.Context, path string) (*MetaData, error)
|
||||||
}, nil
|
}, 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) {
|
if util.ContainsString(supportedImageExt, ext) {
|
||||||
return handler.extractImageMeta(ctx, path)
|
return handler.extractImageMeta(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ type (
|
||||||
Capabilities() *Capabilities
|
Capabilities() *Capabilities
|
||||||
|
|
||||||
// MediaMeta extracts media metadata from the given file.
|
// 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 {
|
Capabilities struct {
|
||||||
|
@ -117,6 +117,7 @@ const (
|
||||||
MetaTypeExif MetaType = "exif"
|
MetaTypeExif MetaType = "exif"
|
||||||
MediaTypeMusic MetaType = "music"
|
MediaTypeMusic MetaType = "music"
|
||||||
MetaTypeStreamMedia MetaType = "stream"
|
MetaTypeStreamMedia MetaType = "stream"
|
||||||
|
MetaTypeGeocoding MetaType = "geocoding"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ForceUsePublicEndpointCtx struct{}
|
type ForceUsePublicEndpointCtx struct{}
|
||||||
|
|
|
@ -505,7 +505,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaMeta 获取媒体元信息
|
// 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")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -298,6 +298,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities {
|
||||||
return 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")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
"github.com/samber/lo"
|
"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{
|
thumbURL, err := d.signSourceURL(&obs.CreateSignedUrlInput{
|
||||||
Method: obs.HttpMethodGet,
|
Method: obs.HttpMethodGet,
|
||||||
Bucket: d.policy.BucketName,
|
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")
|
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) {
|
if util.ContainsString(supportedImageExt, ext) {
|
||||||
return handler.extractImageMeta(ctx, path)
|
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) {
|
if util.ContainsString(supportedImageExt, ext) {
|
||||||
return handler.extractImageMeta(ctx, path)
|
return handler.extractImageMeta(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ type Client interface {
|
||||||
// DeleteUploadSession deletes remote upload session
|
// DeleteUploadSession deletes remote upload session
|
||||||
DeleteUploadSession(ctx context.Context, sessionID string) error
|
DeleteUploadSession(ctx context.Context, sessionID string) error
|
||||||
// MediaMeta gets media meta from remote server
|
// 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 deletes files from remote server
|
||||||
DeleteFiles(ctx context.Context, files ...string) ([]string, error)
|
DeleteFiles(ctx context.Context, files ...string) ([]string, error)
|
||||||
// List lists files from remote server
|
// List lists files from remote server
|
||||||
|
@ -183,10 +183,10 @@ func (c *remoteClient) DeleteFiles(ctx context.Context, files ...string) ([]stri
|
||||||
return nil, nil
|
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(
|
resp, err := c.httpClient.Request(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
routes.SlaveMediaMetaRoute(src, ext),
|
routes.SlaveMediaMetaRoute(src, ext, language),
|
||||||
nil,
|
nil,
|
||||||
request.WithContext(ctx),
|
request.WithContext(ctx),
|
||||||
request.WithLogger(c.l),
|
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) {
|
func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) {
|
||||||
return handler.uploadClient.MediaMeta(ctx, path, ext)
|
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")
|
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)
|
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"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
"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/queue"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
@ -106,6 +107,11 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
language := ""
|
||||||
|
if file.Owner().Settings != nil {
|
||||||
|
language = file.Owner().Settings.Language
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metas []driver.MediaMeta
|
metas []driver.MediaMeta
|
||||||
)
|
)
|
||||||
|
@ -117,7 +123,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti
|
||||||
driverCaps := d.Capabilities()
|
driverCaps := d.Capabilities()
|
||||||
if util.IsInExtensionList(driverCaps.MediaMetaSupportedExts, file.Name()) {
|
if util.IsInExtensionList(driverCaps.MediaMetaSupportedExts, file.Name()) {
|
||||||
m.l.Debug("Using native driver to generate media meta.")
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get media meta using native driver: %w", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to extract media meta using local extractor: %w", err)
|
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
|
// 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)
|
localLimit, remoteLimit := e.settings.MediaMetaExifSizeLimit(ctx)
|
||||||
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -4,12 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"io"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -17,7 +19,7 @@ type (
|
||||||
// Exts returns the supported file extensions.
|
// Exts returns the supported file extensions.
|
||||||
Exts() []string
|
Exts() []string
|
||||||
// Extract extracts the media meta from the given source.
|
// 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{})
|
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{
|
e := &extractorManager{
|
||||||
settings: settings,
|
settings: settings,
|
||||||
extMap: make(map[string][]Extractor),
|
extMap: make(map[string][]Extractor),
|
||||||
|
@ -52,6 +54,11 @@ func NewExtractorManager(ctx context.Context, settings setting.Provider, l loggi
|
||||||
extractors = append(extractors, ffprobeE)
|
extractors = append(extractors, ffprobeE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.settings.MediaMetaGeocodingEnabled(ctx) {
|
||||||
|
geocodingE := newGeocodingExtractor(settings, l, client)
|
||||||
|
extractors = append(extractors, geocodingE)
|
||||||
|
}
|
||||||
|
|
||||||
for _, extractor := range extractors {
|
for _, extractor := range extractors {
|
||||||
for _, ext := range extractor.Exts() {
|
for _, ext := range extractor.Exts() {
|
||||||
if e.extMap[ext] == nil {
|
if e.extMap[ext] == nil {
|
||||||
|
@ -73,12 +80,12 @@ func (e *extractorManager) Exts() []string {
|
||||||
return lo.Keys(e.extMap)
|
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 {
|
if extractor, ok := e.extMap[ext]; ok {
|
||||||
res := []driver.MediaMeta{}
|
res := []driver.MediaMeta{}
|
||||||
for _, e := range extractor {
|
for _, e := range extractor {
|
||||||
_, _ = source.Seek(0, io.SeekStart)
|
_, _ = 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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// checkFileSize checks if the file size exceeds the limit.
|
||||||
func checkFileSize(localLimit, remoteLimit int64, source entitysource.EntitySource) error {
|
func checkFileSize(localLimit, remoteLimit int64, source entitysource.EntitySource) error {
|
||||||
if source.IsLocal() && localLimit > 0 && source.Entity().Size() > localLimit {
|
if source.IsLocal() && localLimit > 0 && source.Entity().Size() > localLimit {
|
||||||
|
|
|
@ -88,7 +88,12 @@ func (f *ffprobeExtractor) Exts() []string {
|
||||||
return ffprobeExts
|
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)
|
localLimit, remoteLimit := f.settings.MediaMetaFFProbeSizeLimit(ctx)
|
||||||
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
||||||
return nil, err
|
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
|
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)
|
localLimit, remoteLimit := a.settings.MediaMetaMusicSizeLimit(ctx)
|
||||||
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
if err := checkFileSize(localLimit, remoteLimit, source); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -102,6 +102,10 @@ type (
|
||||||
MediaMetaFFProbeSizeLimit(ctx context.Context) (int64, int64)
|
MediaMetaFFProbeSizeLimit(ctx context.Context) (int64, int64)
|
||||||
// MediaMetaFFProbePath returns the path of ffprobe executable.
|
// MediaMetaFFProbePath returns the path of ffprobe executable.
|
||||||
MediaMetaFFProbePath(ctx context.Context) string
|
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 returns the size limit of thumbnails.
|
||||||
ThumbSize(ctx context.Context) (int, int)
|
ThumbSize(ctx context.Context) (int, int)
|
||||||
// ThumbEncode returns the thumbnail encoding settings.
|
// 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)
|
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 {
|
func (s *settingProvider) PublicResourceMaxAge(ctx context.Context) int {
|
||||||
return s.getInt(ctx, "public_resource_maxage", 0)
|
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/fs"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
"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/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
@ -135,8 +136,9 @@ func (s *SlaveMetaService) MediaMeta(c *gin.Context) ([]driver.MediaMeta, error)
|
||||||
}
|
}
|
||||||
defer entitySource.Close()
|
defer entitySource.Close()
|
||||||
|
|
||||||
|
language := c.Query("language")
|
||||||
extractor := dep.MediaMetaExtractor(c)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract media meta: %w", err)
|
return nil, fmt.Errorf("failed to extract media meta: %w", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue