Skip to content

Commit d0cf212

Browse files
authored
Merge pull request #44 from Majorfi/feat/filtering
Feat/filtering
2 parents fe9431d + f78dffe commit d0cf212

13 files changed

Lines changed: 1381 additions & 41 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ CLAUDE.md
77
changelog.md
88
immich-stack
99
/docs/plan
10+
immich-openapi-specs.json
1011
REF

cmd/config.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ var withDeleted bool
3535
var logLevel string
3636
var logFormat string
3737
var removeSingleAssetStacks bool
38+
var filterAlbumIDs []string
39+
var filterTakenAfter string
40+
var filterTakenBefore string
3841

3942
/**************************************************************************************************
4043
** Configures the logger based on command-line flags and environment variables. Sets up the
@@ -138,7 +141,7 @@ type LoadEnvConfig struct {
138141
func logStartupSummary(logger *logrus.Logger) {
139142
// Build summary based on format
140143
if format := os.Getenv("LOG_FORMAT"); format == "json" {
141-
logger.WithFields(logrus.Fields{
144+
fields := logrus.Fields{
142145
"runMode": runMode,
143146
"cronInterval": cronInterval,
144147
"logLevel": logger.GetLevel().String(),
@@ -153,7 +156,17 @@ func logStartupSummary(logger *logrus.Logger) {
153156
"criteria": criteria,
154157
"parentFilenamePromote": parentFilenamePromote,
155158
"parentExtPromote": parentExtPromote,
156-
}).Info("Configuration loaded")
159+
}
160+
if len(filterAlbumIDs) > 0 {
161+
fields["filterAlbumIDs"] = filterAlbumIDs
162+
}
163+
if filterTakenAfter != "" {
164+
fields["filterTakenAfter"] = filterTakenAfter
165+
}
166+
if filterTakenBefore != "" {
167+
fields["filterTakenBefore"] = filterTakenBefore
168+
}
169+
logger.WithFields(fields).Info("Configuration loaded")
157170
} else {
158171
// Build human-readable summary
159172
var summary []string
@@ -187,6 +200,15 @@ func logStartupSummary(logger *logrus.Logger) {
187200
if criteria != "" {
188201
summary = append(summary, fmt.Sprintf("criteria=%s", criteria))
189202
}
203+
if len(filterAlbumIDs) > 0 {
204+
summary = append(summary, fmt.Sprintf("filter-albums=%d", len(filterAlbumIDs)))
205+
}
206+
if filterTakenAfter != "" {
207+
summary = append(summary, fmt.Sprintf("filter-after=%s", filterTakenAfter))
208+
}
209+
if filterTakenBefore != "" {
210+
summary = append(summary, fmt.Sprintf("filter-before=%s", filterTakenBefore))
211+
}
190212

191213
logger.Infof("Starting with config: %s", strings.Join(summary, ", "))
192214
}
@@ -277,6 +299,21 @@ func LoadEnvForTesting() LoadEnvConfig {
277299
parentExtPromote = envVal
278300
}
279301
}
302+
if len(filterAlbumIDs) == 0 {
303+
if envVal := os.Getenv("FILTER_ALBUM_IDS"); envVal != "" {
304+
parts := strings.Split(envVal, ",")
305+
for i := range parts {
306+
parts[i] = strings.TrimSpace(parts[i])
307+
}
308+
filterAlbumIDs = utils.RemoveEmptyStrings(parts)
309+
}
310+
}
311+
if filterTakenAfter == "" {
312+
filterTakenAfter = strings.TrimSpace(os.Getenv("FILTER_TAKEN_AFTER"))
313+
}
314+
if filterTakenBefore == "" {
315+
filterTakenBefore = strings.TrimSpace(os.Getenv("FILTER_TAKEN_BEFORE"))
316+
}
280317

281318
// Log startup configuration summary
282319
logStartupSummary(logger)

cmd/config_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,37 @@ func TestStartupConfigurationSummary(t *testing.T) {
5959
`"removeSingleAssetStacks":true`,
6060
},
6161
},
62+
{
63+
name: "text format with filter fields",
64+
envVars: map[string]string{
65+
"API_KEY": "test-key",
66+
"FILTER_ALBUM_IDS": "album1,album2",
67+
"FILTER_TAKEN_AFTER": "2024-01-01T00:00:00Z",
68+
"FILTER_TAKEN_BEFORE": "2024-12-31T23:59:59Z",
69+
},
70+
wantInLog: []string{
71+
"Starting with config:",
72+
"filter-albums=2",
73+
"filter-after=2024-01-01T00:00:00Z",
74+
"filter-before=2024-12-31T23:59:59Z",
75+
},
76+
},
77+
{
78+
name: "json format with filter fields",
79+
envVars: map[string]string{
80+
"API_KEY": "test-key",
81+
"LOG_FORMAT": "json",
82+
"FILTER_ALBUM_IDS": "album1,album2,album3",
83+
"FILTER_TAKEN_AFTER": "2024-06-01T00:00:00Z",
84+
"FILTER_TAKEN_BEFORE": "2024-06-30T23:59:59Z",
85+
},
86+
wantInLog: []string{
87+
"Configuration loaded",
88+
`"filterAlbumIDs":["album1","album2","album3"]`,
89+
`"filterTakenAfter":"2024-06-01T00:00:00Z"`,
90+
`"filterTakenBefore":"2024-06-30T23:59:59Z"`,
91+
},
92+
},
6293
}
6394

6495
for _, tt := range tests {
@@ -342,6 +373,7 @@ func resetTestEnv() {
342373
"REPLACE_STACKS", "WITH_ARCHIVED", "WITH_DELETED",
343374
"REMOVE_SINGLE_ASSET_STACKS", "CRITERIA",
344375
"PARENT_FILENAME_PROMOTE", "PARENT_EXT_PROMOTE",
376+
"FILTER_ALBUM_IDS", "FILTER_TAKEN_AFTER", "FILTER_TAKEN_BEFORE",
345377
}
346378

347379
for _, env := range envVars {
@@ -363,4 +395,191 @@ func resetTestEnv() {
363395
withDeleted = false
364396
logLevel = ""
365397
removeSingleAssetStacks = false
398+
filterAlbumIDs = nil
399+
filterTakenAfter = ""
400+
filterTakenBefore = ""
401+
}
402+
403+
/************************************************************************************************
404+
** Tests for FILTER_ALBUM_IDS environment variable parsing with whitespace handling
405+
************************************************************************************************/
406+
407+
func TestFilterAlbumIDsParsing(t *testing.T) {
408+
tests := []struct {
409+
name string
410+
envValue string
411+
expected []string
412+
}{
413+
{
414+
name: "simple list",
415+
envValue: "album1,album2",
416+
expected: []string{"album1", "album2"},
417+
},
418+
{
419+
name: "with spaces after comma",
420+
envValue: "album1, album2, album3",
421+
expected: []string{"album1", "album2", "album3"},
422+
},
423+
{
424+
name: "with leading and trailing spaces",
425+
envValue: " album1 , album2 ",
426+
expected: []string{"album1", "album2"},
427+
},
428+
{
429+
name: "empty entries filtered",
430+
envValue: "album1,,album2",
431+
expected: []string{"album1", "album2"},
432+
},
433+
{
434+
name: "only spaces filtered",
435+
envValue: "album1, ,album2",
436+
expected: []string{"album1", "album2"},
437+
},
438+
{
439+
name: "single album",
440+
envValue: "album1",
441+
expected: []string{"album1"},
442+
},
443+
{
444+
name: "single album with spaces",
445+
envValue: " album1 ",
446+
expected: []string{"album1"},
447+
},
448+
{
449+
name: "UUIDs with spaces",
450+
envValue: "550e8400-e29b-41d4-a716-446655440000 , 660e8400-e29b-41d4-a716-446655440001",
451+
expected: []string{"550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001"},
452+
},
453+
}
454+
455+
for _, tt := range tests {
456+
t.Run(tt.name, func(t *testing.T) {
457+
// Clear environment
458+
resetTestEnv()
459+
460+
// Set required env vars
461+
os.Setenv("API_KEY", "test-key")
462+
os.Setenv("FILTER_ALBUM_IDS", tt.envValue)
463+
defer resetTestEnv()
464+
465+
// Load config
466+
config := LoadEnvForTesting()
467+
468+
// Assert
469+
assert.NoError(t, config.Error)
470+
assert.Equal(t, tt.expected, filterAlbumIDs, "filterAlbumIDs should be correctly parsed and trimmed")
471+
})
472+
}
473+
}
474+
475+
func TestFilterAlbumIDsEmptyEnv(t *testing.T) {
476+
// Clear environment
477+
resetTestEnv()
478+
479+
// Set required env vars but NOT FILTER_ALBUM_IDS
480+
os.Setenv("API_KEY", "test-key")
481+
defer resetTestEnv()
482+
483+
// Load config
484+
config := LoadEnvForTesting()
485+
486+
// Assert
487+
assert.NoError(t, config.Error)
488+
assert.Nil(t, filterAlbumIDs, "filterAlbumIDs should be nil when env var is not set")
489+
}
490+
491+
/************************************************************************************************
492+
** Tests for date filter environment variable parsing with TrimSpace
493+
************************************************************************************************/
494+
495+
func TestDateFilterEnvVarParsing(t *testing.T) {
496+
tests := []struct {
497+
name string
498+
envAfter string
499+
envBefore string
500+
expectedAfter string
501+
expectedBefore string
502+
}{
503+
{
504+
name: "valid dates without spaces",
505+
envAfter: "2024-01-01T00:00:00Z",
506+
envBefore: "2024-12-31T23:59:59Z",
507+
expectedAfter: "2024-01-01T00:00:00Z",
508+
expectedBefore: "2024-12-31T23:59:59Z",
509+
},
510+
{
511+
name: "with leading space on after",
512+
envAfter: " 2024-01-01T00:00:00Z",
513+
envBefore: "",
514+
expectedAfter: "2024-01-01T00:00:00Z",
515+
expectedBefore: "",
516+
},
517+
{
518+
name: "with trailing space on after",
519+
envAfter: "2024-01-01T00:00:00Z ",
520+
envBefore: "",
521+
expectedAfter: "2024-01-01T00:00:00Z",
522+
expectedBefore: "",
523+
},
524+
{
525+
name: "with leading space on before",
526+
envAfter: "",
527+
envBefore: " 2024-12-31T23:59:59Z",
528+
expectedAfter: "",
529+
expectedBefore: "2024-12-31T23:59:59Z",
530+
},
531+
{
532+
name: "with trailing space on before",
533+
envAfter: "",
534+
envBefore: "2024-12-31T23:59:59Z ",
535+
expectedAfter: "",
536+
expectedBefore: "2024-12-31T23:59:59Z",
537+
},
538+
{
539+
name: "both with leading and trailing spaces",
540+
envAfter: " 2024-01-01T00:00:00Z ",
541+
envBefore: " 2024-12-31T23:59:59Z ",
542+
expectedAfter: "2024-01-01T00:00:00Z",
543+
expectedBefore: "2024-12-31T23:59:59Z",
544+
},
545+
{
546+
name: "empty values",
547+
envAfter: "",
548+
envBefore: "",
549+
expectedAfter: "",
550+
expectedBefore: "",
551+
},
552+
{
553+
name: "only whitespace becomes empty",
554+
envAfter: " ",
555+
envBefore: " ",
556+
expectedAfter: "",
557+
expectedBefore: "",
558+
},
559+
}
560+
561+
for _, tt := range tests {
562+
t.Run(tt.name, func(t *testing.T) {
563+
// Clear environment
564+
resetTestEnv()
565+
566+
// Set required env vars
567+
os.Setenv("API_KEY", "test-key")
568+
if tt.envAfter != "" {
569+
os.Setenv("FILTER_TAKEN_AFTER", tt.envAfter)
570+
}
571+
if tt.envBefore != "" {
572+
os.Setenv("FILTER_TAKEN_BEFORE", tt.envBefore)
573+
}
574+
defer resetTestEnv()
575+
576+
// Load config
577+
config := LoadEnvForTesting()
578+
579+
// Assert
580+
assert.NoError(t, config.Error)
581+
assert.Equal(t, tt.expectedAfter, filterTakenAfter, "filterTakenAfter should be trimmed")
582+
assert.Equal(t, tt.expectedBefore, filterTakenBefore, "filterTakenBefore should be trimmed")
583+
})
584+
}
366585
}

