Skip to content

feat(mobile): add playbackStyle to local asset entity and related database schema#26596

Merged
mertalev merged 11 commits intoimmich-app:mainfrom
LeLunZ:playbackStyle-persistence
Mar 1, 2026
Merged

feat(mobile): add playbackStyle to local asset entity and related database schema#26596
mertalev merged 11 commits intoimmich-app:mainfrom
LeLunZ:playbackStyle-persistence

Conversation

@LeLunZ
Copy link
Collaborator

@LeLunZ LeLunZ commented Feb 28, 2026

This is PR 3 to get animated images working. This one focuses on getting the playbackStyle into the database and having the playbackStyle property on the Assets (Local/Remote).


  • Added playbackStyle to Local Asset Entity and added migration
  • Added playbackStyle getter to BaseAsset where we can detect from existing properties what playbackStyle we have.
  • In the TrashedLocalAssetEntityDataDomainExtension added a AssetPlaybackStyle? get _deducedPlaybackStyle to deduce the playback style from deleted assets when restoring.

Roadmap:
If this and the load encoded images pr #26584 gets merged. I then can implement the actual changes to show animated images with an MultiFrameImageStreamCompleter 🎉

Copilot AI review requested due to automatic review settings February 28, 2026 12:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an AssetPlaybackStyle concept to the mobile domain and persists it for local assets via a Drift schema v21 migration, wiring the new field through sync, queries, and repositories to support upcoming animated-image rendering.

Changes:

  • Introduce AssetPlaybackStyle and a deducing playbackStyle getter on BaseAsset.
  • Add playback_style column to local_asset_entity with Drift schema v21 + migration, and plumb it through inserts/reads.
  • Extend merged/timeline queries and trashed-asset restore path to carry (or deduce) playback style.

Reviewed changes

Copilot reviewed 14 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
mobile/test/drift/main/generated/schema.dart Registers schema v21 for drift test schemas.
mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart Writes playbackStyle when restoring trashed assets into local DB.
mobile/lib/infrastructure/repositories/timeline.repository.dart Maps merged query results into LocalAsset.playbackStyle.
mobile/lib/infrastructure/repositories/local_album.repository.dart Persists playbackStyle during local asset upserts.
mobile/lib/infrastructure/repositories/db.repository.steps.dart Generated schema/migration steps updated for v21 and new column.
mobile/lib/infrastructure/repositories/db.repository.dart Bumps schemaVersion to 21 and adds migration to add playback_style.
mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart Deduces playbackStyle for trashed assets when restoring.
mobile/lib/infrastructure/entities/merged_asset.drift.dart Adds playback_style to merged query result mapping.
mobile/lib/infrastructure/entities/merged_asset.drift Updates SQL to include playback_style in UNION projections.
mobile/lib/infrastructure/entities/local_asset.entity.drift.dart Generated table/data updates for playbackStyle column + converters.
mobile/lib/infrastructure/entities/local_asset.entity.dart Adds playbackStyle column to LocalAssetEntity and maps to DTO.
mobile/lib/domain/services/local_sync.service.dart Maps platform playback style onto LocalAsset.
mobile/lib/domain/models/asset/local_asset.model.dart Adds/overrides playbackStyle on LocalAsset.
mobile/lib/domain/models/asset/base_asset.model.dart Introduces AssetPlaybackStyle + deducing getter on BaseAsset.
mobile/drift_schemas/main/drift_schema_v21.json Adds serialized Drift schema for v21.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +8 to 10
@override
final AssetPlaybackStyle? playbackStyle;

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalAsset overrides BaseAsset.playbackStyle with a nullable field. This shadows the new deducing getter in BaseAsset, so any LocalAsset constructed without an explicit playbackStyle (e.g., locally created/edited assets, legacy DB rows after migration) will now return null instead of a deduced value (image/video/livePhoto/etc.). Consider storing the persisted value in a differently named field and overriding the getter to return storedPlaybackStyle ?? super.playbackStyle so existing deduction still works when the DB value is absent.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats intended. As though LocalAssets we can't detect motion photos or other stuff. So we want null in there if the local detection haven't been run for an asset.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It exists from the start for new syncs, right? I think it's better to have a migration that sets the playbackStyle for existing assets to image/video based on asset type and make it non-nullable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shenlong-tanwen Thoughts on this? This column can be computed from DB metadata in a migration if it's remote, but it can't know if it's animated or a live photo if it's local-only without doing a local sync. Would it be caught in a normal local sync, or would it require a full sync?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From how I understand the sync code in the local_sync service:

// Faster path - only new assets added
      if (await checkAddition(dbAlbum, deviceAlbum)) {
        _log.fine("Fast synced device album ${dbAlbum.name}");
        return true;
      }

      // Slower path - full sync
      return await fullDiff(dbAlbum, deviceAlbum);

It seems normal sync runs as long only assets are added to an album, this probably won't touch old assets.


But if assets where removed, the full sync runs and it will only detect changes if we add playbackStyle to the _assetsEqual method in `local_sync.service.dart.

diffSortedListsSync(
        assetsInDb,
        assetsInDevice,
        compare: (a, b) => a.id.compareTo(b.id),
        both: (dbAsset, deviceAsset) {
          // Custom comparison to check if the asset has been modified without
          // comparing the checksum
          if (!_assetsEqual(dbAsset, deviceAsset)) {
            assetsToUpsert.add(deviceAsset);
            return true;
          }
          return false;
        },
        onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id),
        onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset),
      );

So my guess is that it would require a full sync and changes in _assetEqual method

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, thats exactly why I asked in the discord server today when the old code will be removed 😂 This happens to me way too often.

Copy link
Member

@shenlong-tanwen shenlong-tanwen Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way we handle such additions is to fetch all assets that exists locally and update the column in the DB so the model can have the field as non-nullable. _populateLocalAssetTime in migration.dart is one such migration. We can add a similar migration to populate this field

The local sync, even the full sync version, is really fast and so doing this is as a one time migration doesn't add much to the app startup

Copy link
Collaborator Author

@LeLunZ LeLunZ Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shenlong-tanwen thx for the help.

Is it enough to just update all local assets similar to _populateLocalAssetTime?


Edit: I will change it to update all assets through _populateLocalAssetPlaybackStyle, and for TrashedAssets I will use the db migration/merge with the remote assets

Or are trashedLocalAssets also available through the native sync api?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shenlong-tanwen thx for the help.
Or are trashedLocalAssets also available through the native sync api?

You can get the list of assets in the trash using the following NativeSyncApi::getTrashedAssets. So update all assets similar to _populateLocalAssetTime and another call to getTrashedAssets to update the remaining assets

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx :)

@LeLunZ
Copy link
Collaborator Author

LeLunZ commented Mar 1, 2026

@mertalev relevant changes that where made:

  • made playbackStyle non nullable (default 0 -> unknown)
  • for merged asset instead of setting Null as playbackStyle we have to set an integer since its now not nullable. Thats why we now set 0 as playback_style, but thats no problem as the result of the playbackStyle is never used since its calculated through the getter in the dart class.
  • bumped the key targetVersion to 23, and added _populateLocalAssetPlaybackStyle which updates the playbackStyle of all local assets
  • added a custom db statement to update TrashedLocalAssets with playbackStyle value calculated from remote assets
  • Trashed Local Assets are handled the same as Local Assets
  • rebased with main

@mertalev mertalev merged commit f4e1564 into immich-app:main Mar 1, 2026
48 of 49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants