Skip to content

Commit

Permalink
add duplicate command
Browse files Browse the repository at this point in the history
  • Loading branch information
laaqxdze1k committed Aug 20, 2023
1 parent 12cbe5f commit 9ef7d1d
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 5 deletions.
156 changes: 156 additions & 0 deletions dupcmd/duplicate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
Check the list of photos to list and discard duplicates.
*/
package dupcmd

import (
"context"
"flag"
"immich-go/immich"
"immich-go/immich/logger"
"sort"
"strings"
"time"
)

type DuplicateCmd struct {
logger *logger.Logger
Immich *immich.ImmichClient // Immich client

DryRun bool // Display actions but don't change anything
DateRange immich.DateRange // Set capture date range
}

type duplicateKey struct {
Date time.Time
Name string
}

func NewDuplicateCmd(ctx context.Context, ic *immich.ImmichClient, logger *logger.Logger, args []string) (*DuplicateCmd, error) {
cmd := flag.NewFlagSet("upload", flag.ExitOnError)
validRange := immich.DateRange{}
validRange.Set("1850-01-04,3000-01-01")
app := DuplicateCmd{
logger: logger,
Immich: ic,
DateRange: validRange,
}

cmd.BoolVar(&app.DryRun, "dry-run", true, "display actions but don't touch source or destination")
cmd.Var(&app.DateRange, "date", "Process only document having a capture date in that range.")
err := cmd.Parse(args)
return &app, err
}

func DuplicateCommand(ctx context.Context, ic *immich.ImmichClient, log *logger.Logger, args []string) error {
app, err := NewDuplicateCmd(ctx, ic, log, args)
if err != nil {
return err
}

log.MessageContinue(logger.OK, "Get server's assets...")
var list []*immich.Asset
list, err = app.Immich.GetAllAssets(ctx, nil)
if err != nil {
return err
}
log.MessageTerminate(logger.OK, "%d received", len(list))

log.MessageContinue(logger.OK, "Analyzing...")
duplicate := map[duplicateKey][]*immich.Asset{}

count := 0
dupCount := 0
for _, a := range list {
select {
case <-ctx.Done():
return ctx.Err()
default:
count++
if app.DateRange.InRange(a.ExifInfo.DateTimeOriginal) {
k := duplicateKey{
Date: a.ExifInfo.DateTimeOriginal,
Name: a.OriginalFileName,
}
l := duplicate[k]
if len(l) > 0 {
dupCount++
}
l = append(l, a)
duplicate[k] = l
}
if count%253 == 0 {
log.Progress("%d medias, %d duplicate(s)...", count, dupCount)
}
}
}
log.Progress("%d medias, %d duplicate(s)...", count, dupCount)
log.MessageTerminate(logger.OK, " analyze completed.")

keys := []duplicateKey{}
for k, l := range duplicate {
if len(l) < 2 {
continue
}
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
switch keys[i].Date.Compare(keys[j].Date) {
case -1:
return true
case +1:
return false
}
return strings.Compare(keys[i].Name, keys[j].Name) == -1
})

for _, k := range keys {
select {
case <-ctx.Done():
return ctx.Err()
default:
app.logger.Info("%s:", k.Date.Format(time.RFC3339))
l := duplicate[k]
albums := []string{}
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] {
if p < len(l)-1 {
log.Info(" %s(%s) %dx%d, %d bytes: delete", a.OriginalFileName, a.ID, a.ExifInfo.ExifImageWidth, a.ExifInfo.ExifImageHeight, a.ExifInfo.FileSizeInByte)
delete = append(delete, a.ID)
r, err := app.Immich.GetAssetAlbums(ctx, a.ID)
if err != nil {
log.Error("Can't get asset's albums: %s", err.Error())
} else {
for _, al := range r {
albums = append(albums, al.ID)
}
}
} else {
log.Info(" %s(%s) %dx%d, %d bytes: keep", a.OriginalFileName, a.ID, a.ExifInfo.ExifImageWidth, a.ExifInfo.ExifImageHeight, a.ExifInfo.FileSizeInByte)
if !app.DryRun {
log.OK("Deleting following assets: %s", strings.Join(delete, ","))
_, err = app.Immich.DeleteAssets(ctx, delete)
if err != nil {
log.Error("Can't delete asset: %s", err.Error())
}
} else {
log.Info("Skip deleting following %s, dry run mode", strings.Join(delete, ","))
}
for _, al := range albums {
if !app.DryRun {
log.OK("Adding %s to album %s", a.ID, al)
_, err = app.Immich.AddAssetToAlbum(ctx, al, []string{a.ID})
if err != nil {
log.Error("Can't delete asset: %s", err.Error())
}
} else {
log.OK("Skip Adding %s to album %s, dry run mode", a.ID, al)
}
}
}
}
}
}
return nil
}
8 changes: 8 additions & 0 deletions immich/albums.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,11 @@ func (ic *ImmichClient) CreateAlbum(ctx context.Context, name string, assets []s
}
return r, nil
}

