{"record":{"id":"b7923bde48127416","repo":"immich-app/immich","slug":"live-photo-video-must-be-a-video","errorCode":null,"errorMessage":"Live photo video must be a video","messagePattern":"Live photo video must be a video","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/utils/asset.util.ts","lineNumber":154,"sourceCode":"\n    partnerIds.add(partner.sharedById);\n  }\n\n  return [...partnerIds];\n};\n\nexport type AssetHookRepositories = { asset: AssetRepository; event: EventRepository };\n\nexport const onBeforeLink = async (\n  { asset: assetRepository, event: eventRepository }: AssetHookRepositories,\n  { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },\n) => {\n  const motionAsset = await assetRepository.getById(livePhotoVideoId);\n  if (!motionAsset) {\n    throw new BadRequestException('Live photo video not found');\n  }\n  if (motionAsset.type !== AssetType.Video) {\n    throw new BadRequestException('Live photo video must be a video');\n  }\n  if (motionAsset.ownerId !== userId) {\n    throw new BadRequestException('Live photo video does not belong to the user');\n  }\n\n  if (motionAsset && motionAsset.visibility === AssetVisibility.Timeline) {\n    await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.Hidden });\n    await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });\n  }\n};\n\nexport const onBeforeUnlink = async (\n  { asset: assetRepository }: AssetHookRepositories,\n  { livePhotoVideoId }: { livePhotoVideoId: string },\n) => {\n  const motion = await assetRepository.getById(livePhotoVideoId);\n  if (!motion) {\n    return null;","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/utils/asset.util.ts#L136-L172","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the id passed as livePhotoVideoId corresponds to an asset whose type is 'VIDEO'.","Upload the motion component as a video (correct MIME type) before linking.","Audit the asset row and correct its type if it was misclassified."],"exampleFix":"// before\nawait updateAsset({ id, livePhotoVideoId: stillImageId }); // wrong type\n\n// after\nawait updateAsset({ id, livePhotoVideoId: motionVideoId }); // motionVideoId.type === 'VIDEO'","handlingStrategy":"validation","validationCode":"// ensure the motion asset is a video before linking\nconst motion = await assetRepository.getById(livePhotoVideoId);\nconst ok = !!motion && motion.type === AssetType.Video;","typeGuard":"const isVideoAsset = (a: { type: string } | null | undefined): a is { type: 'VIDEO' } =>\n  !!a && a.type === 'VIDEO';","tryCatchPattern":"try { await updateAsset({ id, livePhotoVideoId }); }\ncatch (e) { if (e instanceof BadRequestException && /must be a video/.test(e.message)) { notify('Motion asset must be a video'); } else throw e; }","preventionTips":["On the upload side, classify motion assets as VIDEO before exposing them in the live-photo picker.","Filter the picker list by asset.type === 'VIDEO'."],"tags":["live-photo","asset","validation","nestjs","bad-request"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}