Skip to content

Conversation

@JW-CH
Copy link
Collaborator

@JW-CH JW-CH commented Oct 20, 2025

Closes #183

Summary by CodeRabbit

  • New Features

    • Video assets are supported in the UI with playback and duration handling.
    • New PlayAudio setting enables audio playback for supported assets.
    • Asset retrieval now supports requesting by asset type.
  • Refactor

    • Image flows replaced with unified asset flows across the UI (preloading, playback, metadata).
    • Settings and filtering updated to honor ShowVideos preference.

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Walkthrough

Replaces image-only flows with asset-aware logic: adds ShowVideos/PlayAudio settings, centralizes asset filtering (AssetHelper/AssetExtensionMethods), extends pools and logic to handle videos, renames GetImage → GetAsset (with optional type), updates API/OpenAPI and frontend to support image/video assets, and updates tests accordingly.

Changes

Cohort / File(s) Summary
Core: Asset helpers
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs, ImmichFrame.Core/Helpers/AssetHelper.cs
New helpers: AssetExtensionMethods adds IsSupportedAsset and ApplyAccountFilters overloads; AssetHelper.GetExcludedAlbumAssets aggregates excluded-album assets.
Core: Interfaces & logic public API
ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs, ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs, ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
Renamed GetImage(Guid)GetAsset(Guid, AssetTypeEnum? assetType = null) across interface and delegates; PooledImmichFrameLogic routes to image/video handlers and adds GetVideoAsset.
Core: Pool implementations
ImmichFrame.Core/Logic/Pool/*.cs (AlbumAssetsPool.cs, AllAssetsPool.cs, CachingApiAssetsPool.cs, FavoriteAssetsPool.cs, MemoryAssetsPool.cs, PeopleAssetsPool.cs, QueuingAssetPool.cs)
Pools updated to honor ShowVideos (use IMAGE filter when false), retrieve excluded assets via AssetHelper, and apply centralized ApplyAccountFilters; AlbumAssetsPool removed in-method exclusion aggregation.
Core: Tests
ImmichFrame.Core.Tests/Logic/Pool/*.cs (AlbumAssetsPoolTests.cs, AllAssetsPoolTests.cs, CachingApiAssetsPoolTests.cs, FavoriteAssetsPoolTests.cs, MemoryAssetsPoolTests.cs, PersonAssetsPoolTests.cs)
Test helpers extended to create IMAGE/VIDEO assets and ratings; tests expanded/adjusted to cover videos, mixed scenarios, nullable ExifInfo handling; AlbumAssetsPoolTests uses real AlbumAssetsPool.
Web API: Controllers & DTOs
ImmichFrame.WebApi/Controllers/AssetController.cs, ImmichFrame.WebApi/Models/ClientSettingsDto.cs
New controller method GetAsset(id, clientIdentifier = "", AssetTypeEnum? assetType = null) returns file result; previous list endpoint renamed GetAssets; GetImage preserved as obsolete wrapper; ClientSettingsDto gains PlayAudio.
OpenAPI & client
openApi/swagger.json, immichFrame.Web/src/lib/immichFrameApi.ts
OpenAPI: list opId changed to GetAssets; added GET /api/Asset/{id}/Asset (operationId GetAsset) with assetType param and binary responses; ClientSettingsDto.playAudio added; TypeScript client updated with getAssets and new getAsset.
WebApi config models & adapters
ImmichFrame.Core/Interfaces/IServerSettings.cs, ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs, ImmichFrame.WebApi/Models/ServerSettings.cs
Added ShowVideos to account settings and PlayAudio to general settings; V1 adapters expose new properties.
Frontend: asset constants & API usage
immichFrame.Web/src/lib/constants/asset-type.ts, immichFrame.Web/src/lib/immichFrameApi.ts
New AssetType enum and type-guards (isImageAsset,isVideoAsset,isSupportedAsset); client adds playAudio in ClientSettingsDto, renames fromAsset → getAssets and adds getAsset(id, {assetType}).
Frontend: components
immichFrame.Web/src/lib/components/home-page/home-page.svelte, .../elements/asset-component.svelte, .../elements/asset.svelte
Replaced image-centric components with asset-aware variants: home page uses AssetComponent, preload and transition logic adjusted, object-URL lifecycle and duration handling added; AssetComponent exports play/pause; Asset component supports video playback, poster/thumbhash, disables zoom/pan for videos.
Tests & examples
ImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.cs, ImmichFrame.WebApi.Tests/Resources/TestV*.json, TestV2.yml, TestV2_NoGeneral.json, docker/Settings.example.*
Test scaffolding updated; test resource files and docker examples include ShowVideos and PlayAudio configuration fields (true in tests, false by default in examples).

Sequence Diagram(s)

sequenceDiagram
    participant UI as Frontend (Home Page)
    participant Logic as PooledImmichFrameLogic
    participant Pool as AssetPool (All/Caching/Memory/...)
    participant API as ImmichApi
    participant Filter as AssetExtensionMethods.ApplyAccountFilters
    participant Settings as AccountSettings

    UI->>Logic: Request assets (getAssets)
    Logic->>Pool: GetAssets()
    Pool->>API: Search assets (Type=IMAGE if !ShowVideos → else allow videos)
    API-->>Pool: Asset list
    Pool->>API: GetExcludedAlbumAssets(...) (via AssetHelper)
    API-->>Pool: Excluded assets
    Pool->>Filter: ApplyAccountFilters(assets, settings, excluded)
    Filter->>Settings: Evaluate ShowVideos, ShowArchived, date, rating
    Filter-->>Pool: Filtered assets
    Pool-->>Logic: Return assets
    Logic-->>UI: Render asset list

    rect rgb(220, 255, 220)
    UI->>Logic: Play specific asset (GetAsset(id, assetType))
    Logic->>Pool: GetAsset(id, assetType)
    Pool->>API: Fetch asset binary/stream
    API-->>Pool: Binary stream (image/video)
    Pool-->>Logic: Stream + metadata
    Logic-->>UI: Stream served (FileResult / blob)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Breadth: changes span backend logic, interfaces, OpenAPI, tests, and substantial frontend component refactors.
  • Pattern consistency: many pool changes are consistent; helpers centralize filtering which reduces ad-hoc logic.
  • Files needing extra attention:
    • PooledImmichFrameLogic.cs — GetAsset routing and error handling for unsupported types.
    • All/Caching/Album/Mem/People pool implementations — correct use of excluded-album aggregation and ApplyAccountFilters.
    • home-page.svelte and asset.svelte — lifecycle, object URL revocation, video poster/thumbhash, and play/pause coordination.
    • openApi/swagger.json and generated client — ensure opId/path changes and parameter typing (assetType) match implementations.

Poem

🐰 A hop, a click — assets leap into view,
Videos and pictures now share one queue,
PlayAudio whispers, ShowVideos lets them roam,
I nibble the code and call this change home,
Hop on, press play — the gallery's grown anew. 🎥✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Video support' directly and clearly describes the primary feature addition in this changeset—implementing video playback functionality throughout the application.
Linked Issues check ✅ Passed The PR successfully implements video playback support as requested in issue #183. Changes include video asset handling, ShowVideos configuration, video component rendering, and API endpoint updates.
Out of Scope Changes check ✅ Passed All changes are directly related to video support implementation: adding video asset types, ShowVideos/PlayAudio settings, video component rendering, API endpoints, and test coverage for video scenarios.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch video_playback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@JW-CH JW-CH added the enhancement New feature or request label Oct 20, 2025
@JW-CH
Copy link
Collaborator Author

JW-CH commented Oct 20, 2025

If you want to give this a try, use the ghcr.io/immichframe/immichframe:video_playback image and make sure ShowVideos is set to true.

@3rob3 3rob3 mentioned this pull request Oct 20, 2025
2 tasks
@kdy1968

This comment was marked as resolved.

@3rob3

This comment was marked as resolved.

@kdy1968

This comment was marked as resolved.

@3rob3

This comment was marked as resolved.

@kdy1968

This comment was marked as resolved.

@JW-CH
Copy link
Collaborator Author

JW-CH commented Oct 21, 2025

@kdy1968 the setting is under Accounts:

Accounts:
    ShowVideos: true

@3rob3
Copy link
Collaborator

3rob3 commented Oct 21, 2025

This is mostly working really well, nice job! I do see some occasional hangs on Frameo (when first launched or often when switching images it just hangs on last image). This is likely due to it's limited CPU and WebView but hopefully there is something we can do to improve this. I also think a setting like video_display_length to only play the first XX seconds would be useful.

@kdy1968
Copy link

kdy1968 commented Oct 21, 2025

seem to be running for 3-5 minute but eventually stops , refreshing browser will resume again , not seeing anything in the log file

@JW-CH
Copy link
Collaborator Author

JW-CH commented Oct 21, 2025

@3rob3 @kdy1968 please move the discussion to #502 to keep it minimal here.

@simowilso
Copy link

If you want to give this a try, use the ghcr.io/immichframe/immichframe:video_playback image and make sure ShowVideos is set to true.

Ooh just found this - fantastic. I tried it, the only slight issue I have noticed so far is that if there is an image displayed next after a video clip, it seems to be shown for a much shorter time than in the settings. (My interval currently set to 3 seconds).

@JW-CH
Copy link
Collaborator Author

JW-CH commented Nov 17, 2025

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

144-152: Guard handleDone against double invocation from ProgressBar and video ended

handleDone is invoked both by the ProgressBar (onDone={handleDone}) and by ImageComponent’s ended event:

  • Line 144–152: handleDone performs navigation + restart logic.
  • Line 429–431: on:ended={() => handleDone(false, false)} on ImageComponent.
  • Line 482–483: onDone={handleDone} on ProgressBar.

For videos, both events can fire near-simultaneously at the end of playback, which means handleDone can run twice in parallel. That can cause behavior like:

  • Skipping one asset ahead (two calls to getNextAssets()).
  • The asset immediately after a video appearing only briefly, since a second transition is triggered almost immediately — this matches feedback about “post-video images” showing shorter than the configured interval.

A simple re-entrancy guard around handleDone will prevent this:

-	let progressBarStatus: ProgressBarStatus = $state(ProgressBarStatus.Playing);
-	let progressBar: ProgressBar = $state() as ProgressBar;
-	let imageComponent: ImageComponentInstance = $state() as ImageComponentInstance;
-	let currentDuration: number = $state($configStore.interval ?? 20);
+	let progressBarStatus: ProgressBarStatus = $state(ProgressBarStatus.Playing);
+	let progressBar: ProgressBar = $state() as ProgressBar;
+	let imageComponent: ImageComponentInstance = $state() as ImageComponentInstance;
+	let currentDuration: number = $state($configStore.interval ?? 20);
+	let isTransitioning: boolean = $state(false);

-	const handleDone = async (previous: boolean = false, instant: boolean = false) => {
-		progressBar.restart(false);
-		$instantTransition = instant;
-		if (previous) await getPreviousAssets();
-		else await getNextAssets();
-		await tick();
-		await imageComponent?.play?.();
-		await progressBar.play();
-	};
+	const handleDone = async (previous: boolean = false, instant: boolean = false) => {
+		if (isTransitioning) {
+			return;
+		}
+
+		isTransitioning = true;
+		try {
+			progressBar.restart(false);
+			$instantTransition = instant;
+			if (previous) {
+				await getPreviousAssets();
+			} else {
+				await getNextAssets();
+			}
+			await tick();
+			await imageComponent?.play?.();
+			await progressBar.play();
+		} finally {
+			isTransitioning = false;
+		}
+	};

This keeps the existing UX while ensuring only one transition is processed per completion.

Also applies to: 429-431, 482-483

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)

99-154: Fix malformed MIME type when serving cached images

In the cached‑image branch you build the content type as:

var ex = Path.GetExtension(file);
return (Path.GetFileName(file), $"image/{ex}", fs);

Path.GetExtension includes the leading dot (e.g., .jpeg), so this yields image/.jpeg, which is not a valid MIME type and may confuse some clients. A minimal fix:

- var ex = Path.GetExtension(file);
- return (Path.GetFileName(file), $"image/{ex}", fs);
+ var ext = Path.GetExtension(file).TrimStart('.').ToLowerInvariant();
+ var contentType = ext == "webp" ? "image/webp" : "image/jpeg";
+ return (Path.GetFileName(file), contentType, fs);

This also keeps cached images consistent with the non‑cached branch.

♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)

3-3: Same dependency concern as AllAssetsPool.

This file also depends on ImmichFrame.WebApi.Helpers. See the comment on AllAssetsPool.cs regarding the Core → WebApi.Helpers dependency direction.

🧹 Nitpick comments (11)
immichFrame.Web/src/lib/constants/asset-type.ts (1)

1-11: Confirm AssetType numeric mapping matches backend AssetTypeEnum

These helpers are clean and convenient, but their correctness depends on IMAGE = 0, VIDEO = 1, etc. staying in sync with the backend AssetTypeEnum values from Immich’s OpenAPI schema.

It’s worth double‑checking the enum mapping in the generated client/spec and adding a small test (e.g., asserting that at least one known image and one known video asset report the expected .type value) so any future backend change is caught quickly.

immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)

95-121: updateAssetPromises doesn’t need to be async

updateAssetPromises doesn’t contain any await and only performs synchronous bookkeeping on assetPromisesDict. Marking it async just wraps the return value in a resolved Promise<void> and can be misleading about its behavior.

You can simplify it to a plain function:

-	async function updateAssetPromises() {
+	function updateAssetPromises() {
 		for (let asset of displayingAssets) {
 			if (!(asset.id in assetPromisesDict)) {
 				assetPromisesDict[asset.id] = loadAsset(asset);
 			}
 		}
 		for (let i = 0; i < PRELOAD_ASSETS; i++) {
 			if (i >= assetBacklog.length) {
 				break;
 			}
 			if (!(assetBacklog[i].id in assetPromisesDict)) {
 				assetPromisesDict[assetBacklog[i].id] = loadAsset(assetBacklog[i]);
 			}
 		}
 		for (let key in assetPromisesDict) {
 			if (
 				!(
 					displayingAssets.find((item) => item.id == key) ||
 					assetBacklog.find((item) => item.id == key)
 				)
 			) {
 				delete assetPromisesDict[key];
 			}
 		}
 	}

Call sites already ignore the returned promise, so this is a safe cleanup.


135-138: Confirm Immich duration format and harden parseAssetDuration

getAssetDurationSeconds/parseAssetDuration currently assume a colon‑separated format (hh:mm[:ss[.fff]]) and return 0 for anything they can’t parse, which then falls back to $configStore.interval:

  • Lines 257–263: updateCurrentDuration derives currentDuration from parsed durations.
  • Lines 265–272: getAssetDurationSeconds special‑cases videos, otherwise uses the configured interval.
  • Lines 274–298: parseAssetDuration splits on : and multiplies by 60 each step, returning 0 on NaN.

Immich’s OpenAPI only documents duration as a string, without constraining the format. If some assets have a plain numeric string (seconds) or another representation, this parser will silently treat them as “invalid” and revert to the fallback interval, which can make progress bar and actual video length diverge.

To make this more robust, you can add a fast path for simple numeric strings before the colon‑based logic:

-	function parseAssetDuration(duration?: string | null) {
-		if (!duration) {
-			return 0;
-		}
-		const parts = duration.split(':').map((value) => value.trim());
-		if (!parts.length) {
-			return 0;
-		}
+	function parseAssetDuration(duration?: string | null) {
+		if (!duration) {
+			return 0;
+		}
+
+		// Handle plain seconds (e.g. "37" or "37.5") before colon-based formats.
+		if (!duration.includes(':')) {
+			const numeric = parseFloat(duration.replace(',', '.'));
+			return Number.isNaN(numeric) ? 0 : numeric;
+		}
+
+		const parts = duration.split(':').map((value) => value.trim());
+		if (!parts.length) {
+			return 0;
+		}
 		let total = 0;
 		let multiplier = 1;
 		while (parts.length) {
 			const value = parts.pop();
 			if (!value) {
 				continue;
 			}
 			const normalized = value.replace(',', '.');
 			const numeric = parseFloat(normalized);
 			if (Number.isNaN(numeric)) {
 				return 0;
 			}
 			total += numeric * multiplier;
 			multiplier *= 60;
 		}
 		return total;
 	}

I’d also recommend adding a couple of unit tests around parseAssetDuration that use real duration strings from your Immich instance (short clip, longer clip, and a failure case) so regressions are caught early.

Also applies to: 257-272, 274-298


135-139: Minor readability: reuse isSupportedAsset in backlog filter

In loadAssets, you manually repeat the image/video checks:

assetBacklog = assetRequest.data.filter(
    (asset) => isImageAsset(asset) || isVideoAsset(asset)
);

Given you already have isSupportedAsset, you could make this a bit clearer and more future‑proof:

-			assetBacklog = assetRequest.data.filter(
-				(asset) => isImageAsset(asset) || isVideoAsset(asset)
-			);
+			assetBacklog = assetRequest.data.filter(isSupportedAsset);

This keeps all knowledge of “what we can render” in a single place.


327-362: Consider revoking object URLs when assets are evicted

loadAsset creates an object URL for each asset blob:

return [getObjectUrl(req.data), assetResponse, album];

and getObjectUrl wraps URL.createObjectURL. When assets fall out of displayingAssets and assetBacklog, updateAssetPromises deletes their promises from assetPromisesDict, but the corresponding object URLs are never revoked.

This was already a minor leak for images; with video blobs added, the memory impact can grow over long runtimes.

Not urgent for this PR, but as a follow‑up you may want to:

  • Track the URL per asset id (e.g. in a separate assetUrlById map or by resolving the promise before deletion), and
  • Call URL.revokeObjectURL(url) inside updateAssetPromises when you remove an entry that’s no longer in history/backlog.

That will keep the frame process footprint more stable during long-running slideshows.

Also applies to: 111-119

ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs (1)

20-23: Video gating in MemoryAssetsPool is correct but could be centralized

Filtering assets to AssetTypeEnum.IMAGE when ShowVideos is false is logically correct and avoids extra per-asset API calls for videos. If you’re standardizing on shared helpers like ApplyAccountFilters elsewhere, consider using the same helper here for consistency so account-level rules (videos, excluded albums/people, ratings) all live in one place.

ImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.cs (1)

42-42: Leverage the new type parameter to cover ShowVideos=true behavior

The extended CreateAsset helper is good, but the tests in this file still only exercise the image-only path (implicit ShowVideos == false). Consider adding at least one test that sets _mockAccountSettings.Setup(a => a.ShowVideos).Returns(true); and verifies that:

  • MetadataSearchDto.Type is not forced to IMAGE, and
  • video assets (created via CreateAsset(id, AssetTypeEnum.VIDEO)) are returned as expected.

That will lock in the new behavior and guard against regressions.

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)

156-177: Align video file extension with actual content type (optional)

GetVideoAsset always names the file {id}.mp4 even if the Content-Type header is something else (e.g., video/webm). That’s probably harmless for streaming but can be misleading if the filename is ever surfaced to clients. Consider deriving the extension from contentType (falling back to .mp4 when unknown) so the name and MIME type match.

ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (1)

26-26: Consider parameterizing the hardcoded limit.

The test helper now calls GetAssets(25, ct) with a hardcoded limit of 25. While this is acceptable for tests, consider whether this value should be a constant or parameter to make test intent clearer.

-        public async Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => await base.GetAssets(25, ct);
+        private const int TestAssetLimit = 25;
+        public async Task<IEnumerable<AssetResponseDto>> TestLoadAssets(CancellationToken ct = default) => await base.GetAssets(TestAssetLimit, ct);
ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs (1)

168-198: Simplify test setup by removing unused video memories.

Lines 171, 177, and the videoMemoriesCount variable create video memories that are never used because ShowVideos defaults to false. These video assets are filtered out by ApplyAccountFilters, making the test setup misleading.

Apply this diff to remove the unnecessary video memory creation:

     [Test]
     public async Task LoadAssets_AggregatesAssetsFromMultipleMemories()
     {
-        var imageMemoriesCount = 2;
-        var videoMemoriesCount = 2;
+        var memoryCount = 2;
         var assetsPerMemory = 2;

         // Arrange
         var memoryYear = DateTime.Now.Year - 3;
-        var memories = CreateSampleImageMemories(imageMemoriesCount, assetsPerMemory, true, memoryYear); // 2 memories, 2 assets each
-        memories.AddRange(CreateSampleVideoMemories(videoMemoriesCount, assetsPerMemory, true, memoryYear)); // 2 video memories, 2 assets each
+        var memories = CreateSampleImageMemories(memoryCount, assetsPerMemory, true, memoryYear); // 2 memories, 2 assets each

         _mockImmichApi.Setup(x => x.SearchMemoriesAsync(It.IsAny<DateTimeOffset>(), null, null, null, It.IsAny<CancellationToken>()))
             .ReturnsAsync(memories).Verifiable(Times.Once);
openApi/swagger.json (1)

211-274: LGTM! New unified asset endpoint properly supports video playback.

The new /api/Asset/{id}/Asset endpoint consolidates image and video retrieval with proper content type support (image/jpeg, image/webp, video/mp4, video/quicktime) and an optional assetType parameter for type-specific retrieval.

The path /api/Asset/{id}/Asset has a slightly redundant naming pattern. Consider /api/Asset/{id} or /api/Asset/{id}/Content for a cleaner API surface in a future refactor. However, the current pattern maintains consistency with existing endpoints like /api/Asset/{id}/Image and /api/Asset/{id}/AssetInfo.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae363bb and 611772b.

📒 Files selected for processing (32)
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (2 hunks)
  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (4 hunks)
  • ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs (11 hunks)
  • ImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.cs (1 hunks)
  • ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs (7 hunks)
  • ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (4 hunks)
  • ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (1 hunks)
  • ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (2 hunks)
  • ImmichFrame.Core/Interfaces/IServerSettings.cs (1 hunks)
  • ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2 hunks)
  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs (2 hunks)
  • ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/QueuingAssetPool.cs (2 hunks)
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (2 hunks)
  • ImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.cs (2 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV1.json (1 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV2.json (2 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV2.yml (2 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV2_NoGeneral.json (1 hunks)
  • ImmichFrame.WebApi/Controllers/AssetController.cs (3 hunks)
  • ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs (2 hunks)
  • ImmichFrame.WebApi/Models/ServerSettings.cs (1 hunks)
  • immichFrame.Web/src/lib/components/elements/image-component.svelte (5 hunks)
  • immichFrame.Web/src/lib/components/elements/image.svelte (6 hunks)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (18 hunks)
  • immichFrame.Web/src/lib/constants/asset-type.ts (1 hunks)
  • immichFrame.Web/src/lib/immichFrameApi.ts (2 hunks)
  • openApi/swagger.json (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs
  • ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs
  • ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs
  • ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
  • ImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/FavoriteAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs
  • ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs
  • ImmichFrame.Core/Helpers/AssetExtensionMethods.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/QueuingAssetPool.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing algorithms with randomization (like Fisher-Yates shuffle), use order-independent assertions such as `Is.SupersetOf` to validate core functionality while maintaining test stability. Order-dependent assertions like `Is.EqualTo` or exact-count assertions like `Is.EquivalentTo` will cause flaky tests when the implementation legitimately produces non-deterministic output.

Applied to files:

  • ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs
🔇 Additional comments (47)
ImmichFrame.WebApi.Tests/Resources/TestV2_NoGeneral.json (1)

30-30: Remove unverified claim about test coverage.

The original review comment incorrectly asserts that tests exercise the ShowVideos configuration difference. However, TestLoadConfigV2Json_NoGeneral() only validates GeneralSettings and does not call VerifyAccounts() or VerifyConfig(). The asymmetric ShowVideos configuration (Account1 without it, Account2 with it) has no test coverage in this test case.

The test file itself is appropriate—the asymmetric configuration is intentional for testing different scenarios. However, the review comment's request to verify test coverage cannot be fulfilled because the relevant test does not exercise the Accounts configuration.

Likely an incorrect or invalid review comment.

ImmichFrame.Core/Logic/Pool/QueuingAssetPool.cs (1)

50-51: No issues found—QueuingAssetPool is confirmed to be unused in production.

Verification confirms the TODO comment is accurate: QueuingAssetPool is never instantiated in production code. The BuildPool method only instantiates AllAssetsPool, FavoriteAssetsPool, MemoryAssetsPool, AlbumAssetsPool, PersonAssetsPool, and MultiAssetPool. No DI registration or production-code references to QueuingAssetPool exist outside the test suite. The missing ShowVideos filter is not a functional concern for unused code.

ImmichFrame.WebApi.Tests/Resources/TestV2.json (1)

47-68: ShowVideos flag correctly wired into JSON test data

The new ShowVideos fields for both accounts align with the C# ServerAccountSettings.ShowVideos property and give you explicit coverage of the video-enabled path in v2 JSON config tests. No issues from a structure or naming perspective.

ImmichFrame.WebApi/Models/ServerSettings.cs (1)

65-81: ServerAccountSettings.ShowVideos added with safe default

ShowVideos is added with a default of false, which is a good backward-compatible choice for existing configs that don’t include the field. It also matches the casing used in your JSON/YAML resources.

ImmichFrame.WebApi.Tests/Resources/TestV2.yml (1)

44-60: YAML test fixture kept consistent with JSON for ShowVideos

Adding ShowVideos: true to both account entries mirrors the JSON resource and ensures YAML-based tests exercise the video-enabled configuration too. Looks consistent and correct.

ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs (1)

15-15: V1 settings and adapter correctly extended with ShowVideos

Extending ServerSettingsV1 and AccountSettingsV1Adapter with ShowVideos keeps the v1 configuration path feature-complete with v2 while preserving a safe default of false for older configs. The adapter projection is straightforward and consistent with the other account flags.

Also applies to: 68-83

ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)

8-15: GetImage → GetAsset interface change looks sound

Switching to GetAsset(Guid id, AssetTypeEnum? assetType = null) is a sensible generalization for video support and retains the existing stream return shape. Just ensure all implementations and callers have been updated to the new name/signature (the compiler should catch any stragglers).

immichFrame.Web/src/lib/immichFrameApi.ts (1)

224-235: getAssets/getAsset client wiring aligns with new API surface

The generated getAssets and getAsset helpers correctly reflect the new /api/Asset list and /api/Asset/{id}/Asset binary endpoints, including the optional assetType query parameter. This matches how the front-end now requests image/video blobs.

Also applies to: 272-285

ImmichFrame.WebApi.Tests/Resources/TestV1.json (1)

15-15: ShowVideos test flag looks consistent

Adding ShowVideos: true alongside other boolean flags keeps the test config aligned with the new account setting; no issues here.

ImmichFrame.Core/Interfaces/IServerSettings.cs (1)

16-16: Interface change requires all implementations to expose ShowVideos

Adding ShowVideos to IAccountSettings is appropriate next to the other Show* flags, but it does require every implementation (and mocks) of IAccountSettings to be updated. Please confirm all concrete settings classes (and configuration mappings) now populate this property with the intended default.

ImmichFrame.Core/Logic/Pool/MemoryAssetsPool.cs (1)

47-49: DailyApiCache expiration at next midnight is fine

Setting AbsoluteExpiration to DateTimeOffset.Now.Date.AddDays(1) keeps the cache aligned to calendar days; the behavior looks intentional and safe.

ImmichFrame.Core/Logic/Pool/FavoriteAssetsPool.cs (1)

26-29: Conditional favorite-type filter matches ShowVideos semantics

Only forcing metadataBody.Type = AssetTypeEnum.IMAGE when ShowVideos is false looks correct and aligns favorites with the per-account video toggle. Please just double‑check that the Immich SearchAssetsAsync API indeed treats a null Type as “all asset types”; if that ever changes, video inclusion here would break silently.

ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (1)

18-18: Album pool now relies entirely on upstream filtering

Returning albumAssets directly keeps this pool simple. Given the previous excluded‑album handling, please confirm that album‑level exclusions (from ExcludedAlbums) are now enforced centrally (e.g., via a shared ApplyAccountFilters step in CachingApiAssetsPool or similar) so behavior doesn’t regress for users relying on exclusions.

ImmichFrame.Core/Logic/Pool/PeopleAssetsPool.cs (1)

28-31: Person-assets video gating matches the intended semantics

Conditionally constraining metadataBody.Type to IMAGE when ShowVideos is false is consistent with the other pools and keeps per-person results aligned with the account’s video preference. Looks good.

ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)

76-97: New GetAsset router cleanly separates image and video handling

The GetAsset entrypoint looks good: resolving assetType via GetAssetInfoAsync when missing, then delegating to image/video helpers and guarding unsupported types with a clear exception. Please just ensure that all external callers passing a non‑null assetType are using the real asset type from Immich (not UI guesses), otherwise you can end up calling the wrong handler and surfacing confusing 404s.

ImmichFrame.WebApi.Tests/Controllers/AssetControllerTests.cs (1)

193-264: Commented-out test needs attention.

The TODO comment indicates this video streaming test requires fixes before activation. Consider tracking this as a follow-up task to ensure video asset retrieval is properly tested.

Would you like me to open a new issue to track completing this test, or is this already covered by issue #502?

ImmichFrame.Core.Tests/Logic/Pool/PersonAssetsPoolTests.cs (1)

44-44: LGTM! Type-aware asset filtering properly tested.

The addition of the optional type parameter with a sensible default (IMAGE) and the consistent verification of type filtering in mock setups aligns well with the video support feature.

Also applies to: 61-84, 103-109

ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (1)

34-38: LGTM! Proper cache mock setup.

The mock setup correctly handles the async factory pattern for GetOrAddAsync, ensuring the cache behavior is properly exercised in tests.

ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)

11-21: LGTM! Video counting logic is correct.

The conditional logic properly includes video assets in the count when ShowVideos is enabled, falling back to images-only otherwise.


32-35: LGTM! Asset type filtering correctly respects ShowVideos setting.

The logic appropriately sets Type = IMAGE only when videos are disabled, allowing mixed asset types when ShowVideos is true. The use of ApplyAccountFilters consolidates filtering logic.

Also applies to: 64-66

ImmichFrame.Core.Tests/Logic/Pool/CachingApiAssetsPoolTests.cs (2)

62-62: LGTM! Comprehensive video filtering test coverage.

The new tests properly validate video asset filtering behavior under different ShowVideos settings. The test data includes a video asset, and assertions correctly verify filtering outcomes.

Also applies to: 85-100, 134-148, 205-220


164-177: Good improvement: Dictionary-based cache store.

Replacing the local cached-value pattern with a dictionary provides more robust simulation of actual caching behavior and better reflects how the cache operates in production.

ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (2)

23-26: LGTM! Sound two-step caching pattern.

Caching excluded album assets separately from the final filtered assets is a good design that allows cache invalidation at the appropriate granularity. The cache keys are properly namespaced.


28-40: LGTM! Excluded assets aggregation is correct.

The method properly iterates over excluded albums with null-safe handling and aggregates their assets. The null-coalescing operator on line 32 prevents null reference exceptions.

ImmichFrame.Core/Logic/MultiImmichFrameLogicDelegate.cs (1)

45-46: LGTM! API properly generalized for multi-asset-type support.

Renaming GetImage to GetAsset with an optional assetType parameter appropriately reflects the expanded scope to support videos. The delegation correctly passes both parameters through.

immichFrame.Web/src/lib/components/elements/image-component.svelte (2)

56-73: LGTM! Clean event handling and control API.

The event dispatcher is properly typed, and the exported pause/play methods provide a clean API for external control. Null-safe optional chaining ensures the methods work correctly even if components aren't yet mounted.


115-116: LGTM! Proper component binding and event propagation.

The bind:this directives correctly capture component references, and the on:ended handlers properly propagate media-end events. The pattern handles both single and split-screen modes consistently.

Also applies to: 132-133, 151-152

ImmichFrame.Core.Tests/Logic/Pool/MemoryAssetsPoolTests.cs (3)

30-47: LGTM! Asset type parameterization is well implemented.

The helper now correctly sets file extensions and asset types based on the provided AssetTypeEnum, enabling test scenarios for both images and videos.


49-57: LGTM! Type-specific helper methods improve test readability.

The specialized helpers provide clear intent when creating image-only or video-only test data.


200-231: LGTM! Video aggregation test correctly validates mixed-asset behavior.

The test properly sets ShowVideos = true and verifies that both image and video memories are aggregated, expecting the correct total count of 8 assets.

ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (5)

46-61: LGTM! Test helpers cleanly support multi-type and rating-aware asset creation.

The helpers provide clear, type-safe wrappers for creating test assets with specific types and ratings, supporting comprehensive test scenarios.


64-95: LGTM! Asset counting tests correctly validate ShowVideos behavior.

The tests properly verify that asset counts include only images when ShowVideos is false (100 assets) and include both images and videos when enabled (140 assets).


98-156: LGTM! Parameter validation tests correctly verify API request construction.

Both tests properly validate that SearchRandomAsync is called with the correct parameters:

  • When ShowVideos is false: Type = IMAGE and size reflects only images.
  • When ShowVideos is true: Type = null (all types) and size includes both images and videos.

159-171: LGTM! Date filter test correctly validates ImagesFromDays behavior.

The test properly verifies that the TakenAfter parameter is set correctly based on ImagesFromDays configuration.


174-197: LGTM! Excluded albums test correctly validates asset filtering.

The test properly verifies that assets from excluded albums are filtered out, using the updated image-specific helper for consistency.

openApi/swagger.json (2)

13-13: LGTM! OperationId rename improves API clarity.

Renaming GetAsset to GetAssets for the list endpoint accurately reflects that it returns multiple assets, improving API documentation clarity.


207-209: LGTM! Proper deprecation strategy maintains backward compatibility.

Marking the /Image endpoint as deprecated while keeping it functional allows existing clients to continue working while signaling the need to migrate to the new /Asset endpoint.

ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (3)

9-12: LGTM! Supported asset type filtering is appropriate.

The method correctly identifies IMAGE and VIDEO as supported asset types, which aligns with the PR's goal of adding video playback support. Other asset types (if any exist in AssetTypeEnum) are intentionally filtered out.


14-17: LGTM! Async overload provides convenient filtering pipeline support.

The async extension method allows seamless integration with async asset retrieval operations.


19-50: Date filtering excludes assets without EXIF DateTimeOriginal; consider whether fallback dates are intended.

The filtering logic in lines 33 and 39 uses only ExifInfo?.DateTimeOriginal when applying date range constraints. Verification confirms that AssetResponseDto has alternative date properties available—fileCreatedAt, fileModifiedAt, and localDateTime—but no fallback logic uses these fields. Assets without EXIF date information will be completely excluded from date-range filtering.

This filtering behavior may be intentional (restricting to EXIF-sourced dates only) or may represent an oversight where date filters should fallback to file or computed timestamps. Verify this aligns with intended user behavior before merging.

immichFrame.Web/src/lib/components/elements/image.svelte (4)

2-47: LGTM! Video handling setup is properly structured.

The component correctly imports necessary utilities, creates a typed event dispatcher for the ended event, derives video status, and maintains a reference to the video element for programmatic control.


52-53: LGTM! Zoom and pan are appropriately disabled for videos.

Disabling these visual effects for videos prevents interference with video playback and maintains expected user experience.


137-151: LGTM! Video control methods are well-guarded against edge cases.

Both pause() and play() properly check for video element existence before calling methods. The try-catch in play() correctly handles autoplay blocking and other expected failures.


190-211: LGTM! Conditional rendering properly handles video and image assets.

The video element is configured correctly for automatic slideshow playback:

  • autoplay and muted enable automatic playback
  • playsinline prevents unwanted fullscreen on mobile
  • poster provides visual continuity while loading
  • on:ended event enables progression to the next asset
ImmichFrame.WebApi/Controllers/AssetController.cs (3)

36-42: LGTM! Endpoint rename improves semantic clarity.

Renaming to GetAssets (plural) accurately reflects that the endpoint returns a list of assets, improving API consistency.


62-84: LGTM! Backward-compatible refactor with proper deprecation.

The approach maintains backward compatibility while introducing video support:

  • Old GetImage endpoint delegates to new GetAsset with IMAGE type hint
  • Properly marked with [Obsolete] attribute
  • New GetAsset accepts optional assetType parameter for performance optimization
  • Logging includes type hint for debugging

95-95: LGTM! Random image endpoint updated to use new GetAsset method.

The call correctly specifies AssetTypeEnum.IMAGE since this endpoint is specifically for image retrieval, as indicated by its name.

@JW-CH JW-CH marked this pull request as ready for review November 17, 2025 22:28
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)

28-40: Same null reference issue and duplication as in AllAssetsPool.cs.

Line 36 has the same potential ArgumentNullException if albumInfo.Assets is null. This method is an exact duplicate of the implementation in AllAssetsPool.cs (lines 69-81).

Apply the same fix as suggested for AllAssetsPool.cs:

-            excludedAlbumAssets.AddRange(albumInfo.Assets);
+            if (albumInfo.Assets != null)
+            {
+                excludedAlbumAssets.AddRange(albumInfo.Assets);
+            }

Consider extracting this duplicated logic to a shared helper as suggested in the review for AllAssetsPool.cs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 611772b and 4814978.

📒 Files selected for processing (4)
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs (5 hunks)
  • ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3 hunks)
  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ImmichFrame.Core/Helpers/AssetExtensionMethods.cs
  • ImmichFrame.Core.Tests/Logic/Pool/AlbumAssetsPoolTests.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs
🧬 Code graph analysis (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (3)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
  • Task (9-21)
  • Task (23-67)
  • Task (69-81)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
🔇 Additional comments (3)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)

9-21: LGTM! Video support in asset counting implemented correctly.

The conditional logic properly includes videos in the count when ShowVideos is enabled, aligning with the PR objectives.


23-67: LGTM! Asset retrieval with centralized filtering looks good.

The refactored approach using ApplyAccountFilters centralizes the filtering logic and properly handles the video support feature. The conditional type restriction (lines 32-35) correctly allows both images and videos when ShowVideos is enabled.

ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)

21-26: LGTM! Smart two-level caching strategy.

The approach of caching excluded album assets separately (line 23) and then caching the filtered results (line 25) is efficient. This avoids re-fetching excluded albums on every cache miss for the main asset list.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)

28-43: Extract duplicated method to shared helper.

GetExcludedAlbumAssets is duplicated in AllAssetsPool.cs (lines 69-84). Extract this to a shared static helper in ImmichFrame.Core.Helpers (e.g., AssetHelper.GetExcludedAlbumAssets) to eliminate duplication and improve maintainability.

Apply this refactor:

1. Create a new helper class:

// ImmichFrame.Core/Helpers/AssetHelper.cs
namespace ImmichFrame.Core.Helpers;

public static class AssetHelper
{
    public static async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(
        ImmichApi immichApi, 
        IAccountSettings accountSettings, 
        CancellationToken ct = default)
    {
        var excludedAlbumAssets = new List<AssetResponseDto>();
        
        foreach (var albumId in accountSettings?.ExcludedAlbums ?? new())
        {
            var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
            if (albumInfo.Assets != null)
            {
                excludedAlbumAssets.AddRange(albumInfo.Assets);
            }
        }
        
        return excludedAlbumAssets;
    }
}

2. Replace in CachingApiAssetsPool.cs:

-    private async Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default)
-    {
-        var excludedAlbumAssets = new List<AssetResponseDto>();
-
-        foreach (var albumId in accountSettings?.ExcludedAlbums ?? new())
-        {
-            var albumInfo = await immichApi.GetAlbumInfoAsync(albumId, null, null, ct);
-
-            if (albumInfo.Assets != null)
-            {
-                excludedAlbumAssets.AddRange(albumInfo.Assets);
-            }
-        }
-
-        return excludedAlbumAssets;
-    }
+    private Task<IEnumerable<AssetResponseDto>> GetExcludedAlbumAssets(CancellationToken ct = default)
+        => AssetHelper.GetExcludedAlbumAssets(immichApi, accountSettings, ct);

3. Replace in AllAssetsPool.cs similarly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4814978 and c45e4f5.

📒 Files selected for processing (2)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3 hunks)
  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧬 Code graph analysis (2)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
  • Task (9-21)
  • Task (23-67)
  • Task (69-84)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (2)
  • Task (8-19)
  • AlbumAssetsPool (7-29)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (5)
  • Task (11-14)
  • Task (16-19)
  • Task (21-26)
  • Task (28-43)
  • Task (45-45)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (5)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (2)

2-2: LGTM: Correct dependency direction.

The using directive correctly imports from ImmichFrame.Core.Helpers, resolving the previously flagged upward dependency issue.


21-26: LGTM: Sound two-tier caching strategy.

The separate cache keys for excluded album assets and main assets enable independent invalidation, improving cache efficiency when excluded albums change.

ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)

2-2: LGTM!

Required for ApplyAccountFilters extension method.


32-35: LGTM: Correct conditional video handling.

The logic correctly restricts to IMAGE type only when ShowVideos is false, allowing both images and videos when enabled.


64-66: LGTM: Consistent filtering flow.

The filtering pattern correctly retrieves excluded album assets and applies consolidated account filters, consistent with CachingApiAssetsPool.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

64-64: Consider caching excluded album assets.

GetExcludedAlbumAssets is called on every GetAssets invocation, which may be inefficient if the excluded albums list is large or API responses are slow. CachingApiAssetsPool caches this result (line 23 of that file), but AllAssetsPool does not.

If GetAssets is called frequently, consider caching excludedAlbumAssets using the existing apiCache instance.

Example approach:

-        var excludedAlbumAssets = await AssetHelper.GetExcludedAlbumAssets(immichApi, accountSettings, ct);
+        var excludedAlbumAssets = await apiCache.GetOrAddAsync(
+            $"{nameof(AllAssetsPool)}_ExcludedAlbums",
+            () => AssetHelper.GetExcludedAlbumAssets(immichApi, accountSettings, ct));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c45e4f5 and 506acb3.

📒 Files selected for processing (3)
  • ImmichFrame.Core/Helpers/AssetHelper.cs (1 hunks)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3 hunks)
  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧬 Code graph analysis (3)
ImmichFrame.Core/Helpers/AssetHelper.cs (3)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
  • Task (9-21)
  • Task (23-67)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
  • Task (11-14)
  • Task (16-19)
  • Task (21-26)
  • Task (28-28)
ImmichFrame.Core/Logic/Pool/AlbumAssetsPool.cs (2)
  • AlbumAssetsPool (7-29)
  • Task (9-28)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
ImmichFrame.Core/Helpers/AssetHelper.cs (2)
  • Task (9-23)
  • AssetHelper (7-24)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)
  • Task (9-21)
  • Task (23-67)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (4)
ImmichFrame.Core/Helpers/AssetHelper.cs (2)
  • Task (9-23)
  • AssetHelper (7-24)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
  • Task (11-14)
  • Task (16-19)
  • Task (21-26)
  • Task (28-28)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (4)
ImmichFrame.Core/Helpers/AssetHelper.cs (1)

7-24: LGTM! Clean extraction of excluded album logic.

The helper properly centralizes excluded album asset retrieval, handles null accountSettings and Assets collections safely, and supports cancellation. This eliminates the duplication flagged in previous reviews.

ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)

11-20: GetAssetCount now respects the ShowVideos setting.

The method correctly returns combined image and video counts when ShowVideos is enabled, and image-only counts otherwise. Note that other filters (excluded albums, date ranges, rating) are not yet applied, as acknowledged by the TODO comment.


32-35: Video filtering and exclusion logic is correct.

The implementation properly restricts searchDto.Type to IMAGE when ShowVideos is false and correctly applies excluded album filters via AssetHelper and ApplyAccountFilters.

Also applies to: 64-66

ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (1)

21-26: LGTM! Efficient caching strategy for excluded albums.

Caching excludedAlbumAssets separately under a dedicated key is a smart approach, as it allows independent cache invalidation. The filtering flow is clean and properly integrates the excluded assets into ApplyAccountFilters.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

144-152: Guard handleDone against overlapping invocations from progress bar, video ended, and controls

handleDone is now the central transition handler, called from:

  • ProgressBar via onDone={handleDone},
  • ImageComponent via on:ended={() => handleDone(false, false)},
  • OverlayControls next/back actions.

Because handleDone is async and mutates assetBacklog, assetHistory, displayingAssets, and restarts both imageComponent and progressBar, concurrent or rapid-fire triggers (e.g., progress bar finishing while a video fires ended, or user clicking “next” at the same time) can cause overlapping transitions, skipped assets, or inconsistent progress/video sync.

Consider adding a simple reentrancy guard so only one transition runs at a time:

- const handleDone = async (previous: boolean = false, instant: boolean = false) => {
+ let isHandlingAssetTransition = false;
+
+ const handleDone = async (previous: boolean = false, instant: boolean = false) => {
+   if (isHandlingAssetTransition) {
+     return;
+   }
+   isHandlingAssetTransition = true;
+   try {
      progressBar.restart(false);
      $instantTransition = instant;
      if (previous) await getPreviousAssets();
      else await getNextAssets();
      await tick();
      await imageComponent?.play?.();
      await progressBar.play();
-   };
+   } finally {
+     isHandlingAssetTransition = false;
+   }
+ };

This keeps your nice wiring (restart/stop subscriptions, OverlayControls pause/info toggles, on:ended, and progress bar onDone) while preventing race conditions during navigation and playback.

Also applies to: 379-382, 386-389, 429-432, 441-458, 460-468, 476-484

♻️ Duplicate comments (1)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

9-21: Filter mismatch between GetAssetCount and GetAssets remains unfixed in AllAssetsPool.

GetAssetCount (line 9-21) uses raw API statistics filtered only by ShowVideos, while GetAssets (line 24-69) applies ShowArchived, date ranges, and rating filters. This causes GetAssetCount to return inflated counts that don't match the actual filtered assets returned by GetAssets.

Example: With ShowArchived=false, GetAssetCount includes archived assets in raw stats, but GetAssets excludes them—counts don't match.

The inline TODO comment acknowledges this is temporary, but the discrepancy remains unresolved and can mislead callers like PooledImmichFrameLogic.GetTotalAssets().

🧹 Nitpick comments (7)
immichFrame.Web/src/lib/components/elements/image-component.svelte (1)

67-75: Consider error handling and caller feedback for playback controls.

The exported pause() and play() functions use optional chaining, which gracefully handles unmounted components but silently fails without feedback to the caller. Additionally, errors from child component methods will propagate uncaught.

Consider whether:

  1. Callers need to know if playback control succeeded
  2. Errors from child components should be caught and logged or re-thrown with context

Example with error handling:

 export const pause = async () => {
-  await primaryImageComponent?.pause?.();
-  await secondaryImageComponent?.pause?.();
+  try {
+    await primaryImageComponent?.pause?.();
+    await secondaryImageComponent?.pause?.();
+  } catch (error) {
+    console.error('Failed to pause media:', error);
+    throw error;
+  }
 };
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (1)

32-68: Consider eliminating redundant filter checks.

Filters are applied both server-side (via searchDto properties at lines 32-61) and client-side (via ApplyAccountFilters at line 68). Specifically:

  • ShowVideos: filtered via searchDto.Type (line 34) and re-checked in ApplyAccountFilters
  • ShowArchived: filtered via searchDto.Visibility (lines 37-44) and re-checked in ApplyAccountFilters
  • Date ranges: filtered via searchDto.TakenBefore/TakenAfter (lines 46-56) and re-checked in ApplyAccountFilters
  • Rating: filtered via searchDto.Rating (lines 58-61) and re-checked in ApplyAccountFilters

The only unique client-side operations are IsSupportedAsset() and excluded-album filtering. If the server-side API is trusted, consider creating a lighter ApplyAccountFilters variant that skips redundant checks.

ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs (1)

10-57: Legacy V1 settings correctly expose ShowVideos and PlayAudio via adapters

The additions of ShowVideos and PlayAudio to ServerSettingsV1, and their exposure through both AccountSettingsV1Adapter and GeneralSettingsV1Adapter, preserve backward compatibility for the flat config while satisfying the newer interfaces. If you ever need per-account PlayAudio distinct from the general value in V1, that would likely warrant a new settings version, but the current approach is reasonable for legacy support.

Also applies to: 69-85, 87-123

openApi/swagger.json (1)

211-275: New /api/Asset/{id}/Asset endpoint cleanly models binary image/video retrieval

The GET /api/Asset/{id}/Asset definition—with assetType referencing AssetTypeEnum and multiple binary content types for images and videos—matches the intended asset-type-aware retrieval and the generated TS getAsset function. Consider adding a short description clarifying how assetType is used (e.g., when it’s required vs inferred) for future readers of the API docs.

immichFrame.Web/src/lib/components/home-page/home-page.svelte (3)

95-101: Make updateAssetPromises synchronous (no async) for clarity

updateAssetPromises contains no await and is always called without being awaited, so marking it async is misleading and may confuse future maintainers about whether its completion needs to be sequenced with other work.

You can simplify it as:

- async function updateAssetPromises() {
+ function updateAssetPromises() {
   for (let asset of displayingAssets) {
     if (!(asset.id in assetPromisesDict)) {
       assetPromisesDict[asset.id] = loadAsset(asset);
     }
   }
   // ...
   for (let key in assetPromisesDict) {
     if (
       !(
         displayingAssets.find((item) => item.id == key) ||
         assetBacklog.find((item) => item.id == key)
       )
     ) {
       delete assetPromisesDict[key];
     }
   }
 }

Also applies to: 105-106, 111-120


257-263: Duration parsing and currentDuration derivation are reasonable; consider adding tests

The combination of:

  • parseAssetDuration handling hh:mm:ss-style strings (with , or . decimals),
  • getAssetDurationSeconds falling back to $configStore.interval when the API duration is missing/invalid,
  • updateCurrentDuration taking the max per-asset duration (and falling back to interval when needed),
  • binding ProgressBar’s duration to currentDuration,

is a solid approach for keeping the progress bar in sync with either video length or the configured image interval.

Given the subtle parsing and fallback rules, it would be valuable to add unit tests around parseAssetDuration/getAssetDurationSeconds for typical cases ('12', '1:30', '01:02:03.5', malformed values) to lock in behavior and avoid regressions if the backend duration format changes.

Also applies to: 265-272, 274-298, 303-305, 312-313, 316-316, 478-478


327-331: Revoke object URLs when assets are evicted to avoid memory leaks

loadAsset creates an object URL via URL.createObjectURL(req.data) and stores only the string in assetPromisesDict. When entries are deleted in the cleanup loop, the URLs themselves are never revoked, so a long-running slideshow can accumulate a growing number of unreclaimed blobs.

You can track URLs per asset ID and revoke them when removing from assetPromisesDict:

 let assetPromisesDict: Record<
   string,
   Promise<[string, api.AssetResponseDto, api.AlbumResponseDto[]]>
 > = {};
+const objectUrls: Record<string, string> = {};

 // ...

   for (let key in assetPromisesDict) {
     if (
       !(
         displayingAssets.find((item) => item.id == key) ||
         assetBacklog.find((item) => item.id == key)
       )
     ) {
-      delete assetPromisesDict[key];
+      const url = objectUrls[key];
+      if (url) {
+        URL.revokeObjectURL(url);
+        delete objectUrls[key];
+      }
+      delete assetPromisesDict[key];
     }
   }

 // ...

-  return [getObjectUrl(req.data), assetResponse, album] as [
+  const objectUrl = getObjectUrl(req.data);
+  objectUrls[assetResponse.id] = objectUrl;
+  return [objectUrl, assetResponse, album] as [
     string,
     api.AssetResponseDto,
     api.AlbumResponseDto[]
   ];

Optionally, you can also revoke any remaining URLs in onDestroy to be extra safe.

Also applies to: 353-357, 360-362, 111-120

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 506acb3 and 5d2e9e0.

📒 Files selected for processing (13)
  • ImmichFrame.Core/Interfaces/IServerSettings.cs (2 hunks)
  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV1.json (1 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV2.json (3 hunks)
  • ImmichFrame.WebApi.Tests/Resources/TestV2.yml (3 hunks)
  • ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs (4 hunks)
  • ImmichFrame.WebApi/Models/ClientSettingsDto.cs (2 hunks)
  • ImmichFrame.WebApi/Models/ServerSettings.cs (2 hunks)
  • immichFrame.Web/src/lib/components/elements/image-component.svelte (6 hunks)
  • immichFrame.Web/src/lib/components/elements/image.svelte (7 hunks)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (18 hunks)
  • immichFrame.Web/src/lib/immichFrameApi.ts (3 hunks)
  • openApi/swagger.json (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • immichFrame.Web/src/lib/components/elements/image.svelte
  • ImmichFrame.WebApi.Tests/Resources/TestV2.json
  • ImmichFrame.Core/Interfaces/IServerSettings.cs
  • ImmichFrame.WebApi.Tests/Resources/TestV1.json
  • ImmichFrame.WebApi.Tests/Resources/TestV2.yml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs
🧬 Code graph analysis (4)
immichFrame.Web/src/lib/immichFrameApi.ts (2)
ImmichFrame.WebApi/Controllers/AssetController.cs (5)
  • ApiController (20-121)
  • ImmichFrame (9-122)
  • HttpGet (53-60)
  • HttpGet (77-120)
  • HttpGet (62-75)
ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
  • IImmichFrameLogic (6-15)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)
ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
  • IImmichFrameLogic (6-15)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (3)
ImmichFrame.Core/Helpers/AssetHelper.cs (2)
  • Task (9-23)
  • AssetHelper (7-24)
ImmichFrame.Core/Helpers/AssetExtensionMethods.cs (2)
  • Task (13-16)
  • IEnumerable (18-49)
ImmichFrame.Core/Helpers/CollectionExtensionMethods.cs (4)
  • IEnumerable (7-14)
  • IEnumerable (16-17)
  • IEnumerable (19-20)
  • IEnumerable (45-45)
openApi/swagger.json (2)
ImmichFrame.WebApi/Controllers/AssetController.cs (7)
  • ApiController (20-121)
  • ImmichFrame (9-122)
  • HttpGet (53-60)
  • HttpGet (62-75)
  • HttpGet (77-120)
  • HttpGet (36-42)
  • HttpGet (44-51)
ImmichFrame.Core/Interfaces/IImmichFrameLogic.cs (1)
  • IImmichFrameLogic (6-15)
🔇 Additional comments (16)
immichFrame.Web/src/lib/components/elements/image-component.svelte (3)

63-65: LGTM!

The handleMediaEnded function correctly forwards the ended event from child components to parent consumers, enabling slideshow auto-advance behavior.


32-32: LGTM! Consistent prop and event wiring across all Image instances.

The playAudio prop (defaulting to false) and event wiring (bind:this, on:ended) are consistently applied to all Image components in both split and default modes, enabling uniform playback control and event propagation.

Also applies to: 51-51, 117-119, 135-137, 155-157


7-7: No issues found. Image component interface verified.

The Image component correctly exports pause and play as async functions, dispatches the ended event with proper typing, and accepts the playAudio prop. The type import and component wiring in image-component.svelte are correct.

ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (2)

2-2: LGTM! Dependency direction corrected.

The using statement now correctly references ImmichFrame.Core.Helpers instead of the previously flagged ImmichFrame.WebApi.Helpers, maintaining proper layering.


64-66: LGTM! Effective caching of excluded album assets.

The caching strategy appropriately reduces repeated API calls for excluded albums, and the cache key is sufficiently unique. The null-safety concern from previous reviews has been addressed in AssetHelper.GetExcludedAlbumAssets.

immichFrame.Web/src/lib/immichFrameApi.ts (2)

188-216: ClientSettingsDto.playAudio wiring looks correct

The new playAudio?: boolean field is consistent with the server-side ClientSettingsDto.PlayAudio and the OpenAPI schema, and its placement alongside imageFill/layout keeps the client settings payload coherent.


225-236: New asset endpoints mapping (getAssets / getAsset) aligns with OpenAPI

getAssets correctly targets GET /api/Asset and returns AssetResponseDto[], and the new getAsset(id, { clientIdentifier, assetType }) uses fetchBlob against GET /api/Asset/{id}/Asset with the optional assetType: AssetTypeEnum, matching the swagger definitions and expected binary responses for images/videos. Just ensure all call sites that previously used the old list-style getAsset have been updated to the new getAssets name.

Also applies to: 273-286

ImmichFrame.WebApi/Models/ClientSettingsDto.cs (1)

31-34: PlayAudio added and mapped cleanly from general settings

The PlayAudio property and its assignment in FromGeneralSettings are consistent with the rest of the DTO mapping and with the new client-side playAudio flag; no issues here.

Also applies to: 35-66

ImmichFrame.WebApi/Models/ServerSettings.cs (1)

54-64: New PlayAudio and ShowVideos settings are well-integrated

Adding PlayAudio to GeneralSettings and ShowVideos to ServerAccountSettings, both defaulting to false, fits the existing configuration model and provides a clear opt-in path for audio/video features without changing existing behavior.

Also applies to: 66-82

openApi/swagger.json (3)

8-56: Renaming operationId to GetAssets avoids conflict and matches client naming

Changing the GET /api/Asset operationId to GetAssets clarifies its plural nature and aligns with the regenerated TypeScript getAssets helper without affecting the on-the-wire API.


164-210: Deprecation of /api/Asset/{id}/Image is properly signaled

Marking GET /api/Asset/{id}/Image as "deprecated": true while keeping its responses unchanged is a clean way to steer new clients toward the asset-aware endpoint without breaking existing consumers.


845-945: ClientSettingsDto schema now correctly exposes playAudio

The playAudio boolean property in the ClientSettingsDto schema reflects the new server/client capability and aligns with both the C# DTO and the generated TypeScript type; this keeps the contract in sync across layers.

immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)

6-9: New imports and playback/preload state look consistent

The additions of tick, the ImageComponentInstance type, PRELOAD_ASSETS, imageComponent, currentDuration, and assetPromisesDict integrate cleanly with the existing state pattern and are typed appropriately for the upcoming video support and duration handling.

Also applies to: 30-30, 41-42, 55-58


125-125: Asset loading and type filtering align with new asset model

Using api.getAssets() and then filtering to isImageAsset(asset) || isVideoAsset(asset) ensures the slideshow only handles supported visual assets while keeping the backend API general. This matches the new asset-type-aware model and should keep unsupported asset types out of the UI.

Also applies to: 136-138


169-171: Split view and orientation guards correctly exclude videos

Using isImageAsset/isVideoAsset in getNextAssets/getPreviousAssets and short-circuiting isHorizontal for videos ensures:

  • split view only ever shows two images (never videos),
  • video assets don’t go through EXIF-based orientation heuristics,
  • imagesState.split is true only for pure image pairs.

This is a good safety measure for layout, especially with mixed asset types.

Also applies to: 191-192, 204-205, 221-222, 226-228, 312-313


379-382: Playback wiring between stores, ImageComponent, and playAudio looks coherent

Subscribing to restartProgress/stopProgress to call imageComponent.play()/.pause(), binding imageComponent via bind:this, forwarding on:ended, and passing playAudio={$configStore.playAudio} into ImageComponent all line up well with the new video/audio features. Once the reentrancy guard around handleDone is in place, this should give predictable synchronized control over both visuals and audio.

Also applies to: 386-389, 429-432, 431-431

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docker/Settings.example.yml (1)

37-52: Add ShowVideos to the Accounts section template.

Per the PR objectives and your clarification in the comments, the correct configuration is Accounts: ShowVideos: true. However, this setting is not present in the example configuration template. Since ShowVideos is a per-account setting (unlike the global PlayAudio), it should be added to the Accounts section alongside other per-account toggles like ShowMemories, ShowFavorites, and ShowArchived.

Please add ShowVideos: false to each account entry in the Accounts section to ensure users have a complete example configuration:

Accounts:
  - ImmichServerUrl: REQUIRED
    ApiKey: REQUIRED
    ImagesFromDate: null
    ShowMemories: false
    ShowFavorites: false
    ShowArchived: false
+   ShowVideos: false
    ImagesFromDays: null
🧹 Nitpick comments (9)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (1)

103-131: Consider clarifying variable naming in test setup.

The test logic is correct, but the variable requestedVideoCount at line 108 may be misleading since videos are not actually requested when ShowVideos is false. The variable is used only to populate test data, possibly to verify that videos are properly filtered out even when present. Consider renaming to unusedVideoCount or adding a comment explaining that videos in test data verify filtering behavior.

Example:

-    var requestedVideoCount = 8;
+    var unusedVideoCount = 8;  // Videos in test data verify they're properly filtered when ShowVideos=false
     var rating = 3;
     _mockAccountSettings.SetupGet(s => s.ShowArchived).Returns(true);
     _mockAccountSettings.SetupGet(s => s.Rating).Returns(3);
     var returnedAssets = CreateSampleImageAssets(requestedImageCount, rating: rating);
-    returnedAssets.AddRange(CreateSampleVideoAssets(requestedVideoCount, rating: rating));
+    returnedAssets.AddRange(CreateSampleVideoAssets(unusedVideoCount, rating: rating));
immichFrame.Web/src/lib/components/elements/image.svelte (3)

26-27: Align playAudio prop typing with parent usage and provide a default

image.svelte requires playAudio: boolean in Props and destructures it without a default, while image-component.svelte treats playAudio as optional with playAudio = false. This mismatch can force call sites that use Image directly to always provide playAudio, even if they don’t care about audio.

Consider relaxing the prop and adding an explicit default for clarity and resilience:

interface Props {
  // ...
- playAudio: boolean;
+ playAudio?: boolean;
}

let {
  // ...
- playAudio
+ playAudio = false
}: Props = $props();

This keeps behavior unchanged for callers that already pass playAudio, while avoiding stricter typing for any existing direct usages of Image.

Also applies to: 40-42


45-48: Video/image branching and zoom/pan disabling look correct; consider minor robustness tweaks

The isVideo / videoElement wiring and enableZoom / enablePan gating ensure that videos render via <video> without zoom/pan animations, while images keep the existing zoom/pan behavior. This is a good separation and avoids awkward Ken Burns–style transforms on video.

Two small robustness/maintenance suggestions:

  1. Guard thumbhash decoding and reuse the computed poster URL

    You now compute thumbHashToDataURL(decodeBase64(image[1].thumbhash ?? '')) both for the <video> poster and for the background <img> at the bottom of the file. If thumbhash is missing or malformed, this can throw in two places and is harder to adjust centrally.

    You can centralize and guard the computation:

    const thumbhashUrl = $derived(() => {
      const hash = image[1].thumbhash;
      if (!hash) return '';
      try {
        return thumbHashToDataURL(decodeBase64(hash));
      } catch {
        return '';
      }
    });

    Then use thumbhashUrl in both poster={thumbhashUrl} and the background <img src={thumbhashUrl}>.

  2. Clarify audio/autoplay expectations

    muted={!playAudio} is a sensible default, but when playAudio is true, autoplay may be blocked by browsers; the play() helper already swallows the resulting promise rejection. It may be worth adding a short comment near the muted={!playAudio} line explaining that enabling audio can disable autoplay on some platforms so future maintainers understand this trade‑off.

These are optional quality-of-life improvements; the current logic is functionally sound.

Also applies to: 52-53, 161-171, 190-201


137-151: Pause/play exports are safely guarded but could be generalized slightly

The exported pause/play helpers correctly no-op when the current asset is not a video or the videoElement ref is not yet bound, which makes their use from parent components safe.

If you expect to support other media types (e.g., audio-only assets) in future, you might consider:

  • Relaxing the isVideo check to a more generic “has playable media element” abstraction, or
  • Adding a comment that these helpers are video-specific today to avoid confusion when new asset types are added.

No functional issue, just a small clarity consideration.

immichFrame.Web/src/lib/components/elements/image-component.svelte (1)

59-70: Pause/play delegation across primary/secondary images is sound; consider centralizing shared props

The introduction of primaryImageComponent / secondaryImageComponent and the exported pause/play functions cleanly coordinate playback across split and non-split layouts. The use of optional chaining (?.pause?.(), ?.play?.()) makes this safe regardless of which layout is active.

One small maintainability suggestion: the <Image> invocations repeat the same long prop list three times, now including {playAudio} and bind:showInfo. You could reduce duplication and the risk of future drift by extracting a small helper component or using a spread object for the shared props, e.g.:

<!-- pseudo-code idea -->
<Image
  {...baseImageProps}
  image={images[0]}
  bind:this={primaryImageComponent}
  bind:showInfo
/>

Not required for correctness, but it will make future changes (like adding new shared props) less error-prone.

Also applies to: 101-115, 118-132, 137-151

immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)

30-31: Asset preloading via assetPromisesDict is well-structured; consider lifecycle cleanup

The new PRELOAD_ASSETS constant and assetPromisesDict logic in updateAssetPromises() do a good job of:

  • Ensuring currently displayed assets and a small backlog are prefetched.
  • Avoiding duplicate requests by reusing promises keyed by asset.id.
  • Cleaning up entries whose assets are no longer in displayingAssets or assetBacklog.

One improvement to consider is explicit lifecycle cleanup for the promise dictionary when the component is destroyed to avoid any lingering references:

onDestroy(() => {
  // existing unsubscribe logic...
  assetPromisesDict = {};
});

This is minor in practice (since the component is long-lived), but it documents the intended lifecycle and prevents surprises if more state is added to the dictionary later.

Also applies to: 55-59, 95-121, 200-201, 231-231


125-139: Filtering to image/video assets is clear; verify behavior for any future asset types

loadAssets() now filters assetBacklog to isImageAsset(asset) || isVideoAsset(asset), which is correct for this PR’s video-focused scope and ensures non-displayable asset types are ignored.

If the backend introduces additional playable asset types (e.g., audio-only) and you intend to support them in the slideshow, remember to extend this filter accordingly; otherwise those assets will silently never be shown.


144-161: Transition/playback coordination via handleDone and subscriptions is solid

The introduction of isHandlingAssetTransition plus the handleDone() flow (restart progress, update assets, await tick(), then kick imageComponent.play() and progressBar.play()) provides a clear, serialized transition path and guards against concurrent navigation calls from the progress bar and overlay controls.

Similarly, wiring the restartProgress/stopProgress subscriptions and the OverlayControls pause/showInfo actions to imageComponent.play()/pause() ensures the video element stays in sync with the progress bar and UI state.

The only very minor tweak you might consider is awaiting the imageComponent?.play?.() call inside handleDone for consistency with the overlay callbacks:

await imageComponent?.play?.();

Functionally this is not required, but it makes the behavior uniform across the different entry points.

Also applies to: 388-399, 449-477


266-307: Dynamic duration handling for videos is good; consider edge cases and documentation

The new updateCurrentDuration, getAssetDurationSeconds, and parseAssetDuration functions, together with duration={currentDuration} on ProgressBar, nicely align the progress bar duration with asset metadata:

  • For videos, valid asset.duration strings (e.g., HH:MM:SS, MM:SS, with optional fractional seconds and , decimals) are parsed; invalid or non-positive values fall back to $configStore.interval.
  • For images, you consistently use $configStore.interval.
  • For split view, you take the max of the durations for the two assets, which keeps the pair on screen long enough for the longest member.

Two optional hardening/documentation suggestions:

  1. Guard against extremely large or tiny durations

    If misconfigured or malformed durations were ever to be very large (hours) or very small (< 1 second), the UI might behave oddly. You could clamp currentDuration within reasonable bounds (e.g., min 1s, max some configured ceiling).

  2. Document expected duration format

    A brief comment on parseAssetDuration() describing the accepted formats (HH:MM:SS[.fraction], MM:SS[.fraction], SS[.fraction], allowing , as decimal separator) will help keep backend and frontend in sync if new formats are introduced.

The core logic is solid; these are just safety/documentation improvements.

Also applies to: 312-327, 484-487

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d2e9e0 and ea67bd4.

📒 Files selected for processing (6)
  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (4 hunks)
  • docker/Settings.example.json (1 hunks)
  • docker/Settings.example.yml (1 hunks)
  • immichFrame.Web/src/lib/components/elements/image-component.svelte (6 hunks)
  • immichFrame.Web/src/lib/components/elements/image.svelte (7 hunks)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (17 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T16:12:49.488Z
Learnt from: JoeRu
Repo: immichFrame/ImmichFrame PR: 481
File: ImmichFrame.Core.Tests/Logic/Pool/ChronologicalAssetsPoolWrapperTests.cs:306-306
Timestamp: 2025-10-09T16:12:49.488Z
Learning: When testing the ChronologicalAssetsPoolWrapper in ImmichFrame.Core.Tests, use `Is.SupersetOf` rather than `Is.EquivalentTo` or `Is.EqualTo` assertions because the wrapper uses Fisher-Yates shuffle to randomize set order, making output non-deterministic between runs. The wrapper also uses a 10x fetch multiplier (capped at 1000) that may return more assets than requested, which is legitimate behavior.

Applied to files:

  • ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs
🧬 Code graph analysis (2)
docker/Settings.example.json (2)
ImmichFrame.Core/Models/Weather.cs (2)
  • Weather (5-13)
  • ImmichFrame (3-14)
ImmichFrame.Core/Helpers/WeatherExtensions.cs (1)
  • ImmichFrame (5-24)
ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (2)
ImmichFrame.Core/Logic/Pool/AllAssetsPool.cs (4)
  • Task (9-21)
  • Task (23-69)
  • AllAssetsPool (7-71)
  • AllAssetsPool (6-78)
ImmichFrame.Core/Logic/Pool/CachingApiAssetsPool.cs (4)
  • Task (11-14)
  • Task (16-19)
  • Task (21-26)
  • Task (28-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: publish_images
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (14)
docker/Settings.example.yml (1)

35-35: ✓ PlayAudio setting correctly added to General section.

The addition follows the established conventions: appropriate placement, consistent naming with existing toggles, and safe default value (false).

docker/Settings.example.json (3)

33-33: Formatting consistency fix applied.

The WeatherIconUrl value is now properly formatted and aligns with its usage in WeatherExtensions.cs, where IconId is interpolated into the URL template.


40-60: Verify ShowVideos setting is present in Accounts section.

Per the PR objectives and user clarification in comments, the configuration key ShowVideos should be documented under Accounts (e.g., Accounts: ShowVideos: true). The provided excerpt shows only a partial Accounts structure; please confirm that ShowVideos is included in the full Accounts configuration example.

If ShowVideos is not currently in the Accounts section of Settings.example.json, it should be added alongside other account-level filters (ShowMemories, ShowFavorites, ShowArchived) for completeness and user discoverability.


37-37: All PlayAudio integration verified and complete.

The setting is properly wired end-to-end:

  1. Server settings — Defined in IServerSettings interface, ServerSettings model, and ServerSettingsV1 adapter with default false
  2. ClientSettingsDto mapping — PlayAudio properly mapped at line 62: dto.PlayAudio = generalSettings.PlayAudio
  3. Frontend consumption — Wired through all layers:
    • Config API endpoint (ConfigController.GetConfig) returns mapped ClientSettingsDto
    • Frontend config.store wraps ClientSettingsDto
    • home-page.svelte passes $configStore.playAudio to image component
    • image.svelte uses it to control audio: muted={!playAudio}

The feature flows from docker/Settings.example.json → backend models → API → frontend store → UI component where it actively controls audio playback muting.

ImmichFrame.Core.Tests/Logic/Pool/AllAssetsPoolTests.cs (6)

45-49: LGTM! Cache mock properly supports async asset enumeration.

The new GetOrAddAsync setup for IEnumerable<AssetResponseDto> correctly handles the factory pattern and aligns with the production code's use of AssetHelper.GetExcludedAlbumAssets.


52-67: LGTM! Helper methods properly support asset type and rating.

The helper methods correctly create typed assets with optional ratings, enabling comprehensive testing of the new video support and rating filters. The structure with a base method and type-specific wrappers promotes code reuse.


69-101: LGTM! Asset counting tests properly verify video support toggle.

Both tests correctly validate that GetAssetCount returns only images when ShowVideos is false (default) and includes videos when enabled, matching the production implementation.


133-162: LGTM! Comprehensive test for mixed image and video retrieval.

The test correctly verifies that when ShowVideos is enabled, the search DTO has Type=null (no type filter) and returns both images and videos. The assertions properly validate the total count and search parameters.


164-177: LGTM! Date filter test correctly validates ImagesFromDays.

The test properly verifies that ImagesFromDays is converted to a TakenAfter date filter with the correct calculation.


179-203: LGTM! Excluded albums test properly validates filtering logic.

The test correctly verifies that assets from excluded albums are filtered out. Good use of CreateSampleImageAssets at line 183 to ensure consistent asset typing.

immichFrame.Web/src/lib/components/elements/image.svelte (1)

252-271: Updated pan/zoom keyframes look consistent with new scale variables

The changes to the pan and zoom-pan keyframes to include scale(var(--start-scale))/scale(var(--end-scale)) keep the scaling behavior consistent with the standalone zoom animation, and they respect the enableZoom / enablePan class gating you added above.

No issues spotted here; the transforms compose correctly and should preserve the intended visual effects.

immichFrame.Web/src/lib/components/elements/image-component.svelte (1)

32-33: playAudio defaulting and wiring through to Image are consistent

playAudio is optional here with a default of false, and it is threaded through to all Image usages. This ensures the top-level config can control video audio behavior uniformly, without breaking existing callers.

Looks good as-is; no changes needed.

Also applies to: 50-52

immichFrame.Web/src/lib/components/home-page/home-page.svelte (2)

174-187: Split-view and orientation logic correctly exclude videos from split layout

Using isImageAsset checks in both getNextAssets() and getPreviousAssets() to decide whether to take two assets for split-view, and treating isVideoAsset(asset) as immediately non-horizontal in isHorizontal(), ensures:

  • Split layout is only used when both assets are images.
  • Videos are presented in a single-column layout, which avoids awkward side-by-side video/image combinations.

This is a sensible constraint for a first iteration of video support and matches typical expectations for a slideshow frame.

Also applies to: 209-232, 234-247


41-43: Binding imageComponent and forwarding playAudio from config is coherent

The new imageComponent ref (bind:this={imageComponent}) combined with playAudio={$configStore.playAudio} cleanly propagates the configuration down to the media layer and enables programmatic control from the home page.

This wiring is consistent with the new API surface introduced in image-component.svelte and image.svelte. No issues here.

Also applies to: 421-440

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

84-87: Align provideClose() with overlay showInfo toggling to resume video playback

OverlayControls.showInfo and the pause handler both pause/resume both the imageComponent and the ProgressBar. In contrast, provideClose() (used via setContext('close', provideClose)) only resumes the ProgressBar:

async function provideClose() {
  infoVisible = false;
  await progressBar.play();
}

If an info overlay triggers provideClose() after having paused media, the bar will resume but the video (and audio) may remain paused, which is a subtle UX inconsistency.

You can make this consistent with the other paths by also resuming the imageComponent:

 async function provideClose() {
   infoVisible = false;
-  await progressBar.play();
+  await imageComponent?.play?.();
+  await progressBar.play();
 }

This keeps all “close/hide info” flows aligned: whenever the info UI is dismissed, both progress and media playback resume together.

Also applies to: 452-455, 474-492

🧹 Nitpick comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

95-122: Object URL lifecycle is now handled; consider hardening against rejected asset promises

You’ve correctly addressed the earlier memory‑leak concern by:

  • Ensuring every displayed/preloaded asset has an entry in assetPromisesDict.
  • Revoking object URLs when entries fall out of both displayingAssets and assetBacklog in updateAssetPromises().
  • Revoking remaining URLs in onDestroy and clearing assetPromisesDict.

One edge case: if loadAsset() ever rejects (network/transport error), the stored promise will reject. Both updateAssetPromises() and onDestroy() currently await those promises without a try/catch, which means:

  • A single rejected promise can abort the cleanup loop.
  • Promise.all(revokes) in onDestroy can surface as an unhandled rejection.

You can make cleanup more robust by swallowing those rejections while still deleting the entry, e.g.:

-    for (let key in assetPromisesDict) {
+    for (let key in assetPromisesDict) {
       if (
         !(
           displayingAssets.find((item) => item.id == key) ||
           assetBacklog.find((item) => item.id == key)
         )
       ) {
-        revokeObjectUrl((await assetPromisesDict[key])[0]);
-        delete assetPromisesDict[key];
+        try {
+          const [url] = await assetPromisesDict[key];
+          revokeObjectUrl(url);
+        } catch (err) {
+          console.warn('Failed to resolve asset during cleanup:', err);
+        } finally {
+          delete assetPromisesDict[key];
+        }
       }
     }

And similarly for destroy:

-  const revokes = Object.values(assetPromisesDict).map(async (p) =>
-    revokeObjectUrl((await p)[0])
-  );
-  await Promise.all(revokes);
+  const revokes = Object.values(assetPromisesDict).map(async (p) => {
+    try {
+      const [url] = await p;
+      revokeObjectUrl(url);
+    } catch (err) {
+      console.warn('Failed to resolve asset during destroy cleanup:', err);
+    }
+  });
+  await Promise.allSettled(revokes);
   assetPromisesDict = {};

This keeps the new URL lifecycle behavior while avoiding cleanup being derailed by a transient fetch failure.

Also applies to: 337-343, 350-363, 370-380, 419-433

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea67bd4 and 985c1f9.

📒 Files selected for processing (1)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (18 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (8)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (8)

6-6: Imports, type-safe imageComponent binding, and asset state wiring look consistent

Using tick and setContext alongside onMount/onDestroy, the separate ImageComponentInstance type for bind:this, and the assetPromisesDict/currentDuration state wiring all line up with how the rest of the file uses them (play/pause calls, duration updates, and promise lookups). I don’t see correctness issues here; this sets up the later logic cleanly.

Also applies to: 9-9, 17-18, 30-30, 41-43, 55-58


126-127: Type-aware filtering in loadAssets is aligned with image/video support

Using getAssets() once and then filtering to isImageAsset(asset) || isVideoAsset(asset) before populating assetBacklog matches the rest of the code’s expectations (splitview and orientation logic assume only images, while video handling is done via isVideoAsset). This is a straightforward and correct way to scope what the front-end rotates through.

Also applies to: 137-139


145-161: Re-entrancy guard around asset transitions is solid and avoids racey navigation

The isHandlingAssetTransition flag combined with the try/finally in handleDone() is a good pattern here. It prevents overlapping calls from the ProgressBar onDone and the overlay controls (next/back) from interleaving getNextAssets()/getPreviousAssets() and updateAssetPromises(), while still guaranteeing the flag is reset even if something throws. The await tick() before imageComponent?.play?.() also ensures the new asset DOM is ready before resuming playback and progress.

Also applies to: 465-472, 499-507


175-185: Restricting splitview to images and treating videos as non-horizontal is coherent

The combination of:

  • isImageAsset(assetBacklog[0/1]) and isImageAsset(assetHistory[...]) checks before entering the splitview branch, and
  • split: assets.length == 2 && assets.every(isImageAsset) in loadImages, and
  • isHorizontal() early-returning false for isVideoAsset(asset)

ensures videos never participate in splitview layout or orientation heuristics. That avoids trying to fit videos into layouts designed around EXIF dimensions, which seems like the right tradeoff for this PR.

Also applies to: 200-203, 210-233, 235-247, 322-323


267-308: Duration helpers provide a robust basis for progress timing across images and videos

updateCurrentDuration() + getAssetDurationSeconds() and parseAssetDuration() give you:

  • Per-asset durations for videos based on asset.duration, with a safe fallback to $configStore.interval.
  • Uniform interval-based durations for images.
  • A max-of-durations policy for multi-asset displays.
  • A clean reset to the configured fallback when loadImages fails.

Binding currentDuration into <ProgressBar duration={currentDuration} /> and updating it on every transition should address the “image after video displays too briefly” behavior without complicating the rest of the flow. The parsing logic (colon-separated or single-value with comma/decimal) also looks defensive enough for typical metadata formats.

Also applies to: 313-314, 326-327, 499-502


337-343: loadAsset integration with assetType and object URLs matches the new API

Passing assetType: assetResponse.type into api.getAsset and wrapping the blob with getObjectUrl(req.data) aligns this component with the asset-type-aware backend changes. The album/people enrichment and the [url, assetResponse, album] tuple structure are consistent with how loadImages consumes these promises. No issues spotted here beyond the general cleanup concerns already mentioned for object URLs.

Also applies to: 350-363, 370-371


397-408: Restart/stop subscriptions correctly keep playback and progress in sync

The restartProgress and stopProgress subscriptions now not only manipulate the ProgressBar but also call imageComponent?.play?.()/pause?.(). That keeps video (and any timed image animations) aligned with the bar state. Unsubscribing in the destroy hook completes the lifecycle; this looks well-wired.

Also applies to: 400-401, 407-408


452-455: playAudio prop threading into ImageComponent

Passing playAudio={$configStore.playAudio} down to ImageComponent is the right place to hook audio-enable config into the media layer; it keeps this component focused on layout/selection while the child handles the actual playback semantics.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

344-375: Empty URL from failed API requests bypasses error handling, causing silent failures.

The loadAsset function returns ['', assetResponse, []] when API requests fail (line 357) without throwing an exception. This causes:

  1. loadImages catch block never executes (no exception thrown)
  2. imagesState.error remains false and imagesState.loaded remains true
  3. ImageComponent renders with src="" instead of showing the error state
  4. User sees blank images with no indication of failure

Both split view and normal modes are affected since both destructure image[0] as the src attribute.

Recommended fix: Throw an error in loadAsset when the request fails:

if (req.status != 200 || ($configStore.showAlbumName && album == null)) {
-	return ['', assetResponse, []] as [string, api.AssetResponseDto, api.AlbumResponseDto[]];
+	throw new Error(`Failed to load asset ${assetResponse.id}: status ${req.status}`);
}

This ensures the exception propagates to loadImages catch block (line 334), which correctly sets error: true and displays error feedback to the user.

🧹 Nitpick comments (2)
immichFrame.Web/src/lib/components/elements/image.svelte (1)

148-162: Consider narrowing the error handling in play().

The play() function catches all errors with a generic catch block and silently ignores them. While this is appropriate for handling autoplay blocking, it could also mask legitimate errors such as network failures or corrupted video files. Consider checking the error type or logging it for debugging.

 	export const play = async () => {
 		if (isVideo && videoElement) {
 			try {
 				await videoElement.play();
-			} catch {
-				// Autoplay might be blocked; ignore.
+			} catch (error) {
+				// Autoplay might be blocked by browser policy; ignore NotAllowedError
+				if (error instanceof DOMException && error.name === 'NotAllowedError') {
+					// Expected when autoplay is blocked
+				} else {
+					console.warn('Video playback failed:', error);
+				}
 			}
 		}
 	};
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

242-254: Confusing function name: isHorizontal checks for portrait orientation.

The function name suggests it checks if an image is horizontally oriented (landscape), but the logic imageHeight > imageWidth (line 253) actually checks for portrait orientation (taller than wide).

While the logic appears correct for split view (which displays portrait images side by side), the misleading name reduces code clarity.

Consider renaming to better reflect the actual check:

-function isHorizontal(asset: api.AssetResponseDto) {
+function isPortraitOrientation(asset: api.AssetResponseDto) {
 	if (isVideoAsset(asset)) {
 		return false;
 	}
 
 	const isFlipped = (orientation: number) => [5, 6, 7, 8].includes(orientation);
 	let imageHeight = asset.exifInfo?.exifImageHeight ?? 0;
 	let imageWidth = asset.exifInfo?.exifImageWidth ?? 0;
 	if (isFlipped(Number(asset.exifInfo?.orientation ?? 0))) {
 		[imageHeight, imageWidth] = [imageWidth, imageHeight];
 	}
-	return imageHeight > imageWidth; // or imageHeight > imageWidth * 1.25;
+	return imageHeight > imageWidth; // Portrait: taller than wide
 }

Then update all call sites (lines 188, 189, 223, 224).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 985c1f9 and 16e14f6.

📒 Files selected for processing (2)
  • immichFrame.Web/src/lib/components/elements/image.svelte (8 hunks)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (19 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (14)
immichFrame.Web/src/lib/components/elements/image.svelte (4)

175-176: LGTM: Correct conditional origin calculation.

The origin calculations correctly exclude video assets from face-based centering, since zoom and pan effects are disabled for videos. This avoids unnecessary computation and maintains consistent behavior.


262-268: LGTM: Improved transform readability.

The keyframe transform declarations have been reformatted for better readability by splitting onto multiple lines. No functional changes.

Also applies to: 273-279


44-44: LGTM: Correct reactive derivations.

The isVideo, enableZoom, and enablePan values correctly use $derived to reactively compute based on the asset type and configuration props. The logic to disable zoom and pan for video assets is appropriate.

Also applies to: 51-52


7-7: Given the web search results about Svelte 5 runes, I now have enough information to provide a comprehensive rewritten review comment. The search confirms that $derived should be used to compute values that depend on reactive state and automatically updates whenever its dependencies change.

However, I need to verify the actual implementation in the file to be certain about the specific issue. Let me generate a final rewritten comment based on what we've verified so far.

Ensure the isVideoAsset import resolves correctly and is used consistently.

The import of isVideoAsset from '$lib/constants/asset-type' is correctly implemented. The function checks if the asset type is VIDEO and is properly exported. The usage patterns in the codebase are consistent.

However, verify that the overall implementation with the video element follows best practices. Modern browsers require the muted attribute together with the autoplay option to allow autoplay video. Ensure that the muted attribute binding is properly connected to the playAudio prop and that the video element has appropriate error handling and cleanup logic.

immichFrame.Web/src/lib/components/home-page/home-page.svelte (10)

6-6: LGTM! Import additions support video playback integration.

The new imports (tick, ImageComponentInstance, asset type guards) and the PRELOAD_ASSETS rename correctly reflect the shift from image-only to multi-asset-type handling.

Also applies to: 9-9, 17-17, 30-30


41-42: LGTM! State variables correctly support video playback.

The imageComponent binding and currentDuration state enable lifecycle coordination, and the assetPromisesDict rename accurately reflects the expanded scope.

Also applies to: 55-58


96-129: LGTM! Object URL cleanup correctly implemented.

The updateAssetPromises function now properly revokes object URLs before deleting entries from assetPromisesDict. The try-catch-finally pattern ensures cleanup happens even when promise resolution fails.


152-169: LGTM! Transition guard prevents race conditions.

The isHandlingAssetTransition flag with try-finally ensures only one transition happens at a time and the guard is always reset. The tick() call correctly synchronizes DOM updates before playing.


171-240: LGTM! Split view correctly restricted to image pairs.

The isImageAsset guards in both getNextAssets and getPreviousAssets ensure that split view is only activated for pairs of images, correctly excluding videos.


274-315: LGTM! Duration parsing handles edge cases correctly.

The parseAssetDuration function correctly:

  • Parses HH:MM:SS format by processing parts right-to-left
  • Handles European decimal notation (comma → dot)
  • Returns 0 for invalid input (empty strings, non-numeric values, missing parts)

The updateCurrentDuration logic using Math.max ensures all displayed assets share the longest duration, which is appropriate for synchronized playback.


377-387: LGTM! Object URL helpers correctly implemented.

The getObjectUrl and revokeObjectUrl helpers appropriately wrap the browser APIs with error handling, ensuring graceful degradation if revocation fails.


389-445: LGTM! Lifecycle hooks correctly manage resources.

The onMount subscriptions coordinate imageComponent playback with progress bar state, and the onDestroy cleanup comprehensively revokes all object URLs using Promise.allSettled, ensuring no memory leaks even on errors.


448-522: LGTM! Template bindings correctly integrate video playback.

The template correctly:

  • Binds imageComponent for lifecycle management
  • Passes playAudio configuration
  • Coordinates play/pause across overlay controls
  • Uses dynamic currentDuration for the progress bar

131-150: Code structure is correct; no issues found.

The getAssets() API call is properly implemented with optional parameters, the return type correctly provides AssetResponseDto[], and the filter functions are correctly implemented:

  • isImageAsset checks asset.type === 0 (AssetType.IMAGE)
  • isVideoAsset checks asset.type === 1 (AssetType.VIDEO)

The filtering logic in home-page.svelte correctly restricts to images and videos. Whether the backend returns video assets depends on backend configuration, not the client code—which is properly structured to handle them.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
immichFrame.Web/src/lib/components/elements/asset-component.svelte (1)

3-3: Remove unused import.

The createEventDispatcher import is not used anywhere in this component.

Apply this diff to remove the unused import:

-	import { createEventDispatcher } from 'svelte';
immichFrame.Web/src/lib/components/elements/asset.svelte (1)

211-224: Consider video looping and verify autoplay with audio behavior.

A few observations:

  1. Autoplay with audio: When playAudio={true}, the video is unmuted. Most browsers block autoplay with audio unless the user has interacted with the page. The try-catch in the play() method handles this, but users may experience videos not autoplaying when audio is enabled.

  2. Missing loop attribute: Videos currently play once and stop. Consider adding a loop attribute if continuous playback is desired during the display interval.

  3. Poster fallback: When thumbhashUrl is empty, the poster attribute will be set to an empty string, which is safe but means no placeholder will show while the video loads.

Based on the PR comments, user "kdy1968" reported "videos run ~3–5 minutes then stop." This aligns with the missing loop attribute. User "3rob3" suggested a configuration option to limit playback duration. Consider:

  • Adding a loop attribute to the video element for continuous playback
  • Or adding a configuration option to control loop behavior and playback duration limits

Do you want me to open an issue to track these enhancements?

immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

291-315: Consider simplifying duration parsing logic.

The duration parsing works correctly but could be more readable. The while loop with pop() and multiplier accumulation is a bit indirect.

Consider this alternative for clarity:

 function parseAssetDuration(duration?: string | null) {
 	if (!duration) {
 		return 0;
 	}
-	const parts = duration.split(':').map((value) => value.trim());
-	if (!parts.length) {
+	const parts = duration.split(':').map((value) => value.trim().replace(',', '.'));
+	if (parts.length === 0 || parts.length > 3) {
 		return 0;
 	}
-	let total = 0;
-	let multiplier = 1;
-	while (parts.length) {
-		const value = parts.pop();
-		if (!value) {
-			continue;
-		}
-		const normalized = value.replace(',', '.');
-		const numeric = parseFloat(normalized);
-		if (Number.isNaN(numeric)) {
-			return 0;
-		}
-		total += numeric * multiplier;
-		multiplier *= 60;
+	// Parse as [hours:]minutes:seconds
+	const multipliers = [3600, 60, 1]; // hours, minutes, seconds
+	const offset = multipliers.length - parts.length;
+	
+	let total = 0;
+	for (let i = 0; i < parts.length; i++) {
+		const numeric = parseFloat(parts[i]);
+		if (Number.isNaN(numeric)) {
+			return 0;
+		}
+		total += numeric * multipliers[offset + i];
 	}
 	return total;
 }

This makes the parsing direction explicit and avoids the pop() loop.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16e14f6 and 92c5ba2.

📒 Files selected for processing (3)
  • immichFrame.Web/src/lib/components/elements/asset-component.svelte (9 hunks)
  • immichFrame.Web/src/lib/components/elements/asset.svelte (7 hunks)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (19 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (9)
immichFrame.Web/src/lib/components/elements/asset-component.svelte (1)

59-70: LGTM! Playback control delegation is well-structured.

The pause/play methods properly delegate to child asset components with safe optional chaining, enabling coordinated playback control across the component hierarchy.

immichFrame.Web/src/lib/components/elements/asset.svelte (3)

46-56: LGTM! Proper video cleanup on unmount.

The effect correctly pauses the video and clears the src to release resources when the asset changes or the component unmounts.


64-74: LGTM! Safe thumbhash decoding with fallback.

The try-catch properly handles invalid or missing thumbhash data, returning an empty string as a safe fallback.


158-172: LGTM! Autoplay restriction handling is correct.

The play method correctly wraps videoElement.play() in a try-catch to handle cases where autoplay might be blocked by the browser, preventing unhandled promise rejections.

immichFrame.Web/src/lib/components/home-page/home-page.svelte (5)

119-126: ✅ Object URL leak fixed!

This properly addresses the past review comment about object URL leaks. The cleanup now correctly awaits the promise, extracts the URL, and revokes it before deleting the dictionary entry.


152-169: LGTM! Race condition guard prevents concurrent transitions.

The isHandlingAssetTransition flag properly serializes asset transitions, and the try-finally ensures the flag is always reset even if an error occurs. The use of tick() correctly waits for the UI to update before starting playback.


426-445: ✅ Object URL cleanup in onDestroy properly implemented!

This correctly addresses the past review comment. The onDestroy handler now properly revokes all remaining object URLs using Promise.allSettled to ensure all cleanup attempts complete, even if some fail. The error handling is appropriate.


274-289: Duration calculation correctly handles videos and images.

The logic properly:

  • Extracts duration from video metadata
  • Falls back to configured interval for images or when video duration is unavailable
  • Takes the maximum duration when multiple assets are displayed

344-359: Batch-level error handling prevents partial failure recovery; error details are lost.

Verification confirms the error handling paths exist but have design limitations:

  1. Partial failures block the batch: If any asset fails in pickAssets, the entire catch block triggers (line 333), returning {error: true, assets: [], ...}. All successfully loaded assets are discarded, not displayed.

  2. Error details are lost: loadAsset throws descriptive errors with asset ID and status (line 358), but pickAssets' generic catch swallows these details. Users only see a generic error state; they don't know which asset failed or why.

The code functions without crashing, but consider:

  • Collecting per-asset results instead of batch-level catch to allow partial display
  • Propagating specific error details to the UI error handler

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (6)
immichFrame.Web/src/lib/components/elements/asset-component.svelte (2)

5-7: Prefer plain let for bind:this component refs instead of $state

primaryAssetComponent and secondaryAssetComponent are only used as bind:this refs and for calling instance exports (pause/play). In Svelte 5 runes mode, bind:this is intended to target a normal variable (the docs show let componentInstance: MyComponent; <MyComponent bind:this={componentInstance} />), and all instance exports are then available on that object. Wrapping these refs in $state adds an unnecessary proxy layer and can make the typing/behavior of the component instance less clear, while not providing any reactive benefit here. Based on Svelte docs.

Consider simplifying to something like:

-import type AssetComponent from './asset.svelte';
+import type AssetComponent from './asset.svelte';

-	let primaryAssetComponent = $state<AssetComponent | undefined>(undefined);
-	let secondaryAssetComponent = $state<AssetComponent | undefined>(undefined);
+	let primaryAssetComponent: AssetComponent | undefined;
+	let secondaryAssetComponent: AssetComponent | undefined;

	export const pause = async () => {
 		await primaryAssetComponent?.pause?.();
 		await secondaryAssetComponent?.pause?.();
 	};

	export const play = async () => {
 		await primaryAssetComponent?.play?.();
 		await secondaryAssetComponent?.play?.();
 	};

The rest of the script changes (asset tuple typing, playAudio prop/default, and the pause/play delegation) look consistent with the new asset/video API.

Also applies to: 16-17, 31-32, 35-36, 49-51, 58-69


91-154: Keying and split-mode assumptions around assets could be tightened

The template wiring to <Asset> (including asset={assets[0|1]}, forwarding playAudio, and binding showInfo and bind:this) looks coherent, but there are a couple of details worth tightening:

  • {#key assets} relies on the array’s reference identity. If upstream logic ever mutates the array in place rather than replacing it, the key won’t change and transitions may not rerun as expected. Keying on something stable like the primary asset’s ID (or a slideshow sequence ID) would be more robust.
  • In the split branch, assets[1] is used without a length check. If the calling code ever passes fewer than two items while split is true, this will render undefined into <Asset>. If that contract isn’t guaranteed elsewhere, consider guarding or asserting on the array length here.
  • When split is true, playAudio is forwarded to both <Asset> instances. If it’s possible for both sides to be videos and you don’t want dual audio, you may want to gate audio to a single side (e.g., only the primary) or by asset type.

None of these are blockers, but clarifying these assumptions now will make the slideshow behavior easier to reason about as video support evolves.

immichFrame.Web/src/lib/components/home-page/home-page.svelte (4)

19-21: Align album tuple typing with possible null values

loadAsset() initializes album as api.AlbumResponseDto[] | null and returns it (via a cast) as api.AlbumResponseDto[], while AssetsState.assets and assetPromisesDict also declare the third tuple element as api.AlbumResponseDto[]. At runtime this value is null when $configStore.showAlbumName is false, so the current types don’t match actual values and may hide bugs in consumers that assume a non‑null array.

Consider either:

  • Allowing null in the tuple type, e.g. api.AlbumResponseDto[] | null, or
  • Always returning an empty array [] when album names are not requested.

This would remove the need for the as [...] cast and keep the types honest.

Also applies to: 55-58, 342-347, 355-372


152-169: Transition guard and play/pause coordination are sensible; verify ProgressBar semantics

The new isHandlingAssetTransition guard in handleDone() plus the sequencing:

  1. progressBar.restart(false)
  2. await getNextAssets()/getPreviousAssets() (which updates currentDuration via pickAssets)
  3. await tick()
  4. await assetComponent?.play?.();
  5. progressBar.play();

should prevent overlapping transitions and keep playback roughly in sync. The restart in step 1 assumes ProgressBar reads the effective duration on play() rather than at restart(), which seems intended but depends on its implementation.

If ProgressBar instead latches duration inside restart(), you may want to move the restart(false) call to after getNextAssets()/getPreviousAssets() so it sees the updated currentDuration.

Also applies to: 402-407, 409-414, 474-483


243-245: Confirm isHorizontal condition vs. naming

isHorizontal() returns false for videos (good for splitview) but then treats an asset as “horizontal” when assetHeight > assetWidth, which is more characteristic of a portrait/vertical image. Given it’s used to gate splitview layout, this naming/condition pair is a bit surprising.

Please double‑check whether the intent is:

  • “portrait‑friendly” assets (height > width), in which case renaming to something like isPortrait would be clearer, or
  • truly horizontal/landscape assets, in which case the condition likely wants assetWidth > assetHeight (or the commented 1.25 ratio).

Marking as a semantic check rather than a required change.

Also applies to: 248-249, 251-251, 253-253


274-280: Duration handling is sensible; clarify intended semantics for long videos

updateCurrentDuration()/getAssetDurationSeconds()/parseAssetDuration() together:

  • Use parsed video durations when available.
  • Fall back to $configStore.interval ?? 20 for images and invalid/absent video durations.
  • Take the max duration across assets in splitview.
  • Bind currentDuration into <ProgressBar duration={currentDuration} />.

This is a good, predictable strategy. One UX detail to confirm: currently, videos longer than the configured interval will play for their full duration. If the intended behavior is “interval applies only to images”, this is perfect; if the expectation is “videos should not exceed the interval” or “should be capped at some max video duration”, you might want to clamp in getAssetDurationSeconds() (e.g., Math.min(parsed, fallback) or a dedicated maxVideoDuration config).

No functional issue, just ensuring the behavior matches the product intent.

Also applies to: 283-289, 291-313, 315-321, 324-324, 327-327, 331-331, 333-333, 511-511

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92c5ba2 and 9c6ebf6.

📒 Files selected for processing (2)
  • immichFrame.Web/src/lib/components/elements/asset-component.svelte (9 hunks)
  • immichFrame.Web/src/lib/components/home-page/home-page.svelte (19 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (1)
immichFrame.Web/src/lib/components/home-page/home-page.svelte (1)

96-102: Object URL lifecycle & cleanup look robust

The combination of assetPromisesDict preloading, cleanup in updateAssetPromises() (revoking URLs for assets no longer in displayingAssets/assetBacklog), and the final sweep in onDestroy closes the previously noted object URL leak and should keep long‑running frames from accumulating Blob URLs. The try/catch around both cleanup paths also avoids unhandled rejections when loadAsset() fails.

No changes requested here; this is a solid improvement.

Also applies to: 106-109, 112-129, 368-385, 424-443

"ImageZoom": true,
"ImagePan": false,
"ImageFill": false,
"PlayAudio": false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you forgot ShowVideos?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Both should also be added to example.env

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

correct

ImageZoom: true
ImagePan: false
ImageFill: false
PlayAudio: false
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same, I think you forgot ShowVideos?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

video playback support

5 participants