immich-app/immich · error · BadRequestException

Live photo video must be a video

Error message

Live photo video must be a video

What it means

Thrown by onBeforeLink when the asset referenced by livePhotoVideoId exists but its type is not AssetType.Video. Live Photo motion components must be video assets; linking an image (or any non-video) is rejected with HTTP 400. The check is motionAsset.type !== AssetType.Video.

Source

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

    partnerIds.add(partner.sharedById);
  }

  return [...partnerIds];
};

export type AssetHookRepositories = { asset: AssetRepository; event: EventRepository };

export const onBeforeLink = async (
  { asset: assetRepository, event: eventRepository }: AssetHookRepositories,
  { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
) => {
  const motionAsset = await assetRepository.getById(livePhotoVideoId);
  if (!motionAsset) {
    throw new BadRequestException('Live photo video not found');
  }
  if (motionAsset.type !== AssetType.Video) {
    throw new BadRequestException('Live photo video must be a video');
  }
  if (motionAsset.ownerId !== userId) {
    throw new BadRequestException('Live photo video does not belong to the user');
  }

  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;

View on GitHub (pinned to 199723261c)

Solutions

  1. Ensure the id passed as livePhotoVideoId corresponds to an asset whose type is 'VIDEO'.
  2. Upload the motion component as a video (correct MIME type) before linking.
  3. Audit the asset row and correct its type if it was misclassified.

Example fix

// before
await updateAsset({ id, livePhotoVideoId: stillImageId }); // wrong type

// after
await updateAsset({ id, livePhotoVideoId: motionVideoId }); // motionVideoId.type === 'VIDEO'
Defensive patterns

Strategy: validation

Validate before calling

// ensure the motion asset is a video before linking
const motion = await assetRepository.getById(livePhotoVideoId);
const ok = !!motion && motion.type === AssetType.Video;

Type guard

const isVideoAsset = (a: { type: string } | null | undefined): a is { type: 'VIDEO' } =>
  !!a && a.type === 'VIDEO';

Try / catch

try { await updateAsset({ id, livePhotoVideoId }); }
catch (e) { if (e instanceof BadRequestException && /must be a video/.test(e.message)) { notify('Motion asset must be a video'); } else throw e; }

Prevention

When it happens

Trigger: AssetService.update (or asset-media upload) with livePhotoVideoId pointing to an IMAGE asset; passing the still photo's own id as its motion component; cross-linking another user's image asset as a motion video.

Common situations: Client mistakenly uses the still image's id for both the asset and livePhotoVideoId; frontend bug that assigns an image asset id into the motion field; migrating/importing data where the motion asset was mislabeled as an image.

Related errors


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