CLI: Update and display the list of supported file formats #2247

This commit is contained in:
Michael Mayer 2022-04-12 19:14:21 +02:00
parent 68ba289d6c
commit e42b870c09
21 changed files with 485 additions and 256 deletions

View file

@ -56,31 +56,7 @@ func main() {
app.Copyright = appCopyright
app.EnableBashCompletion = true
app.Flags = config.GlobalFlags
app.Commands = []cli.Command{
commands.StartCommand,
commands.StopCommand,
commands.StatusCommand,
commands.IndexCommand,
commands.ImportCommand,
commands.CopyCommand,
commands.FacesCommand,
commands.PlacesCommand,
commands.PurgeCommand,
commands.CleanUpCommand,
commands.OptimizeCommand,
commands.MomentsCommand,
commands.ConvertCommand,
commands.ThumbsCommand,
commands.MigrationsCommand,
commands.BackupCommand,
commands.RestoreCommand,
commands.ResetCommand,
commands.PasswdCommand,
commands.UsersCommand,
commands.ShowCommand,
commands.VersionCommand,
}
app.Commands = commands.PhotoPrism
if err := app.Run(os.Args); err != nil {
log.Error(err)

View file

@ -151,10 +151,15 @@ export default class Util {
return "Advanced Video Coding (AVC) / H.264";
case "hvc1":
return "High Efficiency Video Coding (HEVC) / H.265";
case "av01":
return "AOMedia Video 1 (AV1)";
case "mpeg":
return "Moving Picture Experts Group (MPEG)";
case "mjpg":
return "Motion JPEG (MJPEG)";
return "Motion JPEG (M-JPEG)";
case "heif":
case "heic":
return "High Efficiency Image File Format (HEIC)";
return "High Efficiency Image File Format (HEIF)";
case "1":
return "Uncompressed";
case "2":

3
go.mod
View file

@ -80,8 +80,11 @@ require (
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mandykoh/go-parallel v0.1.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect

7
go.sum
View file

@ -233,7 +233,10 @@ github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlW
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -245,6 +248,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
@ -261,6 +266,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=

View file

@ -1,9 +1,28 @@
/*
Package commands contains commands and flags used by the photoprism application.
Additional information concerning the command-line interface can be found in our Developer Guide:
Package commands provides photoprism CLI (sub-)commands.
Copyright (c) 2018 - 2022 Michael Mayer <hello@photoprism.app>
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://photoprism.app/trademark>
Feel free to send an e-mail to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
https://github.com/photoprism/photoprism/wiki/Commands
*/
package commands
@ -11,13 +30,41 @@ import (
"os"
"syscall"
"github.com/sevlyar/go-daemon"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/sevlyar/go-daemon"
)
var log = event.Log
// PhotoPrism contains the photoprism CLI (sub-)commands.
var PhotoPrism = []cli.Command{
StartCommand,
StopCommand,
StatusCommand,
IndexCommand,
ImportCommand,
CopyCommand,
FacesCommand,
PlacesCommand,
PurgeCommand,
CleanUpCommand,
OptimizeCommand,
MomentsCommand,
ConvertCommand,
ThumbsCommand,
MigrationsCommand,
BackupCommand,
RestoreCommand,
ResetCommand,
PasswdCommand,
UsersCommand,
ShowCommand,
VersionCommand,
}
// childAlreadyRunning tests if a .pid file at filePath is a running process.
// it returns the pid value and the running status (true or false).
func childAlreadyRunning(filePath string) (pid int, running bool) {

View file

@ -48,7 +48,8 @@ func TestIndexCommand(t *testing.T) {
// Expected index command output.
assert.Contains(t, output, "indexing originals")
assert.Contains(t, output, "classify: loading")
assert.Contains(t, output, "indexed 0 files")
assert.Contains(t, output, "indexed")
assert.Contains(t, output, "files")
} else {
t.Fatal("log output missing")
}

View file

@ -2,174 +2,34 @@ package commands
import (
"fmt"
"strings"
"time"
"unicode/utf8"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/report"
)
var ShowConfigCommand = cli.Command{
Name: "config",
Usage: "Displays global configuration values",
Name: "config",
Usage: "Displays global configuration values",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-wrap, n",
Usage: "disable text-wrapping",
},
},
Action: showConfigAction,
}
// showConfigAction lists configuration options and their values.
func showConfigAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
conf.SetLogLevel(logrus.FatalLevel)
dbDriver := conf.DatabaseDriver()
rows, cols := conf.Table()
fmt.Printf("%-25s VALUE\n", "NAME")
// Flags.
fmt.Printf("%-25s %t\n", "debug", conf.Debug())
fmt.Printf("%-25s %s\n", "log-level", conf.LogLevel())
fmt.Printf("%-25s %t\n", "public", conf.Public())
fmt.Printf("%-25s %s\n", "admin-password", strings.Repeat("*", utf8.RuneCountInString(conf.AdminPassword())))
fmt.Printf("%-25s %t\n", "read-only", conf.ReadOnly())
fmt.Printf("%-25s %t\n", "experimental", conf.Experimental())
// Config.
fmt.Printf("%-25s %s\n", "config-file", conf.ConfigFile())
fmt.Printf("%-25s %s\n", "config-path", conf.ConfigPath())
fmt.Printf("%-25s %s\n", "settings-file", conf.SettingsFile())
// Originals.
fmt.Printf("%-25s %s\n", "originals-path", conf.OriginalsPath())
fmt.Printf("%-25s %d\n", "originals-limit", conf.OriginalsLimit())
fmt.Printf("%-25s %d\n", "resolution-limit", conf.ResolutionLimit())
// Other paths.
fmt.Printf("%-25s %s\n", "storage-path", conf.StoragePath())
fmt.Printf("%-25s %s\n", "sidecar-path", conf.SidecarPath())
fmt.Printf("%-25s %s\n", "cache-path", conf.CachePath())
fmt.Printf("%-25s %s\n", "albums-path", conf.AlbumsPath())
fmt.Printf("%-25s %s\n", "backup-path", conf.BackupPath())
fmt.Printf("%-25s %s\n", "import-path", conf.ImportPath())
fmt.Printf("%-25s %s\n", "assets-path", conf.AssetsPath())
fmt.Printf("%-25s %s\n", "static-path", conf.StaticPath())
fmt.Printf("%-25s %s\n", "build-path", conf.BuildPath())
fmt.Printf("%-25s %s\n", "img-path", conf.ImgPath())
fmt.Printf("%-25s %s\n", "templates-path", conf.TemplatesPath())
fmt.Printf("%-25s %s\n", "temp-path", conf.TempPath())
// Workers.
fmt.Printf("%-25s %d\n", "workers", conf.Workers())
fmt.Printf("%-25s %s\n", "wakeup-interval", conf.WakeupInterval().String())
fmt.Printf("%-25s %d\n", "auto-index", conf.AutoIndex()/time.Second)
fmt.Printf("%-25s %d\n", "auto-import", conf.AutoImport()/time.Second)
// Feature Flags.
fmt.Printf("%-25s %t\n", "disable-backups", conf.DisableBackups())
fmt.Printf("%-25s %t\n", "disable-settings", conf.DisableSettings())
fmt.Printf("%-25s %t\n", "disable-places", conf.DisablePlaces())
fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow())
fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces())
fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification())
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
fmt.Printf("%-25s %t\n", "disable-darktable", conf.DisableDarktable())
fmt.Printf("%-25s %t\n", "disable-rawtherapee", conf.DisableRawtherapee())
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
fmt.Printf("%-25s %t\n", "disable-raw", conf.DisableRaw())
// Format Flags.
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
fmt.Printf("%-25s %t\n", "exif-bruteforce", conf.ExifBruteForce())
// TensorFlow.
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
fmt.Printf("%-25s %t\n", "upload-nsfw", conf.UploadNSFW())
fmt.Printf("%-25s %s\n", "tensorflow-version", conf.TensorFlowVersion())
fmt.Printf("%-25s %s\n", "tensorflow-model-path", conf.TensorFlowModelPath())
// UI Defaults.
fmt.Printf("%-25s %s\n", "default-locale", conf.DefaultLocale())
// Progressive Web App.
fmt.Printf("%-25s %s\n", "app-icon", conf.AppIcon())
fmt.Printf("%-25s %s\n", "app-name", conf.AppName())
fmt.Printf("%-25s %s\n", "app-mode", conf.AppMode())
// Site Infos.
fmt.Printf("%-25s %s\n", "cdn-url", conf.CdnUrl("/"))
fmt.Printf("%-25s %s\n", "site-url", conf.SiteUrl())
fmt.Printf("%-25s %s\n", "site-author", conf.SiteAuthor())
fmt.Printf("%-25s %s\n", "site-title", conf.SiteTitle())
fmt.Printf("%-25s %s\n", "site-caption", conf.SiteCaption())
fmt.Printf("%-25s %s\n", "site-description", conf.SiteDescription())
fmt.Printf("%-25s %s\n", "site-preview", conf.SitePreview())
// Legal info.
fmt.Printf("%-25s %s\n", "imprint", conf.Imprint())
fmt.Printf("%-25s %s\n", "imprint-url", conf.ImprintUrl())
// URIs.
fmt.Printf("%-25s %s\n", "content-uri", conf.ContentUri())
fmt.Printf("%-25s %s\n", "static-uri", conf.StaticUri())
fmt.Printf("%-25s %s\n", "api-uri", conf.ApiUri())
fmt.Printf("%-25s %s\n", "base-uri", conf.BaseUri("/"))
// Web Server.
fmt.Printf("%-25s %s\n", "http-host", conf.HttpHost())
fmt.Printf("%-25s %d\n", "http-port", conf.HttpPort())
fmt.Printf("%-25s %s\n", "http-mode", conf.HttpMode())
// Database.
fmt.Printf("%-25s %s\n", "database-driver", dbDriver)
fmt.Printf("%-25s %s\n", "database-server", conf.DatabaseServer())
fmt.Printf("%-25s %s\n", "database-host", conf.DatabaseHost())
fmt.Printf("%-25s %s\n", "database-port", conf.DatabasePortString())
fmt.Printf("%-25s %s\n", "database-name", conf.DatabaseName())
fmt.Printf("%-25s %s\n", "database-user", conf.DatabaseUser())
fmt.Printf("%-25s %s\n", "database-password", strings.Repeat("*", utf8.RuneCountInString(conf.DatabasePassword())))
fmt.Printf("%-25s %d\n", "database-conns", conf.DatabaseConns())
fmt.Printf("%-25s %d\n", "database-conns-idle", conf.DatabaseConnsIdle())
// External Tools.
fmt.Printf("%-25s %s\n", "darktable-bin", conf.DarktableBin())
fmt.Printf("%-25s %s\n", "darktable-cache-path", conf.DarktableCachePath())
fmt.Printf("%-25s %s\n", "darktable-config-path", conf.DarktableConfigPath())
fmt.Printf("%-25s %s\n", "darktable-blacklist", conf.DarktableBlacklist())
fmt.Printf("%-25s %s\n", "rawtherapee-bin", conf.RawtherapeeBin())
fmt.Printf("%-25s %s\n", "rawtherapee-blacklist", conf.RawtherapeeBlacklist())
fmt.Printf("%-25s %s\n", "sips-bin", conf.SipsBin())
fmt.Printf("%-25s %s\n", "heifconvert-bin", conf.HeifConvertBin())
fmt.Printf("%-25s %s\n", "ffmpeg-bin", conf.FFmpegBin())
fmt.Printf("%-25s %s\n", "ffmpeg-encoder", conf.FFmpegEncoder())
fmt.Printf("%-25s %d\n", "ffmpeg-bitrate", conf.FFmpegBitrate())
fmt.Printf("%-25s %s\n", "exiftool-bin", conf.ExifToolBin())
// Thumbnails.
fmt.Printf("%-25s %s\n", "download-token", conf.DownloadToken())
fmt.Printf("%-25s %s\n", "preview-token", conf.PreviewToken())
fmt.Printf("%-25s %s\n", "thumb-color", conf.ThumbColor())
fmt.Printf("%-25s %s\n", "thumb-filter", conf.ThumbFilter())
fmt.Printf("%-25s %d\n", "thumb-size", conf.ThumbSizePrecached())
fmt.Printf("%-25s %d\n", "thumb-size-uncached", conf.ThumbSizeUncached())
fmt.Printf("%-25s %t\n", "thumb-uncached", conf.ThumbUncached())
fmt.Printf("%-25s %s\n", "thumb-path", conf.ThumbPath())
fmt.Printf("%-25s %d\n", "jpeg-quality", conf.JpegQuality())
fmt.Printf("%-25s %d\n", "jpeg-size", conf.JpegSize())
// Facial Recognition.
fmt.Printf("%-25s %d\n", "face-size", conf.FaceSize())
fmt.Printf("%-25s %f\n", "face-score", conf.FaceScore())
fmt.Printf("%-25s %d\n", "face-overlap", conf.FaceOverlap())
fmt.Printf("%-25s %d\n", "face-cluster-size", conf.FaceClusterSize())
fmt.Printf("%-25s %d\n", "face-cluster-score", conf.FaceClusterScore())
fmt.Printf("%-25s %d\n", "face-cluster-core", conf.FaceClusterCore())
fmt.Printf("%-25s %f\n", "face-cluster-dist", conf.FaceClusterDist())
fmt.Printf("%-25s %f\n", "face-match-dist", conf.FaceMatchDist())
// Daemon Mode.
fmt.Printf("%-25s %s\n", "pid-filename", conf.PIDFilename())
fmt.Printf("%-25s %s\n", "log-filename", conf.LogFilename())
fmt.Println(report.Markdown(rows, cols, !ctx.Bool("no-wrap")))
return nil
}