cmd/duplicates.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ import (
2323
func runDuplicates(cmd *cobra.Command, args []string) {
2424
logger := loadEnv()
2525

26+
/**********************************************************************************************
27+
** Warn if filter flags are set (they have no effect on this command).
28+
**********************************************************************************************/
29+
if len(filterAlbumIDs) > 0 || filterTakenAfter != "" || filterTakenBefore != "" {
30+
logger.Warnf("Filter flags (--filter-album-ids, --filter-taken-after, --filter-taken-before) have no effect on the duplicates command")
31+
}
32+
2633
/**********************************************************************************************
2734
** Support multiple API keys (comma-separated).
2835
**********************************************************************************************/
@@ -40,7 +47,7 @@ func runDuplicates(cmd *cobra.Command, args []string) {
4047
if i > 0 {
4148
logger.Infof("\n")
4249
}
43-
client := immich.NewClient(apiURL, key, false, false, true, withArchived, withDeleted, false, logger)
50+
client := immich.NewClient(apiURL, key, false, false, true, withArchived, withDeleted, false, nil, "", "", logger)
4451
if client == nil {
4552
logger.Errorf("Invalid client for API key: %s", key)
4653
continue

cmd/fixtrash.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ import (
2727
func runFixTrash(cmd *cobra.Command, args []string) {
2828
logger := loadEnv()
2929

30+
/**********************************************************************************************
31+
** Warn if filter flags are set (they have no effect on this command).
32+
**********************************************************************************************/
33+
if len(filterAlbumIDs) > 0 || filterTakenAfter != "" || filterTakenBefore != "" {
34+
logger.Warnf("Filter flags (--filter-album-ids, --filter-taken-after, --filter-taken-before) have no effect on the fix-trash command")
35+
}
36+
3037
/**********************************************************************************************
3138
** Support multiple API keys (comma-separated).
3239
**********************************************************************************************/
@@ -44,7 +51,7 @@ func runFixTrash(cmd *cobra.Command, args []string) {
4451
if i > 0 {
4552
logger.Infof("\n")
4653
}
47-
client := immich.NewClient(apiURL, key, false, false, dryRun, withArchived, withDeleted, false, logger)
54+
client := immich.NewClient(apiURL, key, false, false, dryRun, withArchived, withDeleted, false, nil, "", "", logger)
4855
if client == nil {
4956
logger.Errorf("Invalid client for API key: %s", key)
5057
continue

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ func bindFlags(rootCmd *cobra.Command) {
3232
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "Log level: debug, info, warn, error (or set LOG_LEVEL env var)")
3333
rootCmd.PersistentFlags().StringVar(&logFormat, "log-format", "", "Log format: text, json (or set LOG_FORMAT env var)")
3434
rootCmd.PersistentFlags().BoolVar(&removeSingleAssetStacks, "remove-single-asset-stacks", false, "Remove stacks with only one asset (or set REMOVE_SINGLE_ASSET_STACKS=true)")
35+
rootCmd.PersistentFlags().StringSliceVar(&filterAlbumIDs, "filter-album-ids", nil, "Filter by album IDs or names, comma-separated (or set FILTER_ALBUM_IDS env var)")
36+
rootCmd.PersistentFlags().StringVar(&filterTakenAfter, "filter-taken-after", "", "Filter assets taken after date, ISO 8601 (or set FILTER_TAKEN_AFTER env var)")
37+
rootCmd.PersistentFlags().StringVar(&filterTakenBefore, "filter-taken-before", "", "Filter assets taken before date, ISO 8601 (or set FILTER_TAKEN_BEFORE env var)")
3538
}
3639

3740
/**************************************************************************************************

0 commit comments

Comments
 (0)