Improve UX and title generation from file names
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
cf773b5714
commit
bfd73932e5
24 changed files with 228 additions and 74 deletions
32
frontend/package-lock.json
generated
32
frontend/package-lock.json
generated
|
@ -2252,9 +2252,9 @@
|
|||
}
|
||||
},
|
||||
"@types/webpack": {
|
||||
"version": "4.41.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz",
|
||||
"integrity": "sha512-6FfeCidTSHozwKI67gIVQQ5Mp0g4X96c2IXxX75hYEQJwST/i6NyZexP//zzMOBb+wG9jJ7oO8fk9yObP2HWAw==",
|
||||
"version": "4.41.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.18.tgz",
|
||||
"integrity": "sha512-mQm2R8vV2BZE/qIDVYqmBVLfX73a8muwjs74SpjEyJWJxeXBbsI9L65Pcia9XfYLYWzD1c1V8m+L0p30y2N7MA==",
|
||||
"requires": {
|
||||
"@types/anymatch": "*",
|
||||
"@types/node": "*",
|
||||
|
@ -3627,12 +3627,12 @@
|
|||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.1.tgz",
|
||||
"integrity": "sha512-WMjXwFtPskSW1pQUDJRxvRKRkeCr7usN0O/Za76N+F4oadaTdQHotSGcX9jT/Hs7mSKPkyMFNvqawB/1HzYDKQ==",
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.2.tgz",
|
||||
"integrity": "sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==",
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001088",
|
||||
"electron-to-chromium": "^1.3.481",
|
||||
"electron-to-chromium": "^1.3.483",
|
||||
"escalade": "^3.0.1",
|
||||
"node-releases": "^1.1.58"
|
||||
}
|
||||
|
@ -3782,9 +3782,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001088",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001088.tgz",
|
||||
"integrity": "sha512-6eYUrlShRYveyqKG58HcyOfPgh3zb2xqs7NvT2VVtP3hEUeeWvc3lqhpeMTxYWBBeeaT9A4bKsrtjATm66BTHg=="
|
||||
"version": "1.0.30001090",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001090.tgz",
|
||||
"integrity": "sha512-QzPRKDCyp7RhjczTPZaqK3CjPA5Ht2UnXhZhCI4f7QiB5JK6KEuZBxIzyWnB3wO4hgAj4GMRxAhuiacfw0Psjg=="
|
||||
},
|
||||
"center-align": {
|
||||
"version": "0.1.3",
|
||||
|
@ -5896,9 +5896,9 @@
|
|||
}
|
||||
},
|
||||
"eslint-plugin-import": {
|
||||
"version": "2.21.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz",
|
||||
"integrity": "sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA==",
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz",
|
||||
"integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==",
|
||||
"requires": {
|
||||
"array-includes": "^3.1.1",
|
||||
"array.prototype.flat": "^1.2.3",
|
||||
|
@ -9515,9 +9515,9 @@
|
|||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"nise": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz",
|
||||
"integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz",
|
||||
"integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==",
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.7.0",
|
||||
"@sinonjs/fake-timers": "^6.0.0",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-istanbul": "^6.0.0",
|
||||
"browserslist": "^4.12.1",
|
||||
"browserslist": "^4.12.2",
|
||||
"chai": "^4.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"chart.js": "^2.9.3",
|
||||
|
@ -54,7 +54,7 @@
|
|||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^3.0.4",
|
||||
"eslint-plugin-html": "^6.0.2",
|
||||
"eslint-plugin-import": "^2.21.2",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/review" @click="" v-if="$config.feature('review') && config.count.review > 0"
|
||||
<v-list-tile to="/review" @click="" v-if="$config.feature('review')"
|
||||
class="p-navigation-review">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
|
|
|
@ -230,6 +230,10 @@
|
|||
if (this.results.length > 1) {
|
||||
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' entries loaded'));
|
||||
}
|
||||
} else if (this.results.length >= Photo.limit()) {
|
||||
this.offset = offset;
|
||||
this.scrollDisabled = true;
|
||||
this.$notify.warn(this.$gettext("Can't load more, limit reached"));
|
||||
} else {
|
||||
this.offset = offset + count;
|
||||
this.page++;
|
||||
|
|
|
@ -163,7 +163,7 @@
|
|||
dirty: false,
|
||||
results: [],
|
||||
loading: true,
|
||||
filesLimit: 500,
|
||||
filesLimit: 1111,
|
||||
filesOffset: 0,
|
||||
page: 0,
|
||||
selection: [],
|
||||
|
|
|
@ -271,6 +271,10 @@
|
|||
if (this.results.length > 1) {
|
||||
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' photos loaded'));
|
||||
}
|
||||
} else if (this.results.length >= Photo.limit()) {
|
||||
this.offset = offset;
|
||||
this.scrollDisabled = true;
|
||||
this.$notify.warn(this.$gettext("Can't load more, limit reached"));
|
||||
} else {
|
||||
this.offset = offset + count;
|
||||
this.page++;
|
||||
|
|
|
@ -273,6 +273,10 @@
|
|||
if (this.results.length > 1) {
|
||||
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' entries loaded'));
|
||||
}
|
||||
} else if (this.results.length >= Photo.limit()) {
|
||||
this.offset = offset;
|
||||
this.scrollDisabled = true;
|
||||
this.$notify.warn(this.$gettext("Can't load more, limit reached"));
|
||||
} else {
|
||||
this.offset = offset + count;
|
||||
this.page++;
|
||||
|
|
2
go.mod
2
go.mod
|
@ -45,7 +45,7 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/image v0.0.0-20200618115811-c13761719519 // indirect
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
||||
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/ugjka/go-tz.v2 v2.0.9
|
||||
|
|
2
go.sum
2
go.sum
|
@ -233,6 +233,8 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
|
||||
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
|
|
|
@ -48,35 +48,31 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
|
|||
break
|
||||
}
|
||||
|
||||
log.Debugf("websocket: received %d bytes", len(m))
|
||||
|
||||
var info clientInfo
|
||||
|
||||
if err := json.Unmarshal(m, &info); err != nil {
|
||||
log.Error(err)
|
||||
// Do nothing.
|
||||
} else {
|
||||
if sess := Session(info.SessionToken); sess.Valid() {
|
||||
log.Debug("websocket: authenticated")
|
||||
|
||||
wsAuth.mutex.Lock()
|
||||
wsAuth.user[connId] = sess.User
|
||||
wsAuth.mutex.Unlock()
|
||||
|
||||
var clientConfig config.ClientConfig
|
||||
|
||||
if sess.User.Guest() {
|
||||
clientConfig = conf.GuestConfig()
|
||||
} else if sess.User.Registered() {
|
||||
clientConfig = conf.UserConfig()
|
||||
} else {
|
||||
clientConfig = conf.PublicConfig()
|
||||
}
|
||||
|
||||
writeMutex.Lock()
|
||||
ws.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
if sess.User.Guest() {
|
||||
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.GuestConfig()}}); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else if sess.User.Registered() {
|
||||
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.UserConfig()}}); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
} else {
|
||||
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.PublicConfig()}}); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": clientConfig}}); err != nil {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
writeMutex.Unlock()
|
||||
|
@ -117,13 +113,24 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
|||
for {
|
||||
select {
|
||||
case <-pingTicker.C:
|
||||
writeMutex.Lock()
|
||||
ws.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
writeMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
writeMutex.Unlock()
|
||||
case msg := <-s.Receiver:
|
||||
wsAuth.mutex.RLock()
|
||||
user := wsAuth.user[connId]
|
||||
|
||||
user := entity.UnknownPerson
|
||||
|
||||
if hit, ok := wsAuth.user[connId]; ok {
|
||||
user = hit
|
||||
}
|
||||
|
||||
wsAuth.mutex.RUnlock()
|
||||
|
||||
if user.Registered() {
|
||||
|
@ -132,9 +139,9 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
|||
|
||||
if err := ws.WriteJSON(gin.H{"event": msg.Name, "data": msg.Fields}); err != nil {
|
||||
writeMutex.Unlock()
|
||||
log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
writeMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
@ -144,14 +151,12 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
|
|||
// GET /api/v1/ws
|
||||
func Websocket(router *gin.RouterGroup) {
|
||||
if router == nil {
|
||||
log.Error("websocket: router is nil")
|
||||
return
|
||||
}
|
||||
|
||||
conf := service.Config()
|
||||
|
||||
if conf == nil {
|
||||
log.Error("websocket: conf is nil")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -160,8 +165,8 @@ func Websocket(router *gin.RouterGroup) {
|
|||
r := c.Request
|
||||
|
||||
ws, err := wsConnection.Upgrade(w, r, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -180,8 +185,6 @@ func Websocket(router *gin.RouterGroup) {
|
|||
}
|
||||
wsAuth.mutex.Unlock()
|
||||
|
||||
log.Debug("websocket: connected")
|
||||
|
||||
go wsWriter(ws, &writeMutex, connId)
|
||||
|
||||
wsReader(ws, &writeMutex, connId, conf)
|
||||
|
|
|
@ -116,7 +116,7 @@ func (c *Config) PublicConfig() ClientConfig {
|
|||
return c.UserConfig()
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), "config: public config created"))
|
||||
defer log.Debug(capture.Time(time.Now(), "client config created (public)"))
|
||||
|
||||
settings := c.Settings()
|
||||
|
||||
|
@ -155,7 +155,7 @@ func (c *Config) PublicConfig() ClientConfig {
|
|||
|
||||
// GuestConfig returns client config values for the sharing with guests.
|
||||
func (c *Config) GuestConfig() ClientConfig {
|
||||
defer log.Debug(capture.Time(time.Now(), "config: guest config created"))
|
||||
defer log.Debug(capture.Time(time.Now(), "client config created (guest)"))
|
||||
|
||||
settings := c.Settings()
|
||||
|
||||
|
@ -196,7 +196,7 @@ func (c *Config) GuestConfig() ClientConfig {
|
|||
|
||||
// UserConfig returns client configuration values for registered users.
|
||||
func (c *Config) UserConfig() ClientConfig {
|
||||
defer log.Debug(capture.Time(time.Now(), "config: user config created"))
|
||||
defer log.Debug(capture.Time(time.Now(), "client config created (user)"))
|
||||
|
||||
result := ClientConfig{
|
||||
Settings: *c.Settings(),
|
||||
|
|
|
@ -535,13 +535,13 @@ func (m *Photo) DetailsLoaded() bool {
|
|||
|
||||
// TitleFromFileName returns a photo title based on the file name and/or path.
|
||||
func (m *Photo) TitleFromFileName() string {
|
||||
if fs.NonCanonical(m.PhotoName) {
|
||||
if !fs.IsID(m.PhotoName) {
|
||||
if title := txt.TitleFromFileName(m.PhotoName); title != "" {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
if m.OriginalName != "" && fs.NonCanonical(m.OriginalName) {
|
||||
if m.OriginalName != "" && !fs.IsID(m.OriginalName) {
|
||||
if title := txt.TitleFromFileName(m.OriginalName); title != "" {
|
||||
return title
|
||||
} else if title := txt.TitleFromFileName(path.Dir(m.OriginalName)); title != "" {
|
||||
|
|
|
@ -24,7 +24,7 @@ func (m *Photo) EstimateCountry() {
|
|||
countryCode = code
|
||||
}
|
||||
|
||||
if countryCode == unknown && fs.NonCanonical(m.PhotoName) {
|
||||
if countryCode == unknown && !fs.IsID(m.PhotoName) {
|
||||
if code := txt.CountryCode(m.PhotoName); code != unknown {
|
||||
countryCode = code
|
||||
} else if code := txt.CountryCode(m.PhotoPath); code != unknown {
|
||||
|
@ -32,7 +32,7 @@ func (m *Photo) EstimateCountry() {
|
|||
}
|
||||
}
|
||||
|
||||
if countryCode == unknown && m.OriginalName != "" && fs.NonCanonical(m.OriginalName) {
|
||||
if countryCode == unknown && m.OriginalName != "" && !fs.IsID(m.OriginalName) {
|
||||
if code := txt.CountryCode(m.OriginalName); code != UnknownCountry.ID {
|
||||
countryCode = code
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DscTitleRegexp = regexp.MustCompile("\\D{3}[\\d_]\\d{4}(.JPG)?")
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
var UnwantedDescriptions = map[string]bool{
|
||||
"OLYMPUS DIGITAL CAMERA": true,
|
||||
|
@ -37,7 +36,7 @@ func SanitizeUID(value string) string {
|
|||
func SanitizeTitle(value string) string {
|
||||
value = SanitizeString(value)
|
||||
|
||||
if dsc := DscTitleRegexp.FindString(value); dsc == value {
|
||||
if fs.IsID(value) {
|
||||
value = ""
|
||||
}
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
fileChanged := true
|
||||
fileExists := false
|
||||
photoExists := false
|
||||
stripSequence := Config().Settings().Index.Group
|
||||
|
||||
event.Publish("index.indexing", event.Data{
|
||||
"fileHash": fileHash,
|
||||
|
@ -138,7 +139,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
} else {
|
||||
photo.PhotoQuality = -1
|
||||
|
||||
if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), Config().Settings().Index.Group); yamlName != "" {
|
||||
if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
|
||||
if err := photo.LoadFromYaml(yamlName); err != nil {
|
||||
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), quotedName)
|
||||
} else {
|
||||
|
@ -168,7 +169,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
file.OriginalName = originalName
|
||||
|
||||
if file.FilePrimary && photo.OriginalName == "" {
|
||||
photo.OriginalName = originalName
|
||||
photo.OriginalName = fs.Base(originalName, stripSequence)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -530,7 +531,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
|
||||
w := txt.Keywords(photo.Details.Keywords)
|
||||
|
||||
if fs.NonCanonical(fileBase) {
|
||||
if !fs.IsID(fileBase) {
|
||||
w = append(w, txt.FilenameKeywords(filePath)...)
|
||||
w = append(w, txt.FilenameKeywords(fileBase)...)
|
||||
}
|
||||
|
|
|
@ -4,12 +4,15 @@ import (
|
|||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gc "github.com/patrickmn/go-cache"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
var fileMutex sync.RWMutex
|
||||
|
||||
// New returns a new session store with an optional cachePath.
|
||||
func New(expiration time.Duration, cachePath string) *Session {
|
||||
s := &Session{}
|
||||
|
@ -17,6 +20,9 @@ func New(expiration time.Duration, cachePath string) *Session {
|
|||
cleanupInterval := 15 * time.Minute
|
||||
|
||||
if cachePath != "" {
|
||||
fileMutex.RLock()
|
||||
defer fileMutex.RUnlock()
|
||||
|
||||
var savedItems map[string]Saved
|
||||
|
||||
items := make(map[string]gc.Item)
|
||||
|
@ -70,6 +76,9 @@ func (s *Session) Save() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
fileMutex.Lock()
|
||||
defer fileMutex.Unlock()
|
||||
|
||||
items := s.cache.Items()
|
||||
savedItems := make(map[string]Saved, len(items))
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
func Base(fileName string, stripSequence bool) string {
|
||||
basename := filepath.Base(fileName)
|
||||
|
||||
// strip file type extension
|
||||
// Strip file type extension.
|
||||
if end := strings.LastIndex(basename, "."); end != -1 {
|
||||
basename = basename[:end]
|
||||
}
|
||||
|
@ -19,19 +19,19 @@ func Base(fileName string, stripSequence bool) string {
|
|||
return basename
|
||||
}
|
||||
|
||||
// strip numeric extensions like .0000, .0001, .4542353245,....
|
||||
// Strip numeric extensions like .00000, .00001, .4542353245,.... (at least 5 digits).
|
||||
if dot := strings.LastIndex(basename, "."); dot != -1 && len(basename[dot+1:]) >= 5 {
|
||||
if i, err := strconv.Atoi(basename[dot+1:]); err == nil && i >= 0 {
|
||||
basename = basename[:dot]
|
||||
}
|
||||
}
|
||||
|
||||
// other common sequential naming schemes
|
||||
// Other common sequential naming schemes.
|
||||
if end := strings.Index(basename, "("); end != -1 {
|
||||
// copies created by Chrome & Windows, example: IMG_1234 (2)
|
||||
// Copies created by Chrome & Windows, example: IMG_1234 (2).
|
||||
basename = basename[:end]
|
||||
} else if end := strings.Index(basename, " copy"); end != -1 {
|
||||
// copies created by OS X, example: IMG_1234 copy 2
|
||||
// Copies created by OS X, example: IMG_1234 copy 2.
|
||||
basename = basename[:end]
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// NonCanonical returns true if the file basename is not canonical.
|
||||
// NonCanonical returns true if the file basename is NOT canonical.
|
||||
func NonCanonical(basename string) bool {
|
||||
if len(basename) != 24 {
|
||||
return true
|
||||
|
@ -22,10 +22,15 @@ func NonCanonical(basename string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// IsCanonical returns true if the file basename is canonical.
|
||||
func IsCanonical(basename string) bool {
|
||||
return !NonCanonical(basename)
|
||||
}
|
||||
|
||||
// CanonicalName returns a canonical name based on time and CRC32 checksum.
|
||||
func CanonicalName(date time.Time, checksum string) string {
|
||||
if len(checksum) != 8 {
|
||||
checksum = "ERROR000"
|
||||
checksum = "EEEEEEEE"
|
||||
} else {
|
||||
checksum = strings.ToUpper(checksum)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,6 @@ func TestCanonicalName(t *testing.T) {
|
|||
date := time.Date(
|
||||
2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||
|
||||
assert.Equal(t, "20091117_203458_ERROR000", CanonicalName(date, "123"))
|
||||
assert.Equal(t, "20091117_203458_EEEEEEEE", CanonicalName(date, "123"))
|
||||
assert.Equal(t, "20091117_203458_12345678", CanonicalName(date, "12345678"))
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ import (
|
|||
)
|
||||
|
||||
// Hash returns the SHA1 hash of a file as string.
|
||||
func Hash(filename string) string {
|
||||
func Hash(fileName string) string {
|
||||
var result []byte
|
||||
|
||||
file, err := os.Open(filename)
|
||||
file, err := os.Open(fileName)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
|
@ -30,10 +30,10 @@ func Hash(filename string) string {
|
|||
}
|
||||
|
||||
// Checksum returns the CRC32 checksum of a file as string.
|
||||
func Checksum(filename string) string {
|
||||
func Checksum(fileName string) string {
|
||||
var result []byte
|
||||
|
||||
file, err := os.Open(filename)
|
||||
file, err := os.Open(fileName)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
|
@ -49,3 +49,23 @@ func Checksum(filename string) string {
|
|||
|
||||
return hex.EncodeToString(hash.Sum(result))
|
||||
}
|
||||
|
||||
// IsHash tests if a string looks like a hash.
|
||||
func IsHash(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch len(s) {
|
||||
case 8, 16, 32, 40, 56, 64, 80, 128, 256:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
55
pkg/fs/id.go
Normal file
55
pkg/fs/id.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
var DscNameRegexp = regexp.MustCompile("\\D{3}[\\d_]\\d{4}(.JPG)?")
|
||||
|
||||
// IsInt tests if the file base is an integer number.
|
||||
func IsInt(base string) bool {
|
||||
if base == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range base {
|
||||
if r < 48 || r > 57 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsID tests if the file name looks like an automatically created identifier.
|
||||
func IsID(fileName string) bool {
|
||||
if fileName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
base := Base(fileName, false)
|
||||
|
||||
if IsHash(base) {
|
||||
return true
|
||||
}
|
||||
|
||||
if IsInt(base) {
|
||||
return true
|
||||
}
|
||||
|
||||
if dsc := DscNameRegexp.FindString(base); dsc == base {
|
||||
return true
|
||||
}
|
||||
|
||||
if rnd.IsUID(base, 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if IsCanonical(base) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
28
pkg/fs/id_test.go
Normal file
28
pkg/fs/id_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsID(t *testing.T) {
|
||||
assert.True(t, IsID("lt9k3pw1wowuy3c2"))
|
||||
assert.True(t, IsID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
|
||||
assert.True(t, IsID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
|
||||
assert.True(t, IsID("55785BAC-9A4B-4747-B090-EE123FFEE437"))
|
||||
assert.True(t, IsID("550e8400-e29b-11d4-a716-446655440000"))
|
||||
assert.True(t, IsID("IMG_0599.JPG"))
|
||||
assert.True(t, IsID("DSC10599"))
|
||||
assert.True(t, IsID("20091117_203458_ERROR000"))
|
||||
assert.True(t, IsID("20091117_203458_12345678"))
|
||||
assert.True(t, IsID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
assert.True(t, IsID("123"))
|
||||
assert.False(t, IsID("_"))
|
||||
assert.False(t, IsID(""))
|
||||
assert.False(t, IsID("20191117-153400-Central-Park-New-York-2019-3qy.mov"))
|
||||
assert.True(t, IsID("e98eb86480a72bd585d228a709f0622f90e86cbc.jpg"))
|
||||
assert.True(t, IsID("IMG_8115.jpg"))
|
||||
assert.False(t, IsID("01 Introduction Businessmodel.pdf"))
|
||||
assert.False(t, IsID("A regular file name with 121345678643 numbers"))
|
||||
}
|
|
@ -35,7 +35,7 @@ func IsHex(s string) bool {
|
|||
}
|
||||
|
||||
for _, r := range s {
|
||||
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 90) && r != 45 {
|
||||
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) && r != 45 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -58,10 +58,15 @@ func IsLowerAlnum(s string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Returns true if the string looks like a standard UUID.
|
||||
func IsUUID(s string) bool {
|
||||
return len(s) == 36 && IsHex(s)
|
||||
}
|
||||
|
||||
// IsUID returns true if string is a seemingly unique id.
|
||||
func IsUID(s string, prefix byte) bool {
|
||||
// Regular UUID.
|
||||
if len(s) == 36 && IsHex(s) {
|
||||
if IsUUID(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -31,9 +31,24 @@ func TestIsPPID(t *testing.T) {
|
|||
assert.True(t, IsPPID("lt9k3pw1wowuy3c2", 'l'))
|
||||
}
|
||||
|
||||
func TestIsHex(t *testing.T) {
|
||||
assert.True(t, IsHex("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
|
||||
assert.True(t, IsHex("6ba7b810-9dad-11d1-80b4"))
|
||||
assert.False(t, IsHex("55785BAC-9A4B-4747-B090-GE123FFEE437"))
|
||||
assert.False(t, IsHex("550e8400-e29b-11d4-a716_446655440000"))
|
||||
assert.True(t, IsHex("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
}
|
||||
|
||||
func TestIsUUID(t *testing.T) {
|
||||
assert.True(t, IsUUID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
|
||||
assert.True(t, IsUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
|
||||
assert.False(t, IsUUID("55785BAC-9H4B-4747-B090-EE123FFEE437"))
|
||||
assert.True(t, IsUUID("550e8400-e29b-11d4-a716-446655440000"))
|
||||
assert.False(t, IsUUID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
|
||||
}
|
||||
|
||||
func TestIsUID(t *testing.T) {
|
||||
assert.True(t, IsUID("lt9k3pw1wowuy3c2", 'l'))
|
||||
// xmp.iid:dafbfeb8-a129-4e7c-9cf0-e7996a701cdb
|
||||
assert.True(t, IsUID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb", 'l'))
|
||||
assert.True(t, IsUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8", 'l'))
|
||||
assert.True(t, IsUID("55785BAC-9A4B-4747-B090-EE123FFEE437", 'l'))
|
||||
|
|
Loading…
Reference in a new issue