workflows

package
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package workflows: album-download workflow. Downloads every (optionally filtered) asset in one album to a local folder, either as a one-shot bulk download or, with --sync, as an ongoing mirror that also detects changes and removes local files for assets that left the album. See DownloadAlbum, PlanAlbumSync and ApplyAlbumSync.

Package workflows: per-file byte progress for uploads/downloads. stdlib-only counting io.Reader wrapper reporting to stderr.

Package workflows implements client-side multi-step orchestrations ("client workflows") that combine several Immich API calls with local processing into one command. They run entirely client-side and are the main purpose of this tool — not to be confused with Immich's server-side Workflows API.

A workflow is an ordered list of named Steps executed for one asset (or asset pair); RunSteps handles step logging, --dry-run, and stopping at the first failed step so later steps (in particular the always-last destructive step) never run after an earlier failure. RunBatch layers the same continue-on-error + summary-exit-code convention used by bulk commands (see internal/commands/helpers.go) on top, for running a workflow across many items. Progress reports "i/N elapsed/eta" lines for long-running batches (e.g. download-album's --resize/ --resize-video-preset re-encoding) so users can gauge how far along a multi-minute run is and roughly how much longer it will take.

Index

Constants

View Source
const (
	SourceKindAlbum = "album"
	SourceKindTag   = "tag"
)

Sync source kinds for Manifest.SourceKind.

View Source
const DefaultOffsetDays = 2

DefaultOffsetDays is the recommended FixAlbumDatesOptions.OffsetDays value ("two days plus/minus"), used as the command-line flag default.

View Source
const DefaultResizeQuality = 85

DefaultResizeQuality is the JPEG quality used by --resize when --resize-quality is not explicitly set.

View Source
const DefaultWatchUploadTagPattern = "immich-admin-cli/watch/{yyyy-MM-dd}"

DefaultWatchUploadTagPattern tags flat uploads with the upload day.

View Source
const ManifestFileName = ".immich-sync.json"

ManifestFileName is the hidden per-target-directory state file written by --sync mode (see Manifest). It never appears among the downloaded media files, and no other command reads or writes it.

View Source
const ResizeVideoPreset1080pWebFriendly = "1080p-web-friendly"

ResizeVideoPreset1080pWebFriendly re-encodes a video to H.264/AAC MP4 scaled to fit 1080p height, tuned for broad web-player compatibility (yuv420p, faststart for progressive playback) at a moderate size/quality tradeoff (CRF 22, "medium" preset).

Variables

View Source
var ValidResizeVideoPresets = []string{ResizeVideoPreset1080pWebFriendly}

ValidResizeVideoPresets lists every accepted --resize-video-preset value, used both for command-line validation and its help text.

Functions

func AlbumHasUser

AlbumHasUser reports whether album already lists userID among its members, returning that member's role if so. Exported so callers (e.g. the review/listing step before sharing) can show existing membership without duplicating this lookup.

func ApplyAlbumSync

func ApplyAlbumSync(ctx context.Context, c *client.Client, album immichapi.AlbumResponseDto, targetDir string, allAssets []immichapi.AssetResponseDto, plan SyncPlan, manifest Manifest, opts DownloadAlbumOptions) error

ApplyAlbumSync executes plan against targetDir: downloads every asset in plan.Additions and plan.Updates, deletes the local file for every plan.Removals entry, and persists the updated manifest along the way — stamping it with album/opts.Size/etc. so future runs can validate against it. The manifest is saved to disk after every single removal and every successful download (not just once at the end): interrupting a long sync (Ctrl+C, crash, closed terminal) never loses more than the one item that was in flight, and every previously downloaded/removed file is correctly reflected on the next run. Downloads continue on a per-asset error and are summarized at the end (the usual bulk convention), but a local-file deletion error aborts immediately — leaving the manifest as of the last successful save is safer than continuing once disk state and the manifest may have diverged.

func ApplySync

func ApplySync(ctx context.Context, c *client.Client, source SyncSource, targetDir string, allAssets []immichapi.AssetResponseDto, plan SyncPlan, manifest Manifest, opts DownloadAlbumOptions) error

ApplySync executes plan against targetDir: downloads every asset in plan.Additions and plan.Updates, deletes the local file for every plan.Removals entry, and persists the updated manifest along the way — stamping it with source/opts.Size/etc. so future runs can validate against it. The manifest is saved to disk after every single removal and every successful download (not just once at the end): interrupting a long sync (Ctrl+C, crash, closed terminal) never loses more than the one item that was in flight, and every previously downloaded/removed file is correctly reflected on the next run. Downloads continue on a per-asset error and are summarized at the end (the usual bulk convention), but a local-file deletion error aborts immediately — leaving the manifest as of the last successful save is safer than continuing once disk state and the manifest may have diverged.

func AssignLocalNames

func AssignLocalNames(assets []immichapi.AssetResponseDto, timestampPrefix bool) map[string]string

AssignLocalNames computes a collision-safe local base file name (without extension) for every asset, derived from OriginalFileName and, if timestampPrefix, prefixed with the asset's capture date/time (LocalDateTime, formatted as timestampPrefixLayout) so a plain directory listing sorts chronologically. Immich allows duplicate original file names within one album (and a timestamp prefix does not fully rule out collisions either, e.g. burst shots within the same second); when two or more assets end up with the same final base name (case-insensitively), every colliding entry gets a short suffix built from its own asset ID appended, so the mapping is deterministic across runs regardless of slice order (assets are sorted by ID first).

func BuildFFmpegArgs

func BuildFFmpegArgs(srcPath, dstPath, preset string) ([]string, error)

BuildFFmpegArgs returns the CLI arguments (excluding the executable itself) to re-encode srcPath into dstPath per preset. Pure (no exec), so the exact argument construction is directly unit-testable. -y (overwrite without prompting) and -nostdin (never wait on keyboard input) are always included since ffmpeg runs non-interactively here.

func BuildImageMagickArgs

func BuildImageMagickArgs(srcPath, dstPath string, opts ResizeOptions) []string

BuildImageMagickArgs returns the CLI arguments (excluding the executable itself) to convert srcPath into dstPath as a JPEG per opts. Pure (no exec), so the exact geometry/quality construction is directly unit-testable. Works identically whether the resolved executable is the modern "magick" or the legacy "convert" — both accept "<input> [-resize GEOM] -quality Q <output>".

func CollectCheckFiles

func CollectCheckFiles(paths []string) ([]string, error)

CollectCheckFiles expands FILE|DIR args into files. Dirs are walked recursively; hidden files/dirs (base starting with '.') and symlinks are skipped. Pure filesystem, no network.

func ComputeFixedDateTime

func ComputeFixedDateTime(asset immichapi.AssetResponseDto, p AlbumDatePattern) (time.Time, bool)

ComputeFixedDateTime returns the corrected local timestamp for asset given pattern: the album's date combined with the asset's own existing hour/minute/second (preserving relative ordering within the album). It only applies to day-precision albums — a year-precision album has no single unambiguous date to reset an outlier to, so ok is false and no fix is offered (report-only).

func DeleteTags

func DeleteTags(ctx context.Context, c *client.Client, tags []immichapi.TagResponseDto, opts TagDeleteOptions) error

DeleteTags deletes each tag (DELETE /tags/{id}) as a single destructive step per tag, continuing on error and returning a summary error if any deletion failed (the same bulk convention used by the other workflows). In dry-run mode it prints the planned step for each tag and deletes nothing.

Tag deletion is permanent: the Tags API has no trash, so there is no restore step and no --force distinction.

func DownloadAlbum

func DownloadAlbum(ctx context.Context, c *client.Client, album immichapi.AlbumResponseDto, targetDir string, opts DownloadAlbumOptions) error

DownloadAlbum is the plain (non-sync) mode: it fetches, filters, and downloads every matching asset into targetDir, one HTTP request per asset, always overwriting any existing file of the same name. It builds no manifest and never deletes anything — reusable as a one-shot "get me this album's files" command. In dry-run mode it only prints the planned downloads.

func ExtensionForContentType

func ExtensionForContentType(contentType string) string

ExtensionForContentType maps an HTTP Content-Type (as returned when downloading a thumbnail/preview asset via GET /assets/{id}/thumbnail) to a filesystem extension, including the leading dot. Immich's thumbnail image format is a server-side configuration choice, not derived from the original asset's own file extension, so callers saving these binaries to disk must inspect the actual response Content-Type rather than assuming one. Unrecognized or missing content types fall back to ".jpg", Immich's most common thumbnail format.

func FetchAlbumAssets

func FetchAlbumAssets(ctx context.Context, c *client.Client, albumID openapi_types.UUID) ([]immichapi.AssetResponseDto, error)

FetchAlbumAssets returns every asset in albumID via the album-scoped metadata search (POST /search/metadata, MetadataSearchDto.AlbumIds), following the result cursor until exhausted. AlbumResponseDto itself carries no assets list in this API version.

func FetchFilteredAlbumAssets

func FetchFilteredAlbumAssets(ctx context.Context, c *client.Client, albumID openapi_types.UUID, ignoreVideos bool) ([]immichapi.AssetResponseDto, error)

FetchFilteredAlbumAssets fetches every asset in albumID (paginated) and, if ignoreVideos, drops VIDEO assets.

func FetchFilteredTagAssets

func FetchFilteredTagAssets(ctx context.Context, c *client.Client, tagID openapi_types.UUID, ignoreVideos bool) ([]immichapi.AssetResponseDto, error)

FetchFilteredTagAssets fetches every asset with tagID and, if ignoreVideos, drops VIDEO assets.

func FetchTagAssets

func FetchTagAssets(ctx context.Context, c *client.Client, tagID openapi_types.UUID) ([]immichapi.AssetResponseDto, error)

FetchTagAssets returns every asset carrying tagID via tag-scoped metadata search (POST /search/metadata, MetadataSearchDto.TagIds), following the result cursor until exhausted.

func FileSHA1Base64

func FileSHA1Base64(path string) (string, error)

FileSHA1Base64 returns the base64-standard-encoded SHA1 hash of the file at path, matching Immich's Checksum format. Exported for the native check command; workflows reuse the same helper as replace-asset verify.

func FilterOutVideos

func FilterOutVideos(assets []immichapi.AssetResponseDto) []immichapi.AssetResponseDto

FilterOutVideos returns the assets whose Type is not VIDEO, preserving order. Pure (no network) so it is unit-testable in isolation.

func FixAlbumDates

func FixAlbumDates(ctx context.Context, c *client.Client, checks []AlbumDateCheck, opts FixAlbumDatesOptions) error

FixAlbumDates applies the date fix to every day-precision mismatch found by CheckAlbumDates, continuing on error and returning a summary error if any fix failed (the same bulk convention used by the other workflows). Year-precision albums are report-only per design (see ComputeFixedDateTime) and are skipped here with an informational message, never counted as a failure. In dry-run mode it prints the planned step for each asset and changes nothing.

This is the repo's one deliberate exception to "never use deprecated endpoints": PUT /assets/{id} (updateAsset) is the only Immich API endpoint that can set an asset's capture date, and it is marked deprecated upstream with a self-referential (non-existent) replacementId — no stable alternative exists (see AGENTS.md).

func FormatByteProgressLine

func FormatByteProgressLine(pos, n int, label string, done, total int64, elapsed time.Duration) string

FormatByteProgressLine renders one progress line. Pure for testing.

func GetAlbumSummary

func GetAlbumSummary(ctx context.Context, c *client.Client, id openapi_types.UUID) (name string, count int, err error)

GetAlbumSummary fetches an album by ID (GET /albums/{id}, getAlbumInfo) and returns its name and asset count, validating that the album exists. It is used to confirm an --album-id and give friendly output before scanning the album's assets. (This spec's AlbumResponseDto carries no asset list, so the assets themselves are fetched via the metadata-search finder, filtered by albumIds.)

