immich-app/immich · error · BadRequestException

Cannot unlink Android motion photos

Error message

Cannot unlink Android motion photos

What it means

Thrown by onBeforeUnlink when unsetting livePhotoVideoId on an asset whose motion video lives on an Android motion-photo path (StorageCore.isAndroidMotionPath). Android motion photos embed their video internally and Immich stores the extracted video under the encoded-video base folder; such motion assets cannot be unlinked because they are managed automatically. HTTP 400.

Source

Thrown at server/src/utils/asset.util.ts:176

  }

  if (motionAsset && motionAsset.visibility === AssetVisibility.Timeline) {
    await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.Hidden });
    await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });
  }
};

export const onBeforeUnlink = async (
  { asset: assetRepository }: AssetHookRepositories,
  { livePhotoVideoId }: { livePhotoVideoId: string },
) => {
  const motion = await assetRepository.getById(livePhotoVideoId);
  if (!motion) {
    return null;
  }

  if (StorageCore.isAndroidMotionPath(motion.originalPath)) {
    throw new BadRequestException('Cannot unlink Android motion photos');
  }

  return motion;
};

export const onAfterUnlink = async (
  { asset: assetRepository, event: eventRepository }: AssetHookRepositories,
  { userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility },
) => {
  await assetRepository.update({ id: livePhotoVideoId, visibility });
  await eventRepository.emit('AssetShow', { assetId: livePhotoVideoId, userId });
};

export function mapToUploadFile(file: ImmichFile): UploadFile {
  return {
    uuid: file.uuid,
    checksum: file.checksum,
    originalPath: file.path,

View on GitHub (pinned to 199723261c)

Solutions

  1. Do not send livePhotoVideoId=null for assets whose motion component is Android-extracted; let Immich manage them.
  2. In the client, detect the motion asset's origin (originalPath under encoded-video folder) and skip the unlink call.
  3. If separation is truly required, delete/re-upload the asset rather than unlinking the embedded motion video.

Example fix

// before
if (asset.livePhotoVideoId) await updateAsset({ id, livePhotoVideoId: null });

// after
const isAndroidMotion = asset.originalPath?.includes('/encoded-video/');
if (asset.livePhotoVideoId && !isAndroidMotion) {
  await updateAsset({ id, livePhotoVideoId: null });
}
Defensive patterns

Strategy: validation

Validate before calling

import { StorageCore } from 'src/cores/storage.core';
const isAndroidMotion = (p?: string) => !!p && StorageCore.isAndroidMotionPath(p);
// before unlinking, skip Android motion assets
if (asset.livePhotoVideoId && !isAndroidMotion(motionOriginalPath)) { await updateAsset({ id, livePhotoVideoId: null }); }

Type guard

const isAndroidMotionAsset = (originalPath?: string): boolean =>
  !!originalPath && originalPath.includes('/encoded-video/');

Try / catch

try { await updateAsset({ id, livePhotoVideoId: null }); }
catch (e) { if (/Android motion photos/.test(e.message)) { /* skip silently, managed by Immich */ } else throw e; }

Prevention

When it happens

Trigger: AssetService.update with dto.livePhotoVideoId === null on an asset whose existing motion video originalPath begins with the encoded-video storage folder (i.e. it was extracted from an Android motion photo).

Common situations: User uploads an Android .MP motion photo (JPEG+embedded MP4) and then tries to manually detach the motion video through an asset update; client treats all live photos uniformly and sends unlink for Android ones.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/a283d219c66ca03f. Report an issue: GitHub.