Skip to content

Commit

Permalink
Merge pull request #326 from simulot:simulot/issue208
Browse files Browse the repository at this point in the history
Live Photo / Montion picture
  • Loading branch information
simulot authored Jun 23, 2024
2 parents 8ca7184 + e730d89 commit ab55a49
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 272 deletions.
305 changes: 153 additions & 152 deletions browser/files/localassets.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@ import (
"context"
"io/fs"
"path"
"runtime"
"path/filepath"
"sort"
"strings"
"time"

"github.com/simulot/immich-go/browser"
"github.com/simulot/immich-go/helpers/fileevent"
"github.com/simulot/immich-go/helpers/gen"
"github.com/simulot/immich-go/immich"
"github.com/simulot/immich-go/immich/metadata"
)

type fileLinks struct {
image string
video string
sidecar string
}

type LocalAssetBrowser struct {
fsyss []fs.FS
albums map[string]string
catalogs map[fs.FS]map[string]map[string]fileLinks // per FS, DIR and base name
log *fileevent.Recorder
sm immich.SupportedMedia
whenNoDate string
Expand All @@ -26,15 +35,12 @@ func NewLocalFiles(ctx context.Context, l *fileevent.Recorder, fsyss ...fs.FS) (
return &LocalAssetBrowser{
fsyss: fsyss,
albums: map[string]string{},
catalogs: map[fs.FS]map[string]map[string]fileLinks{},
log: l,
whenNoDate: "FILE",
}, nil
}

func (la *LocalAssetBrowser) Prepare(ctx context.Context) error {
return nil
}

func (la *LocalAssetBrowser) SetSupportedMedia(sm immich.SupportedMedia) *LocalAssetBrowser {
la.sm = sm
return la
Expand All @@ -45,41 +51,138 @@ func (la *LocalAssetBrowser) SetWhenNoDate(opt string) *LocalAssetBrowser {
return la
}

var toOldDate = time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)
func (la *LocalAssetBrowser) Prepare(ctx context.Context) error {
for _, fsys := range la.fsyss {
err := la.passOneFsWalk(ctx, fsys)
if err != nil {
return err
}
}

return nil
}

func (la *LocalAssetBrowser) passOneFsWalk(ctx context.Context, fsys fs.FS) error {
fsCatalog := map[string]map[string]fileLinks{}
err := fs.WalkDir(fsys, ".",
func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
fsCatalog[name] = map[string]fileLinks{}
return nil
}

select {
case <-ctx.Done():
// If the context has been cancelled, return immediately
return ctx.Err()
default:
dir, base := filepath.Split(name)
dir = strings.TrimSuffix(dir, "/")
if dir == "" {
dir = "."
}
ext := filepath.Ext(base)
mediaType := la.sm.TypeFromExt(ext)

if mediaType == immich.TypeUnknown {
la.log.Record(ctx, fileevent.DiscoveredUnsupported, nil, name, "reason", "unsupported file type")
return nil
}

linkBase := strings.TrimSuffix(base, ext)
for {
e := path.Ext(linkBase)
if la.sm.IsMedia(e) {
linkBase = strings.TrimSuffix(linkBase, e)
continue
}
break
}
dirLinks := fsCatalog[dir]
links := dirLinks[linkBase]

switch mediaType {
case immich.TypeImage:
links.image = name
la.log.Record(ctx, fileevent.DiscoveredImage, nil, name)
case immich.TypeVideo:
links.video = name
la.log.Record(ctx, fileevent.DiscoveredVideo, nil, name)
case immich.TypeSidecar:
links.sidecar = name
la.log.Record(ctx, fileevent.DiscoveredSidecar, nil, name)
}
dirLinks[linkBase] = links
fsCatalog[dir] = dirLinks
}
return nil
})
la.catalogs[fsys] = fsCatalog
return err
}