func MergeAlbums

func MergeAlbums(ctx context.Context, c *client.Client, opts MergeAlbumOptions) error

MergeAlbums moves every asset from opts.From to opts.Into (PUT then DELETE /albums/{id}/assets, 500 IDs per request) and, when DeleteEmptySource is set and the move left nothing behind, deletes the source album (DELETE /albums/{id}). Only asset IDs confirmed present in the target (added, or already there) are removed from the source, so a failed add never loses an asset; the destructive delete is always last. Failures are logged to stderr and summarized in the returned error.

func PlanAlbumSync

PlanAlbumSync is the read-only "getting the information" phase of --sync: it loads the target directory's existing manifest (if any), fetches and filters the album's current assets, and classifies them with ComputeSyncPlan. It performs no writes and is used both for --dry-run and as the first phase of a real sync run (see ApplyAlbumSync).

It refuses to proceed if an existing manifest names a different album, or was built with a different --size, than the current run — either would make removal detection unsafe or nonsensical; the caller should point --target-dir at a fresh folder instead.

func PlanSync

PlanSync is the generic read-only sync planner shared by album and tag sources. fetch lists the source's current (already filtered) assets.

func PlanTagSync

PlanTagSync plans a tag-source sync (read-only).

func RenderTagPattern

func RenderTagPattern(pattern string, t time.Time) (string, error)

RenderTagPattern replaces {yyyy-MM-dd} with t's date. Only that placeholder exists in v1; any other {…} is an error. Pure for testing.

func ReplaceAsset

func ReplaceAsset(ctx context.Context, c *client.Client, pair ReplacePair, opts ReplaceAssetOptions) error

