Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ CLAUDE.md
changelog.md
immich-stack
/docs/plan
immich-openapi-specs.json
REF
41 changes: 39 additions & 2 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var withDeleted bool
var logLevel string
var logFormat string
var removeSingleAssetStacks bool
var filterAlbumIDs []string
var filterTakenAfter string
var filterTakenBefore string

/**************************************************************************************************
** Configures the logger based on command-line flags and environment variables. Sets up the
Expand Down Expand Up @@ -138,7 +141,7 @@ type LoadEnvConfig struct {
func logStartupSummary(logger *logrus.Logger) {
// Build summary based on format
if format := os.Getenv("LOG_FORMAT"); format == "json" {
logger.WithFields(logrus.Fields{
fields := logrus.Fields{
"runMode": runMode,
"cronInterval": cronInterval,
"logLevel": logger.GetLevel().String(),
Expand All @@ -153,7 +156,17 @@ func logStartupSummary(logger *logrus.Logger) {
"criteria": criteria,
"parentFilenamePromote": parentFilenamePromote,
"parentExtPromote": parentExtPromote,
}).Info("Configuration loaded")
}
if len(filterAlbumIDs) > 0 {
fields["filterAlbumIDs"] = filterAlbumIDs
}
if filterTakenAfter != "" {
fields["filterTakenAfter"] = filterTakenAfter
}
if filterTakenBefore != "" {
fields["filterTakenBefore"] = filterTakenBefore
}
logger.WithFields(fields).Info("Configuration loaded")
} else {
// Build human-readable summary
var summary []string
Expand Down Expand Up @@ -187,6 +200,15 @@ func logStartupSummary(logger *logrus.Logger) {
if criteria != "" {
summary = append(summary, fmt.Sprintf("criteria=%s", criteria))
}
if len(filterAlbumIDs) > 0 {
summary = append(summary, fmt.Sprintf("filter-albums=%d", len(filterAlbumIDs)))
}
if filterTakenAfter != "" {
summary = append(summary, fmt.Sprintf("filter-after=%s", filterTakenAfter))
}
if filterTakenBefore != "" {
summary = append(summary, fmt.Sprintf("filter-before=%s", filterTakenBefore))
}

logger.Infof("Starting with config: %s", strings.Join(summary, ", "))
}
Expand Down Expand Up @@ -277,6 +299,21 @@ func LoadEnvForTesting() LoadEnvConfig {
parentExtPromote = envVal
}
}
if len(filterAlbumIDs) == 0 {
if envVal := os.Getenv("FILTER_ALBUM_IDS"); envVal != "" {
parts := strings.Split(envVal, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
filterAlbumIDs = utils.RemoveEmptyStrings(parts)
}
}
if filterTakenAfter == "" {
filterTakenAfter = strings.TrimSpace(os.Getenv("FILTER_TAKEN_AFTER"))
}
if filterTakenBefore == "" {
filterTakenBefore = strings.TrimSpace(os.Getenv("FILTER_TAKEN_BEFORE"))
}

// Log startup configuration summary
logStartupSummary(logger)
Expand Down
219 changes: 219 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,37 @@ func TestStartupConfigurationSummary(t *testing.T) {
`"removeSingleAssetStacks":true`,
},
},
{
name: "text format with filter fields",
envVars: map[string]string{
"API_KEY": "test-key",
"FILTER_ALBUM_IDS": "album1,album2",
"FILTER_TAKEN_AFTER": "2024-01-01T00:00:00Z",
"FILTER_TAKEN_BEFORE": "2024-12-31T23:59:59Z",
},
wantInLog: []string{
"Starting with config:",
"filter-albums=2",
"filter-after=2024-01-01T00:00:00Z",
"filter-before=2024-12-31T23:59:59Z",
},
},
{
name: "json format with filter fields",
envVars: map[string]string{
"API_KEY": "test-key",
"LOG_FORMAT": "json",
"FILTER_ALBUM_IDS": "album1,album2,album3",
"FILTER_TAKEN_AFTER": "2024-06-01T00:00:00Z",
"FILTER_TAKEN_BEFORE": "2024-06-30T23:59:59Z",
},
wantInLog: []string{
"Configuration loaded",
`"filterAlbumIDs":["album1","album2","album3"]`,
`"filterTakenAfter":"2024-06-01T00:00:00Z"`,
`"filterTakenBefore":"2024-06-30T23:59:59Z"`,
},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -342,6 +373,7 @@ func resetTestEnv() {
"REPLACE_STACKS", "WITH_ARCHIVED", "WITH_DELETED",
"REMOVE_SINGLE_ASSET_STACKS", "CRITERIA",
"PARENT_FILENAME_PROMOTE", "PARENT_EXT_PROMOTE",
"FILTER_ALBUM_IDS", "FILTER_TAKEN_AFTER", "FILTER_TAKEN_BEFORE",
}

for _, env := range envVars {
Expand All @@ -363,4 +395,191 @@ func resetTestEnv() {
withDeleted = false
logLevel = ""
removeSingleAssetStacks = false
filterAlbumIDs = nil
filterTakenAfter = ""
filterTakenBefore = ""
}

/************************************************************************************************
** Tests for FILTER_ALBUM_IDS environment variable parsing with whitespace handling
************************************************************************************************/

func TestFilterAlbumIDsParsing(t *testing.T) {
tests := []struct {
name string
envValue string
expected []string
}{
{
name: "simple list",
envValue: "album1,album2",
expected: []string{"album1", "album2"},
},
{
name: "with spaces after comma",
envValue: "album1, album2, album3",
expected: []string{"album1", "album2", "album3"},
},
{
name: "with leading and trailing spaces",
envValue: " album1 , album2 ",
expected: []string{"album1", "album2"},
},
{
name: "empty entries filtered",
envValue: "album1,,album2",
expected: []string{"album1", "album2"},
},
{
name: "only spaces filtered",
envValue: "album1, ,album2",
expected: []string{"album1", "album2"},
},
{
name: "single album",
envValue: "album1",
expected: []string{"album1"},
},
{
name: "single album with spaces",
envValue: " album1 ",
expected: []string{"album1"},
},
{
name: "UUIDs with spaces",
envValue: "550e8400-e29b-41d4-a716-446655440000 , 660e8400-e29b-41d4-a716-446655440001",
expected: []string{"550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear environment
resetTestEnv()

// Set required env vars
os.Setenv("API_KEY", "test-key")
os.Setenv("FILTER_ALBUM_IDS", tt.envValue)
defer resetTestEnv()

// Load config
config := LoadEnvForTesting()

// Assert
assert.NoError(t, config.Error)
assert.Equal(t, tt.expected, filterAlbumIDs, "filterAlbumIDs should be correctly parsed and trimmed")
})
}
}

func TestFilterAlbumIDsEmptyEnv(t *testing.T) {
// Clear environment
resetTestEnv()

// Set required env vars but NOT FILTER_ALBUM_IDS
os.Setenv("API_KEY", "test-key")
defer resetTestEnv()

// Load config
config := LoadEnvForTesting()

// Assert
assert.NoError(t, config.Error)
assert.Nil(t, filterAlbumIDs, "filterAlbumIDs should be nil when env var is not set")
}

/************************************************************************************************
** Tests for date filter environment variable parsing with TrimSpace
************************************************************************************************/

func TestDateFilterEnvVarParsing(t *testing.T) {
tests := []struct {
name string
envAfter string
envBefore string
expectedAfter string
expectedBefore string
}{
{
name: "valid dates without spaces",
envAfter: "2024-01-01T00:00:00Z",
envBefore: "2024-12-31T23:59:59Z",
expectedAfter: "2024-01-01T00:00:00Z",
expectedBefore: "2024-12-31T23:59:59Z",
},
{
name: "with leading space on after",
envAfter: " 2024-01-01T00:00:00Z",
envBefore: "",
expectedAfter: "2024-01-01T00:00:00Z",
expectedBefore: "",
},
{
name: "with trailing space on after",
envAfter: "2024-01-01T00:00:00Z ",
envBefore: "",
expectedAfter: "2024-01-01T00:00:00Z",
expectedBefore: "",
},
{
name: "with leading space on before",
envAfter: "",
envBefore: " 2024-12-31T23:59:59Z",
expectedAfter: "",
expectedBefore: "2024-12-31T23:59:59Z",
},
{
name: "with trailing space on before",
envAfter: "",
envBefore: "2024-12-31T23:59:59Z ",
expectedAfter: "",
expectedBefore: "2024-12-31T23:59:59Z",
},
{
name: "both with leading and trailing spaces",
envAfter: " 2024-01-01T00:00:00Z ",
envBefore: " 2024-12-31T23:59:59Z ",
expectedAfter: "2024-01-01T00:00:00Z",
expectedBefore: "2024-12-31T23:59:59Z",
},
{
name: "empty values",
envAfter: "",
envBefore: "",
expectedAfter: "",
expectedBefore: "",
},
{
name: "only whitespace becomes empty",
envAfter: " ",
envBefore: " ",
expectedAfter: "",
expectedBefore: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear environment
resetTestEnv()

// Set required env vars
os.Setenv("API_KEY", "test-key")
if tt.envAfter != "" {
os.Setenv("FILTER_TAKEN_AFTER", tt.envAfter)
}
if tt.envBefore != "" {
os.Setenv("FILTER_TAKEN_BEFORE", tt.envBefore)
}
defer resetTestEnv()

// Load config
config := LoadEnvForTesting()

// Assert
assert.NoError(t, config.Error)
assert.Equal(t, tt.expectedAfter, filterTakenAfter, "filterTakenAfter should be trimmed")
assert.Equal(t, tt.expectedBefore, filterTakenBefore, "filterTakenBefore should be trimmed")
})
}
}
9 changes: 8 additions & 1 deletion cmd/duplicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import (
func runDuplicates(cmd *cobra.Command, args []string) {
logger := loadEnv()

/**********************************************************************************************
** Warn if filter flags are set (they have no effect on this command).
**********************************************************************************************/
if len(filterAlbumIDs) > 0 || filterTakenAfter != "" || filterTakenBefore != "" {
logger.Warnf("Filter flags (--filter-album-ids, --filter-taken-after, --filter-taken-before) have no effect on the duplicates command")
}

/**********************************************************************************************
** Support multiple API keys (comma-separated).
**********************************************************************************************/
Expand All @@ -40,7 +47,7 @@ func runDuplicates(cmd *cobra.Command, args []string) {
if i > 0 {
logger.Infof("\n")
}
client := immich.NewClient(apiURL, key, false, false, true, withArchived, withDeleted, false, logger)
client := immich.NewClient(apiURL, key, false, false, true, withArchived, withDeleted, false, nil, "", "", logger)
if client == nil {
logger.Errorf("Invalid client for API key: %s", key)
continue
Expand Down
9 changes: 8 additions & 1 deletion cmd/fixtrash.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import (
func runFixTrash(cmd *cobra.Command, args []string) {
logger := loadEnv()

/**********************************************************************************************
** Warn if filter flags are set (they have no effect on this command).
**********************************************************************************************/
if len(filterAlbumIDs) > 0 || filterTakenAfter != "" || filterTakenBefore != "" {
logger.Warnf("Filter flags (--filter-album-ids, --filter-taken-after, --filter-taken-before) have no effect on the fix-trash command")
}

/**********************************************************************************************
** Support multiple API keys (comma-separated).
**********************************************************************************************/
Expand All @@ -44,7 +51,7 @@ func runFixTrash(cmd *cobra.Command, args []string) {
if i > 0 {
logger.Infof("\n")
}
client := immich.NewClient(apiURL, key, false, false, dryRun, withArchived, withDeleted, false, logger)
client := immich.NewClient(apiURL, key, false, false, dryRun, withArchived, withDeleted, false, nil, "", "", logger)
if client == nil {
logger.Errorf("Invalid client for API key: %s", key)
continue
Expand Down
3 changes: 3 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func bindFlags(rootCmd *cobra.Command) {
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "Log level: debug, info, warn, error (or set LOG_LEVEL env var)")
rootCmd.PersistentFlags().StringVar(&logFormat, "log-format", "", "Log format: text, json (or set LOG_FORMAT env var)")
rootCmd.PersistentFlags().BoolVar(&removeSingleAssetStacks, "remove-single-asset-stacks", false, "Remove stacks with only one asset (or set REMOVE_SINGLE_ASSET_STACKS=true)")
rootCmd.PersistentFlags().StringSliceVar(&filterAlbumIDs, "filter-album-ids", nil, "Filter by album IDs or names, comma-separated (or set FILTER_ALBUM_IDS env var)")
rootCmd.PersistentFlags().StringVar(&filterTakenAfter, "filter-taken-after", "", "Filter assets taken after date, ISO 8601 (or set FILTER_TAKEN_AFTER env var)")
rootCmd.PersistentFlags().StringVar(&filterTakenBefore, "filter-taken-before", "", "Filter assets taken before date, ISO 8601 (or set FILTER_TAKEN_BEFORE env var)")
}

/**************************************************************************************************
Expand Down
Loading
Loading