11package upload
22
33import (
4+ "bytes"
45 "context"
56 "errors"
67 "fmt"
8+ "io"
9+ "os"
10+ "path"
11+ "path/filepath"
12+ "sort"
713 "strings"
814 "sync"
915
@@ -12,9 +18,11 @@ import (
1218 "github.com/simulot/immich-go/immich"
1319 "github.com/simulot/immich-go/internal/assets"
1420 "github.com/simulot/immich-go/internal/assets/cache"
21+ "github.com/simulot/immich-go/internal/exif/sidecars/xmpsidecar"
1522 "github.com/simulot/immich-go/internal/fileevent"
1623 "github.com/simulot/immich-go/internal/filters"
1724 "github.com/simulot/immich-go/internal/fshelper"
25+ "github.com/simulot/immich-go/internal/fshelper/osfs"
1826 "github.com/simulot/immich-go/internal/worker"
1927)
2028
@@ -98,8 +106,12 @@ func (uc *UpCmd) finishing(ctx context.Context) error {
98106 }
99107 defer func () { uc .finished = true }()
100108 // do waiting operations
101- uc .albumsCache .Close ()
102- uc .tagsCache .Close ()
109+ if uc .albumsCache != nil {
110+ uc .albumsCache .Close ()
111+ }
112+ if uc .tagsCache != nil {
113+ uc .tagsCache .Close ()
114+ }
103115
104116 // Resume immich background jobs if requested
105117 err := uc .resumeJobs (ctx )
@@ -116,6 +128,13 @@ func (uc *UpCmd) finishing(ctx context.Context) error {
116128 }
117129 }
118130
131+ if uc .tagSidecarDir != "" {
132+ if remErr := os .RemoveAll (uc .tagSidecarDir ); remErr != nil {
133+ uc .app .Log ().Warn ("failed to clean temporary tag sidecar directory" , "dir" , uc .tagSidecarDir , "err" , remErr )
134+ }
135+ uc .tagSidecarDir = ""
136+ }
137+
119138 return nil
120139}
121140
@@ -137,9 +156,13 @@ func (uc *UpCmd) upload(ctx context.Context, adapter adapters.Reader) error {
137156 uc .albumsCache = cache .NewCollectionCache (50 , func (album assets.Album , ids []string ) (assets.Album , error ) {
138157 return uc .saveAlbum (ctx , album , ids )
139158 })
140- uc .tagsCache = cache .NewCollectionCache (50 , func (tag assets.Tag , ids []string ) (assets.Tag , error ) {
141- return uc .saveTags (ctx , tag , ids )
142- })
159+ if ! uc .TagViaSidecar {
160+ uc .tagsCache = cache .NewCollectionCache (50 , func (tag assets.Tag , ids []string ) (assets.Tag , error ) {
161+ return uc .saveTags (ctx , tag , ids )
162+ })
163+ } else {
164+ uc .tagsCache = nil
165+ }
143166
144167 uc .adapter = adapter
145168
@@ -424,6 +447,10 @@ func (uc *UpCmd) uploadAsset(ctx context.Context, a *assets.Asset) (string, erro
424447 for _ , tag := range uc .Tags {
425448 a .AddTag (tag )
426449 }
450+ if err := uc .prepareTagsSidecar (ctx , a ); err != nil {
451+ uc .app .Jnl ().Record (ctx , fileevent .Error , a .File , "error" , err .Error ())
452+ return "" , err
453+ }
427454
428455 ar , err := uc .client .Immich .AssetUpload (ctx , a )
429456 if err != nil {
@@ -471,6 +498,10 @@ func (uc *UpCmd) uploadAsset(ctx context.Context, a *assets.Asset) (string, erro
471498
472499func (uc * UpCmd ) replaceAsset (ctx context.Context , ID string , a , old * assets.Asset ) (string , error ) {
473500 defer uc .app .Log ().Debug ("replaced by" , "ID" , ID , "file" , a )
501+ if err := uc .prepareTagsSidecar (ctx , a ); err != nil {
502+ uc .app .Jnl ().Record (ctx , fileevent .Error , a .File , "error" , err .Error ())
503+ return "" , err
504+ }
474505 ar , err := uc .client .Immich .ReplaceAsset (ctx , ID , a )
475506 if err != nil {
476507 uc .app .Jnl ().Record (ctx , fileevent .UploadServerError , a .File , "error" , err .Error ())
@@ -513,6 +544,9 @@ func (uc *UpCmd) manageAssetAlbums(ctx context.Context, f fshelper.FSAndName, ID
513544}
514545
515546func (uc * UpCmd ) manageAssetTags (ctx context.Context , a * assets.Asset ) {
547+ if uc .TagViaSidecar {
548+ return
549+ }
516550 if len (a .Tags ) == 0 {
517551 return
518552 }
@@ -528,6 +562,117 @@ func (uc *UpCmd) manageAssetTags(ctx context.Context, a *assets.Asset) {
528562 }
529563}
530564
565+ func (uc * UpCmd ) prepareTagsSidecar (ctx context.Context , a * assets.Asset ) error {
566+ if ! uc .TagViaSidecar {
567+ return nil
568+ }
569+
570+ loggedTags := make (map [string ]struct {})
571+ if uc .app .Jnl () != nil {
572+ for _ , t := range a .Tags {
573+ if t .Value == "" {
574+ continue
575+ }
576+ if _ , seen := loggedTags [t .Value ]; seen {
577+ continue
578+ }
579+ loggedTags [t .Value ] = struct {}{}
580+ uc .app .Jnl ().Record (ctx , fileevent .Tagged , a .File , "tag" , t .Value , "method" , "sidecar" )
581+ }
582+ } else {
583+ for _ , t := range a .Tags {
584+ if t .Value == "" {
585+ continue
586+ }
587+ loggedTags [t .Value ] = struct {}{}
588+ }
589+ }
590+
591+ tagSet := make (map [string ]struct {})
592+ for value := range loggedTags {
593+ tagSet [value ] = struct {}{}
594+ }
595+ for _ , t := range a .Tags {
596+ if t .Value != "" {
597+ tagSet [t .Value ] = struct {}{}
598+ }
599+ }
600+ if a .FromSideCar != nil {
601+ for _ , t := range a .FromSideCar .Tags {
602+ if t .Value != "" {
603+ tagSet [t .Value ] = struct {}{}
604+ }
605+ }
606+ }
607+
608+ tagValues := make ([]string , 0 , len (tagSet ))
609+ for value := range tagSet {
610+ tagValues = append (tagValues , value )
611+ }
612+ sort .Strings (tagValues )
613+
614+ if len (tagValues ) == 0 {
615+ uc .app .Log ().Debug ("tag via sidecar skipped" , "file" , a .OriginalFileName , "reason" , "no tags" )
616+ return nil
617+ }
618+
619+ tagList := strings .Join (tagValues , ", " )
620+ if len (loggedTags ) > 0 {
621+ uc .app .Log ().Info ("tag via sidecar applied" , "file" , a .OriginalFileName , "tags" , tagList )
622+ } else {
623+ uc .app .Log ().Debug ("tag via sidecar preserved existing tags" , "file" , a .OriginalFileName , "tags" , tagList )
624+ }
625+
626+ var source []byte
627+ if a .FromSideCar != nil && a .FromSideCar .File .FS () != nil && a .FromSideCar .File .Name () != "" {
628+ f , err := a .FromSideCar .File .Open ()
629+ if err != nil {
630+ return fmt .Errorf ("open existing sidecar: %w" , err )
631+ }
632+ source , err = io .ReadAll (f )
633+ _ = f .Close ()
634+ if err != nil {
635+ return fmt .Errorf ("read existing sidecar: %w" , err )
636+ }
637+ }
638+
639+ if len (source ) == 0 && len (tagValues ) == 0 {
640+ return nil
641+ }
642+
643+ updated , err := xmpsidecar .UpdateTags (source , tagValues )
644+ if err != nil {
645+ return fmt .Errorf ("update sidecar tags: %w" , err )
646+ }
647+
648+ if uc .tagSidecarDir == "" {
649+ dir , dirErr := os .MkdirTemp ("" , "immich-go-tag-sidecars-*" )
650+ if dirErr != nil {
651+ return fmt .Errorf ("create temporary sidecar directory: %w" , dirErr )
652+ }
653+ uc .tagSidecarDir = dir
654+ }
655+
656+ fileName := path .Base (a .OriginalFileName ) + ".xmp"
657+ fullPath := filepath .Join (uc .tagSidecarDir , fileName )
658+ if writeErr := os .WriteFile (fullPath , updated , 0o600 ); writeErr != nil {
659+ return fmt .Errorf ("write temporary sidecar: %w" , writeErr )
660+ }
661+
662+ uc .app .Log ().Debug ("tag via sidecar wrote temporary sidecar" , "file" , a .OriginalFileName , "path" , fullPath )
663+
664+ md := & assets.Metadata {}
665+ if err = xmpsidecar .ReadXMP (bytes .NewReader (updated ), md ); err != nil {
666+ return fmt .Errorf ("reload updated sidecar metadata: %w" , err )
667+ }
668+
669+ fs := osfs .DirFS (uc .tagSidecarDir )
670+ md .File = fshelper .FSName (fs , fileName )
671+ a .FromSideCar = a .UseMetadata (md )
672+
673+ return nil
674+ }
675+
531676func (uc * UpCmd ) DeleteServerAssets (ctx context.Context , ids []string ) error {
532677 uc .app .Log ().Message ("%d server assets to delete." , len (ids ))
533678 return uc .client .Immich .DeleteAssets (ctx , ids , false )
0 commit comments