ReplaceAsset uploads pair.NewFilePath as a new asset, verifies the upload, copies metadata from pair.AssetID onto the new asset, and — unless opts.KeepOriginal is set — removes the original asset. It implements the `client-workflow replace-asset` steps documented in README.md:

  1. Upload the new file as a new asset
  2. Verify the upload (asset exists, checksum matches the local file)
  3. Copy metadata from the old asset (albums, favorite, shared links, sidecar, stack association)
  4. Remove the old asset (to trash by default; Force for permanent deletion) — only when !opts.KeepOriginal

The destructive step (4) is only ever appended to the step list when it is meant to run, and it is always last, so a failure in any earlier step leaves the original asset untouched.

func ResolveAlbum

func ResolveAlbum(ctx context.Context, c *client.Client, albumID *openapi_types.UUID, albumName string) (immichapi.AlbumResponseDto, error)

ResolveAlbum finds exactly one album given albumID (preferred, GET /albums/{id}) or, if albumID is nil, albumName (GET /albums?name=...). The caller (command layer) is responsible for ensuring exactly one of the two is provided. A name lookup errors if it matches zero or more than one album — Immich does not enforce unique album names — listing the matches' IDs so the caller can switch to --album-id.

func ResolveFFmpegPath

func ResolveFFmpegPath(explicit string) (string, error)

