Skip to content

Commit 06d6e6d

Browse files
Cedric Reginstersimulot
authored andcommitted
fix from-icloud csv error and import memories
process icloud metadata (csv files) in separate pass. fix logic to detect csv files and ignore unprocessed csv files causing upload errors. Also added option to import memories as albums, defaulting to false. fixes #853
1 parent 46cc6b2 commit 06d6e6d

File tree

7 files changed

+79
-41
lines changed

7 files changed

+79
-41
lines changed

adapters/folder/icloud.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,45 @@ type iCloudMeta struct {
1717
originalCreationDate time.Time
1818
}
1919

20-
func UseICloudAlbum(m *gen.SyncMap[string, iCloudMeta], fsys fs.FS, filename string) (string, error) {
20+
func UseICloudMemory(m *gen.SyncMap[string, iCloudMeta], fsys fs.FS, filename string) (string, error) {
2121
file, err := fsys.Open(filename)
2222
if err != nil {
2323
return "", err
2424
}
2525
defer file.Close()
26+
albumName := "Memory " + strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
27+
28+
return albumName, useAlbum(m, file, albumName)
29+
}
2630

31+
func UseICloudAlbum(m *gen.SyncMap[string, iCloudMeta], fsys fs.FS, filename string) (string, error) {
32+
file, err := fsys.Open(filename)
33+
if err != nil {
34+
return "", err
35+
}
36+
defer file.Close()
2737
albumName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
38+
39+
return albumName, useAlbum(m, file, albumName)
40+
}
41+
42+
func useAlbum(m *gen.SyncMap[string, iCloudMeta], file fs.File, albumName string) error {
2843
reader := csv.NewReader(file)
2944
records, err := reader.ReadAll()
3045
if err != nil {
31-
return "", errors.Join(err, errors.New("failed to read all csv records"))
46+
return errors.Join(err, errors.New("failed to read all csv records"))
3247
}
3348
for _, record := range records[1:] {
3449
if len(record) != 1 {
35-
return "", errors.Join(err, errors.New("invalid record"))
50+
return errors.Join(err, errors.New("invalid record"))
3651
}
3752
fileName := record[0]
3853
meta, _ := m.Load(fileName)
3954
meta.albums = append(meta.albums, assets.Album{Title: albumName})
4055
m.Store(fileName, meta)
4156
}
4257

43-
return albumName, nil
58+
return nil
4459
}
4560

4661
// Example:

adapters/folder/options.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ type ImportFolderOptions struct {
8282
PicasaAlbum bool
8383

8484
// Use icloud takeout metadata (albums & creation date)
85-
ICloudTakeout bool
85+
ICloudTakeout bool
86+
ICloudMemoriesAsAlbums bool
8687

8788
// local time zone
8889
TZ *time.Location
@@ -131,7 +132,6 @@ func (o *ImportFolderOptions) AddFromFolderFlags(cmd *cobra.Command, parent *cob
131132
cmd.Flags().Var(&o.ManageBurst, "manage-burst", "Manage burst photos. Possible values: NoStack, Stack, StackKeepRaw, StackKeepJPEG")
132133
cmd.Flags().BoolVar(&o.ManageEpsonFastFoto, "manage-epson-fastfoto", false, "Manage Epson FastFoto file (default: false)")
133134
cmd.Flags().BoolVar(&o.PicasaAlbum, "album-picasa", false, "Use Picasa album name found in .picasa.ini file (default: false)")
134-
cmd.Flags().BoolVar(&o.ICloudTakeout, "icloud-takeout", false, "Use metadata from icloud takeout (Albums & original creation dates) (default: false)")
135135
}
136136
}
137137

@@ -151,9 +151,11 @@ func (o *ImportFolderOptions) AddFromICloudFlags(cmd *cobra.Command, parent *cob
151151
`.DS_Store/`, // Mac OS custom attributes
152152
`/._*`, // MacOS resource files
153153
`.photostructure/`, // PhotoStructure
154+
`Recently Deleted/`, // ICloud recently deleted
154155
)
155156

156157
o.ICloudTakeout = true
158+
cmd.Flags().BoolVar(&o.ICloudMemoriesAsAlbums, "memories", false, "Import icloud memories as albums (default: false)")
157159
o.PicasaAlbum = false
158160
cmd.Flags().StringVar(&o.ImportIntoAlbum, "into-album", "", "Specify an album to import all files into")
159161

adapters/folder/readFolder.go

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type LocalAssetBrowser struct {
4242
requiresDateInformation bool // true if we need to read the date from the file for the options
4343
picasaAlbums *gen.SyncMap[string, PicasaAlbum] // ap[string]PicasaAlbum
4444
icloudMetas *gen.SyncMap[string, iCloudMeta]
45+
icloudMetaPass bool
4546
}
4647

4748
func NewLocalFiles(ctx context.Context, l *fileevent.Recorder, flags *ImportFolderOptions, fsyss ...fs.FS) (*LocalAssetBrowser, error) {
@@ -64,6 +65,7 @@ func NewLocalFiles(ctx context.Context, l *fileevent.Recorder, flags *ImportFold
6465
}
6566
if flags.ICloudTakeout {
6667
la.icloudMetas = gen.NewSyncMap[string, iCloudMeta]()
68+
la.icloudMetaPass = true
6769
}
6870

6971
if flags.InfoCollector == nil {
@@ -101,6 +103,14 @@ func (la *LocalAssetBrowser) Browse(ctx context.Context) chan *assets.Group {
101103
gOut := make(chan *assets.Group)
102104
go func() {
103105
defer close(gOut)
106+
// two passes for icloud takouts
107+
if la.icloudMetaPass {
108+
for _, fsys := range la.fsyss {
109+
la.concurrentParseDir(ctx, fsys, ".", gOut)
110+
}
111+
la.wg.Wait()
112+
la.icloudMetaPass = false
113+
}
104114
for _, fsys := range la.fsyss {
105115
la.concurrentParseDir(ctx, fsys, ".", gOut)
106116
}
@@ -152,42 +162,55 @@ func (la *LocalAssetBrowser) parseDir(ctx context.Context, fsys fs.FS, dir strin
152162
continue
153163
}
154164

155-
if la.flags.BannedFiles.Match(name) {
156-
la.log.Record(ctx, fileevent.DiscoveredDiscarded, fshelper.FSName(fsys, entry.Name()), "reason", "banned file")
157-
continue
165+
// process csv files on icloud meta pass
166+
if la.icloudMetaPass && ext == icloudMetadataExt {
167+
if strings.HasSuffix(strings.ToLower(dir), "albums") {
168+
a, err := UseICloudAlbum(la.icloudMetas, fsys, name)
169+
if err != nil {
170+
la.log.Record(ctx, fileevent.Error, fshelper.FSName(fsys, name), "error", err.Error())
171+
} else {
172+
la.log.Log().Info("iCloud album detected", "file", fshelper.FSName(fsys, name), "album", a)
173+
}
174+
continue
175+
}
176+
if la.flags.ICloudMemoriesAsAlbums && strings.HasSuffix(strings.ToLower(dir), "memories") {
177+
a, err := UseICloudMemory(la.icloudMetas, fsys, name)
178+
if err != nil {
179+
la.log.Record(ctx, fileevent.Error, fshelper.FSName(fsys, name), "error", err.Error())
180+
} else {
181+
la.log.Log().Info("iCloud memory detected", "file", fshelper.FSName(fsys, name), "album", a)
182+
}
183+
continue
184+
}
185+
// iCloud photo details (csv). File name pattern: "Photo Details.csv"
186+
if strings.HasPrefix(strings.ToLower(base), "photo details") {
187+
err := UseICloudPhotoDetails(la.icloudMetas, fsys, name)
188+
if err != nil {
189+
la.log.Record(ctx, fileevent.Error, fshelper.FSName(fsys, name), "error", err.Error())
190+
} else {
191+
la.log.Log().Info("iCloud photo details detected", "file", fshelper.FSName(fsys, name))
192+
}
193+
continue
194+
}
158195
}
159196

160-
if la.flags.SupportedMedia.IsUseLess(name) {
161-
la.log.Record(ctx, fileevent.DiscoveredUseless, fshelper.FSName(fsys, entry.Name()))
197+
// skip all other files in icloud meta pass
198+
if la.icloudMetaPass {
162199
continue
163200
}
164201

165-
// iCloud albums
166-
if la.flags.ICloudTakeout && strings.ToLower(dir) == "albums" && ext == icloudMetadataExt {
167-
a, err := UseICloudAlbum(la.icloudMetas, fsys, name)
168-
if err != nil {
169-
la.log.Record(ctx, fileevent.Error, fshelper.FSName(fsys, name), "error", err.Error())
170-
} else {
171-
la.log.Log().Info("iCloud album detected", "file", fshelper.FSName(fsys, name), "album", a)
172-
}
202+
// silently ignore .csv files after icloud meta pass
203+
if la.flags.ICloudTakeout && !la.icloudMetaPass && ext == icloudMetadataExt {
173204
continue
174205
}
175206

176-
// iCloud memories
177-
if la.flags.ICloudTakeout && strings.ToLower(dir) == "memories" && ext == ".csv" {
178-
// ignore
179-
la.log.Record(ctx, fileevent.DiscoveredDiscarded, fshelper.FSName(fsys, name), "reason", "iCloud memories ignored")
207+
if la.flags.BannedFiles.Match(name) {
208+
la.log.Record(ctx, fileevent.DiscoveredDiscarded, fshelper.FSName(fsys, entry.Name()), "reason", "banned file")
180209
continue
181210
}
182211

183-
// iCloud photo details (csv). File name pattern: "Photo Details.csv"
184-
if la.flags.ICloudTakeout && strings.HasPrefix(strings.ToLower(base), "photo details") && ext == ".csv" {
185-
err := UseICloudPhotoDetails(la.icloudMetas, fsys, name)
186-
if err != nil {
187-
la.log.Record(ctx, fileevent.Error, fshelper.FSName(fsys, name), "error", err.Error())
188-
} else {
189-
la.log.Log().Info("iCloud photo details detected", "file", fshelper.FSName(fsys, name))
190-
}
212+
if la.flags.SupportedMedia.IsUseLess(name) {
213+
la.log.Record(ctx, fileevent.DiscoveredUseless, fshelper.FSName(fsys, entry.Name()))
191214
continue
192215
}
193216

adapters/folder/readFolderWithFIles_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,11 @@ func TestLocalAssets(t *testing.T) {
122122
{
123123
name: "icloud-takeout",
124124
flags: ImportFolderOptions{
125-
SupportedMedia: filetypes.DefaultSupportedMedia,
126-
Recursive: true,
127-
ICloudTakeout: true,
128-
TZ: time.Local,
125+
SupportedMedia: filetypes.DefaultSupportedMedia,
126+
Recursive: true,
127+
ICloudTakeout: true,
128+
ICloudMemoriesAsAlbums: true,
129+
TZ: time.Local,
129130
InclusionFlags: cliflags.InclusionFlags{
130131
DateRange: cliflags.InitDateRange(time.Local, "2023-10-06"),
131132
},
@@ -139,11 +140,11 @@ func TestLocalAssets(t *testing.T) {
139140
"Photos/photo_wo_exif.jpg",
140141
},
141142
expectedAlbums: map[string][]string{
142-
"Spécial album": {"Photos/photo1.jpg", "Photos/photo2.jpg"},
143-
"Spécial album 2": {"Photos/photo2.jpg"},
143+
"Spécial album": {"Photos/photo1.jpg", "Photos/photo2.jpg"},
144+
"Spécial album 2": {"Photos/photo2.jpg"},
145+
"Memory 1. April 2025": {"Photos/photo1.jpg", "Photos/photo2.jpg"},
144146
},
145147
expectedCounts: fileevent.NewCounts().
146-
Set(fileevent.DiscoveredDiscarded, 1).
147148
Set(fileevent.DiscoveredImage, 3).
148149
Set(fileevent.Uploaded, 3).
149150
Value(),

immich/ping.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,5 @@ func (ic *ImmichClient) GetSupportedMediaTypes(ctx context.Context) (filetypes.S
127127
}
128128
sm[".mp"] = filetypes.TypeUseless
129129
sm[".json"] = filetypes.TypeSidecar
130-
sm[".csv"] = filetypes.TypeMeta
131130
return sm, err
132131
}

internal/filetypes/supported.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const (
1515
TypeImage = "image"
1616
TypeSidecar = "sidecar"
1717
TypeUseless = "useless"
18-
TypeMeta = "meta"
1918
TypeUnknown = ""
2019
)
2120

@@ -28,7 +27,6 @@ var DefaultSupportedMedia = SupportedMedia{
2827
".xmp": TypeSidecar,
2928
".json": TypeSidecar,
3029
".mp": TypeUseless,
31-
".csv": TypeMeta,
3230
}
3331

3432
func (sm SupportedMedia) TypeFromName(name string) string {

0 commit comments

Comments
 (0)