From 7ca446b0d093bed90f5a170d4e79620d58767624 Mon Sep 17 00:00:00 2001 From: Russell Greene Date: Mon, 11 Mar 2024 21:53:29 -0600 Subject: [PATCH] update create time when remote does not have one I uploaded my takeout directly naively, so all the times were incorrect. I updated this tool to be able to detect when the local and remote file are the same, but the local timestamp is *older* then the remote one, which indicates that the local one is better because bad timestamps seem to always be newer than real ones as they are fabricated from when downloaded from google or when uploaded to immich There were other bugs that needed fixing for this--OriginalFileName already had the extension, so I removed the + ext. Also, the updateAsset API seems to have been updated since you added it. --- cmd/upload/assets.go | 5 ++--- cmd/upload/upload.go | 35 ++++++++++++++++++++++++++++++++++- immich/asset.go | 26 +++++++++++++++----------- logger/journal.go | 42 ++++++++++++++++++++++-------------------- 4 files changed, 73 insertions(+), 35 deletions(-) diff --git a/cmd/upload/assets.go b/cmd/upload/assets.go index 96fb192d..4ca2824d 100644 --- a/cmd/upload/assets.go +++ b/cmd/upload/assets.go @@ -23,13 +23,12 @@ func (ai *AssetIndex) ReIndex() { ai.byID = map[string]*immich.Asset{} for _, a := range ai.assets { - ext := path.Ext(a.OriginalPath) - ID := fmt.Sprintf("%s-%d", strings.ToUpper(path.Base(a.OriginalFileName)+ext), a.ExifInfo.FileSizeInByte) + ID := fmt.Sprintf("%s-%d", strings.ToUpper(path.Base(a.OriginalFileName)), a.ExifInfo.FileSizeInByte) l := ai.byHash[a.Checksum] l = append(l, a) ai.byHash[a.Checksum] = l - n := a.OriginalFileName + ext + n := a.OriginalFileName l = ai.byName[n] l = append(l, a) ai.byName[n] = l diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go index c70c5ce7..79143d27 100644 --- a/cmd/upload/upload.go +++ b/cmd/upload/upload.go @@ -433,6 +433,9 @@ func (app *UpCmd) handleAsset(ctx context.Context, a *browser.LocalAssetFile) er app.journalAsset(a, logger.INFO, willBeAddedToAlbum+app.PartnerAlbum) app.AddToAlbum(advice.ServerAsset.ID, app.PartnerAlbum) } + case LocalMetadataBetter: + app.journalAsset(a, logger.LocalMetadataBetter, advice.Message) + app.UpdateAsset(ctx, advice.ServerAsset, a) } if err != nil { @@ -560,6 +563,19 @@ func (app *UpCmd) UploadAsset(ctx context.Context, a *browser.LocalAssetFile) (s return resp.ID, nil } +func (app *UpCmd) UpdateAsset(ctx context.Context, s *immich.Asset, l *browser.LocalAssetFile) error { + var err error = nil + if !app.DryRun { + _, err = app.Immich.UpdateAsset(ctx, s.ID, l) + } + + if err != nil { + app.journalAsset(l, logger.ServerError, err.Error()) + } + + return err +} + func (app *UpCmd) albumName(al browser.LocalAlbum) string { Name := al.Name if app.GooglePhotos { @@ -689,6 +705,7 @@ const ( IDontKnow AdviceCode = iota SmallerOnServer BetterOnServer + LocalMetadataBetter SameOnServer NotOnServer ) @@ -747,6 +764,14 @@ func (ai *AssetIndex) adviceNotOnServer() *Advice { } } +func (ai *AssetIndex) adviceLocalMetadataBetter(sa *immich.Asset, la *browser.LocalAssetFile) *Advice { + return &Advice{ + Advice: LocalMetadataBetter, + Message: fmt.Sprintf("An asset with the same name:%q exists, but local metadata is better (local date: %q, server date: %q)", sa.OriginalFileName, la.DateTaken.Format(time.DateTime), sa.ExifInfo.DateTimeOriginal.Format(time.DateTime)), + ServerAsset: sa, + } +} + // ShouldUpload check if the server has this asset // // The server may have different assets with the same name. This happens with photos produced by digital cameras. @@ -761,9 +786,17 @@ func (ai *AssetIndex) ShouldUpload(la *browser.LocalAssetFile) (*Advice, error) ID := la.DeviceAssetID() sa := ai.byID[ID] + if sa != nil { // the same ID exist on the server - return ai.adviceSameOnServer(sa), nil + // logic here is *older* is almost certinally better than newer timestamps, because + // if a file is missing a timestamp, it will be replaced with the download timestamp + // (in the case of Photos Takeout) or upload timestamp (otherwise), which has to be after the take date + if compareDate(la.DateTaken, sa.ExifInfo.DateTimeOriginal.Time) < 0 { + return ai.adviceLocalMetadataBetter(sa, la), nil + } else { + return ai.adviceSameOnServer(sa), nil + } } var l []*immich.Asset diff --git a/immich/asset.go b/immich/asset.go index af6f705b..e1b7b8bd 100644 --- a/immich/asset.go +++ b/immich/asset.go @@ -226,21 +226,25 @@ func (ic *ImmichClient) UpdateAssets(ctx context.Context, ids []string, func (ic *ImmichClient) UpdateAsset(ctx context.Context, id string, a *browser.LocalAssetFile) (*Asset, error) { type updAsset struct { - IsArchived bool `json:"isArchived"` - IsFavorite bool `json:"isFavorite"` - Latitude float64 `json:"latitude,omitempty"` - Longitude float64 `json:"longitude,omitempty"` - Description string `json:"description,omitempty"` + DateTimeOriginal string `json:"dateTimeOriginal"` + IDs []string `json:"ids"` + IsArchived bool `json:"isArchived"` + IsFavorite bool `json:"isFavorite"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` + Description string `json:"description,omitempty"` } param := updAsset{ - Description: a.Description, - IsArchived: a.Archived, - IsFavorite: a.Favorite, - Latitude: a.Latitude, - Longitude: a.Longitude, + DateTimeOriginal: a.DateTaken.Format(time.RFC3339), + IDs: []string{id}, + Description: a.Description, + IsArchived: a.Archived, + IsFavorite: a.Favorite, + Latitude: a.Latitude, + Longitude: a.Longitude, } r := Asset{} - err := ic.newServerCall(ctx, "updateAsset").do(put("/asset/"+id, setJSONBody(param)), responseJSON(&r)) + err := ic.newServerCall(ctx, "updateAsset").do(put("/asset/", setJSONBody(param)), responseJSON(&r)) return &r, err } diff --git a/logger/journal.go b/logger/journal.go index 0dbac89f..b85f50c4 100644 --- a/logger/journal.go +++ b/logger/journal.go @@ -14,26 +14,27 @@ type Journal struct { type Action string const ( - DiscoveredFile Action = "File" - ScannedImage Action = "Scanned image" - ScannedVideo Action = "Scanned video" - Discarded Action = "Discarded" - Uploaded Action = "Uploaded" - Upgraded Action = "Server's asset upgraded" - ERROR Action = "Error" - LocalDuplicate Action = "Local duplicate" - ServerDuplicate Action = "Server has photo" - Stacked Action = "Stacked" - ServerBetter Action = "Server's asset is better" - Album Action = "Added to an album" - LivePhoto Action = "Live photo" - FailedVideo Action = "Failed video" - Unsupported Action = "File type not supported" - Metadata Action = "Metadata files" - AssociatedMetadata Action = "Associated with metadata" - INFO Action = "Info" - NotSelected Action = "Not selected because options" - ServerError Action = "Server error" + DiscoveredFile Action = "File" + ScannedImage Action = "Scanned image" + ScannedVideo Action = "Scanned video" + Discarded Action = "Discarded" + Uploaded Action = "Uploaded" + Upgraded Action = "Server's asset upgraded" + ERROR Action = "Error" + LocalDuplicate Action = "Local duplicate" + ServerDuplicate Action = "Server has photo" + Stacked Action = "Stacked" + ServerBetter Action = "Server's asset is better" + LocalMetadataBetter Action = "Server's asset is identical, but better local metadata" + Album Action = "Added to an album" + LivePhoto Action = "Live photo" + FailedVideo Action = "Failed video" + Unsupported Action = "File type not supported" + Metadata Action = "Metadata files" + AssociatedMetadata Action = "Associated with metadata" + INFO Action = "Info" + NotSelected Action = "Not selected because options" + ServerError Action = "Server error" ) func NewJournal(log Logger) *Journal { @@ -89,6 +90,7 @@ func (j *Journal) Report() { j.Log.OK("%6d uploaded files on the server", j.counts[Uploaded]) j.Log.OK("%6d upgraded files on the server", j.counts[Upgraded]) j.Log.OK("%6d files already on the server", j.counts[ServerDuplicate]) + j.Log.OK("%6d files adready on the server, but updated create time", j.counts[LocalMetadataBetter]) j.Log.OK("%6d discarded files because of options", j.counts[NotSelected]) j.Log.OK("%6d discarded files because duplicated in the input", j.counts[LocalDuplicate]) j.Log.OK("%6d discarded files because server has a better image", j.counts[ServerBetter])