From aa220a06fee24426fb1ddab56d7822d00104cedc Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sun, 5 Apr 2020 22:26:53 +0200 Subject: [PATCH] Backend: Refactor package structure Signed-off-by: Michael Mayer --- internal/api/import.go | 19 +- internal/api/index.go | 34 +--- internal/api/upload.go | 3 +- internal/commands/convert.go | 9 +- internal/commands/copy.go | 13 +- internal/commands/import.go | 13 +- internal/commands/index.go | 8 +- internal/commands/start.go | 2 + internal/commands/thumbs.go | 5 +- internal/entity/account.go | 8 +- internal/form/account.go | 4 +- internal/photoprism/share.go | 8 +- internal/photoprism/sync.go | 9 +- internal/remote/service.go | 176 ++++++++++++++++ internal/{service => remote}/service_test.go | 2 +- .../webdav/testdata/example.jpg | Bin internal/{service => remote}/webdav/webdav.go | 0 .../{service => remote}/webdav/webdav_test.go | 0 internal/service/classify.go | 19 ++ internal/service/convert.go | 19 ++ internal/service/import.go | 19 ++ internal/service/index.go | 19 ++ internal/service/nsfw.go | 19 ++ internal/service/resample.go | 19 ++ internal/service/service.go | 191 +++--------------- 25 files changed, 360 insertions(+), 258 deletions(-) create mode 100644 internal/remote/service.go rename internal/{service => remote}/service_test.go (99%) rename internal/{service => remote}/webdav/testdata/example.jpg (100%) rename internal/{service => remote}/webdav/webdav.go (100%) rename internal/{service => remote}/webdav/webdav_test.go (100%) create mode 100644 internal/service/classify.go create mode 100644 internal/service/convert.go create mode 100644 internal/service/import.go create mode 100644 internal/service/index.go create mode 100644 internal/service/nsfw.go create mode 100644 internal/service/resample.go diff --git a/internal/api/import.go b/internal/api/import.go index 81cfa46b0..de4456894 100644 --- a/internal/api/import.go +++ b/internal/api/import.go @@ -13,24 +13,11 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" ) -var imp *photoprism.Import - -func initImport(conf *config.Config) { - if imp != nil { - return - } - - initIndex(conf) - - convert := photoprism.NewConvert(conf) - - imp = photoprism.NewImport(conf, ind, convert) -} - // POST /api/v1/import* func StartImport(router *gin.RouterGroup, conf *config.Config) { router.POST("/import/*path", func(c *gin.Context) { @@ -64,7 +51,7 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) { path = filepath.Clean(path) - initImport(conf) + imp := service.Import() var opt photoprism.ImportOptions @@ -105,7 +92,7 @@ func CancelImport(router *gin.RouterGroup, conf *config.Config) { return } - initImport(conf) + imp := service.Import() imp.Cancel() diff --git a/internal/api/index.go b/internal/api/index.go index 94647620a..07237d2e5 100644 --- a/internal/api/index.go +++ b/internal/api/index.go @@ -7,38 +7,14 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" - "github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/pkg/txt" ) -var ind *photoprism.Index -var nd *nsfw.Detector - -func initIndex(conf *config.Config) { - if ind != nil { - return - } - - initNsfwDetector(conf) - - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) - - ind = photoprism.NewIndex(conf, tf, nd) -} - -func initNsfwDetector(conf *config.Config) { - if nd != nil { - return - } - - nd = nsfw.New(conf.NSFWModelPath()) -} - // POST /api/v1/index func StartIndexing(router *gin.RouterGroup, conf *config.Config) { router.POST("/index", func(c *gin.Context) { @@ -67,7 +43,7 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) { } if f.ConvertRaw && !conf.ReadOnly() { - convert := photoprism.NewConvert(conf) + convert := service.Convert() if err := convert.Start(conf.OriginalsPath()); err != nil { cancel(err) @@ -76,7 +52,7 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) { } if f.CreateThumbs { - rs := photoprism.NewResample(conf) + rs := service.Resample() if err := rs.Start(false); err != nil { cancel(err) @@ -84,7 +60,7 @@ func StartIndexing(router *gin.RouterGroup, conf *config.Config) { } } - initIndex(conf) + ind := service.Index() if f.SkipUnchanged { ind.Start(photoprism.IndexOptionsNone()) @@ -110,7 +86,7 @@ func CancelIndexing(router *gin.RouterGroup, conf *config.Config) { return } - initIndex(conf) + ind := service.Index() ind.Cancel() diff --git a/internal/api/upload.go b/internal/api/upload.go index 39c3fa8bb..48e77dde5 100644 --- a/internal/api/upload.go +++ b/internal/api/upload.go @@ -10,6 +10,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/pkg/txt" "github.com/gin-gonic/gin" @@ -65,7 +66,7 @@ func Upload(router *gin.RouterGroup, conf *config.Config) { } if !conf.UploadNSFW() { - initNsfwDetector(conf) + nd := service.NsfwDetector() containsNSFW := false diff --git a/internal/commands/convert.go b/internal/commands/convert.go index c7a2d26ce..332d9fc32 100644 --- a/internal/commands/convert.go +++ b/internal/commands/convert.go @@ -4,7 +4,7 @@ import ( "time" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/urfave/cli" ) @@ -20,6 +20,7 @@ func convertAction(ctx *cli.Context) error { start := time.Now() conf := config.NewConfig(ctx) + service.SetConfig(conf) if conf.ReadOnly() { return config.ErrReadOnly @@ -31,9 +32,11 @@ func convertAction(ctx *cli.Context) error { log.Infof("converting RAW images in %s to JPEG", conf.OriginalsPath()) - convert := photoprism.NewConvert(conf) + convert := service.Convert() - convert.Start(conf.OriginalsPath()) + if err := convert.Start(conf.OriginalsPath()); err != nil { + log.Error(err) + } elapsed := time.Since(start) diff --git a/internal/commands/copy.go b/internal/commands/copy.go index 5130b2729..6ff4e119b 100644 --- a/internal/commands/copy.go +++ b/internal/commands/copy.go @@ -7,10 +7,9 @@ import ( "strings" "time" - "github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/urfave/cli" ) @@ -27,6 +26,7 @@ func copyAction(ctx *cli.Context) error { start := time.Now() conf := config.NewConfig(ctx) + service.SetConfig(conf) // very if copy directory exist and is writable if conf.ReadOnly() { @@ -66,14 +66,7 @@ func copyAction(ctx *cli.Context) error { log.Infof("copying media files from %s to %s", sourcePath, conf.OriginalsPath()) - tensorFlow := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) - nsfwDetector := nsfw.New(conf.NSFWModelPath()) - - ind := photoprism.NewIndex(conf, tensorFlow, nsfwDetector) - - convert := photoprism.NewConvert(conf) - - imp := photoprism.NewImport(conf, ind, convert) + imp := service.Import() opt := photoprism.ImportOptionsCopy(sourcePath) imp.Start(opt) diff --git a/internal/commands/import.go b/internal/commands/import.go index dd5b07d9d..65e634e6c 100644 --- a/internal/commands/import.go +++ b/internal/commands/import.go @@ -7,10 +7,9 @@ import ( "strings" "time" - "github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/urfave/cli" ) @@ -27,6 +26,7 @@ func importAction(ctx *cli.Context) error { start := time.Now() conf := config.NewConfig(ctx) + service.SetConfig(conf) // very if copy directory exist and is writable if conf.ReadOnly() { @@ -66,14 +66,7 @@ func importAction(ctx *cli.Context) error { log.Infof("moving media files from %s to %s", sourcePath, conf.OriginalsPath()) - tensorFlow := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) - nsfwDetector := nsfw.New(conf.NSFWModelPath()) - - ind := photoprism.NewIndex(conf, tensorFlow, nsfwDetector) - - convert := photoprism.NewConvert(conf) - - imp := photoprism.NewImport(conf, ind, convert) + imp := service.Import() opt := photoprism.ImportOptionsMove(sourcePath) imp.Start(opt) diff --git a/internal/commands/index.go b/internal/commands/index.go index dbdaadf03..62c11428b 100644 --- a/internal/commands/index.go +++ b/internal/commands/index.go @@ -4,10 +4,9 @@ import ( "context" "time" - "github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/nsfw" "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/urfave/cli" ) @@ -31,6 +30,7 @@ func indexAction(ctx *cli.Context) error { start := time.Now() conf := config.NewConfig(ctx) + service.SetConfig(conf) if err := conf.CreateDirectories(); err != nil { return err @@ -49,9 +49,7 @@ func indexAction(ctx *cli.Context) error { log.Infof("read-only mode enabled") } - tf := classify.New(conf.ResourcesPath(), conf.TensorFlowDisabled()) - nd := nsfw.New(conf.NSFWModelPath()) - ind := photoprism.NewIndex(conf, tf, nd) + ind := service.Index() var opt photoprism.IndexOptions diff --git a/internal/commands/start.go b/internal/commands/start.go index 9cfcd3b28..42a3250b0 100644 --- a/internal/commands/start.go +++ b/internal/commands/start.go @@ -12,6 +12,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/server" + "github.com/photoprism/photoprism/internal/service" "github.com/photoprism/photoprism/pkg/fs" "github.com/sevlyar/go-daemon" "github.com/urfave/cli" @@ -41,6 +42,7 @@ var startFlags = []cli.Flag{ // startAction start the web server and initializes the daemon func startAction(ctx *cli.Context) error { conf := config.NewConfig(ctx) + service.SetConfig(conf) if err := conf.CreateDirectories(); err != nil { return err diff --git a/internal/commands/thumbs.go b/internal/commands/thumbs.go index 7097c1a72..fc03076e1 100644 --- a/internal/commands/thumbs.go +++ b/internal/commands/thumbs.go @@ -4,7 +4,7 @@ import ( "time" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/photoprism" + "github.com/photoprism/photoprism/internal/service" "github.com/urfave/cli" ) @@ -26,6 +26,7 @@ func thumbsAction(ctx *cli.Context) error { start := time.Now() conf := config.NewConfig(ctx) + service.SetConfig(conf) if err := conf.CreateDirectories(); err != nil { return err @@ -33,7 +34,7 @@ func thumbsAction(ctx *cli.Context) error { log.Infof("creating thumbnails in \"%s\"", conf.ThumbnailsPath()) - rs := photoprism.NewResample(conf) + rs := service.Resample() if err := rs.Start(ctx.Bool("force")); err != nil { log.Error(err) diff --git a/internal/entity/account.go b/internal/entity/account.go index f37d20cd8..24efc60e0 100644 --- a/internal/entity/account.go +++ b/internal/entity/account.go @@ -7,8 +7,8 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/form" - "github.com/photoprism/photoprism/internal/service" - "github.com/photoprism/photoprism/internal/service/webdav" + "github.com/photoprism/photoprism/internal/remote" + "github.com/photoprism/photoprism/internal/remote/webdav" "github.com/photoprism/photoprism/pkg/fs" "github.com/ulule/deepcopier" ) @@ -73,7 +73,7 @@ func (m *Account) Save(form form.Account, db *gorm.DB) error { return err } - if m.AccType != string(service.TypeWebDAV) { + if m.AccType != string(remote.ServiceWebDAV) { // TODO: Only WebDAV supported at the moment m.AccShare = false m.AccSync = false @@ -97,7 +97,7 @@ func (m *Account) Delete(db *gorm.DB) error { // Directories returns a list of directories or albums in an account. func (m *Account) Directories() (result fs.FileInfos, err error) { - if m.AccType == service.TypeWebDAV { + if m.AccType == remote.ServiceWebDAV { c := webdav.New(m.AccURL, m.AccUser, m.AccPass) result, err = c.Directories("/", true) } diff --git a/internal/form/account.go b/internal/form/account.go index 5a4a9b1f4..223ab37e8 100644 --- a/internal/form/account.go +++ b/internal/form/account.go @@ -1,7 +1,7 @@ package form import ( - "github.com/photoprism/photoprism/internal/service" + "github.com/photoprism/photoprism/internal/remote" "github.com/ulule/deepcopier" ) @@ -38,7 +38,7 @@ func NewAccount(m interface{}) (f Account, err error) { } func (f *Account) ServiceDiscovery() error { - acc, err := service.Discover(f.AccURL, f.AccUser, f.AccPass) + acc, err := remote.Discover(f.AccURL, f.AccUser, f.AccPass) if err != nil { return err diff --git a/internal/photoprism/share.go b/internal/photoprism/share.go index 5863432bb..a80ac1a3e 100644 --- a/internal/photoprism/share.go +++ b/internal/photoprism/share.go @@ -11,8 +11,8 @@ import ( "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/query" - "github.com/photoprism/photoprism/internal/service" - "github.com/photoprism/photoprism/internal/service/webdav" + "github.com/photoprism/photoprism/internal/remote" + "github.com/photoprism/photoprism/internal/remote/webdav" "github.com/photoprism/photoprism/internal/thumb" ) @@ -49,7 +49,7 @@ func (s *Share) Start() (err error) { return nil } - if a.AccType != service.TypeWebDAV { + if a.AccType != remote.ServiceWebDAV { continue } @@ -129,7 +129,7 @@ func (s *Share) Start() (err error) { return nil } - if a.AccType != service.TypeWebDAV { + if a.AccType != remote.ServiceWebDAV { continue } diff --git a/internal/photoprism/sync.go b/internal/photoprism/sync.go index cd8371176..ea01f11f8 100644 --- a/internal/photoprism/sync.go +++ b/internal/photoprism/sync.go @@ -10,8 +10,8 @@ import ( "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/mutex" "github.com/photoprism/photoprism/internal/query" - "github.com/photoprism/photoprism/internal/service" - "github.com/photoprism/photoprism/internal/service/webdav" + "github.com/photoprism/photoprism/internal/remote" + "github.com/photoprism/photoprism/internal/remote/webdav" ) // Sync represents a sync worker. @@ -43,7 +43,7 @@ func (s *Sync) Start() (err error) { accounts, err := q.Accounts(f) for _, a := range accounts { - if a.AccType != service.TypeWebDAV { + if a.AccType != remote.ServiceWebDAV { continue } @@ -112,7 +112,7 @@ func (s *Sync) Start() (err error) { } func (s *Sync) getRemoteFiles(a entity.Account) (complete bool, err error) { - if a.AccType != service.TypeWebDAV { + if a.AccType != remote.ServiceWebDAV { return false, nil } @@ -221,5 +221,6 @@ func (s *Sync) download(a entity.Account) (complete bool, err error) { } func (s *Sync) upload(a entity.Account) (complete bool, err error) { + // TODO: Not implemented yet return false, nil } diff --git a/internal/remote/service.go b/internal/remote/service.go new file mode 100644 index 000000000..fdecaca5e --- /dev/null +++ b/internal/remote/service.go @@ -0,0 +1,176 @@ +/* +Package service implements a remote service abstraction. + +Additional information can be found in our Developer Guide: + +https://github.com/photoprism/photoprism/wiki +*/ +package remote + +import ( + "errors" + "net/http" + "net/url" + "strings" + + "github.com/photoprism/photoprism/pkg/txt" +) + +var client = &http.Client{} + +const ( + ServiceWeb = "web" + ServiceWebDAV = "webdav" + ServiceFacebook = "facebook" + ServiceTwitter = "twitter" + ServiceFlickr = "flickr" + ServiceInstagram = "instagram" + ServiceEyeEm = "eyeem" + ServiceTelegram = "telegram" + ServiceWhatsApp = "whatsapp" + ServiceGPhotos = "gphotos" + ServiceGDrive = "gdrive" + ServiceOneDrive = "onedrive" +) + +type Account struct { + AccName string + AccURL string + AccType string + AccKey string + AccUser string + AccPass string +} + +type Heuristic struct { + ServiceType string + Domains []string + Paths []string + Method string +} + +var Heuristics = []Heuristic{ + {ServiceFacebook, []string{"facebook.com", "www.facebook.com"}, []string{}, "GET"}, + {ServiceTwitter, []string{"twitter.com"}, []string{}, "GET"}, + {ServiceFlickr, []string{"flickr.com", "www.flickr.com"}, []string{}, "GET"}, + {ServiceInstagram, []string{"instagram.com", "www.instagram.com"}, []string{}, "GET"}, + {ServiceEyeEm, []string{"eyeem.com", "www.eyeem.com"}, []string{}, "GET"}, + {ServiceTelegram, []string{"web.telegram.org", "www.telegram.org", "telegram.org"}, []string{}, "GET"}, + {ServiceWhatsApp, []string{"web.whatsapp.com", "www.whatsapp.com", "whatsapp.com"}, []string{}, "GET"}, + {ServiceOneDrive, []string{"onedrive.live.com"}, []string{}, "GET"}, + {ServiceGDrive, []string{"drive.google.com"}, []string{}, "GET"}, + {ServiceGPhotos, []string{"photos.google.com"}, []string{}, "GET"}, + {ServiceWebDAV, []string{}, []string{"/", "/webdav", "/remote.php/dav/files/{user}", "/remote.php/webdav", "/dav/files/{user}", "/servlet/webdav.infostore/"}, "PROPFIND"}, + {ServiceWeb, []string{}, []string{}, "GET"}, +} + +func HttpOk(method, rawUrl string) bool { + req, err := http.NewRequest(method, rawUrl, nil) + + if err != nil { + return false + } + + if resp, err := client.Do(req); err != nil { + return false + } else if resp.StatusCode < 400 { + return true + } + + return false +} + +func (h Heuristic) MatchDomain(match string) bool { + if len(h.Domains) == 0 { + return true + } + + for _, m := range h.Domains { + if m == match { + return true + } + } + + return false +} + +func (h Heuristic) Discover(rawUrl, user string) *url.URL { + u, err := url.Parse(rawUrl) + + if err != nil { + return nil + } + + if HttpOk(h.Method, u.String()) { + return u + } + + for _, p := range h.Paths { + strings.Replace(p, "{user}", user, -1) + u.Path = p + + if HttpOk(h.Method, u.String()) { + return u + } + } + + return nil +} + +func Discover(rawUrl, user, pass string) (result Account, err error) { + if rawUrl == "" { + return result, errors.New("service URL is empty") + } + + u, err := url.Parse(rawUrl) + + if err != nil { + return result, err + } + + u.Host = strings.ToLower(u.Host) + + result.AccUser = u.User.Username() + result.AccPass, _ = u.User.Password() + + // Extract user info + if user != "" { + result.AccUser = user + } + + if pass != "" { + result.AccPass = pass + } + + if user != "" || pass != "" { + u.User = url.UserPassword(result.AccUser, result.AccPass) + } + + // Set default scheme + if u.Scheme == "" { + u.Scheme = "https" + } + + for _, h := range Heuristics { + if !h.MatchDomain(u.Host) { + continue + } + + if serviceUrl := h.Discover(u.String(), result.AccUser); serviceUrl != nil { + serviceUrl.User = nil + + if w := txt.Keywords(serviceUrl.Host); len(w) > 0 { + result.AccName = strings.Title(w[0]) + } else { + result.AccName = serviceUrl.Host + } + + result.AccType = h.ServiceType + result.AccURL = serviceUrl.String() + + return result, nil + } + } + + return result, errors.New("could not connect") +} diff --git a/internal/service/service_test.go b/internal/remote/service_test.go similarity index 99% rename from internal/service/service_test.go rename to internal/remote/service_test.go index b3cc2c7a9..d8ddfd2ed 100644 --- a/internal/service/service_test.go +++ b/internal/remote/service_test.go @@ -1,4 +1,4 @@ -package service +package remote import ( "testing" diff --git a/internal/service/webdav/testdata/example.jpg b/internal/remote/webdav/testdata/example.jpg similarity index 100% rename from internal/service/webdav/testdata/example.jpg rename to internal/remote/webdav/testdata/example.jpg diff --git a/internal/service/webdav/webdav.go b/internal/remote/webdav/webdav.go similarity index 100% rename from internal/service/webdav/webdav.go rename to internal/remote/webdav/webdav.go diff --git a/internal/service/webdav/webdav_test.go b/internal/remote/webdav/webdav_test.go similarity index 100% rename from internal/service/webdav/webdav_test.go rename to internal/remote/webdav/webdav_test.go diff --git a/internal/service/classify.go b/internal/service/classify.go new file mode 100644 index 000000000..5906a0359 --- /dev/null +++ b/internal/service/classify.go @@ -0,0 +1,19 @@ +package service + +import ( + "sync" + + "github.com/photoprism/photoprism/internal/classify" +) + +var onceClassify sync.Once + +func initClassify() { + services.Classify = classify.New(Config().ResourcesPath(), Config().TensorFlowDisabled()) +} + +func Classify() *classify.TensorFlow { + onceClassify.Do(initClassify) + + return services.Classify +} diff --git a/internal/service/convert.go b/internal/service/convert.go new file mode 100644 index 000000000..e0a9ade3e --- /dev/null +++ b/internal/service/convert.go @@ -0,0 +1,19 @@ +package service + +import ( + "sync" + + "github.com/photoprism/photoprism/internal/photoprism" +) + +var onceConvert sync.Once + +func initConvert() { + services.Convert = photoprism.NewConvert(Config()) +} + +func Convert() *photoprism.Convert { + onceConvert.Do(initConvert) + + return services.Convert +} diff --git a/internal/service/import.go b/internal/service/import.go new file mode 100644 index 000000000..6a4f0c51f --- /dev/null +++ b/internal/service/import.go @@ -0,0 +1,19 @@ +package service + +import ( + "sync" + + "github.com/photoprism/photoprism/internal/photoprism" +) + +var onceImport sync.Once + +func initImport() { + services.Import = photoprism.NewImport(Config(), Index(), Convert()) +} + +func Import() *photoprism.Import { + onceImport.Do(initImport) + + return services.Import +} diff --git a/internal/service/index.go b/internal/service/index.go new file mode 100644 index 000000000..90afe2651 --- /dev/null +++ b/internal/service/index.go @@ -0,0 +1,19 @@ +package service + +import ( + "sync" + + "github.com/photoprism/photoprism/internal/photoprism" +) + +var onceIndex sync.Once + +func initIndex() { + services.Index = photoprism.NewIndex(Config(), Classify(), NsfwDetector()) +} + +func Index() *photoprism.Index { + onceIndex.Do(initIndex) + + return services.Index +} diff --git a/internal/service/nsfw.go b/internal/service/nsfw.go new file mode 100644 index 000000000..5736bb629 --- /dev/null +++ b/internal/service/nsfw.go @@ -0,0 +1,19 @@ +package service + +import ( + "sync" + + "github.com/photoprism/photoprism/internal/nsfw" +) + +var onceNsfwDetector sync.Once + +func initNsfwDetector() { + services.Nsfw = nsfw.New(conf.NSFWModelPath()) +} + +func NsfwDetector() *nsfw.Detector { + onceNsfwDetector.Do(initNsfwDetector) + + return services.Nsfw +} diff --git a/internal/service/resample.go b/internal/service/resample.go new file mode 100644 index 000000000..27eff9e4d --- /dev/null +++ b/internal/service/resample.go @@ -0,0 +1,19 @@ +package service + +import ( + "sync" + + "github.com/photoprism/photoprism/internal/photoprism" +) + +var onceResample sync.Once + +func initResample() { + services.Resample = photoprism.NewResample(Config()) +} + +func Resample() *photoprism.Resample { + onceResample.Do(initResample) + + return services.Resample +} diff --git a/internal/service/service.go b/internal/service/service.go index 0c905a071..55b4b5cf8 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,178 +1,35 @@ -/* -Package service implements a remote service abstraction. - -Additional information can be found in our Developer Guide: - -https://github.com/photoprism/photoprism/wiki -*/ package service import ( - "errors" - "net/http" - "net/url" - "strings" - - "github.com/photoprism/photoprism/internal/event" - "github.com/photoprism/photoprism/pkg/txt" + "github.com/photoprism/photoprism/internal/classify" + "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/nsfw" + "github.com/photoprism/photoprism/internal/photoprism" ) -var log = event.Log -var client = &http.Client{} +var conf *config.Config -const ( - TypeWeb = "web" - TypeWebDAV = "webdav" - TypeFacebook = "facebook" - TypeTwitter = "twitter" - TypeFlickr = "flickr" - TypeInstagram = "instagram" - TypeEyeEm = "eyeem" - TypeTelegram = "telegram" - TypeWhatsApp = "whatsapp" - TypeGooglePhotos = "gphotos" - TypeGoogleDrive = "gdrive" - TypeOneDrive = "onedrive" -) - -type Account struct { - AccName string - AccURL string - AccType string - AccKey string - AccUser string - AccPass string +var services struct { + Import *photoprism.Import + Index *photoprism.Index + Nsfw *nsfw.Detector + Convert *photoprism.Convert + Resample *photoprism.Resample + Classify *classify.TensorFlow } -type Heuristic struct { - ServiceType string - Domains []string - Paths []string - Method string +func SetConfig(c *config.Config) { + if c == nil { + panic("config is nil") + } + + conf = c } -var Heuristics = []Heuristic{ - {TypeFacebook, []string{"facebook.com", "www.facebook.com"}, []string{}, "GET"}, - {TypeTwitter, []string{"twitter.com"}, []string{}, "GET"}, - {TypeFlickr, []string{"flickr.com", "www.flickr.com"}, []string{}, "GET"}, - {TypeInstagram, []string{"instagram.com", "www.instagram.com"}, []string{}, "GET"}, - {TypeEyeEm, []string{"eyeem.com", "www.eyeem.com"}, []string{}, "GET"}, - {TypeTelegram, []string{"web.telegram.org", "www.telegram.org", "telegram.org"}, []string{}, "GET"}, - {TypeWhatsApp, []string{"web.whatsapp.com", "www.whatsapp.com", "whatsapp.com"}, []string{}, "GET"}, - {TypeOneDrive, []string{"onedrive.live.com"}, []string{}, "GET"}, - {TypeGoogleDrive, []string{"drive.google.com"}, []string{}, "GET"}, - {TypeGooglePhotos, []string{"photos.google.com"}, []string{}, "GET"}, - {TypeWebDAV, []string{}, []string{"/", "/webdav", "/remote.php/dav/files/{user}", "/remote.php/webdav", "/dav/files/{user}", "/servlet/webdav.infostore/"}, "PROPFIND"}, - {TypeWeb, []string{}, []string{}, "GET"}, -} - -func HttpOk(method, rawUrl string) bool { - req, err := http.NewRequest(method, rawUrl, nil) - - if err != nil { - return false - } - - if resp, err := client.Do(req); err != nil { - return false - } else if resp.StatusCode < 400 { - return true - } - - return false -} - -func (h Heuristic) MatchDomain(match string) bool { - if len(h.Domains) == 0 { - return true - } - - for _, m := range h.Domains { - if m == match { - return true - } - } - - return false -} - -func (h Heuristic) Discover(rawUrl, user string) *url.URL { - u, err := url.Parse(rawUrl) - - if err != nil { - return nil - } - - if HttpOk(h.Method, u.String()) { - return u - } - - for _, p := range h.Paths { - strings.Replace(p, "{user}", user, -1) - u.Path = p - - if HttpOk(h.Method, u.String()) { - return u - } - } - - return nil -} - -func Discover(rawUrl, user, pass string) (result Account, err error) { - if rawUrl == "" { - return result, errors.New("service URL is empty") - } - - u, err := url.Parse(rawUrl) - - if err != nil { - return result, err - } - - u.Host = strings.ToLower(u.Host) - - result.AccUser = u.User.Username() - result.AccPass, _ = u.User.Password() - - // Extract user info - if user != "" { - result.AccUser = user - } - - if pass != "" { - result.AccPass = pass - } - - if user != "" || pass != "" { - u.User = url.UserPassword(result.AccUser, result.AccPass) - } - - // Set default scheme - if u.Scheme == "" { - u.Scheme = "https" - } - - for _, h := range Heuristics { - if !h.MatchDomain(u.Host) { - continue - } - - if serviceUrl := h.Discover(u.String(), result.AccUser); serviceUrl != nil { - serviceUrl.User = nil - - if w := txt.Keywords(serviceUrl.Host); len(w) > 0 { - result.AccName = strings.Title(w[0]) - } else { - result.AccName = serviceUrl.Host - } - - result.AccType = h.ServiceType - result.AccURL = serviceUrl.String() - - return result, nil - } - } - - return result, errors.New("could not connect") +func Config() *config.Config { + if conf == nil { + panic("config is nil") + } + + return conf }