diff --git a/cmdduplicate/duplicate.go b/cmdduplicate/duplicate.go index c7c16210..da751ac7 100644 --- a/cmdduplicate/duplicate.go +++ b/cmdduplicate/duplicate.go @@ -10,6 +10,7 @@ import ( "immich-go/immich/logger" "immich-go/ui" "sort" + "strconv" "strings" "time" ) @@ -18,7 +19,7 @@ type DuplicateCmd struct { logger *logger.Logger Immich *immich.ImmichClient // Immich client - Confirm bool // Display actions but don't change anything + AssumeYes bool // Display actions but don't change anything DateRange immich.DateRange // Set capture date range } @@ -30,14 +31,18 @@ type duplicateKey struct { func NewDuplicateCmd(ctx context.Context, ic *immich.ImmichClient, logger *logger.Logger, args []string) (*DuplicateCmd, error) { cmd := flag.NewFlagSet("duplicate", flag.ExitOnError) validRange := immich.DateRange{} - validRange.Set("1850-01-04,2100-01-01") + validRange.Set("1850-01-04,2030-01-01") app := DuplicateCmd{ logger: logger, Immich: ic, DateRange: validRange, } - cmd.BoolVar(&app.Confirm, "confirm", true, "When true, actions must be confirmed") + cmd.BoolFunc("yes", "When true, assume Yes to all actions", func(s string) error { + var err error + app.AssumeYes, err = strconv.ParseBool(s) + return err + }) cmd.Var(&app.DateRange, "date", "Process only document having a capture date in that range.") err := cmd.Parse(args) return &app, err @@ -111,7 +116,7 @@ func DuplicateCommand(ctx context.Context, ic *immich.ImmichClient, log *logger. default: app.logger.OK("There are %d copies of the asset %s, taken on %s ", len(duplicate[k]), k.Name, k.Date.Format(time.RFC3339)) l := duplicate[k] - albums := []string{} + albums := []immich.AlbumSimplified{} delete := []string{} sort.Slice(l, func(i, j int) bool { return l[i].ExifInfo.FileSizeInByte < l[j].ExifInfo.FileSizeInByte }) for p, a := range duplicate[k] { @@ -123,27 +128,30 @@ func DuplicateCommand(ctx context.Context, ic *immich.ImmichClient, log *logger. log.Error("Can't get asset's albums: %s", err.Error()) } else { for _, al := range r { - albums = append(albums, al.ID) + albums = append(albums, al) } } } else { log.OK(" %s %dx%d, %s, %s to be kept", a.OriginalFileName, a.ExifInfo.ExifImageWidth, a.ExifInfo.ExifImageHeight, ui.FormatBytes(a.ExifInfo.FileSizeInByte), a.OriginalPath) - yes := !app.Confirm - if app.Confirm { - r := ui.ConfirmYesNo("Proceed?", "n") + yes := app.AssumeYes + if !app.AssumeYes { + r, err := ui.ConfirmYesNo(ctx, "Proceed?", "n") + if err != nil { + return err + } if r == "y" { yes = true } } if yes { - log.OK("Asset removed") + log.OK(" Asset removed") _, err = app.Immich.DeleteAssets(ctx, delete) if err != nil { log.Error("Can't delete asset: %s", err.Error()) } for _, al := range albums { - log.OK("Update the album %s with the best copy", al) - _, err = app.Immich.AddAssetToAlbum(ctx, al, []string{a.ID}) + log.OK(" Update the album %s with the best copy", al.AlbumName) + _, err = app.Immich.AddAssetToAlbum(ctx, al.ID, []string{a.ID}) if err != nil { log.Error("Can't delete asset: %s", err.Error()) } diff --git a/readme.md b/readme.md index a99621db..bc5ea7f0 100644 --- a/readme.md +++ b/readme.md @@ -107,7 +107,8 @@ Use this command for analyzing the content of your `immich` server to find any f Before deleting the inferior copies, the system get all albums they belong to, and add the superior copy to them. ### Switches and options: -`-dry-run` Preview all actions as they would be done (default: TRUE).
+`-yes` Assume Yes to all questions (default: FALSE).
+`-date` Check only assets have a date of capture in the given range. (default: 1850-01-04,2030-01-01) ### Example Usage: clean the `immich` server after having merged a google photo archive and original files @@ -245,6 +246,10 @@ Additionally, deploying a Node.js program on user machines presents challenges. # Release notes +- Improvement of duplicate command + - `-yes` option to assume Yes to all questions + - `-date` to limit the check to a a given date range + ### Release 0.2.2 - improvement of date of capture when there isn't any exif data in the file diff --git a/ui/ask.go b/ui/ask.go index 33b4e43a..a37030c2 100644 --- a/ui/ask.go +++ b/ui/ask.go @@ -4,12 +4,14 @@ import ( "bufio" "context" "fmt" - "io" "os" "strings" ) -func ConfirmYesNo(prompt string, defaultAnswer string) string { +func ConfirmYesNo(ctx context.Context, prompt string, defaultAnswer string) (string, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + reader := bufio.NewReader(os.Stdin) defaultAnswer = strings.ToLower(defaultAnswer) other := "n" @@ -17,62 +19,32 @@ func ConfirmYesNo(prompt string, defaultAnswer string) string { other = "y" } - for { - fmt.Printf("%s [%s]/%s: ", prompt, defaultAnswer, other) - userInput, _ := reader.ReadString('\n') - userInput = strings.ToLower(strings.TrimSpace(userInput)) - switch userInput { - case "": - return defaultAnswer - case "y", "n": - return userInput - } - } -} - -type CancelableReader struct { - ctx context.Context - data chan []byte - err error - r io.Reader -} + runeChan := make(chan (rune)) -func (c *CancelableReader) begin() { - buf := make([]byte, 1024) - for { - n, err := c.r.Read(buf) - if n > 0 { - tmp := make([]byte, n) - copy(tmp, buf[:n]) - c.data <- tmp + go func() { + for { + r, _, _ := reader.ReadRune() + select { + case runeChan <- r: + case <-ctx.Done(): + return + } } - if err != nil { - c.err = err - close(c.data) - return - } - } -} + }() -func (c *CancelableReader) Read(p []byte) (int, error) { - select { - case <-c.ctx.Done(): - return 0, c.ctx.Err() - case d, ok := <-c.data: - if !ok { - return 0, c.err + for { + fmt.Printf("%s [%s]/%s: ", prompt, defaultAnswer, other) + select { + case r := <-runeChan: + userInput := strings.ToLower(string(r)) + switch userInput { + case "": + return defaultAnswer, nil + case "y", "n": + return userInput, nil + } + case <-ctx.Done(): + return "", ctx.Err() } - copy(p, d) - return len(d), nil - } -} - -func New(ctx context.Context, r io.Reader) *CancelableReader { - c := &CancelableReader{ - r: r, - ctx: ctx, - data: make(chan []byte), } - go c.begin() - return c }