View file

@ -23,7 +23,6 @@ func TestConfigCommand(t *testing.T) {
}
// Expected config command output.
assert.Contains(t, output, "NAME VALUE")
assert.Contains(t, output, "config-file")
assert.Contains(t, output, "darktable-cli")
assert.Contains(t, output, "originals-path")

View file

@ -3,19 +3,33 @@ package commands
import (
"fmt"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/report"
)
var ShowFormatsCommand = cli.Command{
Name: "formats",
Usage: "Displays supported media and sidecar file formats",
Name: "formats",
Usage: "Displays supported media and sidecar file formats",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "compact, c",
Usage: "hide format descriptions to make the output more compact",
},
cli.BoolFlag{
Name: "no-wrap, n",
Usage: "disable text-wrapping so the output can be pasted into Markdown files",
},
},
Action: showFormatsAction,
}
// showFormatsAction lists supported media and sidecar file formats.
func showFormatsAction(ctx *cli.Context) error {
formats := fs.Extensions.Formats(true).Markdown()
fmt.Println(formats)
rows, cols := fs.Extensions.Formats(true).Table(!ctx.Bool("compact"), true, true)
fmt.Println(report.Markdown(rows, cols, !ctx.Bool("no-wrap")))
return nil
}

View file

@ -37,6 +37,7 @@ type ClientConfig struct {
AppMode string `json:"appMode"`
AppIcon string `json:"appIcon"`
Debug bool `json:"debug"`
Trace bool `json:"trace"`
Test bool `json:"test"`
Demo bool `json:"demo"`
Sponsor bool `json:"sponsor"`
@ -227,6 +228,7 @@ func (c *Config) PublicConfig() ClientConfig {
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
Trace: c.Trace(),
Test: c.Test(),
Demo: c.Demo(),
Sponsor: c.Sponsor(),
@ -299,6 +301,7 @@ func (c *Config) GuestConfig() ClientConfig {
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
Trace: c.Trace(),
Test: c.Test(),
Demo: c.Demo(),
Sponsor: c.Sponsor(),
@ -365,6 +368,7 @@ func (c *Config) UserConfig() ClientConfig {
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
Trace: c.Trace(),
Test: c.Test(),
Demo: c.Demo(),
Sponsor: c.Sponsor(),

View file

@ -445,11 +445,19 @@ func (c *Config) ImprintUrl() string {
return c.options.ImprintUrl
}
// Debug checks if debug mode is enabled.
// Debug checks if debug mode is enabled, shows non-essential log messages.
func (c *Config) Debug() bool {
if c.Trace() {
return true
}
return c.options.Debug
}
// Trace checks if trace mode is enabled, shows all log messages.
func (c *Config) Trace() bool {
return c.options.Trace
}
// Test checks if test mode is enabled.
func (c *Config) Test() bool {
return c.options.Test
@ -467,7 +475,9 @@ func (c *Config) Sponsor() bool {
// Public checks if app runs in public mode and requires no authentication.
func (c *Config) Public() bool {
if c.Demo() {
if c.Auth() {
return false
} else if c.Demo() {
return true
}
@ -506,12 +516,19 @@ func (c *Config) AdminPassword() string {
return c.options.AdminPassword
}
// Auth checks if authentication is always required.
func (c *Config) Auth() bool {
return c.options.Auth
}
// LogLevel returns the Logrus log level.
func (c *Config) LogLevel() logrus.Level {
// Normalize string.
c.options.LogLevel = strings.ToLower(strings.TrimSpace(c.options.LogLevel))
if c.Debug() && c.options.LogLevel != logrus.TraceLevel.String() {
if c.Trace() {
c.options.LogLevel = logrus.TraceLevel.String()
} else if c.Debug() && c.options.LogLevel != logrus.TraceLevel.String() {
c.options.LogLevel = logrus.DebugLevel.String()
}
@ -522,6 +539,11 @@ func (c *Config) LogLevel() logrus.Level {
}
}
// SetLogLevel sets the Logrus log level.
func (c *Config) SetLogLevel(level logrus.Level) {
log.SetLevel(level)
}
// Shutdown services and workers.
func (c *Config) Shutdown() {
mutex.People.Cancel()

View file

@ -22,14 +22,16 @@ type Options struct {
Version string `json:"-"`
Copyright string `json:"-"`
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
Debug bool `yaml:"Debug" json:"Debug" flag:"debug"`
Trace bool `yaml:"Trace" json:"Trace" flag:"Trace"`
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
Auth bool `yaml:"Auth" json:"-" flag:"auth"`
Public bool `yaml:"Public" json:"-" flag:"public"`
Test bool `yaml:"-" json:"Test,omitempty" flag:"test"`
Unsafe bool `yaml:"-" json:"-" flag:"unsafe"`
Demo bool `yaml:"Demo" json:"-" flag:"demo"`
Sponsor bool `yaml:"-" json:"-" flag:"sponsor"`
Public bool `yaml:"Public" json:"-" flag:"public"`
ReadOnly bool `yaml:"ReadOnly" json:"ReadOnly" flag:"read-only"`
Experimental bool `yaml:"Experimental" json:"Experimental" flag:"experimental"`
ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"`

186
internal/config/table.go Normal file
View file

@ -0,0 +1,186 @@
package config
import (
"bytes"
"fmt"
"strings"
"time"
"unicode/utf8"
"github.com/olekukonko/tablewriter"
)
// Table returns global config values as a table for reporting.
func (c *Config) Table() (rows [][]string, cols []string) {
cols = []string{"Value", "Name"}
rows = [][]string{
{"log-level", c.LogLevel().String()},
{"debug", fmt.Sprintf("%t", c.Debug())},
{"trace", fmt.Sprintf("%t", c.Trace())},
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
{"auth", fmt.Sprintf("%t", c.Auth())},
{"public", fmt.Sprintf("%t", c.Public())},
{"read-only", fmt.Sprintf("%t", c.ReadOnly())},
{"experimental", fmt.Sprintf("%t", c.Experimental())},
// Config.
{"config-file", c.ConfigFile()},
{"config-path", c.ConfigPath()},
{"settings-file", c.SettingsFile()},
// Originals.
{"originals-path", c.OriginalsPath()},
{"originals-limit", fmt.Sprintf("%d", c.OriginalsLimit())},
{"resolution-limit", fmt.Sprintf("%d", c.ResolutionLimit())},
// Other paths.
{"storage-path", c.StoragePath()},
{"sidecar-path", c.SidecarPath()},
{"cache-path", c.CachePath()},
{"albums-path", c.AlbumsPath()},
{"backup-path", c.BackupPath()},
{"import-path", c.ImportPath()},
{"assets-path", c.AssetsPath()},
{"static-path", c.StaticPath()},
{"build-path", c.BuildPath()},
{"img-path", c.ImgPath()},
{"templates-path", c.TemplatesPath()},
{"temp-path", c.TempPath()},
// Workers.
{"workers", fmt.Sprintf("%d", c.Workers())},
{"wakeup-interval", c.WakeupInterval().String()},
{"auto-index", fmt.Sprintf("%d", c.AutoIndex()/time.Second)},
{"auto-import", fmt.Sprintf("%d", c.AutoImport()/time.Second)},
// Feature Flags.
{"disable-backups", fmt.Sprintf("%t", c.DisableBackups())},
{"disable-settings", fmt.Sprintf("%t", c.DisableSettings())},
{"disable-places", fmt.Sprintf("%t", c.DisablePlaces())},
{"disable-tensorflow", fmt.Sprintf("%t", c.DisableTensorFlow())},
{"disable-faces", fmt.Sprintf("%t", c.DisableFaces())},
{"disable-classification", fmt.Sprintf("%t", c.DisableClassification())},
{"disable-ffmpeg", fmt.Sprintf("%t", c.DisableFFmpeg())},
{"disable-exiftool", fmt.Sprintf("%t", c.DisableExifTool())},
{"disable-heifconvert", fmt.Sprintf("%t", c.DisableHeifConvert())},
{"disable-darktable", fmt.Sprintf("%t", c.DisableDarktable())},
{"disable-rawtherapee", fmt.Sprintf("%t", c.DisableRawtherapee())},
{"disable-sips", fmt.Sprintf("%t", c.DisableSips())},
{"disable-raw", fmt.Sprintf("%t", c.DisableRaw())},
// Format Flags.
{"raw-presets", fmt.Sprintf("%t", c.RawPresets())},
{"exif-bruteforce", fmt.Sprintf("%t", c.ExifBruteForce())},
// TensorFlow.
{"detect-nsfw", fmt.Sprintf("%t", c.DetectNSFW())},
{"upload-nsfw", fmt.Sprintf("%t", c.UploadNSFW())},
{"tensorflow-version", c.TensorFlowVersion()},
{"tensorflow-model-path", c.TensorFlowModelPath()},
// UI Defaults.
{"default-locale", c.DefaultLocale()},
// Progressive Web App.
{"app-icon", c.AppIcon()},
{"app-name", c.AppName()},
{"app-mode", c.AppMode()},
// Site Infos.
{"cdn-url", c.CdnUrl("/")},
{"site-url", c.SiteUrl()},
{"site-author", c.SiteAuthor()},
{"site-title", c.SiteTitle()},
{"site-caption", c.SiteCaption()},
{"site-description", c.SiteDescription()},
{"site-preview", c.SitePreview()},
// Legal info.
{"imprint", c.Imprint()},
{"imprint-url", c.ImprintUrl()},
// URIs.
{"content-uri", c.ContentUri()},
{"static-uri", c.StaticUri()},
{"api-uri", c.ApiUri()},
{"base-uri", c.BaseUri("/")},
// Web Server.
{"http-host", c.HttpHost()},
{"http-port", fmt.Sprintf("%d", c.HttpPort())},
{"http-mode", c.HttpMode()},
// Database.
{"database-driver", c.DatabaseDriver()},
{"database-server", c.DatabaseServer()},
{"database-host", c.DatabaseHost()},
{"database-port", c.DatabasePortString()},
{"database-name", c.DatabaseName()},
{"database-user", c.DatabaseUser()},
{"database-password", strings.Repeat("*", utf8.RuneCountInString(c.DatabasePassword()))},
{"database-conns", fmt.Sprintf("%d", c.DatabaseConns())},
{"database-conns-idle", fmt.Sprintf("%d", c.DatabaseConnsIdle())},
// External Tools.
{"darktable-bin", c.DarktableBin()},
{"darktable-cache-path", c.DarktableCachePath()},
{"darktable-config-path", c.DarktableConfigPath()},
{"darktable-blacklist", c.DarktableBlacklist()},
{"rawtherapee-bin", c.RawtherapeeBin()},
{"rawtherapee-blacklist", c.RawtherapeeBlacklist()},
{"sips-bin", c.SipsBin()},
{"heifconvert-bin", c.HeifConvertBin()},
{"ffmpeg-bin", c.FFmpegBin()},
{"ffmpeg-encoder", c.FFmpegEncoder().String()},
{"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())},
{"exiftool-bin", c.ExifToolBin()},
// Thumbnails.
{"download-token", c.DownloadToken()},
{"preview-token", c.PreviewToken()},
{"thumb-color", c.ThumbColor()},
{"thumb-filter", string(c.ThumbFilter())},
{"thumb-size", fmt.Sprintf("%d", c.ThumbSizePrecached())},
{"thumb-size-uncached", fmt.Sprintf("%d", c.ThumbSizeUncached())},
{"thumb-uncached", fmt.Sprintf("%t", c.ThumbUncached())},
{"thumb-path", c.ThumbPath()},
{"jpeg-quality", fmt.Sprintf("%d", c.JpegQuality())},
{"jpeg-size", fmt.Sprintf("%d", c.JpegSize())},
// Facial Recognition.
{"face-size", fmt.Sprintf("%d", c.FaceSize())},
{"face-score", fmt.Sprintf("%f", c.FaceScore())},
{"face-overlap", fmt.Sprintf("%d", c.FaceOverlap())},
{"face-cluster-size", fmt.Sprintf("%d", c.FaceClusterSize())},
{"face-cluster-score", fmt.Sprintf("%d", c.FaceClusterScore())},
{"face-cluster-core", fmt.Sprintf("%d", c.FaceClusterCore())},
{"face-cluster-dist", fmt.Sprintf("%f", c.FaceClusterDist())},
{"face-match-dist", fmt.Sprintf("%f", c.FaceMatchDist())},
// Daemon Mode.
{"pid-filename", c.PIDFilename()},
{"log-filename", c.LogFilename()},
}
return rows, cols
}
// MarkdownTable returns global config values as a markdown formatted table.
func (c *Config) MarkdownTable(autoWrap bool) string {
buf := &bytes.Buffer{}
rows, cols := c.Table()
table := tablewriter.NewWriter(buf)
table.SetAutoWrapText(autoWrap)
table.SetAutoFormatHeaders(false)
table.SetHeader(cols)
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.AppendBulk(rows)
table.Render()
return buf.String()
}

View file

@ -78,9 +78,11 @@ func NewTestOptions(pkg string) *Options {
Name: "PhotoPrism",
Version: "0.0.0",
Copyright: "(c) 2018-2022 Michael Mayer",
Public: true,
Auth: false,
Test: true,
Debug: true,
Public: true,
Trace: false,
Experimental: true,
ReadOnly: false,
DetectNSFW: true,

27
internal/event/event.go Normal file
View file

@ -0,0 +1,27 @@
/*
Package event provides a publish-subscribe event hub and a global logger.
Copyright (c) 2018 - 2022 Michael Mayer <hello@photoprism.app>
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://photoprism.app/trademark>
Feel free to send an e-mail to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package event

View file

@ -72,8 +72,10 @@ var Extensions = FileExtensions{
".avi": FormatAvi,
".av1": FormatAV1,
".avc": FormatAVC,
".mpg": FormatMpg,
".mpeg": FormatMpg,
".mpg": FormatMPEG,
".mpeg": FormatMPEG,
".mjpg": FormatMJPEG,
".mjpeg": FormatMJPEG,
".mp2": FormatMp2,
".mpv": FormatMp2,
".mp": FormatMp4,

View file

@ -1,7 +1,6 @@
package fs
import (
"fmt"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
@ -35,6 +34,8 @@ const (
FormatHEVC Format = "hevc" // H.265, High Efficiency Video Coding (HEVC)
FormatAVC Format = "avc" // H.264, Advanced Video Coding (AVC), MPEG-4 Part 10, used internally
FormatAV1 Format = "av1" // Alliance for Open Media Video
FormatMPEG Format = "mpg" // Moving Picture Experts Group (MPEG)
FormatMJPEG Format = "mjpg" // Motion JPEG (M-JPEG)
FormatMov Format = "mov" // QuickTime File Format, can contain AVC, HEVC,...
FormatMp2 Format = "mp2" // MPEG-2, H.222/H.262
FormatMp4 Format = "mp4" // MPEG-4 Container based on QuickTime, can contain AVC, HEVC,...
@ -43,7 +44,6 @@ const (
Format3g2 Format = "3g2" // Similar to 3GP, consumes less space & bandwidth
FormatFlv Format = "flv" // Flash Video
FormatMkv Format = "mkv" // Matroska Multimedia Container, free and open
FormatMpg Format = "mpg" // Moving Picture Experts Group (MPEG)
FormatMts Format = "mts" // AVCHD (Advanced Video Coding High Definition)
FormatOgv Format = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
FormatWMV Format = "wmv" // Windows Media Video
@ -60,55 +60,70 @@ const (
// FormatDesc contains human-readable descriptions for supported file formats
var FormatDesc = map[Format]string{
FormatJpeg: "JPEG (Joint Photographic Experts Group)",
FormatJpeg: "Joint Photographic Experts Group (JPEG)",
FormatPng: "Portable Network Graphics",
FormatGif: "Graphics Interchange Format",
FormatTiff: "Tag Image File Format",
FormatBitmap: "Bitmap",
FormatRaw: "Unprocessed Image Data",
FormatMpo: "Stereoscopic 3D Format based on JPEG",
FormatHEIF: "High Efficiency Image File Format",
FormatRaw: "Unprocessed Sensor Data",
FormatMpo: "Stereoscopic (3D JPEG)",
FormatHEIF: "High Efficiency Image File Format (HEIF)",
FormatWebP: "Google WebP",
FormatWebM: "Google WebM",
FormatHEVC: "H.265, High Efficiency Video Coding",
FormatAVC: "H.264, Advanced Video Coding, MPEG-4 Part 10",
FormatAV1: "Alliance for Open Media",
FormatMov: "QuickTime File Format",
FormatMp2: "MPEG-2, H.222, H.262",
FormatMp4: "MPEG-4 Part 14 Multimedia Container",
FormatHEVC: "High Efficiency Video Coding (HEVC, HVC1, H.265)",
FormatAVC: "Advanced Video Coding (AVC, AVC1, H.264, MPEG-4 Part 10)",
FormatAV1: "AOMedia Video 1 (AV1, AV01)",
FormatMov: "Apple QuickTime (QT)",
FormatMp2: "MPEG 2 (H.262, H.222)",
FormatMp4: "Multimedia Container (MPEG-4 Part 14)",
FormatAvi: "Microsoft Audio Video Interleave",
Format3gp: "MPEG-4 Part 12, Mobile Multimedia Container",
Format3g2: "Multimedia Container for 3G CDMA2000, based on 3GP",
FormatWMV: "Microsoft Windows Media",
Format3gp: "Mobile Multimedia Container (MPEG-4 Part 12)",
Format3g2: "Mobile Multimedia Container for CDMA2000 (based on 3GP)",
FormatFlv: "Flash Video",
FormatMkv: "Matroska Multimedia Container",
FormatMpg: "MPEG (Moving Picture Experts Group)",
FormatMts: "AVCHD (Advanced Video Coding High Definition)",
FormatMkv: "Matroska Multimedia Container (MKV, MCF, EBML)",
FormatMPEG: "Moving Picture Experts Group (MPEG)",
FormatMJPEG: "Motion JPEG (M-JPEG)",
FormatMts: "Advanced Video Coding High Definition (AVCHD)",
FormatOgv: "Ogg Media by Xiph.Org",
FormatWMV: "Windows Media",
FormatXMP: "Adobe Extensible Metadata Platform",
FormatAAE: "Apple Image Edits",
FormatXML: "Extensible Markup Language",
FormatJson: "Serialized JSON Data (Exiftool, Google Photos)",
FormatYaml: "Serialized YAML Data (Metadata, Config Values)",
FormatYaml: "Serialized YAML Data (Config, Metadata)",
FormatToml: "Serialized TOML Data (Tom's Obvious, Minimal Language)",
FormatText: "Plain Text",
FormatMarkdown: "Markdown Formatted Text",
FormatOther: "Other",
}
// Markdown returns a file format table in markdown text format.
func (m FileFormats) Markdown() string {
// Table returns a file format documentation table.
func (m FileFormats) Table(withDesc, withType, withExt bool) (rows [][]string, cols []string) {
cols = make([]string, 0, 4)
cols = append(cols, "Format")
results := make([][]string, 0, len(m))
t := 0
max := func(x, y int) int {
if x < y {
return y
if withDesc {
cols = append(cols, "Description")
}
if withType {
if withDesc {
t = 2
} else {
t = 1
}
return x
cols = append(cols, "Type")
}
if withExt {
cols = append(cols, "Extensions")
}
rows = make([][]string, 0, len(m))
ucFirst := func(str string) string {
for i, v := range str {
return string(unicode.ToUpper(v)) + str[i+1:]
@ -116,40 +131,36 @@ func (m FileFormats) Markdown() string {
return ""
}
l0, l1, l2, l3 := 12, 12, 12, 12
for f, ext := range m {
sort.Slice(ext, func(i, j int) bool {
return ext[i] < ext[j]
})
v := make([]string, 4)
v[0] = strings.ToUpper(f.String())
v[1] = FormatDesc[f]
v[2] = ucFirst(string(MediaTypes[f]))
v[3] = strings.Join(ext, ", ")
l0, l1, l2, l3 = max(l0, len(v[0])), max(l1, len(v[1])), max(l2, len(v[2])), max(l3, len(v[3]))
results = append(results, v)
v := make([]string, 0, 4)
v = append(v, strings.ToUpper(f.String()))
if withDesc {
v = append(v, FormatDesc[f])
}
if withType {
v = append(v, ucFirst(string(MediaTypes[f])))
}
if withExt {
v = append(v, strings.Join(ext, ", "))
}
rows = append(rows, v)
}
sort.Slice(results, func(i, j int) bool {
if results[i][2] == results[j][2] {
return results[i][0] < results[j][0]
sort.Slice(rows, func(i, j int) bool {
if t > 0 && rows[i][t] == rows[j][t] {
return rows[i][0] < rows[j][0]
} else {
return results[i][2] < results[j][2]
return rows[i][t] < rows[j][t]
}
})
rows := make([]string, len(results)+2)
cols := fmt.Sprintf("| %%-%ds | %%-%ds | %%-%ds | %%-%ds |\n", l0, l1, l2, l3)
rows = append(rows, fmt.Sprintf(cols, "Format", "Description", "Type", "Extensions"))
rows = append(rows, fmt.Sprintf("|:%s-|:%s-|:%s-|:%s-|\n", strings.Repeat("-", l0), strings.Repeat("-", l1), strings.Repeat("-", l2), strings.Repeat("-", l3)))
for _, r := range results {
rows = append(rows, fmt.Sprintf(cols, r[0], r[1], r[2], r[3]))
}
return strings.Join(rows, "")
return rows, cols
}

View file

@ -7,11 +7,20 @@ import (
)
func TestFileFormats_Markdown(t *testing.T) {
t.Run("Render", func(t *testing.T) {
t.Run("All", func(t *testing.T) {
f := Extensions.Formats(true)
result := f.Markdown()
// fmt.Print(result)
assert.NotEmpty(t, result)
rows, cols := f.Table(true, true, true)
assert.NotEmpty(t, rows)
assert.NotEmpty(t, cols)
assert.Len(t, cols, 4)
assert.GreaterOrEqual(t, len(rows), 30)
})
t.Run("Compact", func(t *testing.T) {
f := Extensions.Formats(true)
rows, cols := f.Table(false, false, false)
assert.NotEmpty(t, rows)
assert.NotEmpty(t, cols)
assert.Len(t, cols, 1)
assert.GreaterOrEqual(t, len(rows), 30)
})
}

View file

@ -28,7 +28,8 @@ var MediaTypes = map[Format]MediaType{
FormatAvi: MediaVideo,
FormatAVC: MediaVideo,
FormatAV1: MediaVideo,
FormatMpg: MediaVideo,
FormatMPEG: MediaVideo,
FormatMJPEG: MediaVideo,
FormatMp2: MediaVideo,
FormatMp4: MediaVideo,
FormatMkv: MediaVideo,

24
pkg/report/markdown.go Normal file
View file

@ -0,0 +1,24 @@
package report
import (
"bytes"
"github.com/olekukonko/tablewriter"
)
// Markdown returns markdown formatted table.
func Markdown(rows [][]string, cols []string, autoWrap bool) string {
buf := &bytes.Buffer{}
table := tablewriter.NewWriter(buf)
table.SetAutoWrapText(autoWrap)
table.SetAutoFormatHeaders(false)
table.SetHeader(cols)
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.AppendBulk(rows)
table.Render()
return buf.String()
}

27
pkg/report/report.go Normal file
View file

@ -0,0 +1,27 @@
/*
Package report provides rendering of report results, for example as Markdown.
Copyright (c) 2018 - 2022 Michael Mayer <hello@photoprism.app>
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://photoprism.app/trademark>
Feel free to send an e-mail to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package report