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 as the live photo's motion component is not of type Video. A live photo's motion part must be a video asset; linking an image asset as motion data is invalid and would break live photo playback.
Solutions
- Confirm the motion asset's type is Video via the API before linking
- Swap the arguments if assetId and livePhotoVideoId were reversed
- Re-classify the asset if it was imported with the wrong type
- Fix client-side live photo pairing so the video file maps to livePhotoVideoId
Example fix
// before
await onBeforeLink(repos, { userId, livePhotoVideoId: photoAsset.id });
// after
const motion = await assetRepository.getById(candidateId);
if (motion && motion.type === AssetType.Video) {
await onBeforeLink(repos, { userId, livePhotoVideoId: candidateId });
} Defensive patterns
Strategy: validation
Validate before calling
if (motionAsset.type !== 'VIDEO') {
throw new Error('livePhotoVideoId must reference a video asset');
} Type guard
const isVideoAsset = (a) => a != null && a.type === 'VIDEO';
Try / catch
try {
await linkLivePhoto(assetId, livePhotoVideoId);
} catch (e) {
if (e.status === 400 && /must be a video/.test(e.message)) {
// swap assetId/livePhotoVideoId or re-import with correct type
}
} Prevention
- Map the video component of a live photo to livePhotoVideoId explicitly
- Check asset.type before any live photo pairing
- Don't infer types from file extension alone during imports
When it happens
Trigger: Passing an image asset's ID as livePhotoVideoId when linking a live photo, typically due to swapped IDs in the client or wrong field mapping in the upload pairing logic.
Common situations: Client bug that pairs the image asset as its own motion video; API consumers who reverse the order of assetId and livePhotoVideoId; migrated libraries where file types were misclassified.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- Asset not found or asset is not a video
- Cannot unlink Android motion photos
- Editing live photos is not supported
- Live photo video not found
- Asset not found
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/b7923bde48127416.
Report an issue: GitHub.
Appendix: 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 e55ac299a4)