{"record":{"id":"a283d219c66ca03f","repo":"immich-app/immich","slug":"cannot-unlink-android-motion-photos","errorCode":null,"errorMessage":"Cannot unlink Android motion photos","messagePattern":"Cannot unlink Android motion photos","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/utils/asset.util.ts","lineNumber":176,"sourceCode":"  }\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;\n  }\n\n  if (StorageCore.isAndroidMotionPath(motion.originalPath)) {\n    throw new BadRequestException('Cannot unlink Android motion photos');\n  }\n\n  return motion;\n};\n\nexport const onAfterUnlink = async (\n  { asset: assetRepository, event: eventRepository }: AssetHookRepositories,\n  { userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility },\n) => {\n  await assetRepository.update({ id: livePhotoVideoId, visibility });\n  await eventRepository.emit('AssetShow', { assetId: livePhotoVideoId, userId });\n};\n\nexport function mapToUploadFile(file: ImmichFile): UploadFile {\n  return {\n    uuid: file.uuid,\n    checksum: file.checksum,\n    originalPath: file.path,","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/utils/asset.util.ts#L158-L194","documentation":"Thrown by onBeforeUnlink when attempting to unlink a live photo motion video that lives at an Android motion photo path. Android motion photos embed the video inside the photo's storage layout, so unlinking would corrupt the paired file; Immich blocks it.","triggerScenarios":"Calling onBeforeUnlink on a motion video whose originalPath matches StorageCore.isAndroidMotionPath, i.e. the video was extracted from / belongs to an Android motion photo.","commonSituations":"Bulk unlink scripts processing all live photo videos indiscriminately; users trying to detach motion from an Android-captured photo; migration or dedup jobs touching Android motion pairs.","solutions":["Skip unlinking for assets whose originalPath matches an Android motion path","Use the Android motion photo removal workflow (reprocess the asset without motion extraction) instead of unlink","Only unlink motion videos that were uploaded as separate iOS-style live photo videos"],"exampleFix":"// before\nawait onBeforeUnlink(repos, { livePhotoVideoId: motion.id });\n// after\nif (StorageCore.isAndroidMotionPath(motion.originalPath)) {\n  console.warn(`Skip unlink: ${motion.id} is an Android motion photo`);\n} else {\n  await onBeforeUnlink(repos, { livePhotoVideoId: motion.id });\n}","handlingStrategy":"validation","validationCode":"if (motion.originalPath && /MotionPhoto|motionphoto|EmbeddedVideo/i.test(motion.originalPath)) {\n  // treat as Android motion photo; skip unlink\n}","typeGuard":null,"tryCatchPattern":"try {\n  await unlinkLivePhoto(livePhotoVideoId);\n} catch (e) {\n  if (e.status === 400 && /Android motion/.test(e.message)) {\n    // use the motion-photo removal/reprocess workflow instead\n  }\n}","preventionTips":["Filter out Android motion photo assets in bulk unlink jobs","Detect Android motion pairs by path pattern before mutating","Prefer Immich's dedicated motion photo workflows over manual unlink"],"tags":["asset","live-photo","android","unsupported-operation"],"backgroundTag":"unsupported-operation","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"}