func (ic *ImmichClient) GetAssetAlbums(ctx context.Context, id string) ([]AlbumSimplified, error) {
var r []AlbumSimplified
err := ic.newServerCall(ctx, "GetAssetAlbums").do(
get("/album?assetId="+id, setAcceptJSON()),
responseJSON(&r))
return r, err
}
2 changes: 1 addition & 1 deletion immich/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ type deleteResponse []struct {
Status string `json:"status"`
}

func (ic *ImmichClient) DeleteAsset(ctx context.Context, id []string) (*deleteResponse, error) {
func (ic *ImmichClient) DeleteAssets(ctx context.Context, id []string) (*deleteResponse, error) {
req := struct {
IDs []string `json:"ids"`
}{
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"flag"
"fmt"
"immich-go/dupcmd"
"immich-go/immich"
"immich-go/immich/logger"
"immich-go/upcmd"
Expand Down Expand Up @@ -110,6 +111,8 @@ func Run(ctx context.Context) error {
switch cmd {
case "upload":
err = upcmd.UploadCommand(ctx, app.Immich, app.Logger, flag.Args()[1:])
case "duplicate":
err = dupcmd.DuplicateCommand(ctx, app.Immich, app.Logger, flag.Args()[1:])
default:
err = fmt.Errorf("unknwon command: %q", cmd)
}
Expand Down
28 changes: 27 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ immich-go -server URL -key KEY COMMAND -options... folder1|zip1 folder2|zip2..

## Command `upload`

Use this command for uploading photos and videos from a local directory, a zipped folder or all zip files that google photo takeout procedure has generated.
### Switches and options:
`-album "ALBUM NAME"` Import assets into the Immich album `ALBUM NAME`.<br>
`-device-uuid VALUE` Force the device identification (default $HOSTNAME).<br>
Expand All @@ -81,7 +82,7 @@ Specialized options for Google Photos management:
`-keep-trashed <bool>` Determines whether to import trashed images (default: FALSE).<br>


## Example Usage
### Example Usage: uploading a Google photos takeout archive

To illustrate, here's a command importing photos from a Google Photos takeout archive captured between June 1st and June 30th, 2019, while auto-generating albums:

Expand All @@ -90,6 +91,25 @@ To illustrate, here's a command importing photos from a Google Photos takeout ar
-create-albums -google-photos -date=2019-06 ~/Download/takeout-20230715T073439Z-001.zip ~/Download/takeout-20230715T073439Z-002.zip
```

## Command `duplicate`

Use this command for analyzing the content of your `immich` server to find any files that share the same file name, the date of capture, but having different size.
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).<br>


### Example Usage: clean the `immich` server after having merged a google photo archive and original files

This command examine the immich server content, remove less quality images, and preserve albums.

NOTE: You should disable the dry run mode explicitly.

```sh
./immich-go -server=http://mynas:2283 -key=zzV6k65KGLNB9mpGeri9n8Jk1VaNGHSCdoH1dY8jQ duplicate -dry-run=false
```


# Merging strategy

Expand Down Expand Up @@ -208,3 +228,9 @@ Additionally, deploying a Node.js program on user machines presents challenges.
- [ ] samba
- [ ] import remote folder

# Wanted features
- [ ] Import instead of Upload
- [x] Cleaning duplicates
- [ ] Set GPS location for images taken with a GPS-less camera based on
- [ ] Google location history
- [ ] KML,GPX track files
6 changes: 3 additions & 3 deletions upcmd/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (app *UpCmd) handleAsset(ctx context.Context, a *assets.LocalAssetFile) err
if app.DateRange.IsSet() {
d, err := a.DateTaken()
if err != nil {
app.logger.Error("Can't get capture date of the file. File %q skiped", a.FileName)
app.logger.Error("Can't get capture date of the file. File %q skipped", a.FileName)
return nil
}
if !app.DateRange.InRange(d) {
Expand Down Expand Up @@ -243,7 +243,7 @@ func (app *UpCmd) handleAsset(ctx context.Context, a *assets.LocalAssetFile) err

func NewUpCmd(ctx context.Context, ic *immich.ImmichClient, logger *logger.Logger, args []string) (*UpCmd, error) {
var err error
cmd := flag.NewFlagSet("generate", flag.ExitOnError)
cmd := flag.NewFlagSet("upload", flag.ExitOnError)

app := UpCmd{
updateAlbums: map[string][]string{},
Expand Down Expand Up @@ -354,7 +354,7 @@ func (app *UpCmd) DeleteServerAssets(ctx context.Context, ids []string) error {
app.logger.Warning("%d server assets to delete.", len(ids))

if !app.DryRun {
_, err := app.Immich.DeleteAsset(ctx, ids)
_, err := app.Immich.DeleteAssets(ctx, ids)
return err
}
app.logger.Warning("%d server assets to delete. skipped dry-run mode", len(ids))
Expand Down

0 comments on commit 9ef7d1d

Please sign in to comment.