ResolveFFmpegPath returns the ffmpeg executable to invoke: explicit (from the config file's tools.ffmpeg_path, or the IMMICH_FFMPEG_PATH env var — see internal/config) if non-empty, otherwise "ffmpeg" found on PATH. Meant to be called once before a download batch starts, so a missing tool fails fast rather than partway through.

func ResolveImageMagickPath

func ResolveImageMagickPath(explicit string) (string, error)

ResolveImageMagickPath returns the ImageMagick executable to invoke: explicit (from the config file's tools.imagemagick_path, or the IMMICH_IMAGEMAGICK_PATH env var — see internal/config) if non-empty, otherwise the first of "magick" (ImageMagick v7+, preferred) or "convert" (legacy v6) found on PATH. Meant to be called once before a download batch starts, so a missing tool fails fast rather than partway through.

func ResolveOrCreateAlbumByName

func ResolveOrCreateAlbumByName(ctx context.Context, c *client.Client, name string, dryRun, yes bool) (immichapi.AlbumResponseDto, error)

ResolveOrCreateAlbumByName finds an album by exact name or creates it. dryRun prints instead of creating; yes skips the creation prompt.

func ResolveOrCreateTag

func ResolveOrCreateTag(ctx context.Context, c *client.Client, value string) (openapi_types.UUID, error)

ResolveOrCreateTag ensures a tag with the given full-path value (e.g. "immich-admin-cli/corrupt-heic") exists — creating it, and any missing parent tags implied by "/" in the path, via PUT /tags — and returns its ID.

func ResolveTagByValue

func ResolveTagByValue(ctx context.Context, c *client.Client, value string) (immichapi.TagResponseDto, error)

ResolveTagByValue finds exactly one tag by full-path value.

func ResolveUser

func ResolveUser(ctx context.Context, c *client.Client, query string) (*immichapi.UserResponseDto, error)

ResolveUser identifies exactly one user matching query: a raw UUID is matched by exact ID; anything else is resolved by fetching all users (GET /users) and applying matchUsers (case-insensitive substring match on name or email). It returns an error listing all candidates if there are zero or more than one match.

func RunBatch

func RunBatch[T any](items []T, label func(T) string, fn func(T) error) error

RunBatch runs fn(item) for every item, continuing on error. Failures are logged to stderr via label(item); the batch always continues to the next item. It returns a summary error if any item failed, or nil if all succeeded — the same convention used by the bulk commands in internal/commands (e.g. assetsInfo, assetsDownloadOriginal).

func RunFFmpegResize

func RunFFmpegResize(ctx context.Context, srcPath, dstPath string, opts ResizeVideoOptions) error

RunFFmpegResize invokes opts.ExecutablePath to re-encode srcPath to dstPath (always MP4) per opts.Preset. Stderr is captured (ffmpeg's own progress/log output, quieted to errors only by -loglevel error in BuildFFmpegArgs) and included in the error on failure for diagnosis.

func RunImageMagickResize

func RunImageMagickResize(ctx context.Context, srcPath, dstPath string, opts ResizeOptions) error

RunImageMagickResize invokes opts.ExecutablePath to convert srcPath to dstPath (always JPEG) per opts. Stderr is captured and included in the error on failure for diagnosis.

func RunSteps

func RunSteps(ctx context.Context, opts RunOptions, label string, steps []Step) error

RunSteps executes steps in order for one item identified by label.

In dry-run mode it only prints the planned steps and returns nil. Normally it runs each step in order, printing a line as each completes, and stops at the first failing step — the failure is wrapped with the step name and label and returned immediately, so no later step (in particular a destructive step placed last) ever runs after an earlier one failed.

func SaveManifest

func SaveManifest(targetDir string, m Manifest) error

SaveManifest writes m to ManifestFileName inside targetDir, overwriting any existing manifest. The write is atomic (temp file + rename) so a process interrupted mid-write (e.g. Ctrl+C, crash, power loss) can never leave a corrupt, half-written manifest on disk — see ApplyAlbumSync, which calls this after every single asset so interrupting a long sync never loses more than the one in-flight item's progress.

func SelectAlbumsForSharing

func SelectAlbumsForSharing(ctx context.Context, c *client.Client, opts AddUsersToAlbumOptions) ([]immichapi.AlbumResponseDto, error)

SelectAlbumsForSharing fetches all albums (GET /albums) and returns those whose AlbumName matches opts.Include and does not match opts.Exclude, sorted by AlbumName for deterministic output.

It performs no sharing — this is the read-only "getting the information" path used both by the command's review step and by the integration test.

func SelectTagsForDeletion

func SelectTagsForDeletion(ctx context.Context, c *client.Client, opts TagDeleteOptions) ([]immichapi.TagResponseDto, error)

SelectTagsForDeletion fetches all tags (GET /tags) and returns those whose Value (full path) matches opts.Include and does not match opts.Exclude, sorted by Value for deterministic output.

It performs no deletion — this is the read-only "getting the information" path used both by the command's display step and by the integration test.

func ShareAlbumsWithUser

ShareAlbumsWithUser shares each album with user at the configured role (PUT /albums/{id}/users), continuing on error and returning a summary error if any share failed (the same bulk convention used by the other workflows). Albums where the user already has access are skipped with an informational message and are not counted as failures. In dry-run mode it prints the planned step for each album (or the "already shared" notice) and shares nothing.

func TagAssets

func TagAssets(ctx context.Context, c *client.Client, assetIDs []openapi_types.UUID, tagID openapi_types.UUID) error

TagAssets assigns tagID to every asset in assetIDs in a single request (PUT /tags/assets). It is a no-op when assetIDs is empty.

func UploadAssetFile

func UploadAssetFile(ctx context.Context, c *client.Client, path string, opts UploadOptions) (openapi_types.UUID, error)

UploadAssetFile uploads the local file at path as a new asset (POST /assets, multipart/form-data) and returns its new asset ID.

oapi-codegen only generates a type alias for the multipart body of binary fields (UploadAssetMultipartRequestBody = AssetMediaCreateDto); it does not generate a multipart writer, so the request body is built by hand here.

The body streams via io.Pipe (never buffered in RAM) so GB videos don't OOM and a counting reader reports real upload bytes.

Types

type AddUsersToAlbumOptions

type AddUsersToAlbumOptions struct {
	// Include, when non-nil, keeps only albums whose AlbumName matches it.
	// A nil Include matches every album.
	Include *regexp.Regexp
	// Exclude, when non-nil, drops any album whose AlbumName matches it,
	// even if it matched Include. A nil Exclude excludes nothing.
	Exclude *regexp.Regexp
	// Role is the album role granted to the target user (e.g. "viewer").
	Role immichapi.AlbumUserRole
	// DryRun prints the planned share steps without calling the API.
	DryRun bool
}

AddUsersToAlbumOptions controls the add-users-to-album workflow.

type AlbumDateCheck

type AlbumDateCheck struct {
	Album      immichapi.AlbumResponseDto
	Pattern    AlbumDatePattern
	Mismatches []immichapi.AssetResponseDto
}

AlbumDateCheck is one date-pattern album together with the assets whose LocalDateTime falls outside the range implied by its name. Mismatches is empty for an album where every asset matches.

func CheckAlbumDates

func CheckAlbumDates(ctx context.Context, c *client.Client, opts FixAlbumDatesOptions) ([]AlbumDateCheck, error)

CheckAlbumDates fetches all albums (GET /albums), keeps the ones whose name matches the day or year date pattern, and for each fetches its assets (POST /search/metadata, scoped by album) to find any whose LocalDateTime falls outside the range implied by the album name. It performs no writes — this is the read-only "getting the information" path used both by the command's review step and by the integration test. Every date-pattern album is returned, ordered worst-first (the album with the single largest out-of-range deviation first; see maxDeviation), including ones with zero mismatches at the end.

type AlbumDatePattern

type AlbumDatePattern struct {
	Kind PatternKind
	From time.Time
	To   time.Time
}

AlbumDatePattern is the date range implied by a date-pattern album name. To is an exclusive upper bound.

type AssetHEICTileDefect

type AssetHEICTileDefect struct {
	ID               string
	OriginalFileName string
	OriginalPath     string
	Width            int
	Height           int
}

AssetHEICTileDefect holds the minimal information about a HEIC/HEIF asset whose pixel dimensions are not an exact multiple of the grid tile size.

func FindAssetsWithHEICTileDefect

func FindAssetsWithHEICTileDefect(
	ctx context.Context,
	c *client.Client,
	opts FindHEICTileDefectOptions,
) ([]AssetHEICTileDefect, error)

FindAssetsWithHEICTileDefect pages through all IMAGE assets matching the optional pre-filters and returns the HEIC/HEIF ones whose dimensions are not an exact multiple of the grid tile size (see hasHEICTileDefect).

The search is done via POST /search/metadata with automatic pagination, so no per-asset download or info call is needed: width/height/file name are all present on the search result.

type AssetNoThumbhash

type AssetNoThumbhash struct {
	ID               string
	OriginalFileName string
	Type             string
	OriginalPath     string
}

AssetNoThumbhash holds the minimal information about an asset that has no thumbhash.

func FindAssetsWithNoThumbhash

func FindAssetsWithNoThumbhash(
	ctx context.Context,
	c *client.Client,
	opts FindNoThumbhashOptions,
) ([]AssetNoThumbhash, error)

FindAssetsWithNoThumbhash pages through all assets matching the optional pre-filters and returns those whose thumbhash field is null or empty.

The search is done via POST /search/metadata with automatic pagination, so no per-asset info call is needed.

type BulkCheckEntry

type BulkCheckEntry struct {
	File      string
	Checksum  string
	Status    BulkCheckStatus
	AssetID   *openapi_types.UUID
	IsTrashed bool
}

BulkCheckEntry is one file's bulk-upload-check result.

func CheckBulkUploadChecksums

func CheckBulkUploadChecksums(ctx context.Context, c *client.Client, files []string, checksums map[string]string) ([]BulkCheckEntry, error)

CheckBulkUploadChecksums calls POST /assets/bulk-upload-check for files (checksums[file] must be base64 SHA1) in ~500/request chunks and strictly classifies each result. Pure network wrapper; hashing happens outside so callers can continue-on-error per file.

func ClassifyBulkCheckResult

func ClassifyBulkCheckResult(file, checksum string, r immichapi.AssetBulkUploadCheckResult) BulkCheckEntry

ClassifyBulkCheckResult strictly splits duplicate vs unsupported per AssetBulkUploadCheckResult. Pure for testing.

type BulkCheckStatus

type BulkCheckStatus string

BulkCheckStatus is the per-file outcome of a bulk-upload-check.

const (
	BulkCheckUploaded    BulkCheckStatus = "uploaded"
	BulkCheckMissing     BulkCheckStatus = "missing"
	BulkCheckUnsupported BulkCheckStatus = "unsupported"
)

type ByteProgress

type ByteProgress struct {
	Label string
	Total int64
	Done  int64
	Start time.Time
	Pos   int
	N     int
	Quiet bool
	Out   io.Writer
	// contains filtered or unexported fields
}

ByteProgress tracks one file transfer, printing a single-line bar to stderr (stdout stays clean for --json/piping). Total < 0 means unknown (chunked response): bytes+rate only, no %.

func NewByteProgress

func NewByteProgress(label string, total int64, pos, n int, quiet bool) *ByteProgress

NewByteProgress creates a transfer tracker. total < 0 = unknown length. quiet disables all output (--quiet, or --json which implies quiet).

func (*ByteProgress) Add

func (p *ByteProgress) Add(n int64)

Add counts n bytes and prints a throttled update.

func (*ByteProgress) Finish

func (p *ByteProgress) Finish()

Finish prints the final state (full bar) and ends the TTY line.

func (*ByteProgress) Wrap

func (p *ByteProgress) Wrap(r io.Reader) io.Reader

Wrap returns r wrapped so every Read counts toward the bar.

type DownloadAlbumOptions

type DownloadAlbumOptions struct {
	// Size selects the media variant: immichapi.AssetMediaSizeOriginal,
	// AssetMediaSizeFullsize, AssetMediaSizePreview, or
	// AssetMediaSizeThumbnail (the command layer rejects any other
	// AssetMediaSize value before it reaches this package).
	Size immichapi.AssetMediaSize
	// IgnoreVideos drops every AssetTypeEnum VIDEO asset before planning or
	// downloading anything.
	IgnoreVideos bool
	// Resize, when Enabled, re-encodes every downloaded file to JPEG via
	// ImageMagick, optionally resizing it (see ResizeOptions).
	Resize ResizeOptions
	// ResizeVideo, when Enabled, re-encodes every downloaded VIDEO asset
	// (--size original only) to MP4 via ffmpeg, per its Preset (see
	// ResizeVideoOptions).
	ResizeVideo ResizeVideoOptions
	// TimestampPrefix prefixes each local file name with the asset's
	// capture date/time ("yyyy-MM-dd_HH_mm_ss", from LocalDateTime) so a
	// plain directory listing sorts chronologically.
	TimestampPrefix bool
	// DryRun previews the planned actions without downloading, deleting, or
	// writing the manifest.
	DryRun bool
	// Quiet disables per-file byte progress bars on stderr
	// (--json implies quiet at the command layer).
	Quiet bool
}

DownloadAlbumOptions controls both DownloadAlbum (plain) and PlanAlbumSync/ApplyAlbumSync (--sync).

type DuplicateUploadError

type DuplicateUploadError struct {
	ExistingID openapi_types.UUID
	HasID      bool
}

DuplicateUploadError is returned when the server matches the upload checksum to an existing asset (200) instead of creating one (201). Callers that only link duplicates (watch-upload) can errors.As this to get the existing ID; callers that must act on their own upload (replace-asset) treat it as a plain aborting error.

func (*DuplicateUploadError) Error

func (e *DuplicateUploadError) Error() string

type FindHEICTileDefectOptions

type FindHEICTileDefectOptions struct {
	// PageSize is the number of assets to request per page (max 1000).
	PageSize int
	// TileSize is the assumed HEIF grid tile size in pixels. Defaults to 512
	// (defaultHEICGridTileSize) when <= 0.
	TileSize int
	// OriginalFileName, if non-empty, pre-filters the search to assets whose
	// original file name matches (substring, same as the API's behaviour).
	OriginalFileName string
	// AlbumIDs, if non-empty, restricts the search to assets in these albums
	// (MetadataSearchDto.albumIds).
	AlbumIDs []openapi_types.UUID
}

FindHEICTileDefectOptions controls the find-heic-tile-defect workflow.

type FindNoThumbhashOptions

type FindNoThumbhashOptions struct {
	// PageSize is the number of assets to request per page (max 1000).
	PageSize int
	// OriginalFileName, if non-empty, pre-filters the search to assets whose
	// original file name matches (substring, same as the API's behaviour).
	OriginalFileName string
	// Type, if non-empty, pre-filters by asset type (IMAGE, VIDEO, …).
	Type string
	// AlbumIDs, if non-empty, restricts the search to assets in these albums
	// (MetadataSearchDto.albumIds).
	AlbumIDs []openapi_types.UUID
}

FindNoThumbhashOptions controls the find-assets-with-no-thumbhash workflow.

type FixAlbumDatesOptions

type FixAlbumDatesOptions struct {
	// DryRun prints the planned date fixes without calling the API.
	DryRun bool
	// OffsetDays widens the accepted range by this many days on each side
	// before flagging an asset as a mismatch (e.g. 1 means an asset up to a
	// day before or after the nominal range is still considered in range).
	// This absorbs boundary discrepancies caused by camera/EXIF timezone or
	// DST mismatches near midnight — see parseAlbumDatePattern and
	// findDateMismatches for why LocalDateTime itself needs no timezone
	// conversion. It does not affect the fix target, which is always the
	// pattern's exact nominal date (see ComputeFixedDateTime).
	OffsetDays int
}

FixAlbumDatesOptions controls the fix-album-dates workflow.

type JPEGAnalysis

type JPEGAnalysis struct {
	// HasSOI reports whether the file starts with the Start-of-Image marker
	// FF D8.
	HasSOI bool
	// HasEOI reports whether the file ends with the End-of-Image marker FF D9.
	HasEOI bool
	// Size is the file size in bytes.
	Size int64
}

JPEGAnalysis is the cheap byte-level classification of a JPEG file used to decide whether (and which) repair strategy applies. It deliberately does NOT attempt a full image/jpeg decode: Go's decoder is far stricter than Immich's libjpeg/libvips and rejects files Immich accepts, so a decode is neither a reliable corruption detector nor a reliable repair verifier here.

type Manifest

type Manifest struct {
	Version   int    `json:"version"`
	AlbumID   string `json:"albumId,omitempty"`
	AlbumName string `json:"albumName,omitempty"`
	// SourceKind is "album" or "tag". Empty means a legacy album manifest
	// (written before tag sources existed); see normalizedSource.
	SourceKind string                   `json:"sourceKind,omitempty"`
	SourceID   string                   `json:"sourceId,omitempty"`
	SourceName string                   `json:"sourceName,omitempty"`
	Size       immichapi.AssetMediaSize `json:"size"`
	// Resize, ResizeVideoPreset, and TimestampPrefix record whether this
	// target directory was built with --resize / --resize-video-preset /
	// --timestamp-prefix, mirroring the Size guard: all change the local
	// file's identity (format/name) entirely, so a later --sync run with a
	// different setting is refused rather than silently mixing conventions
	// in one folder (see PlanAlbumSync).
	Resize bool `json:"resize"`
	// ResizeVideoPreset is "" when --resize-video-preset was not used, or
	// the preset name otherwise.
	ResizeVideoPreset string `json:"resizeVideoPreset,omitempty"`
	TimestampPrefix   bool   `json:"timestampPrefix"`
	// Assets is keyed by asset ID (string form of openapi_types.UUID).
	Assets map[string]ManifestAsset `json:"assets"`
}

Manifest tracks, for one target directory, the album/tag and files SyncAlbum/ApplyAlbumSync downloaded there, so later runs can detect changes (re-download) and removals (delete locally) — and, just as importantly, so files not tracked here (anything the user placed in the folder themselves, or files from an unrelated source) are never touched.

func LoadManifest

func LoadManifest(targetDir string) (m Manifest, existed bool, err error)

LoadManifest reads ManifestFileName from targetDir, falling back to the legacy album filename. A missing file is not an error: it returns a zero-value Manifest (with an initialized Assets map) and existed=false, the normal state for a brand-new target directory or the first ever --sync run against it.

type ManifestAsset

type ManifestAsset struct {
	// FileName is the file's name (no directory) inside the target
	// directory, already including whatever extension was assigned at
	// download time (see AssignLocalNames and ExtensionForContentType).
	FileName string `json:"fileName"`
	// Checksum is the *original* asset's checksum (base64 SHA1) at the time
	// it was last downloaded, used for change detection even in
	// non-original --size modes: Immich exposes no separate checksum for
	// the fullsize/preview/thumbnail variants, and each is derived
	// deterministically from the original, so an unchanged original
	// checksum is treated as "nothing to refresh". A metadata-only edit
	// that doesn't change the original file's bytes (e.g. a pure EXIF
	// rotation) will therefore not trigger a re-download — a known,
	// documented limitation.
	Checksum string `json:"checksum"`
	Type     string `json:"type"`
}

ManifestAsset is one asset SyncAlbum is tracking in a target directory.

type ManifestRemoval

type ManifestRemoval struct {
	AssetID  string
	FileName string
}

ManifestRemoval is one manifest entry ComputeSyncPlan found to no longer correspond to any (filtered) album asset.

type MergeAlbumOptions

type MergeAlbumOptions struct {
	// From is the source album: assets move out of it.
	From openapi_types.UUID
	// Into is the target album: assets move into it.
	Into openapi_types.UUID
	// DeleteEmptySource deletes the source album when it holds no assets
	// after the move (never deletes a non-empty album).
	DeleteEmptySource bool
	// DryRun prints the merge plan without calling any mutating endpoint.
	DryRun bool
}

MergeAlbumOptions controls the merge-album workflow: every asset in From is added to Into, then removed from From; optionally the emptied source album is deleted afterwards.

type PatternKind

type PatternKind string

PatternKind identifies which date-name convention an album matched.

const (
	// PatternDay is an album named "yyyy-MM-dd <title>", e.g. "2025-07-04 Garten".
	PatternDay PatternKind = "day"
	// PatternYear is an album named "yyyy <title>", e.g. "2010 USA".
	PatternYear PatternKind = "year"
)

type Progress

type Progress struct {
	// contains filtered or unexported fields
}

Progress prints a "[i/total P%] elapsed .., eta ..: label" line to stderr before each item of a long-running batch starts, so a slow multi-minute run (e.g. downloading and re-encoding many large photos and videos) gives the user a running sense of how far along it is and roughly how much longer it will take, instead of going silent until it finishes. It is not a generic progress-bar library — just enough for this project's sequential, one-item-at-a-time batches (see RunBatch); concurrent use from multiple goroutines is not supported.

func NewProgress

func NewProgress(total int) *Progress

NewProgress creates a Progress tracker for a batch of total items, with its elapsed-time clock starting now.

func (*Progress) Step

func (p *Progress) Step(label string)

Step prints the progress line for the next item (labeled, e.g., by its file name) and advances the internal counter. The ETA shown is a simple linear extrapolation from the average time per item completed so far; it is omitted before the second item, since one data point can't be averaged into a rate yet.

type RepairAssetsOptions

type RepairAssetsOptions struct {
	// Mode selects the repair strategies to try.
	Mode RepairMode
	// DryRun prints the planned steps without changing anything.
	DryRun bool
	// Force permanently deletes the original instead of trashing it.
	Force bool
	// KeepOriginal repairs and re-imports but leaves the original untouched.
	KeepOriginal bool
	// TempDir is the per-run scratch directory downloaded originals and
	// repaired copies are written to. It must exist for the duration of the run.
	TempDir string
}

RepairAssetsOptions controls the repair-assets workflow.

type RepairMode

type RepairMode string

RepairMode selects which repair strategies the repair-assets workflow runs.

const (
	// RepairModeMarker runs only the JPEG End-of-Image marker strategy
	// (append the missing FF D9). Safe and lossless.
	RepairModeMarker RepairMode = "marker"
	// RepairModeTIFFTags runs only the TIFF zero-count IFD tag strategy (patch
	// any IFD entry whose count field is literally 0 to 1 in place). Safe and
	// lossless: layout, pixel data and all other metadata are untouched.
	RepairModeTIFFTags RepairMode = "tiff-tags"
	// RepairModeTakeoutJSON is a DELETE mode, not a repair mode: it identifies
	// assets whose stored bytes are actually a Google Photos Takeout metadata
	// JSON sidecar imported in place of the real photo (a known Takeout
	// export/import failure — the file has no image data and is unrecoverable)
	// and removes them (to trash by default; --force to delete permanently).
	// It is deliberately opt-in only and is NOT included in RepairModeAll,
	// because unlike the repair strategies it deletes an asset outright rather
	// than re-importing a fixed copy.
	RepairModeTakeoutJSON RepairMode = "takeout-json"
	// RepairModeAll runs every registered safe strategy, across all supported
	// file types, in order until one applies. Adding a new strategy to the
	// registries automatically extends "all" — no other code changes needed.
	// It intentionally excludes RepairModeTakeoutJSON (a destructive delete
	// mode), which must always be requested explicitly.
	RepairModeAll RepairMode = "all"
)

func ParseRepairMode

func ParseRepairMode(s string) (RepairMode, error)

ParseRepairMode validates s and returns the corresponding RepairMode.

type RepairOutcome

type RepairOutcome string

RepairOutcome classifies what happened to one asset, for the batch summary.

const (
	// OutcomeRepaired means the file was repaired and re-imported.
	OutcomeRepaired RepairOutcome = "repaired"
	// OutcomeAlreadyOK means no strategy applied because the file is not
	// missing anything this mode repairs (e.g. it already has an EOI marker).
	OutcomeAlreadyOK RepairOutcome = "already-ok"
	// OutcomeSkippedUnsupported means the asset has no applicable repair
	// strategy for its type/extension (e.g. not a JPEG or TIFF image) and was
	// skipped without attempting anything.
	OutcomeSkippedUnsupported RepairOutcome = "skipped-unsupported"
	// OutcomeUnrepairable means the file is damaged beyond what any strategy in
	// this mode can fix (e.g. missing SOI marker, or a TIFF whose IFD chain
	// could not be walked / has no recognized zero-count defect).
	OutcomeUnrepairable RepairOutcome = "unrepairable"
	// OutcomeDeletedSidecar means the asset was confirmed (structurally) to be
	// a Google Photos Takeout JSON sidecar imported in place of the real photo
	// and was deleted (takeout-json mode).
	OutcomeDeletedSidecar RepairOutcome = "deleted-sidecar"
)

func RepairAsset

func RepairAsset(ctx context.Context, c *client.Client, assetID openapi_types.UUID, opts RepairAssetsOptions) (RepairOutcome, error)

RepairAsset attempts to repair one asset and, on success, re-imports it via the replace-asset flow (upload → checksum verify → copy metadata → remove original). It returns a RepairOutcome describing what happened. A non-nil error means the asset failed (and, unless KeepOriginal, the original was left untouched — removal only ever runs last, after the upload and metadata copy succeeded). Note that removal is NOT gated on Immich having generated a thumbhash for the new asset yet: server-side thumbnail generation is asynchronous and its timing is affected by too many factors (queue depth, job scheduling, server load) to reliably bound with a timeout, so repair-assets no longer waits for it. Use `find-no-thumbhash` afterwards to confirm a repair actually produced a thumbnail, or pass --keep-original to be able to re-check before the original is gone.

type RepairStrategy

type RepairStrategy interface {
	// Name is the strategy's short identifier (e.g. "marker").
	Name() string
	// Applicable reports whether this strategy can repair a file with the
	// given analysis.
	Applicable(a JPEGAnalysis) bool
	// Repair reads src and writes a repaired copy to dst. It must not modify
	// src. It is only called when Applicable returned true.
	Repair(src, dst string) error
}

RepairStrategy is one named repair technique. Strategies are registered in repairStrategies; adding a new repair mode is done by implementing this interface and appending to that slice — the command and orchestration layers are untouched.

type ReplaceAssetOptions

type ReplaceAssetOptions struct {
	// DryRun prints the planned steps without calling the API.
	DryRun bool
	// Force permanently deletes the original instead of trashing it. Only
	// relevant when KeepOriginal is false.
	Force bool
	// KeepOriginal, when true, skips the "remove original" step entirely
	// (it is not added to the step list at all), leaving the old asset
	// completely untouched.
	KeepOriginal bool
	// RollbackOnFailure, when true, best-effort trashes the newly uploaded
	// asset if any step after the upload fails, so a verified-bad duplicate is
	// not left behind. The original is left untouched (removal is always last
	// and only runs after every earlier step succeeded).
	RollbackOnFailure bool
}

ReplaceAssetOptions controls the replace-asset workflow.

type ReplacePair

type ReplacePair struct {
	AssetID     openapi_types.UUID
	NewFilePath string
}

ReplacePair identifies one existing asset to replace and the local file to replace it with.

type ResizeOptions

type ResizeOptions struct {
	// Enabled turns the feature on. When false, every other field is
	// ignored and downloaded files keep their natural format.
	Enabled bool
	// Width and Height are the target bounding box in pixels; 0 means
	// unconstrained on that axis. ImageMagick's default -resize geometry
	// (WxH) fits the image within the box preserving aspect ratio; giving
	// only one of the two scales by that axis alone.
	Width, Height int
	// Quality is the JPEG quality (1-100). Zero is normalized to
	// DefaultResizeQuality by BuildImageMagickArgs.
	Quality int
	// ExecutablePath is the resolved ImageMagick binary ("magick" for v7+,
	// or the legacy "convert") — see ResolveImageMagickPath. Resolved once
	// by the command layer before a batch starts (fail fast), not by this
	// package.
	ExecutablePath string
}

ResizeOptions controls the optional ImageMagick post-processing step: every downloaded file (original, or a fullsize/preview/thumbnail variant) is re-encoded to JPEG, optionally resized to fit within Width/Height. It is a deliberate, documented exception to "always original format" for cases where local disk/transfer size matters more than preserving the exact source format.

type ResizeVideoOptions

type ResizeVideoOptions struct {
	// Enabled turns the feature on. When false, Preset/ExecutablePath are
	// ignored and downloaded videos keep their natural format.
	Enabled bool
	// Preset selects the ffmpeg encode recipe; must be one of
	// ValidResizeVideoPresets.
	Preset string
	// ExecutablePath is the resolved ffmpeg binary — see ResolveFFmpegPath.
	// Resolved once by the command layer before a batch starts (fail
	// fast), not by this package.
	ExecutablePath string
}

ResizeVideoOptions controls the optional ffmpeg re-encoding step applied to VIDEO assets downloaded with --size original (see shouldResizeVideo — the fullsize/preview/thumbnail variants are always a static image, never a video stream, so this never applies to those --size values).

type RunOptions

type RunOptions struct {
	// DryRun, when true, prints the planned steps without calling any Run
	// function.
	DryRun bool
}

RunOptions controls how RunSteps executes a step list.

type SearchPager

type SearchPager struct {
	// contains filtered or unexported fields
}

SearchPager tracks POST /search/metadata pagination across requests. It speaks both the cursor scheme (cursor/nextCursor, Immich v3.2+, where the page/nextPage fields are deprecated) and the legacy page scheme (page/nextPage, older servers): the cursor is preferred whenever the server returns one, otherwise the numeric legacy token is used. The zero value starts at page 1. Exported because the search command (internal/commands) pages the same endpoint.

func (*SearchPager) Apply

func (p *SearchPager) Apply(body *immichapi.MetadataSearchDto)

Apply sets the request body's pagination fields from the pager state — the cursor when known, otherwise the current page — clearing the other field so a server never sees both. It counts the request for Pages.

func (*SearchPager) Next

Next advances the pager from a response's asset listing, reporting whether another request should follow. An unparseable legacy token stops the listing rather than risking a loop.

func (*SearchPager) Pages

func (p *SearchPager) Pages() int

Pages reports how many requests Apply has prepared — for status footers, since cursor listings have no server page numbering to display.

type Step

type Step struct {
	// Name is a short human-readable description shown in --dry-run output
	// and progress logging (e.g. "Upload new file").
	Name string
	// Run performs the step. A non-nil error aborts the remaining steps for
	// this item.
	Run func(ctx context.Context) error
}

Step is a single named operation within a workflow, executed for one item (e.g. one asset or asset pair).

type SyncPlan

type SyncPlan struct {
	// Additions are assets not yet tracked in the manifest.
	Additions []immichapi.AssetResponseDto
	// Updates are tracked assets whose checksum has changed since the last
	// sync.
	Updates []immichapi.AssetResponseDto
	// Unchanged are tracked assets whose checksum is identical; nothing to
	// do for them.
	Unchanged []immichapi.AssetResponseDto
	// Removals are manifest entries whose asset ID is no longer present
	// among the (filtered) remote assets — the local file would be deleted.
	Removals []ManifestRemoval
}

SyncPlan is the result of classifying every filtered remote asset against a target directory's existing Manifest (see ComputeSyncPlan).

func ComputeSyncPlan

func ComputeSyncPlan(assets []immichapi.AssetResponseDto, manifest Manifest) SyncPlan

ComputeSyncPlan classifies assets (already filtered, e.g. by FetchFilteredAlbumAssets) against manifest. Pure (no network, no filesystem access) so the decision logic is unit-testable directly.

func RunWatchDownloadOnce

func RunWatchDownloadOnce(ctx context.Context, c *client.Client, opts WatchDownloadOptions) (SyncPlan, error)

RunWatchDownloadOnce runs a single sync scan from source into targetDir.

type SyncSource

type SyncSource struct {
	Kind string
	ID   string
	Name string
}

SyncSource identifies one download mirror source: an album or a tag.

type TIFFAnalysis

type TIFFAnalysis struct {
	// Valid reports whether the file has a recognizable TIFF header (II/MM +
	// magic 42) and every IFD in the main chain (IFD0, IFD1, ...) could be
	// walked without a structural read error. This does NOT claim the file is
	// otherwise undamaged — only that the walk that produced ZeroCountTags
	// below is trustworthy. If Valid is false, ZeroCountTags is always empty:
	// we do not report a defect we can't be sure about.
	Valid bool
	// ZeroCountTags lists every zero-count IFD entry found while walking the
	// main IFD chain. A strategy is Applicable only when this is non-empty —
	// i.e. only when the specific, verified defect was actually found, never
	// as a blanket "this is a TIFF" heuristic.
	ZeroCountTags []TIFFZeroCountTag
}

TIFFAnalysis is the result of structurally walking a file's TIFF IFD chain to look for the zero-count defect. It deliberately does NOT attempt a full image decode (no golang.org/x/image/tiff): a raw IFD walk works for any TIFF variant/compression/bit-depth libtiff itself would accept, whereas a decode-based check would only work for the narrow subset of TIFFs Go's image libraries can decode.

type TIFFRepairStrategy

type TIFFRepairStrategy interface {
	// Name is the strategy's short identifier (e.g. "tiff-zero-count").
	Name() string
	// Applicable reports whether this strategy can repair a file with the
	// given analysis.
	Applicable(a TIFFAnalysis) bool
	// Repair reads src and writes a repaired copy to dst. It must not modify
	// src. It is only called when Applicable returned true.
	Repair(src, dst string) error
}

TIFFRepairStrategy is one named TIFF repair technique, mirroring RepairStrategy but keyed on TIFFAnalysis. Kept as a separate interface (rather than a generic one) because JPEG and TIFF detection are structurally unrelated — this keeps each Applicable() check precise and avoids a shared "one size fits all" analysis type.

type TIFFZeroCountTag

type TIFFZeroCountTag struct {
	// Tag is the field tag ID (e.g. 0x8657).
	Tag uint16
	// Type is the field's declared TIFF data type (e.g. 1=BYTE, 2=ASCII).
	Type uint16
	// CountFieldOffset is the absolute byte offset, within the file, of the
	// 4-byte count field to patch during repair.
	CountFieldOffset int64
}

TIFFZeroCountTag records one IFD entry whose 4-byte "count" field is literally 0. Every TIFF field must have at least one value (TIFF 6.0 §2), so count==0 is unconditionally invalid — this is exactly the condition libtiff's _TIFFVSetField rejects fatally ("Null count for Tag N"), which is the confirmed root cause of Immich's "Input file has corrupt header" thumbnail failures for this defect (validated against real libtiff 4.5.1).

type TagDeleteOptions

type TagDeleteOptions struct {
	// Include, when non-nil, keeps only tags whose Value (full hierarchical
	// path) matches it. A nil Include matches every tag.
	Include *regexp.Regexp
	// Exclude, when non-nil, drops any tag whose Value matches it, even if it
	// matched Include. A nil Exclude excludes nothing.
	Exclude *regexp.Regexp
	// DryRun prints the planned delete steps without calling the API.
	DryRun bool
}

TagDeleteOptions controls the tag-delete workflow.

type TakeoutSidecarAnalysis

type TakeoutSidecarAnalysis struct {
	// IsSidecar is true only when the file's leading bytes parse as a JSON
	// object carrying the full Google Takeout fingerprint (see
	// analyzeTakeoutSidecar). It is deliberately conservative: a false here on
	// a real sidecar merely means it won't be deleted, whereas a false-positive
	// would delete a real photo, so detection is biased hard against the latter.
	IsSidecar bool
	// Title is the original file name recorded inside the sidecar (e.g.
	// "IMG_1366.jpg"), surfaced purely for human-readable logging.
	Title string
	// JSONSize is the byte length of the leading JSON object.
	JSONSize int
}

TakeoutSidecarAnalysis is the result of checking whether a file's bytes are actually a Google Photos Takeout metadata JSON sidecar that was imported in place of the real photo (a known Takeout export/import failure mode). Such a file contains no image data at all, so there is nothing to repair — the only safe action is to remove it.

type UploadOptions

type UploadOptions struct {
	FileCreatedAt    *time.Time
	FileModifiedAt   *time.Time
	Filename         *string
	Duration         *int
	IsFavorite       *bool
	Visibility       *immichapi.AssetVisibility
	LivePhotoVideoId *openapi_types.UUID
	// SidecarPath, when non-empty, is sent as the sidecarData part.
	SidecarPath string
	Key         *string
	Slug        *string
	Checksum    *string
	// Progress, when non-nil, counts assetData bytes for the per-file bar.
	// Total/Label should already be set by the caller (total = file size).
	Progress *ByteProgress
}

UploadOptions controls UploadAssetFile. Nil pointers leave the field unset (server default), except the timestamps which fall back to the file's mtime.

type WatchDownloadOptions

type WatchDownloadOptions struct {
	DownloadAlbumOptions
	TargetDir string
	Source    SyncSource
	TagID     *openapi_types.UUID
	AlbumID   *openapi_types.UUID
	Interval  int64 // seconds; <=0 means run once (per user decision: treat as once)
	Once      bool
	DryRun    bool
	Quiet     bool
	JSON      bool
}

WatchDownloadOptions controls one watch-download scan / loop.

type WatchFile

type WatchFile struct {
	AbsPath   string
	Rel       string
	Subfolder string
	Size      int64
	ModTime   time.Time
}

WatchFile is one candidate local file.

func ScanWatchDir

func ScanWatchDir(watchDir, mode string, depth int) ([]WatchFile, error)

ScanWatchDir lists candidate files per mode (depth 1 only in v1). flat: top-level files. by-subfolder: immediate subdir files only.

type WatchUploadOptions

type WatchUploadOptions struct {
	WatchDir   string
	Mode       string // flat | by-subfolder
	Depth      int
	TagPattern string
	AlbumID    *openapi_types.UUID
	AlbumName  string
	Interval   time.Duration
	StableFor  time.Duration
	Once       bool
	DryRun     bool
	Yes        bool
	Quiet      bool
	JSON       bool
}

WatchUploadOptions controls watch-upload.

type WatchUploadStats

type WatchUploadStats struct {
	Uploaded        int `json:"uploaded"`
	LinkedDuplicate int `json:"linkedDuplicate"`
	SkippedUnstable int `json:"skippedUnstable"`
	Failed          int `json:"failed"`
	// SkippedDone counts files already uploaded+linked in a previous run
	// (recognized via the state file, no re-hash, no API call). Reported
	// so re-runs show what happened instead of a silent all-zeros line.
	SkippedDone int `json:"alreadyDone"`
}

WatchUploadStats is the per-interval summary.

func RunWatchUploadOnce

func RunWatchUploadOnce(ctx context.Context, c *client.Client, opts WatchUploadOptions) (WatchUploadStats, error)

RunWatchUploadOnce performs one watch-upload scan.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL