{"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/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/utils/asset.util.ts#L136-L172","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait onBeforeLink(repos, { userId, livePhotoVideoId: photoAsset.id });\n// after\nconst motion = await assetRepository.getById(candidateId);\nif (motion && motion.type === AssetType.Video) {\n  await onBeforeLink(repos, { userId, livePhotoVideoId: candidateId });\n}","handlingStrategy":"validation","validationCode":"if (motionAsset.type !== 'VIDEO') {\n  throw new Error('livePhotoVideoId must reference a video asset');\n}","typeGuard":"const isVideoAsset = (a) => a != null && a.type === 'VIDEO';","tryCatchPattern":"try {\n  await linkLivePhoto(assetId, livePhotoVideoId);\n} catch (e) {\n  if (e.status === 400 && /must be a video/.test(e.message)) {\n    // swap assetId/livePhotoVideoId or re-import with correct type\n  }\n}","preventionTips":["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"],"tags":["asset","live-photo","type-mismatch"],"backgroundTag":"incompatible-source-type","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}