{"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":"http","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/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/utils/asset.util.ts#L158-L194","documentation":"Thrown by onBeforeUnlink when unsetting livePhotoVideoId on an asset whose motion video lives on an Android motion-photo path (StorageCore.isAndroidMotionPath). Android motion photos embed their video internally and Immich stores the extracted video under the encoded-video base folder; such motion assets cannot be unlinked because they are managed automatically. HTTP 400.","triggerScenarios":"AssetService.update with dto.livePhotoVideoId === null on an asset whose existing motion video originalPath begins with the encoded-video storage folder (i.e. it was extracted from an Android motion photo).","commonSituations":"User uploads an Android .MP motion photo (JPEG+embedded MP4) and then tries to manually detach the motion video through an asset update; client treats all live photos uniformly and sends unlink for Android ones.","solutions":["Do not send livePhotoVideoId=null for assets whose motion component is Android-extracted; let Immich manage them.","In the client, detect the motion asset's origin (originalPath under encoded-video folder) and skip the unlink call.","If separation is truly required, delete/re-upload the asset rather than unlinking the embedded motion video."],"exampleFix":"// before\nif (asset.livePhotoVideoId) await updateAsset({ id, livePhotoVideoId: null });\n\n// after\nconst isAndroidMotion = asset.originalPath?.includes('/encoded-video/');\nif (asset.livePhotoVideoId && !isAndroidMotion) {\n  await updateAsset({ id, livePhotoVideoId: null });\n}","handlingStrategy":"validation","validationCode":"import { StorageCore } from 'src/cores/storage.core';\nconst isAndroidMotion = (p?: string) => !!p && StorageCore.isAndroidMotionPath(p);\n// before unlinking, skip Android motion assets\nif (asset.livePhotoVideoId && !isAndroidMotion(motionOriginalPath)) { await updateAsset({ id, livePhotoVideoId: null }); }","typeGuard":"const isAndroidMotionAsset = (originalPath?: string): boolean =>\n  !!originalPath && originalPath.includes('/encoded-video/');","tryCatchPattern":"try { await updateAsset({ id, livePhotoVideoId: null }); }\ncatch (e) { if (/Android motion photos/.test(e.message)) { /* skip silently, managed by Immich */ } else throw e; }","preventionTips":["Treat Android motion photos as immutable wrt linking; never send livePhotoVideoId=null for them.","In the client, hide the 'unlink motion' control when the motion asset originates from the encoded-video folder."],"tags":["live-photo","android","motion-photo","storage","bad-request"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}