Skip to content

Commit 88e46cb

Browse files
authored
Merge pull request #33 from Majorfi/feat/advancedFiltering
Feat/advanced filtering
2 parents afae323 + eb138c8 commit 88e46cb

32 files changed

Lines changed: 5755 additions & 691 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
.claude
66
CLAUDE.md
77
changelog.md
8-
immich-stack
8+
immich-stack
9+
/docs/plan
10+
REF

cmd/config.go

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package main
77

88
import (
9+
"fmt"
10+
"io"
911
"os"
1012
"strconv"
1113
"time"
@@ -39,8 +41,24 @@ var removeSingleAssetStacks bool
3941
** @return *logrus.Logger - Configured logger instance
4042
**************************************************************************************************/
4143
func configureLogger() *logrus.Logger {
44+
return configureLoggerWithOutput(nil)
45+
}
46+
47+
/**************************************************************************************************
48+
** configureLoggerWithOutput is like configureLogger but accepts an optional output writer
49+
** for testing purposes. If output is nil, uses the default output.
50+
**
51+
** @param output - Optional output writer for testing
52+
** @return *logrus.Logger - Configured logger instance
53+
**************************************************************************************************/
54+
func configureLoggerWithOutput(output io.Writer) *logrus.Logger {
4255
logger := logrus.New()
4356

57+
// Set output if provided (for testing)
58+
if output != nil {
59+
logger.SetOutput(output)
60+
}
61+
4462
// Set log level - flag takes precedence over environment variable
4563
level := logLevel
4664
if level == "" {
@@ -75,19 +93,28 @@ func configureLogger() *logrus.Logger {
7593
}
7694

7795
/**************************************************************************************************
78-
** Loads environment variables and command-line flags, with flags taking precedence over env
79-
** variables. Handles critical configuration like API credentials and operation modes.
96+
** LoadEnvConfig represents the result of environment loading, including any validation errors.
97+
**************************************************************************************************/
98+
type LoadEnvConfig struct {
99+
Logger *logrus.Logger
100+
Error error
101+
}
102+
103+
/**************************************************************************************************
104+
** LoadEnvForTesting loads environment variables and validates configuration without calling Fatal().
105+
** Returns errors instead of terminating, allowing tests to verify error conditions.
80106
**
81-
** @param logger - Logger instance for outputting configuration status and errors
107+
** @return LoadEnvConfig - Configuration result with logger and any validation error
82108
**************************************************************************************************/
83-
func loadEnv() *logrus.Logger {
109+
func LoadEnvForTesting() LoadEnvConfig {
84110
_ = godotenv.Load()
85111
logger := configureLogger()
112+
86113
if apiKey == "" {
87114
apiKey = os.Getenv("API_KEY")
88115
}
89116
if apiKey == "" {
90-
logger.Fatal("API_KEY is not set")
117+
return LoadEnvConfig{Logger: logger, Error: fmt.Errorf("API_KEY is not set")}
91118
}
92119
if apiURL == "" {
93120
apiURL = os.Getenv("API_URL")
@@ -116,12 +143,12 @@ func loadEnv() *logrus.Logger {
116143
}
117144
if resetStacks {
118145
if runMode != "once" {
119-
logger.Fatal("RESET_STACKS can only be used in 'once' run mode. Aborting.")
146+
return LoadEnvConfig{Logger: logger, Error: fmt.Errorf("RESET_STACKS can only be used in 'once' run mode")}
120147
}
121148
confirmReset := os.Getenv("CONFIRM_RESET_STACK")
122149
const requiredConfirm = "I acknowledge all my current stacks will be deleted and new one will be created"
123150
if confirmReset != requiredConfirm {
124-
logger.Fatalf("To use RESET_STACKS, you must set CONFIRM_RESET_STACK to: '%s'", requiredConfirm)
151+
return LoadEnvConfig{Logger: logger, Error: fmt.Errorf("to use RESET_STACKS, you must set CONFIRM_RESET_STACK to: '%s'", requiredConfirm)}
125152
}
126153
logger.Info("RESET_STACKS is set to true, all existing stacks will be deleted")
127154
}
@@ -153,8 +180,19 @@ func loadEnv() *logrus.Logger {
153180
parentExtPromote = envVal
154181
}
155182
}
156-
if criteria == "" {
157-
criteria = os.Getenv("CRITERIA")
183+
return LoadEnvConfig{Logger: logger, Error: nil}
184+
}
185+
186+
/**************************************************************************************************
187+
** Loads environment variables and command-line flags, with flags taking precedence over env
188+
** variables. Handles critical configuration like API credentials and operation modes.
189+
**
190+
** @param logger - Logger instance for outputting configuration status and errors
191+
**************************************************************************************************/
192+
func loadEnv() *logrus.Logger {
193+
config := LoadEnvForTesting()
194+
if config.Error != nil {
195+
config.Logger.Fatal(config.Error.Error())
158196
}
159-
return logger
197+
return config.Logger
160198
}

cmd/fixtrash.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func runFixTrash(cmd *cobra.Command, args []string) {
106106

107107
for idx, trashedAsset := range trashedAssets {
108108
// Show progress every 50 assets or in debug mode
109-
if logger.Level == logrus.DebugLevel || (idx > 0 && idx%50 == 0) {
109+
if logger.IsLevelEnabled(logrus.DebugLevel) || (idx > 0 && idx%50 == 0) {
110110
logger.Infof(" Analyzing trashed asset %d/%d...", idx+1, len(trashedAssets))
111111
}
112112
logger.Debugf("Analyzing trashed asset: %s", trashedAsset.OriginalFileName)
@@ -163,7 +163,7 @@ func runFixTrash(cmd *cobra.Command, args []string) {
163163
assetIDs := make([]string, 0, len(assetsToTrash))
164164

165165
// In debug mode, show detailed mapping
166-
if logger.Level == logrus.DebugLevel {
166+
if logger.IsLevelEnabled(logrus.DebugLevel) {
167167
logger.Debug("\n📋 Summary of assets to trash:")
168168
// Group by the trashed asset that caused them to be marked
169169
groupedByTrashed := make(map[string][]utils.TAsset)

cmd/main.go

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,10 @@ import (
1313
)
1414

1515
/**************************************************************************************************
16-
** Application entry point. Sets up the CLI command structure using Cobra, including all
17-
** available commands and their associated flags. Handles command execution and error
18-
** reporting.
16+
** bindFlags adds all persistent flags to the root command. This shared function eliminates
17+
** duplication between CreateRootCommand and CreateTestableRootCommand.
1918
**************************************************************************************************/
20-
func main() {
21-
var rootCmd = &cobra.Command{
22-
Use: "immich-stack",
23-
Short: "Immich Stack CLI",
24-
Long: "A tool to automatically stack Immich assets.",
25-
Run: runStacker,
26-
}
27-
28-
var duplicatesCmd = &cobra.Command{
29-
Use: "duplicates",
30-
Short: "List duplicate assets",
31-
Long: "Scan your Immich library and list duplicate assets based on filename and timestamp.",
32-
Run: runDuplicates,
33-
}
34-
35-
var fixTrashCmd = &cobra.Command{
36-
Use: "fix-trash",
37-
Short: "Fix incomplete stack trash operations",
38-
Long: "Scan trash for assets and move related stack members to trash to maintain consistency.",
39-
Run: runFixTrash,
40-
}
41-
19+
func bindFlags(rootCmd *cobra.Command) {
4220
rootCmd.PersistentFlags().StringVar(&apiKey, "api-key", "", "API key (or set API_KEY env var)")
4321
rootCmd.PersistentFlags().StringVar(&apiURL, "api-url", "", "API URL (or set API_URL env var)")
4422
rootCmd.PersistentFlags().BoolVar(&resetStacks, "reset-stacks", false, "Delete all existing stacks (or set RESET_STACKS=true)")
@@ -53,10 +31,54 @@ func main() {
5331
rootCmd.PersistentFlags().IntVar(&cronInterval, "cron-interval", 0, "Cron interval (or set CRON_INTERVAL env var)")
5432
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "", "Log level: debug, info, warn, error (or set LOG_LEVEL env var)")
5533
rootCmd.PersistentFlags().BoolVar(&removeSingleAssetStacks, "remove-single-asset-stacks", false, "Remove stacks with only one asset (or set REMOVE_SINGLE_ASSET_STACKS=true)")
34+
}
35+
36+
/**************************************************************************************************
37+
** addSubcommands adds all subcommands to the root command with their run functions.
38+
**************************************************************************************************/
39+
func addSubcommands(rootCmd *cobra.Command) {
40+
var duplicatesCmd = &cobra.Command{
41+
Use: "duplicates",
42+
Short: "List duplicate assets",
43+
Long: "Scan your Immich library and list duplicate assets based on filename and timestamp.",
44+
Run: runDuplicates,
45+
}
46+
47+
var fixTrashCmd = &cobra.Command{
48+
Use: "fix-trash",
49+
Short: "Fix incomplete stack trash operations",
50+
Long: "Scan trash for assets and move related stack members to trash to maintain consistency.",
51+
Run: runFixTrash,
52+
}
5653

5754
rootCmd.AddCommand(duplicatesCmd)
5855
rootCmd.AddCommand(fixTrashCmd)
56+
}
57+
58+
/**************************************************************************************************
59+
** CreateRootCommand creates and returns the root command with all subcommands and flags.
60+
** This is exported to allow testing of the real command structure.
61+
**************************************************************************************************/
62+
func CreateRootCommand() *cobra.Command {
63+
var rootCmd = &cobra.Command{
64+
Use: "immich-stack",
65+
Short: "Immich Stack CLI",
66+
Long: "A tool to automatically stack Immich assets.",
67+
Run: runStacker,
68+
}
5969

70+
bindFlags(rootCmd)
71+
addSubcommands(rootCmd)
72+
return rootCmd
73+
}
74+
75+
/**************************************************************************************************
76+
** Application entry point. Sets up the CLI command structure using Cobra, including all
77+
** available commands and their associated flags. Handles command execution and error
78+
** reporting.
79+
**************************************************************************************************/
80+
func main() {
81+
rootCmd := CreateRootCommand()
6082
if err := rootCmd.Execute(); err != nil {
6183
os.Exit(1)
6284
}

cmd/main_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**************************************************************************************************
2+
** Test-only command creation utilities - only available during testing
3+
**************************************************************************************************/
4+
5+
package main
6+
7+
import (
8+
"io"
9+
10+
"github.com/sirupsen/logrus"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
/**************************************************************************************************
15+
** CreateTestableRootCommand mirrors CreateRootCommand but is kept separate for tests that want
16+
** to override Run or inject args without affecting the real command symbol.
17+
**************************************************************************************************/
18+
func CreateTestableRootCommand() *cobra.Command {
19+
var rootCmd = &cobra.Command{
20+
Use: "immich-stack",
21+
Short: "Immich Stack CLI",
22+
Long: "A tool to automatically stack Immich assets.",
23+
Run: runStacker,
24+
}
25+
26+
bindFlags(rootCmd)
27+
addSubcommands(rootCmd)
28+
return rootCmd
29+
}
30+
31+
/**************************************************************************************************
32+
** configureLoggerForTesting allows tests to capture log output from configureLogger.
33+
** This enables proper testing of warning messages.
34+
**************************************************************************************************/
35+
func configureLoggerForTesting(output io.Writer) *logrus.Logger {
36+
return configureLoggerWithOutput(output)
37+
}

cmd/stacker.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,11 @@ func runStackerOnce(client *immich.Client, logger *logrus.Logger) {
245245
** Adding info logs, but only if we are not in debug mode.
246246
******************************************************************************************/
247247
{
248-
if logger.Level != logrus.DebugLevel {
248+
if !logger.IsLevelEnabled(logrus.DebugLevel) {
249249
logger.Infof("--------------------------------")
250250
logger.Infof("%d/%d Key: %s", i+1, len(stacks), stack[0].OriginalFileName)
251251
}
252-
if logger.Level != logrus.DebugLevel {
252+
if !logger.IsLevelEnabled(logrus.DebugLevel) {
253253
logger.WithFields(logrus.Fields{
254254
"Name": stack[0].OriginalFileName,
255255
"ID": stack[0].ID,

0 commit comments

Comments
 (0)