func (la *LocalAssetBrowser) Browse(ctx context.Context) chan *browser.LocalAssetFile {
fileChan := make(chan *browser.LocalAssetFile)
// Browse all given FS to collect the list of files
go func(ctx context.Context) {
defer close(fileChan)
var err error

errFn := func(name string, err error) {
if err != nil {
la.log.Record(ctx, fileevent.Error, nil, name, "error", err.Error())
}
}
for _, fsys := range la.fsyss {
err := fs.WalkDir(fsys, ".",
func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
dirLinks := la.catalogs[fsys]
dirKeys := gen.MapKeys(dirLinks)
sort.Strings(dirKeys)
for _, d := range dirKeys {
linksList := la.catalogs[fsys][d]
linksKeys := gen.MapKeys(linksList)
sort.Strings(linksKeys)
for _, l := range linksKeys {
var a *browser.LocalAssetFile
links := linksList[l]

if links.image != "" {
a, err = la.assetFromFile(fsys, links.image)
if err != nil {
errFn(links.image, err)
return
}
if links.video != "" {
a.LivePhoto, err = la.assetFromFile(fsys, links.video)
if err != nil {
errFn(links.video, err)
return
}
}
} else if links.video != "" {
a, err = la.assetFromFile(fsys, links.video)
if err != nil {
errFn(links.video, err)
return
}
}

if links.sidecar != "" {
a.SideCar = &metadata.SideCar{
FileName: links.sidecar,
OnFSsys: true,
}
la.log.Record(ctx, fileevent.AnalysisAssociatedMetadata, nil, links.sidecar, "main", a.FileName)
}

// Check if the context has been cancelled
select {
case <-ctx.Done():
// If the context has been cancelled, return immediately
return ctx.Err()
return
default:
if d.IsDir() {
return la.handleFolder(ctx, fsys, fileChan, name)
}
fileChan <- a
}
return nil
})
if err != nil {
// Check if the context has been cancelled before sending the error
select {
case <-ctx.Done():
// If the context has been cancelled, return immediately
return
case fileChan <- &browser.LocalAssetFile{
Err: err,
}:
}
}
}
Expand All @@ -88,138 +191,36 @@ func (la *LocalAssetBrowser) Browse(ctx context.Context) chan *browser.LocalAsse
return fileChan
}

func (la *LocalAssetBrowser) handleFolder(ctx context.Context, fsys fs.FS, fileChan chan *browser.LocalAssetFile, folder string) error {
entries, err := fs.ReadDir(fsys, folder)
if err != nil {
return err
}

nextFile:
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
fileName := path.Join(folder, name)
ext := strings.ToLower(path.Ext(name))

t := la.sm.TypeFromExt(ext)
switch t {
default:
la.log.Record(ctx, fileevent.DiscoveredUnsupported, nil, fileName, "reason", "unsupported file type")
continue nextFile
case immich.TypeIgnored:
la.log.Record(ctx, fileevent.DiscoveredUnsupported, nil, fileName, "reason", "useless file")
continue nextFile
case immich.TypeSidecar:
la.log.Record(ctx, fileevent.DiscoveredSidecar, nil, fileName)
continue nextFile
case immich.TypeImage:
case immich.TypeVideo:
}

f := browser.LocalAssetFile{
FSys: fsys,
FileName: path.Join(folder, name),
Title: path.Base(name),
FileSize: 0,
Err: err,
DateTaken: metadata.TakeTimeFromPath(name),
}

s, err := e.Info()
if err != nil {
f.Err = err
} else {
f.FileSize = int(s.Size())
if f.DateTaken.IsZero() {
err = la.ReadMetadataFromFile(&f)
_ = err
if f.DateTaken.Before(toOldDate) {
switch la.whenNoDate {
case "FILE":
f.DateTaken = s.ModTime()
case "NOW":
f.DateTaken = time.Now()
}
}
}
la.checkSidecar(ctx, &f, entries, folder, name)
}

switch t {
case immich.TypeImage:
la.log.Record(ctx, fileevent.DiscoveredImage, f, fileName)
case immich.TypeVideo:
la.log.Record(ctx, fileevent.DiscoveredVideo, f, fileName)
}
var toOldDate = time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)

// Check if the context has been cancelled
select {
case <-ctx.Done():
// If the context has been cancelled, return immediately
return ctx.Err()
default:
fileChan <- &f
}
func (la *LocalAssetBrowser) assetFromFile(fsys fs.FS, name string) (*browser.LocalAssetFile, error) {
a := &browser.LocalAssetFile{
FileName: name,
Title: filepath.Base(name),
DateTaken: metadata.TakeTimeFromPath(name),
FSys: fsys,
}
return nil
}

func (la *LocalAssetBrowser) checkSidecar(ctx context.Context, f *browser.LocalAssetFile, entries []fs.DirEntry, dir, name string) bool {
assetBase := la.baseNames(name)

for _, name := range assetBase {
xmp := name + ".[xX][mM][pP]"
for _, e := range entries {
m, err := path.Match(xmp, e.Name())
if err != nil {
panic(err)
}
if m {
f.SideCar = &metadata.SideCar{
FileName: path.Join(dir, e.Name()),
OnFSsys: true,
}
la.log.Record(ctx, fileevent.AnalysisAssociatedMetadata, nil, path.Join(dir, e.Name()), "main", f.FileName)
return true
}
}
i, err := fs.Stat(fsys, name)
if err != nil {
return nil, err
}
return false
}

func (la *LocalAssetBrowser) baseNames(n string) []string {
n = escapeName(n)
names := []string{n}
ext := path.Ext(n)
for {
if ext == "" {
return names
}
if la.sm.TypeFromExt(ext) == "" {
return names
a.FileSize = int(i.Size())
if a.DateTaken.IsZero() {
err = la.ReadMetadataFromFile(a)
if err != nil {
return nil, err
}
n = strings.TrimSuffix(n, ext)
names = append(names, n, n+".*")
ext = path.Ext(n)
}
}

func escapeName(n string) string {
b := strings.Builder{}
for _, c := range n {
switch c {
case '*', '?', '[', ']', '^':
b.WriteRune('\\')
case '\\':
if runtime.GOOS != "windows" {
b.WriteRune('\\')
if a.DateTaken.Before(toOldDate) {
switch la.whenNoDate {
case "FILE":
a.DateTaken = i.ModTime()
case "NOW":
a.DateTaken = time.Now()
}
}
b.WriteRune(c)
}
return b.String()
return a, nil
}

func (la *LocalAssetBrowser) ReadMetadataFromFile(a *browser.LocalAssetFile) error {
Expand All @@ -234,5 +235,5 @@ func (la *LocalAssetBrowser) ReadMetadataFromFile(a *browser.LocalAssetFile) err
if err == nil {
a.DateTaken = m.DateTaken
}
return err
return nil
}
5 changes: 5 additions & 0 deletions browser/files/localassets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ func TestLocalAssets(t *testing.T) {
b.SetSupportedMedia(immich.DefaultSupportedMedia)
b.SetWhenNoDate("FILE")

err = b.Prepare(ctx)
if err != nil {
t.Error(err)
}

results := []string{}
for a := range b.Browse(ctx) {
results = append(results, a.FileName)
Expand Down
Loading

0 comments on commit ab55a49

Please sign in to comment.