From 620dd1800a527b87f24abf931247279e4d833e26 Mon Sep 17 00:00:00 2001 From: Chaoscontrol Date: Fri, 25 Jul 2025 23:41:56 +0100 Subject: [PATCH 1/4] feat: add --include-shared-album option for Google Photos import - Add FromSharedAlbum field to GoogleMetaData, Metadata, and Asset structs - Add isSharedAlbum() method to detect shared album assets - Add --include-shared-album CLI flag (default: true) - Update asset filtering logic to respect the new flag - Update documentation in readme.md This option allows users to exclude photos from shared albums during import, similar to how --include-partner works for partner photos. --- adapters/googlePhotos/googlephotos.go | 5 +++++ adapters/googlePhotos/json.go | 9 +++++++++ adapters/googlePhotos/options.go | 4 ++++ internal/assets/asset.go | 7 ++++--- internal/assets/metadata.go | 2 ++ readme.md | 1 + 6 files changed, 25 insertions(+), 3 deletions(-) diff --git a/adapters/googlePhotos/googlephotos.go b/adapters/googlePhotos/googlephotos.go index 87cd9c69..6e3e551f 100644 --- a/adapters/googlePhotos/googlephotos.go +++ b/adapters/googlePhotos/googlephotos.go @@ -573,6 +573,11 @@ func (to *Takeout) filterOnMetadata(ctx context.Context, a *assets.Asset) fileev a.Close() return fileevent.DiscoveredDiscarded } + if !to.flags.KeepSharedAlbum && a.FromSharedAlbum { + to.logMessage(ctx, fileevent.DiscoveredDiscarded, a, "discarding shared album file") + a.Close() + return fileevent.DiscoveredDiscarded + } if !to.flags.KeepTrashed && a.Trashed { to.logMessage(ctx, fileevent.DiscoveredDiscarded, a, "discarding trashed file") a.Close() diff --git a/adapters/googlePhotos/json.go b/adapters/googlePhotos/json.go index 7c039afa..d3fa3708 100644 --- a/adapters/googlePhotos/json.go +++ b/adapters/googlePhotos/json.go @@ -28,6 +28,7 @@ type GoogleMetaData struct { People []Person `json:"people,omitempty"` // People tags GooglePhotosOrigin struct { FromPartnerSharing googIsPresent `json:"fromPartnerSharing,omitempty"` // true when this is a partner's asset + FromSharedAlbum googIsPresent `json:"fromSharedAlbum,omitempty"` // true when this is from a shared album } `json:"googlePhotosOrigin"` } @@ -87,6 +88,7 @@ func (gmd GoogleMetaData) AsMetadata(name fshelper.FSAndName, tagPeople bool) *a Archived: gmd.Archived, Favorited: gmd.Favorited, FromPartner: gmd.isPartner(), + FromSharedAlbum: gmd.isSharedAlbum(), } if gmd.GeoDataExif != nil { md.Latitude, md.Longitude = gmd.GeoDataExif.Latitude, gmd.GeoDataExif.Longitude @@ -130,6 +132,13 @@ func (gmd *GoogleMetaData) isPartner() bool { return bool(gmd.GooglePhotosOrigin.FromPartnerSharing) } +func (gmd *GoogleMetaData) isSharedAlbum() bool { + if gmd == nil { + return false + } + return bool(gmd.GooglePhotosOrigin.FromSharedAlbum) +} + // Key return an expected unique key for the asset // based on the title and the timestamp func (gmd GoogleMetaData) Key() string { diff --git a/adapters/googlePhotos/options.go b/adapters/googlePhotos/options.go index bc56bd58..9923c213 100644 --- a/adapters/googlePhotos/options.go +++ b/adapters/googlePhotos/options.go @@ -33,6 +33,9 @@ type ImportFlags struct { // KeepPartner determines whether to import photos from the partner's Google Photos account. KeepPartner bool + // KeepSharedAlbum determines whether to import photos from shared albums. + KeepSharedAlbum bool + // KeepUntitled determines whether to include photos from albums without a title in the import process. KeepUntitled bool @@ -99,6 +102,7 @@ func (o *ImportFlags) AddFromGooglePhotosFlags(cmd *cobra.Command, parent *cobra cmd.Flags().BoolVar(&o.KeepUntitled, "include-untitled-albums", false, "Include photos from albums without a title in the import process") cmd.Flags().BoolVarP(&o.KeepTrashed, "include-trashed", "t", false, "Import photos that are marked as trashed in Google Photos") cmd.Flags().BoolVarP(&o.KeepPartner, "include-partner", "p", true, "Import photos from your partner's Google Photos account") + cmd.Flags().BoolVar(&o.KeepSharedAlbum, "include-shared-album", true, "Import photos from shared albums in Google Photos") cmd.Flags().StringVar(&o.PartnerSharedAlbum, "partner-shared-album", "", "Add partner's photo to the specified album name") cmd.Flags().BoolVarP(&o.KeepArchived, "include-archived", "a", true, "Import archived Google Photos") cmd.Flags().BoolVarP(&o.KeepJSONLess, "include-unmatched", "u", false, "Import photos that do not have a matching JSON file in the takeout") diff --git a/internal/assets/asset.go b/internal/assets/asset.go index 2d7b1c71..83ed2784 100644 --- a/internal/assets/asset.go +++ b/internal/assets/asset.go @@ -43,6 +43,7 @@ type Asset struct { Trashed bool // The asset is trashed Archived bool // The asset is archived FromPartner bool // the asset comes from a partner + FromSharedAlbum bool // the asset comes from a shared album Favorite bool // the asset is marked as favorite Rating int // the asset is marked with stars Albums []Album // List of albums the asset is in @@ -100,13 +101,12 @@ func (a *Asset) UseMetadata(md *Metadata) *Metadata { a.Latitude = md.Latitude a.Longitude = md.Longitude a.CaptureDate = md.DateTaken - a.FromPartner = md.FromPartner a.Trashed = md.Trashed a.Archived = md.Archived a.Favorite = md.Favorited a.Rating = int(md.Rating) - a.MergeAlbums(md.Albums) - a.MergeTags(md.Tags) + a.FromPartner = md.FromPartner + a.FromSharedAlbum = md.FromSharedAlbum return md } @@ -127,6 +127,7 @@ func (a Asset) LogValue() slog.Value { slog.Bool("Trashed", a.Trashed), slog.Bool("Archived", a.Archived), slog.Bool("FromPartner", a.FromPartner), + slog.Bool("FromSharedAlbum", a.FromSharedAlbum), slog.Bool("Favorite", a.Favorite), slog.Int("Stars", a.Rating), slog.String("Latitude", fmt.Sprintf("%.0f.xxxxx", a.Latitude)), diff --git a/internal/assets/metadata.go b/internal/assets/metadata.go index f3a95a29..06f71b67 100644 --- a/internal/assets/metadata.go +++ b/internal/assets/metadata.go @@ -25,6 +25,7 @@ type Metadata struct { Archived bool `json:"archived,omitempty"` // Flag to indicate if the image has been archived Favorited bool `json:"favorited,omitempty"` // Flag to indicate if the image has been favorited FromPartner bool `json:"fromPartner,omitempty"` // Flag to indicate if the image is from a partner + FromSharedAlbum bool `json:"fromSharedAlbum,omitempty"` // Flag to indicate if the image is from a shared album } func (m Metadata) LogValue() slog.Value { @@ -46,6 +47,7 @@ func (m Metadata) LogValue() slog.Value { slog.Bool("archived", m.Archived), slog.Bool("favorited", m.Favorited), slog.Bool("fromPartner", m.FromPartner), + slog.Bool("fromSharedAlbum", m.FromSharedAlbum), slog.Any("albums", m.Albums), slog.Any("tags", m.Tags), ) diff --git a/readme.md b/readme.md index aa1480a7..39564826 100644 --- a/readme.md +++ b/readme.md @@ -401,6 +401,7 @@ The **from-google-photos** sub-command processes a Google Photos takeout archive | --include-extensions | `all` | Comma-separated list of extension to include. (e.g. .jpg, .heic) | | --include-type | `all` | Single file type to include. (`VIDEO` or `IMAGE`) | | -p, --include-partner | `TRUE` | Import photos from your partner's Google Photos account | +| --include-shared-album | `TRUE` | Import photos from others in shared albums in Google Photos | | -t, --include-trashed | `FALSE` | Import photos that are marked as trashed in Google Photos | | -u, --include-unmatched | `FALSE` | Import photos that do not have a matching JSON file in the takeout | | --include-untitled-albums | `FALSE` | Include photos from albums without a title in the import process | From 0a4847c7c235c22a2028e8a68e53bd064eb87b09 Mon Sep 17 00:00:00 2001 From: Chaoscontrol Date: Sat, 26 Jul 2025 00:07:21 +0100 Subject: [PATCH 2/4] fix: restore MergeAlbums and MergeTags calls in UseMetadata Accidentally removed these important calls when adding FromSharedAlbum field. These calls are needed to preserve album and tag information from metadata. --- internal/assets/asset.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/assets/asset.go b/internal/assets/asset.go index 83ed2784..5c87bc0b 100644 --- a/internal/assets/asset.go +++ b/internal/assets/asset.go @@ -39,15 +39,15 @@ type Asset struct { FileSize int // File size in bytes // Metadata for the process and the upload to Immich - CaptureDate time.Time // Date of the capture - Trashed bool // The asset is trashed - Archived bool // The asset is archived - FromPartner bool // the asset comes from a partner - FromSharedAlbum bool // the asset comes from a shared album - Favorite bool // the asset is marked as favorite - Rating int // the asset is marked with stars - Albums []Album // List of albums the asset is in - Tags []Tag // List of tags the asset is tagged with + CaptureDate time.Time // Date of the capture + Trashed bool // The asset is trashed + Archived bool // The asset is archived + FromPartner bool // the asset comes from a partner + FromSharedAlbum bool // the asset comes from a shared album + Favorite bool // the asset is marked as favorite + Rating int // the asset is marked with stars + Albums []Album // List of albums the asset is in + Tags []Tag // List of tags the asset is tagged with // Information inferred from the original file name NameInfo @@ -107,6 +107,8 @@ func (a *Asset) UseMetadata(md *Metadata) *Metadata { a.Rating = int(md.Rating) a.FromPartner = md.FromPartner a.FromSharedAlbum = md.FromSharedAlbum + a.MergeAlbums(md.Albums) + a.MergeTags(md.Tags) return md } From 7d433d4d711448c490b2e0a2b5621574347d2928 Mon Sep 17 00:00:00 2001 From: Chaoscontrol Date: Sun, 27 Jul 2025 14:48:40 +0100 Subject: [PATCH 3/4] Some file cleaning and ordering --- adapters/googlePhotos/json.go | 14 +++++++------- internal/assets/asset.go | 4 ++-- internal/assets/metadata.go | 30 +++++++++++++++--------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/adapters/googlePhotos/json.go b/adapters/googlePhotos/json.go index d3fa3708..a5dfbcf6 100644 --- a/adapters/googlePhotos/json.go +++ b/adapters/googlePhotos/json.go @@ -81,13 +81,13 @@ func (gmd GoogleMetaData) LogValue() slog.Value { func (gmd GoogleMetaData) AsMetadata(name fshelper.FSAndName, tagPeople bool) *assets.Metadata { md := assets.Metadata{ - File: name, - FileName: sanitizedTitle(gmd.Title), - Description: gmd.Description, - Trashed: gmd.Trashed, - Archived: gmd.Archived, - Favorited: gmd.Favorited, - FromPartner: gmd.isPartner(), + File: name, + FileName: sanitizedTitle(gmd.Title), + Description: gmd.Description, + Trashed: gmd.Trashed, + Archived: gmd.Archived, + Favorited: gmd.Favorited, + FromPartner: gmd.isPartner(), FromSharedAlbum: gmd.isSharedAlbum(), } if gmd.GeoDataExif != nil { diff --git a/internal/assets/asset.go b/internal/assets/asset.go index 5c87bc0b..3a21b588 100644 --- a/internal/assets/asset.go +++ b/internal/assets/asset.go @@ -101,12 +101,12 @@ func (a *Asset) UseMetadata(md *Metadata) *Metadata { a.Latitude = md.Latitude a.Longitude = md.Longitude a.CaptureDate = md.DateTaken + a.FromPartner = md.FromPartner + a.FromSharedAlbum = md.FromSharedAlbum a.Trashed = md.Trashed a.Archived = md.Archived a.Favorite = md.Favorited a.Rating = int(md.Rating) - a.FromPartner = md.FromPartner - a.FromSharedAlbum = md.FromSharedAlbum a.MergeAlbums(md.Albums) a.MergeTags(md.Tags) return md diff --git a/internal/assets/metadata.go b/internal/assets/metadata.go index 06f71b67..7c9f24b6 100644 --- a/internal/assets/metadata.go +++ b/internal/assets/metadata.go @@ -11,21 +11,21 @@ import ( ) type Metadata struct { - File fshelper.FSAndName `json:"-"` // File name and file system that holds the metadata. Could be empty - FileName string `json:"fileName,omitempty"` // File name as presented to users - Latitude float64 `json:"latitude,omitempty"` // GPS - Longitude float64 `json:"longitude,omitempty"` // GPS - FileDate time.Time `json:"fileDate,omitzero"` // Date of the file - DateTaken time.Time `json:"dateTaken,omitzero"` // Date of exposure - Description string `json:"description,omitempty"` // Long description - Albums []Album `json:"albums,omitempty"` // Used to list albums that contain the file - Tags []Tag `json:"tags,omitempty"` // Used to list tags - Rating byte `json:"rating,omitempty"` // 0 to 5 - Trashed bool `json:"trashed,omitempty"` // Flag to indicate if the image has been trashed - Archived bool `json:"archived,omitempty"` // Flag to indicate if the image has been archived - Favorited bool `json:"favorited,omitempty"` // Flag to indicate if the image has been favorited - FromPartner bool `json:"fromPartner,omitempty"` // Flag to indicate if the image is from a partner - FromSharedAlbum bool `json:"fromSharedAlbum,omitempty"` // Flag to indicate if the image is from a shared album + File fshelper.FSAndName `json:"-"` // File name and file system that holds the metadata. Could be empty + FileName string `json:"fileName,omitempty"` // File name as presented to users + Latitude float64 `json:"latitude,omitempty"` // GPS + Longitude float64 `json:"longitude,omitempty"` // GPS + FileDate time.Time `json:"fileDate,omitzero"` // Date of the file + DateTaken time.Time `json:"dateTaken,omitzero"` // Date of exposure + Description string `json:"description,omitempty"` // Long description + Albums []Album `json:"albums,omitempty"` // Used to list albums that contain the file + Tags []Tag `json:"tags,omitempty"` // Used to list tags + Rating byte `json:"rating,omitempty"` // 0 to 5 + Trashed bool `json:"trashed,omitempty"` // Flag to indicate if the image has been trashed + Archived bool `json:"archived,omitempty"` // Flag to indicate if the image has been archived + Favorited bool `json:"favorited,omitempty"` // Flag to indicate if the image has been favorited + FromPartner bool `json:"fromPartner,omitempty"` // Flag to indicate if the image is from a partner + FromSharedAlbum bool `json:"fromSharedAlbum,omitempty"` // Flag to indicate if the image is from a shared album } func (m Metadata) LogValue() slog.Value { From 795e6a6647586a2b1684811592ea4b8ec14d1bfa Mon Sep 17 00:00:00 2001 From: Chaoscontrol Date: Sun, 27 Jul 2025 17:39:03 +0100 Subject: [PATCH 4/4] Added feature to tag photos "From Shared Album" --- adapters/googlePhotos/googlephotos.go | 2 +- adapters/googlePhotos/json.go | 7 ++++++- adapters/googlePhotos/options.go | 5 +++++ readme.md | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/adapters/googlePhotos/googlephotos.go b/adapters/googlePhotos/googlephotos.go index 6e3e551f..0ce5aa1a 100644 --- a/adapters/googlePhotos/googlephotos.go +++ b/adapters/googlePhotos/googlephotos.go @@ -213,7 +213,7 @@ func (to *Takeout) passOneFsWalk(ctx context.Context, w fs.FS) error { if err == nil { switch { case md.isAsset(): - md := md.AsMetadata(fshelper.FSName(w, name), to.flags.PeopleTag) // Keep metadata + md := md.AsMetadata(fshelper.FSName(w, name), to.flags.PeopleTag, to.flags) // Keep metadata dirCatalog.jsons[base] = md to.log.Log().Debug("Asset JSON", "metadata", md) to.log.Record(ctx, fileevent.DiscoveredSidecar, fshelper.FSName(w, name), "type", "asset metadata", "title", md.FileName, "date", md.DateTaken) diff --git a/adapters/googlePhotos/json.go b/adapters/googlePhotos/json.go index a5dfbcf6..466fb3ca 100644 --- a/adapters/googlePhotos/json.go +++ b/adapters/googlePhotos/json.go @@ -79,7 +79,7 @@ func (gmd GoogleMetaData) LogValue() slog.Value { ) } -func (gmd GoogleMetaData) AsMetadata(name fshelper.FSAndName, tagPeople bool) *assets.Metadata { +func (gmd GoogleMetaData) AsMetadata(name fshelper.FSAndName, tagPeople bool, flags *ImportFlags) *assets.Metadata { md := assets.Metadata{ File: name, FileName: sanitizedTitle(gmd.Title), @@ -108,6 +108,11 @@ func (gmd GoogleMetaData) AsMetadata(name fshelper.FSAndName, tagPeople bool) *a md.AddTag("People/" + p.Name) } } + + if flags.SharedAlbumTag && md.FromSharedAlbum { + md.AddTag("From Shared Album") + } + return &md } diff --git a/adapters/googlePhotos/options.go b/adapters/googlePhotos/options.go index 9923c213..8538c589 100644 --- a/adapters/googlePhotos/options.go +++ b/adapters/googlePhotos/options.go @@ -82,6 +82,10 @@ type ImportFlags struct { // PeopleTag indicates whether to add a people tag to the imported assets. PeopleTag bool + + // SharedAlbumTag indicates whether to add \"From Shared Album\" tag. + SharedAlbumTag bool + // Timezone TZ *time.Location } @@ -111,6 +115,7 @@ func (o *ImportFlags) AddFromGooglePhotosFlags(cmd *cobra.Command, parent *cobra cmd.Flags().BoolVar(&o.SessionTag, "session-tag", false, "Tag uploaded photos with a tag \"{immich-go}/YYYY-MM-DD HH-MM-SS\"") cmd.Flags().BoolVar(&o.TakeoutTag, "takeout-tag", true, "Tag uploaded photos with a tag \"{takeout}/takeout-YYYYMMDDTHHMMSSZ\"") cmd.Flags().BoolVar(&o.PeopleTag, "people-tag", true, "Tag uploaded photos with tags \"people/name\" found in the JSON file") + cmd.Flags().BoolVar(&o.SharedAlbumTag, "shared-album-tag", true, "Tag photos from shared albums with \"From Shared Album\"") cliflags.AddInclusionFlags(cmd, &o.InclusionFlags) // exif.AddExifToolFlags(cmd, &o.ExifToolFlags) diff --git a/readme.md b/readme.md index 39564826..cbe1a3ec 100644 --- a/readme.md +++ b/readme.md @@ -415,6 +415,7 @@ The **from-google-photos** sub-command processes a Google Photos takeout archive | --tag strings | | Add tags to the imported assets. Can be specified multiple times. Hierarchy is supported using a / separator (e.g. 'tag1/subtag1') | | --takeout-tag | `TRUE` | Tag uploaded photos with a tag "{takeout}/takeout-YYYYMMDDTHHMMSSZ" | | --people-tag | `TRUE` | Tag uploaded photos with tags \"people/name\" found in the JSON file | +| --shared-album-tag | `TRUE` | Tag photos from others in shared albums with \"From Shared Album\". | ## Google Photos Best Practices: